diff --git a/.coveragerc b/.coveragerc
index aa8f2d8c03d..748ca511dd5 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -10,7 +10,15 @@ omit =
homeassistant/util/async.py
# omit pieces of code that rely on external devices being present
- homeassistant/components/abode/*
+ homeassistant/components/abode/__init__.py
+ homeassistant/components/abode/alarm_control_panel.py
+ homeassistant/components/abode/binary_sensor.py
+ homeassistant/components/abode/camera.py
+ homeassistant/components/abode/cover.py
+ homeassistant/components/abode/light.py
+ homeassistant/components/abode/lock.py
+ homeassistant/components/abode/sensor.py
+ homeassistant/components/abode/switch.py
homeassistant/components/acer_projector/switch.py
homeassistant/components/actiontec/device_tracker.py
homeassistant/components/adguard/__init__.py
@@ -19,6 +27,10 @@ omit =
homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
homeassistant/components/aftership/sensor.py
+ homeassistant/components/airly/__init__.py
+ homeassistant/components/airly/air_quality.py
+ homeassistant/components/airly/sensor.py
+ homeassistant/components/airly/const.py
homeassistant/components/airvisual/sensor.py
homeassistant/components/aladdin_connect/cover.py
homeassistant/components/alarm_control_panel/manual_mqtt.py
@@ -88,6 +100,7 @@ omit =
homeassistant/components/bt_home_hub_5/device_tracker.py
homeassistant/components/bt_smarthub/device_tracker.py
homeassistant/components/buienradar/sensor.py
+ homeassistant/components/buienradar/util.py
homeassistant/components/buienradar/weather.py
homeassistant/components/caldav/calendar.py
homeassistant/components/canary/alarm_control_panel.py
@@ -113,7 +126,9 @@ omit =
homeassistant/components/comfoconnect/*
homeassistant/components/concord232/alarm_control_panel.py
homeassistant/components/concord232/binary_sensor.py
+ homeassistant/components/coolmaster/__init__.py
homeassistant/components/coolmaster/climate.py
+ homeassistant/components/coolmaster/const.py
homeassistant/components/cppm_tracker/device_tracker.py
homeassistant/components/cpuspeed/sensor.py
homeassistant/components/crimereports/sensor.py
@@ -221,6 +236,7 @@ omit =
homeassistant/components/fortios/device_tracker.py
homeassistant/components/fortigate/*
homeassistant/components/foscam/camera.py
+ homeassistant/components/foscam/const.py
homeassistant/components/foursquare/*
homeassistant/components/free_mobile/notify.py
homeassistant/components/freebox/*
@@ -240,6 +256,7 @@ omit =
homeassistant/components/github/sensor.py
homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py
+ homeassistant/components/glances/__init__.py
homeassistant/components/glances/sensor.py
homeassistant/components/gntp/notify.py
homeassistant/components/goalfeed/*
@@ -271,7 +288,6 @@ omit =
homeassistant/components/heatmiser/climate.py
homeassistant/components/hikvision/binary_sensor.py
homeassistant/components/hikvisioncam/switch.py
- homeassistant/components/hipchat/notify.py
homeassistant/components/hitron_coda/device_tracker.py
homeassistant/components/hive/*
homeassistant/components/hlk_sw16/*
@@ -279,7 +295,6 @@ omit =
homeassistant/components/homematic/climate.py
homeassistant/components/homematic/cover.py
homeassistant/components/homematic/notify.py
- homeassistant/components/homematicip_cloud/*
homeassistant/components/homeworks/*
homeassistant/components/honeywell/climate.py
homeassistant/components/hook/switch.py
@@ -405,6 +420,7 @@ omit =
homeassistant/components/mpchc/media_player.py
homeassistant/components/mpd/media_player.py
homeassistant/components/mqtt_room/sensor.py
+ homeassistant/components/msteams/notify.py
homeassistant/components/mvglive/sensor.py
homeassistant/components/mychevy/*
homeassistant/components/mycroft/*
@@ -417,7 +433,10 @@ omit =
homeassistant/components/n26/*
homeassistant/components/nad/media_player.py
homeassistant/components/nanoleaf/light.py
- homeassistant/components/neato/*
+ homeassistant/components/neato/camera.py
+ homeassistant/components/neato/sensor.py
+ homeassistant/components/neato/switch.py
+ homeassistant/components/neato/vacuum.py
homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/nello/lock.py
homeassistant/components/nest/*
@@ -461,7 +480,10 @@ omit =
homeassistant/components/openhome/media_player.py
homeassistant/components/opensensemap/air_quality.py
homeassistant/components/opensky/sensor.py
- homeassistant/components/opentherm_gw/*
+ homeassistant/components/opentherm_gw/__init__.py
+ homeassistant/components/opentherm_gw/binary_sensor.py
+ homeassistant/components/opentherm_gw/climate.py
+ homeassistant/components/opentherm_gw/sensor.py
homeassistant/components/openuv/__init__.py
homeassistant/components/openuv/binary_sensor.py
homeassistant/components/openuv/sensor.py
@@ -469,6 +491,7 @@ omit =
homeassistant/components/openweathermap/weather.py
homeassistant/components/opple/light.py
homeassistant/components/orangepi_gpio/*
+ homeassistant/components/oru/*
homeassistant/components/orvibo/switch.py
homeassistant/components/osramlightify/light.py
homeassistant/components/otp/sensor.py
@@ -491,6 +514,7 @@ omit =
homeassistant/components/plex/media_player.py
homeassistant/components/plex/sensor.py
homeassistant/components/plex/server.py
+ homeassistant/components/plex/websockets.py
homeassistant/components/plugwise/*
homeassistant/components/plum_lightpad/*
homeassistant/components/pocketcasts/sensor.py
@@ -586,6 +610,7 @@ omit =
homeassistant/components/skybeacon/sensor.py
homeassistant/components/skybell/*
homeassistant/components/slack/notify.py
+ homeassistant/components/sinch/*
homeassistant/components/slide/*
homeassistant/components/sma/sensor.py
homeassistant/components/smappee/*
@@ -599,6 +624,7 @@ omit =
homeassistant/components/solaredge/__init__.py
homeassistant/components/solaredge/sensor.py
homeassistant/components/solaredge_local/sensor.py
+ homeassistant/components/solarlog/*
homeassistant/components/solax/sensor.py
homeassistant/components/soma/cover.py
homeassistant/components/soma/__init__.py
@@ -618,7 +644,6 @@ omit =
homeassistant/components/steam_online/sensor.py
homeassistant/components/stiebel_eltron/*
homeassistant/components/streamlabswater/*
- homeassistant/components/stride/notify.py
homeassistant/components/suez_water/*
homeassistant/components/supervisord/sensor.py
homeassistant/components/swiss_hydrological_data/sensor.py
@@ -676,9 +701,9 @@ omit =
homeassistant/components/tradfri/*
homeassistant/components/tradfri/light.py
homeassistant/components/tradfri/cover.py
+ homeassistant/components/tradfri/base_class.py
homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_weatherstation/sensor.py
- homeassistant/components/transmission/__init__.py
homeassistant/components/transmission/sensor.py
homeassistant/components/transmission/switch.py
homeassistant/components/transmission/const.py
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index afb273331aa..5bfd37fab36 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -5,7 +5,7 @@
"dockerFile": "../Dockerfile.dev",
"postCreateCommand": "mkdir -p config && pip3 install -e .",
"appPort": 8123,
- "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""],
+ "runArgs": ["-e", "GIT_EDITOR=code --wait"],
"extensions": [
"ms-python.python",
"visualstudioexptteam.vscodeintellicode",
diff --git a/.gitignore b/.gitignore
index 15f0896975d..2473aeb4bf6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -128,3 +128,6 @@ monkeytype.sqlite3
# This is left behind by Azure Restore Cache
tmp_cache
+
+# python-language-server / Rope
+.ropeproject
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 78b7ec29859..268cff9ea78 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,6 +6,7 @@ repos:
args:
- --safe
- --quiet
+ files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.8
hooks:
@@ -13,3 +14,18 @@ repos:
additional_dependencies:
- flake8-docstrings==1.3.1
- pydocstyle==4.0.0
+ files: ^(homeassistant|script|tests)/.+\.py$
+# Using a local "system" mypy instead of the mypy hook, because its
+# results depend on what is installed. And the mypy hook runs in a
+# virtualenv of its own, meaning we'd need to install and maintain
+# another set of our dependencies there... no. Use the "system" one
+# and reuse the environment that is set up anyway already instead.
+- repo: local
+ hooks:
+ - id: mypy
+ name: mypy
+ entry: mypy
+ language: system
+ types: [python]
+ require_serial: true
+ files: ^homeassistant/.+\.py$
diff --git a/.travis.yml b/.travis.yml
index 0e9e030128e..6d5b43c2f03 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,7 +19,7 @@ matrix:
- python: "3.6.1"
env: TOXENV=lint
- python: "3.6.1"
- env: TOXENV=pylint
+ env: TOXENV=pylint PYLINT_ARGS=--jobs=0
- python: "3.6.1"
env: TOXENV=typing
- python: "3.6.1"
@@ -27,7 +27,10 @@ matrix:
- python: "3.7"
env: TOXENV=py37
-cache: pip
+cache:
+ pip: true
+ directories:
+ - $HOME/.cache/pre-commit
install: pip install -U tox
language: python
script: travis_wait 50 tox --develop
diff --git a/CODEOWNERS b/CODEOWNERS
index d2cda1f1d07..eb29ee28915 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -13,10 +13,12 @@ homeassistant/util/* @home-assistant/core
homeassistant/scripts/check_config.py @kellerza
# Integrations
+homeassistant/components/abode/* @shred86
homeassistant/components/adguard/* @frenck
+homeassistant/components/airly/* @bieniu
homeassistant/components/airvisual/* @bachya
homeassistant/components/alarm_control_panel/* @colinodell
-homeassistant/components/alexa/* @home-assistant/cloud
+homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
homeassistant/components/alpha_vantage/* @fabaff
homeassistant/components/amazon_polly/* @robbiet480
homeassistant/components/ambiclimate/* @danielhiversen
@@ -24,6 +26,7 @@ homeassistant/components/ambient_station/* @bachya
homeassistant/components/androidtv/* @JeffLIrion
homeassistant/components/apache_kafka/* @bachya
homeassistant/components/api/* @home-assistant/core
+homeassistant/components/apprise/* @caronc
homeassistant/components/aprs/* @PhilRW
homeassistant/components/arcam_fmj/* @elupus
homeassistant/components/arduino/* @fabaff
@@ -49,7 +52,7 @@ homeassistant/components/broadlink/* @danielhiversen
homeassistant/components/brunt/* @eavanvalkenburg
homeassistant/components/bt_smarthub/* @jxwolstenholme
homeassistant/components/buienradar/* @mjj4791 @ties
-homeassistant/components/cert_expiry/* @cereal2nd
+homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren
homeassistant/components/cisco_ios/* @fbradyirl
homeassistant/components/cisco_mobility_express/* @fbradyirl
homeassistant/components/cisco_webex_teams/* @fbradyirl
@@ -97,15 +100,17 @@ homeassistant/components/flock/* @fabaff
homeassistant/components/flunearyou/* @bachya
homeassistant/components/fortigate/* @kifeo
homeassistant/components/fortios/* @kimfrellsen
+homeassistant/components/foscam/* @skgsergio
homeassistant/components/foursquare/* @robbiet480
homeassistant/components/freebox/* @snoof85
homeassistant/components/fronius/* @nielstron
homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/gearbest/* @HerrHofrat
homeassistant/components/geniushub/* @zxdavb
+homeassistant/components/geo_rss_events/* @exxamalte
homeassistant/components/geonetnz_quakes/* @exxamalte
homeassistant/components/gitter/* @fabaff
-homeassistant/components/glances/* @fabaff
+homeassistant/components/glances/* @fabaff @engrbm87
homeassistant/components/gntp/* @robbiet480
homeassistant/components/google_assistant/* @home-assistant/cloud
homeassistant/components/google_cloud/* @lufton
@@ -152,6 +157,7 @@ homeassistant/components/izone/* @Swamp-Ig
homeassistant/components/jewish_calendar/* @tsvi
homeassistant/components/kaiterra/* @Michsior14
homeassistant/components/keba/* @dannerph
+homeassistant/components/keenetic_ndms2/* @foxel
homeassistant/components/knx/* @Julius2342
homeassistant/components/kodi/* @armills
homeassistant/components/konnected/* @heythisisnate
@@ -165,7 +171,7 @@ homeassistant/components/liveboxplaytv/* @pschmitt
homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
homeassistant/components/lovelace/* @home-assistant/frontend
-homeassistant/components/luci/* @fbradyirl
+homeassistant/components/luci/* @fbradyirl @mzdrale
homeassistant/components/luftdaten/* @fabaff
homeassistant/components/mastodon/* @fabaff
homeassistant/components/matrix/* @tinloaf
@@ -184,8 +190,10 @@ homeassistant/components/monoprice/* @etsinko
homeassistant/components/moon/* @fabaff
homeassistant/components/mpd/* @fabaff
homeassistant/components/mqtt/* @home-assistant/core
+homeassistant/components/msteams/* @peroyvind
homeassistant/components/mysensors/* @MartinHjelmare
homeassistant/components/mystrom/* @fabaff
+homeassistant/components/neato/* @dshokouhi @Santobert
homeassistant/components/nello/* @pschmitt
homeassistant/components/ness_alarm/* @nickw444
homeassistant/components/nest/* @awarecan
@@ -209,6 +217,7 @@ homeassistant/components/opentherm_gw/* @mvn23
homeassistant/components/openuv/* @bachya
homeassistant/components/openweathermap/* @fabaff
homeassistant/components/orangepi_gpio/* @pascallj
+homeassistant/components/oru/* @bvlaicu
homeassistant/components/owlet/* @oblogic7
homeassistant/components/panel_custom/* @home-assistant/frontend
homeassistant/components/panel_iframe/* @home-assistant/frontend
@@ -249,6 +258,7 @@ homeassistant/components/shell_command/* @home-assistant/core
homeassistant/components/shiftr/* @fabaff
homeassistant/components/shodan/* @fabaff
homeassistant/components/simplisafe/* @bachya
+homeassistant/components/sinch/* @bendikrb
homeassistant/components/slide/* @ualex73
homeassistant/components/sma/* @kellerza
homeassistant/components/smarthab/* @outadoc
@@ -256,6 +266,7 @@ homeassistant/components/smartthings/* @andrewsayre
homeassistant/components/smarty/* @z0mbieprocess
homeassistant/components/smtp/* @fabaff
homeassistant/components/solaredge_local/* @drobtravels @scheric
+homeassistant/components/solarlog/* @Ernst79
homeassistant/components/solax/* @squishykid
homeassistant/components/soma/* @ratsept
homeassistant/components/somfy/* @tetienne
@@ -312,6 +323,7 @@ homeassistant/components/velux/* @Julius2342
homeassistant/components/version/* @fabaff
homeassistant/components/vesync/* @markperdue @webdjoe
homeassistant/components/vicare/* @oischinger
+homeassistant/components/vivotek/* @HarlemSquirrel
homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf
homeassistant/components/waqi/* @andrey-git
diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml
index 2c3728f1f8c..f1abf2ff9db 100644
--- a/azure-pipelines-ci.yml
+++ b/azure-pipelines-ci.yml
@@ -45,9 +45,10 @@ stages:
. venv/bin/activate
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
+ pre-commit install-hooks
- script: |
. venv/bin/activate
- flake8 homeassistant tests script
+ pre-commit run flake8 --all-files
displayName: 'Run flake8'
- job: 'Validate'
pool:
@@ -83,9 +84,10 @@ stages:
. venv/bin/activate
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
+ pre-commit install-hooks
- script: |
. venv/bin/activate
- ./script/check_format
+ pre-commit run black --all-files
displayName: 'Check Black formatting'
- stage: 'Tests'
@@ -112,7 +114,7 @@ stages:
python -m venv venv
. venv/bin/activate
- pip install -U pip setuptools pytest-azurepipelines -c homeassistant/package_constraints.txt
+ pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt
pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
# Find offending deps with `pipdeptree -r -p typing`
@@ -125,7 +127,7 @@ stages:
set -e
. venv/bin/activate
- pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar tests
+ pytest --timeout=9 --durations=10 -n 2 --dist loadfile -qq -o console_output_style=count -p no:sugar tests
script/check_dirty
displayName: 'Run pytest for python $(python.container)'
condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain']))
@@ -133,7 +135,7 @@ stages:
set -e
. venv/bin/activate
- pytest --timeout=9 --durations=10 --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
+ pytest --timeout=9 --durations=10 -n 2 --dist loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
codecov --token $(codecovToken)
script/check_dirty
displayName: 'Run pytest for python $(python.container) / coverage'
@@ -172,17 +174,16 @@ stages:
vmImage: 'ubuntu-latest'
container: $[ variables['PythonMain'] ]
steps:
- - script: |
- python -m venv venv
+ - template: templates/azp-step-cache.yaml@azure
+ parameters:
+ keyfile: 'requirements_test.txt | setup.py | homeassistant/package_constraints.txt'
+ build: |
+ python -m venv venv
- . venv/bin/activate
- pip install -e .
- pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
- displayName: 'Setup Env'
+ . venv/bin/activate
+ pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt
+ pre-commit install-hooks
- script: |
- TYPING_FILES=$(cat mypyrc)
- echo -e "Run mypy on: \n$TYPING_FILES"
-
. venv/bin/activate
- mypy $TYPING_FILES
+ pre-commit run mypy --all-files
displayName: 'Run mypy'
diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml
index 42815d8c8ae..5092010c49c 100644
--- a/azure-pipelines-wheels.yml
+++ b/azure-pipelines-wheels.yml
@@ -18,7 +18,7 @@ schedules:
always: true
variables:
- name: versionWheels
- value: '1.3-3.7-alpine3.10'
+ value: '1.4-3.7-alpine3.10'
resources:
repositories:
- repository: azure
diff --git a/docs/source/api/helpers.rst b/docs/source/api/helpers.rst
index 28f4059d60d..8ad645b7977 100644
--- a/docs/source/api/helpers.rst
+++ b/docs/source/api/helpers.rst
@@ -56,7 +56,7 @@ homeassistant.helpers.data_entry_flow module
homeassistant.helpers.deprecation module
----------------------------------------
-.. automodule:: homeassistant.helpers.depracation
+.. automodule:: homeassistant.helpers.deprecation
:members:
:undoc-members:
:show-inheritance:
diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py
index ee0d6c08441..921bec71e78 100644
--- a/homeassistant/auth/__init__.py
+++ b/homeassistant/auth/__init__.py
@@ -45,7 +45,7 @@ async def auth_manager_from_config(
)
)
else:
- providers = ()
+ providers = []
# So returned auth providers are in same order as config
provider_hash: _ProviderDict = OrderedDict()
for provider in providers:
@@ -57,7 +57,7 @@ async def auth_manager_from_config(
*(auth_mfa_module_from_config(hass, config) for config in module_configs)
)
else:
- modules = ()
+ modules = []
# So returned auth modules are in same order as config
module_hash: _MfaModuleDict = OrderedDict()
for module in modules:
@@ -86,18 +86,6 @@ class AuthManager:
hass, self._async_create_login_flow, self._async_finish_login_flow
)
- @property
- def support_legacy(self) -> bool:
- """
- Return if legacy_api_password auth providers are registered.
-
- Should be removed when we removed legacy_api_password auth providers.
- """
- for provider_type, _ in self._providers:
- if provider_type == "legacy_api_password":
- return True
- return False
-
@property
def auth_providers(self) -> List[AuthProvider]:
"""Return a list of available auth providers."""
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 7c4ec731b49..6118f4f2bd7 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -64,13 +64,9 @@ async def async_from_config_dict(
)
core_config = config.get(core.DOMAIN, {})
- api_password = config.get("http", {}).get("api_password")
- trusted_networks = config.get("http", {}).get("trusted_networks")
try:
- await conf_util.async_process_ha_core_config(
- hass, core_config, api_password, trusted_networks
- )
+ await conf_util.async_process_ha_core_config(hass, core_config)
except vol.Invalid as config_err:
conf_util.async_log_exception(config_err, "homeassistant", core_config, hass)
return None
@@ -97,11 +93,11 @@ async def async_from_config_dict(
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
- if sys.version_info[:3] < (3, 6, 1):
+ if sys.version_info[:3] < (3, 7, 0):
msg = (
- "Python 3.6.0 support is deprecated and will "
- "be removed in the first release after October 2. Please "
- "upgrade Python to 3.6.1 or higher."
+ "Python 3.6 support is deprecated and will "
+ "be removed in the first release after December 15, 2019. Please "
+ "upgrade Python to 3.7.0 or higher."
)
_LOGGER.warning(msg)
hass.components.persistent_notification.async_create(
@@ -264,7 +260,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
# Add config entry domains
- domains.update(hass.config_entries.async_domains()) # type: ignore
+ domains.update(hass.config_entries.async_domains())
# Make sure the Hass.io component is loaded
if "HASSIO" in os.environ:
diff --git a/homeassistant/components/.translations/airly.de.json b/homeassistant/components/.translations/airly.de.json
deleted file mode 100644
index cb290dc46c0..00000000000
--- a/homeassistant/components/.translations/airly.de.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "config": {
- "error": {
- "name_exists": "Name existiert bereits"
- },
- "step": {
- "user": {
- "data": {
- "latitude": "Breitengrad",
- "longitude": "L\u00e4ngengrad",
- "name": "Name der Integration"
- },
- "title": "Airly"
- }
- },
- "title": "Airly"
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/ca.json b/homeassistant/components/abode/.translations/ca.json
new file mode 100644
index 00000000000..2424fd9b5f0
--- /dev/null
+++ b/homeassistant/components/abode/.translations/ca.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'Abode."
+ },
+ "error": {
+ "connection_error": "No es pot connectar amb Abode.",
+ "identifier_exists": "Compte ja registrat.",
+ "invalid_credentials": "Credencials inv\u00e0lides."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "username": "Correu electr\u00f2nic"
+ },
+ "title": "Introdueix la teva informaci\u00f3 d'inici de sessi\u00f3 a Abode."
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/da.json b/homeassistant/components/abode/.translations/da.json
new file mode 100644
index 00000000000..3f094cb93bd
--- /dev/null
+++ b/homeassistant/components/abode/.translations/da.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Abode."
+ },
+ "error": {
+ "connection_error": "Kunne ikke oprette forbindelse til Abode.",
+ "identifier_exists": "Konto er allerede registreret.",
+ "invalid_credentials": "Ugyldige legitimationsoplysninger."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Adgangskode",
+ "username": "Email adresse"
+ },
+ "title": "Udfyld dine Abode-loginoplysninger"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/de.json b/homeassistant/components/abode/.translations/de.json
new file mode 100644
index 00000000000..ed5ec85a5d7
--- /dev/null
+++ b/homeassistant/components/abode/.translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt."
+ },
+ "error": {
+ "connection_error": "Es kann keine Verbindung zu Abode hergestellt werden.",
+ "identifier_exists": "Das Konto ist bereits registriert.",
+ "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "E-Mail-Adresse"
+ },
+ "title": "Gib deine Abode-Anmeldeinformationen ein"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/en.json b/homeassistant/components/abode/.translations/en.json
new file mode 100644
index 00000000000..e8daeb22c0a
--- /dev/null
+++ b/homeassistant/components/abode/.translations/en.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of Abode is allowed."
+ },
+ "error": {
+ "connection_error": "Unable to connect to Abode.",
+ "identifier_exists": "Account already registered.",
+ "invalid_credentials": "Invalid credentials."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Email Address"
+ },
+ "title": "Fill in your Abode login information"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/es.json b/homeassistant/components/abode/.translations/es.json
new file mode 100644
index 00000000000..908e8f0fbc3
--- /dev/null
+++ b/homeassistant/components/abode/.translations/es.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode."
+ },
+ "error": {
+ "connection_error": "No se puede conectar a Abode.",
+ "identifier_exists": "Cuenta ya registrada.",
+ "invalid_credentials": "Credenciales inv\u00e1lidas."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Direcci\u00f3n de correo electr\u00f3nico"
+ },
+ "title": "Rellene la informaci\u00f3n de acceso Abode"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/fr.json b/homeassistant/components/abode/.translations/fr.json
new file mode 100644
index 00000000000..c0c2a35081b
--- /dev/null
+++ b/homeassistant/components/abode/.translations/fr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Une seule configuration d'Abode est autoris\u00e9e."
+ },
+ "error": {
+ "connection_error": "Impossible de se connecter \u00e0 Abode.",
+ "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9.",
+ "invalid_credentials": "Informations d'identification invalides."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Adresse e-mail"
+ },
+ "title": "Remplissez vos informations de connexion Abode"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/it.json b/homeassistant/components/abode/.translations/it.json
new file mode 100644
index 00000000000..af51aca8af9
--- /dev/null
+++ b/homeassistant/components/abode/.translations/it.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u00c8 consentita una sola configurazione di Abode."
+ },
+ "error": {
+ "connection_error": "Impossibile connettersi ad Abode.",
+ "identifier_exists": "Account gi\u00e0 registrato",
+ "invalid_credentials": "Credenziali non valide"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Indirizzo email"
+ },
+ "title": "Inserisci le tue informazioni di accesso Abode"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/ko.json b/homeassistant/components/abode/.translations/ko.json
new file mode 100644
index 00000000000..9560dde6b3d
--- /dev/null
+++ b/homeassistant/components/abode/.translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\ud558\ub098\uc758 Abode \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "connection_error": "Abode \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc774\uba54\uc77c \uc8fc\uc18c"
+ },
+ "title": "Abode \uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/lb.json b/homeassistant/components/abode/.translations/lb.json
new file mode 100644
index 00000000000..ed65a5df7c5
--- /dev/null
+++ b/homeassistant/components/abode/.translations/lb.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun ZHA ass erlaabt."
+ },
+ "error": {
+ "connection_error": "Kann sech net mat Abode verbannen.",
+ "identifier_exists": "Konto ass scho registr\u00e9iert",
+ "invalid_credentials": "Ong\u00eblteg Login Informatioune"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "username": "E-Mail Adress"
+ },
+ "title": "F\u00ebllt \u00e4r Abode Login Informatiounen aus."
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/nl.json b/homeassistant/components/abode/.translations/nl.json
new file mode 100644
index 00000000000..89b5ae0c4a5
--- /dev/null
+++ b/homeassistant/components/abode/.translations/nl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan."
+ },
+ "error": {
+ "connection_error": "Kan geen verbinding maken met Abode.",
+ "identifier_exists": "Account is al geregistreerd.",
+ "invalid_credentials": "Ongeldige inloggegevens."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "E-mailadres"
+ },
+ "title": "Vul uw Abode-inloggegevens in"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/nn.json b/homeassistant/components/abode/.translations/nn.json
new file mode 100644
index 00000000000..e0c1b6d6a7d
--- /dev/null
+++ b/homeassistant/components/abode/.translations/nn.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/no.json b/homeassistant/components/abode/.translations/no.json
new file mode 100644
index 00000000000..542381cbb64
--- /dev/null
+++ b/homeassistant/components/abode/.translations/no.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Bare en enkelt konfigurasjon av Abode er tillatt."
+ },
+ "error": {
+ "connection_error": "Kan ikke koble til Abode.",
+ "identifier_exists": "Kontoen er allerede registrert.",
+ "invalid_credentials": "Ugyldig brukerinformasjon"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "username": "E-postadresse"
+ },
+ "title": "Fyll ut innloggingsinformasjonen for Abode"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/pl.json b/homeassistant/components/abode/.translations/pl.json
new file mode 100644
index 00000000000..c3f3b8f2c88
--- /dev/null
+++ b/homeassistant/components/abode/.translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja Abode."
+ },
+ "error": {
+ "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.",
+ "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane",
+ "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Adres e-mail"
+ },
+ "title": "Wprowad\u017a informacje logowania Abode"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/pt-BR.json b/homeassistant/components/abode/.translations/pt-BR.json
new file mode 100644
index 00000000000..7a117a81993
--- /dev/null
+++ b/homeassistant/components/abode/.translations/pt-BR.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Endere\u00e7o de e-mail"
+ }
+ }
+ },
+ "title": ""
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/pt.json b/homeassistant/components/abode/.translations/pt.json
new file mode 100644
index 00000000000..512bf59906c
--- /dev/null
+++ b/homeassistant/components/abode/.translations/pt.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Conta j\u00e1 registada"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "username": "Endere\u00e7o de e-mail"
+ }
+ }
+ },
+ "title": ""
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/ru.json b/homeassistant/components/abode/.translations/ru.json
new file mode 100644
index 00000000000..f39e6b1443b
--- /dev/null
+++ b/homeassistant/components/abode/.translations/ru.json
@@ -0,0 +1,22 @@
+{
+ "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a Abode.",
+ "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
+ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
+ },
+ "title": "Abode"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/sl.json b/homeassistant/components/abode/.translations/sl.json
new file mode 100644
index 00000000000..b840913b7be
--- /dev/null
+++ b/homeassistant/components/abode/.translations/sl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Dovoljena je samo ena konfiguracija Abode."
+ },
+ "error": {
+ "connection_error": "Ni mogo\u010de vzpostaviti povezave z Abode.",
+ "identifier_exists": "Ra\u010dun je \u017ee registriran.",
+ "invalid_credentials": "Neveljavne poverilnice."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Geslo",
+ "username": "E-po\u0161tni naslov"
+ },
+ "title": "Izpolnite svoje podatke za prijavo v Abode"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/zh-Hant.json b/homeassistant/components/abode/.translations/zh-Hant.json
new file mode 100644
index 00000000000..5bc9efc3696
--- /dev/null
+++ b/homeassistant/components/abode/.translations/zh-Hant.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Abode\u3002"
+ },
+ "error": {
+ "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 Abode\u3002",
+ "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a\u3002",
+ "invalid_credentials": "\u6191\u8b49\u7121\u6548\u3002"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740"
+ },
+ "title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py
index f43cbc50f98..6a72ac64145 100644
--- a/homeassistant/components/abode/__init__.py
+++ b/homeassistant/components/abode/__init__.py
@@ -1,45 +1,36 @@
-"""Support for Abode Home Security system."""
-import logging
+"""Support for the Abode Security System."""
+from asyncio import gather
+from copy import deepcopy
from functools import partial
-from requests.exceptions import HTTPError, ConnectTimeout
+import logging
+from abodepy import Abode
+from abodepy.exceptions import AbodeException
+import abodepy.helpers.timeline as TIMELINE
+from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DATE,
- ATTR_TIME,
ATTR_ENTITY_ID,
- CONF_USERNAME,
+ ATTR_TIME,
CONF_PASSWORD,
- CONF_EXCLUDE,
- CONF_NAME,
- CONF_LIGHTS,
+ CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
- EVENT_HOMEASSISTANT_START,
)
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
+from .const import ATTRIBUTION, DOMAIN
+
_LOGGER = logging.getLogger(__name__)
-ATTRIBUTION = "Data provided by goabode.com"
-
CONF_POLLING = "polling"
-DOMAIN = "abode"
DEFAULT_CACHEDB = "./abodepy_cache.pickle"
-NOTIFICATION_ID = "abode_notification"
-NOTIFICATION_TITLE = "Abode Security Setup"
-
-EVENT_ABODE_ALARM = "abode_alarm"
-EVENT_ABODE_ALARM_END = "abode_alarm_end"
-EVENT_ABODE_AUTOMATION = "abode_automation"
-EVENT_ABODE_FAULT = "abode_panel_fault"
-EVENT_ABODE_RESTORE = "abode_panel_restore"
-
SERVICE_SETTINGS = "change_setting"
SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER = "trigger_quick_action"
@@ -53,6 +44,8 @@ ATTR_EVENT_TYPE = "event_type"
ATTR_EVENT_UTC = "event_utc"
ATTR_SETTING = "setting"
ATTR_USER_NAME = "user_name"
+ATTR_APP_TYPE = "app_type"
+ATTR_EVENT_BY = "event_by"
ATTR_VALUE = "value"
ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str])
@@ -63,10 +56,7 @@ CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_POLLING, default=False): cv.boolean,
- vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
- vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
}
)
},
@@ -96,83 +86,86 @@ ABODE_PLATFORMS = [
class AbodeSystem:
"""Abode System class."""
- def __init__(self, username, password, cache, name, polling, exclude, lights):
+ def __init__(self, abode, polling):
"""Initialize the system."""
- import abodepy
- self.abode = abodepy.Abode(
- username,
- password,
- auto_login=True,
- get_devices=True,
- get_automations=True,
- cache_path=cache,
- )
- self.name = name
+ self.abode = abode
self.polling = polling
- self.exclude = exclude
- self.lights = lights
self.devices = []
-
- def is_excluded(self, device):
- """Check if a device is configured to be excluded."""
- return device.device_id in self.exclude
-
- def is_automation_excluded(self, automation):
- """Check if an automation is configured to be excluded."""
- return automation.automation_id in self.exclude
-
- def is_light(self, device):
- """Check if a switch device is configured as a light."""
- import abodepy.helpers.constants as CONST
-
- return device.generic_type == CONST.TYPE_LIGHT or (
- device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights
- )
+ self.logout_listener = None
-def setup(hass, config):
- """Set up Abode component."""
- from abodepy.exceptions import AbodeException
+async def async_setup(hass, config):
+ """Set up Abode integration."""
+ if DOMAIN not in config:
+ return True
conf = config[DOMAIN]
- username = conf.get(CONF_USERNAME)
- password = conf.get(CONF_PASSWORD)
- name = conf.get(CONF_NAME)
- polling = conf.get(CONF_POLLING)
- exclude = conf.get(CONF_EXCLUDE)
- lights = conf.get(CONF_LIGHTS)
+
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=deepcopy(conf)
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up Abode integration from a config entry."""
+ username = config_entry.data.get(CONF_USERNAME)
+ password = config_entry.data.get(CONF_PASSWORD)
+ polling = config_entry.data.get(CONF_POLLING)
try:
cache = hass.config.path(DEFAULT_CACHEDB)
- hass.data[DOMAIN] = AbodeSystem(
- username, password, cache, name, polling, exclude, lights
+ abode = await hass.async_add_executor_job(
+ Abode, username, password, True, True, True, cache
)
+ hass.data[DOMAIN] = AbodeSystem(abode, polling)
+
except (AbodeException, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
-
- hass.components.persistent_notification.create(
- "Error: {}
"
- "You will need to restart hass after fixing."
- "".format(ex),
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID,
- )
return False
- setup_hass_services(hass)
- setup_hass_events(hass)
- setup_abode_events(hass)
+ for platform in ABODE_PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
+
+ await setup_hass_events(hass)
+ await hass.async_add_executor_job(setup_hass_services, hass)
+ await hass.async_add_executor_job(setup_abode_events, hass)
+
+ return True
+
+
+async def async_unload_entry(hass, config_entry):
+ """Unload a config entry."""
+ hass.services.async_remove(DOMAIN, SERVICE_SETTINGS)
+ hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE)
+ hass.services.async_remove(DOMAIN, SERVICE_TRIGGER)
+
+ tasks = []
for platform in ABODE_PLATFORMS:
- discovery.load_platform(hass, platform, DOMAIN, {}, config)
+ tasks.append(
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ )
+
+ await gather(*tasks)
+
+ await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop)
+ await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout)
+
+ hass.data[DOMAIN].logout_listener()
+ hass.data.pop(DOMAIN)
return True
def setup_hass_services(hass):
"""Home assistant services."""
- from abodepy.exceptions import AbodeException
def change_setting(call):
"""Change an Abode system setting."""
@@ -223,13 +216,9 @@ def setup_hass_services(hass):
)
-def setup_hass_events(hass):
+async def setup_hass_events(hass):
"""Home Assistant start and stop callbacks."""
- def startup(event):
- """Listen for push events."""
- hass.data[DOMAIN].abode.events.start()
-
def logout(event):
"""Logout of Abode."""
if not hass.data[DOMAIN].polling:
@@ -239,14 +228,15 @@ def setup_hass_events(hass):
_LOGGER.info("Logged out of Abode")
if not hass.data[DOMAIN].polling:
- hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup)
+ await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start)
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout)
+ hass.data[DOMAIN].logout_listener = hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, logout
+ )
def setup_abode_events(hass):
"""Event callbacks."""
- import abodepy.helpers.timeline as TIMELINE
def event_callback(event, event_json):
"""Handle an event callback from Abode."""
@@ -259,6 +249,8 @@ def setup_abode_events(hass):
ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""),
ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""),
ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""),
+ ATTR_APP_TYPE: event_json.get(ATTR_APP_TYPE, ""),
+ ATTR_EVENT_BY: event_json.get(ATTR_EVENT_BY, ""),
ATTR_DATE: event_json.get(ATTR_DATE, ""),
ATTR_TIME: event_json.get(ATTR_TIME, ""),
}
@@ -271,6 +263,12 @@ def setup_abode_events(hass):
TIMELINE.PANEL_FAULT_GROUP,
TIMELINE.PANEL_RESTORE_GROUP,
TIMELINE.AUTOMATION_GROUP,
+ TIMELINE.DISARM_GROUP,
+ TIMELINE.ARM_GROUP,
+ TIMELINE.TEST_GROUP,
+ TIMELINE.CAPTURE_GROUP,
+ TIMELINE.DEVICE_GROUP,
+ TIMELINE.AUTOMATION_EDIT_GROUP,
]
for event in events:
@@ -283,30 +281,36 @@ class AbodeDevice(Entity):
"""Representation of an Abode device."""
def __init__(self, data, device):
- """Initialize a sensor for Abode device."""
+ """Initialize Abode device."""
self._data = data
self._device = device
async def async_added_to_hass(self):
- """Subscribe Abode events."""
+ """Subscribe to device events."""
self.hass.async_add_job(
self._data.abode.events.add_device_callback,
self._device.device_id,
self._update_callback,
)
+ async def async_will_remove_from_hass(self):
+ """Unsubscribe from device events."""
+ self.hass.async_add_job(
+ self._data.abode.events.remove_all_device_callbacks, self._device.device_id
+ )
+
@property
def should_poll(self):
"""Return the polling state."""
return self._data.polling
def update(self):
- """Update automation state."""
+ """Update device and automation states."""
self._device.refresh()
@property
def name(self):
- """Return the name of the sensor."""
+ """Return the name of the device."""
return self._device.name
@property
@@ -320,6 +324,21 @@ class AbodeDevice(Entity):
"device_type": self._device.type,
}
+ @property
+ def unique_id(self):
+ """Return a unique ID to use for this device."""
+ return self._device.device_uuid
+
+ @property
+ def device_info(self):
+ """Return device registry information for this entity."""
+ return {
+ "identifiers": {(DOMAIN, self._device.device_id)},
+ "manufacturer": "Abode",
+ "name": self._device.name,
+ "device_type": self._device.type,
+ }
+
def _update_callback(self, device):
"""Update the device state."""
self.schedule_update_ha_state()
@@ -354,7 +373,7 @@ class AbodeAutomation(Entity):
@property
def name(self):
- """Return the name of the sensor."""
+ """Return the name of the automation."""
return self._automation.name
@property
@@ -368,6 +387,6 @@ class AbodeAutomation(Entity):
}
def _update_callback(self, device):
- """Update the device state."""
+ """Update the automation state."""
self._automation.refresh()
self.schedule_update_ha_state()
diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py
index c5c10e65302..f774e773cb5 100644
--- a/homeassistant/components/abode/alarm_control_panel.py
+++ b/homeassistant/components/abode/alarm_control_panel.py
@@ -9,32 +9,31 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
)
-from . import ATTRIBUTION, DOMAIN as ABODE_DOMAIN, AbodeDevice
+from . import AbodeDevice
+from .const import ATTRIBUTION, DOMAIN
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:security"
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Platform uses config entry setup."""
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up an alarm control panel for an Abode device."""
- data = hass.data[ABODE_DOMAIN]
+ data = hass.data[DOMAIN]
- alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)]
-
- data.devices.extend(alarm_devices)
-
- add_entities(alarm_devices)
+ async_add_entities(
+ [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))]
+ )
class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel):
"""An alarm_control_panel implementation for Abode."""
- def __init__(self, data, device, name):
- """Initialize the alarm control panel."""
- super().__init__(data, device)
- self._name = name
-
@property
def icon(self):
"""Return the icon."""
@@ -65,11 +64,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel):
"""Send arm away command."""
self._device.set_away()
- @property
- def name(self):
- """Return the name of the alarm."""
- return self._name or super().name
-
@property
def device_state_attributes(self):
"""Return the state attributes."""
diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py
index e37f6a465a4..31f74448496 100644
--- a/homeassistant/components/abode/binary_sensor.py
+++ b/homeassistant/components/abode/binary_sensor.py
@@ -1,19 +1,25 @@
"""Support for Abode Security System binary sensors."""
import logging
+import abodepy.helpers.constants as CONST
+import abodepy.helpers.timeline as TIMELINE
+
from homeassistant.components.binary_sensor import BinarySensorDevice
-from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice
+from . import AbodeAutomation, AbodeDevice
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up a sensor for an Abode device."""
- import abodepy.helpers.constants as CONST
- import abodepy.helpers.timeline as TIMELINE
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Platform uses config entry setup."""
+ pass
- data = hass.data[ABODE_DOMAIN]
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up a sensor for an Abode device."""
+ data = hass.data[DOMAIN]
device_types = [
CONST.TYPE_CONNECTIVITY,
@@ -24,25 +30,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
]
devices = []
- for device in data.abode.get_devices(generic_type=device_types):
- if data.is_excluded(device):
- continue
+ for device in data.abode.get_devices(generic_type=device_types):
devices.append(AbodeBinarySensor(data, device))
for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION):
- if data.is_automation_excluded(automation):
- continue
-
devices.append(
AbodeQuickActionBinarySensor(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP
)
)
- data.devices.extend(devices)
-
- add_entities(devices)
+ async_add_entities(devices)
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py
index 95755a644e2..e98a59a985c 100644
--- a/homeassistant/components/abode/camera.py
+++ b/homeassistant/components/abode/camera.py
@@ -2,35 +2,36 @@
from datetime import timedelta
import logging
+import abodepy.helpers.constants as CONST
+import abodepy.helpers.timeline as TIMELINE
import requests
from homeassistant.components.camera import Camera
from homeassistant.util import Throttle
-from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
+from . import AbodeDevice
+from .const import DOMAIN
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up Abode camera devices."""
- import abodepy.helpers.constants as CONST
- import abodepy.helpers.timeline as TIMELINE
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Platform uses config entry setup."""
+ pass
- data = hass.data[ABODE_DOMAIN]
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up a camera for an Abode device."""
+
+ data = hass.data[DOMAIN]
devices = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA):
- if data.is_excluded(device):
- continue
-
devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE))
- data.devices.extend(devices)
-
- add_entities(devices)
+ async_add_entities(devices)
class AbodeCamera(AbodeDevice, Camera):
diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py
new file mode 100644
index 00000000000..d8d914f7998
--- /dev/null
+++ b/homeassistant/components/abode/config_flow.py
@@ -0,0 +1,79 @@
+"""Config flow for the Abode Security System component."""
+import logging
+
+from abodepy import Abode
+from abodepy.exceptions import AbodeException
+from requests.exceptions import ConnectTimeout, HTTPError
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import callback
+
+from .const import DOMAIN # pylint: disable=W0611
+
+CONF_POLLING = "polling"
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Config flow for Abode."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self):
+ """Initialize."""
+ self.data_schema = {
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ if not user_input:
+ return self._show_form()
+
+ username = user_input[CONF_USERNAME]
+ password = user_input[CONF_PASSWORD]
+ polling = user_input.get(CONF_POLLING, False)
+
+ try:
+ await self.hass.async_add_executor_job(Abode, username, password, True)
+
+ except (AbodeException, ConnectTimeout, HTTPError) as ex:
+ _LOGGER.error("Unable to connect to Abode: %s", str(ex))
+ if ex.errcode == 400:
+ return self._show_form({"base": "invalid_credentials"})
+ return self._show_form({"base": "connection_error"})
+
+ return self.async_create_entry(
+ title=user_input[CONF_USERNAME],
+ data={
+ CONF_USERNAME: username,
+ CONF_PASSWORD: password,
+ CONF_POLLING: polling,
+ },
+ )
+
+ @callback
+ def _show_form(self, errors=None):
+ """Show the form to the user."""
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(self.data_schema),
+ errors=errors if errors else {},
+ )
+
+ async def async_step_import(self, import_config):
+ """Import a config entry from configuration.yaml."""
+ if self._async_current_entries():
+ _LOGGER.warning("Only one configuration of abode is allowed.")
+ return self.async_abort(reason="single_instance_allowed")
+
+ return await self.async_step_user(import_config)
diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py
new file mode 100644
index 00000000000..35e74e154cf
--- /dev/null
+++ b/homeassistant/components/abode/const.py
@@ -0,0 +1,3 @@
+"""Constants for the Abode Security System component."""
+DOMAIN = "abode"
+ATTRIBUTION = "Data provided by goabode.com"
diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py
index 4c868daf4ba..ebe59ee45c7 100644
--- a/homeassistant/components/abode/cover.py
+++ b/homeassistant/components/abode/cover.py
@@ -1,29 +1,31 @@
"""Support for Abode Security System covers."""
import logging
+import abodepy.helpers.constants as CONST
+
from homeassistant.components.cover import CoverDevice
-from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
+from . import AbodeDevice
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up Abode cover devices."""
- import abodepy.helpers.constants as CONST
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Platform uses config entry setup."""
+ pass
- data = hass.data[ABODE_DOMAIN]
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Abode cover devices."""
+
+ data = hass.data[DOMAIN]
devices = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER):
- if data.is_excluded(device):
- continue
-
devices.append(AbodeCover(data, device))
- data.devices.extend(devices)
-
- add_entities(devices)
+ async_add_entities(devices)
class AbodeCover(AbodeDevice, CoverDevice):
diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py
index 8e6691560e5..163982d040e 100644
--- a/homeassistant/components/abode/light.py
+++ b/homeassistant/components/abode/light.py
@@ -2,6 +2,8 @@
import logging
from math import ceil
+import abodepy.helpers.constants as CONST
+
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
@@ -16,31 +18,27 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin,
)
-from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
+from . import AbodeDevice
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Platform uses config entry setup."""
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Abode light devices."""
- import abodepy.helpers.constants as CONST
-
- data = hass.data[ABODE_DOMAIN]
-
- device_types = [CONST.TYPE_LIGHT, CONST.TYPE_SWITCH]
+ data = hass.data[DOMAIN]
devices = []
- # Get all regular lights that are not excluded or switches marked as lights
- for device in data.abode.get_devices(generic_type=device_types):
- if data.is_excluded(device) or not data.is_light(device):
- continue
-
+ for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT):
devices.append(AbodeLight(data, device))
- data.devices.extend(devices)
-
- add_entities(devices)
+ async_add_entities(devices)
class AbodeLight(AbodeDevice, Light):
diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py
index c1272a3de5f..11f792f88fd 100644
--- a/homeassistant/components/abode/lock.py
+++ b/homeassistant/components/abode/lock.py
@@ -1,29 +1,31 @@
-"""Support for Abode Security System locks."""
+"""Support for the Abode Security System locks."""
import logging
+import abodepy.helpers.constants as CONST
+
from homeassistant.components.lock import LockDevice
-from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
+from . import AbodeDevice
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up Abode lock devices."""
- import abodepy.helpers.constants as CONST
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Platform uses config entry setup."""
+ pass
- data = hass.data[ABODE_DOMAIN]
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Abode lock devices."""
+
+ data = hass.data[DOMAIN]
devices = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK):
- if data.is_excluded(device):
- continue
-
devices.append(AbodeLock(data, device))
- data.devices.extend(devices)
-
- add_entities(devices)
+ async_add_entities(devices)
class AbodeLock(AbodeDevice, LockDevice):
diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json
index 793c19cc466..b54120c7cbd 100644
--- a/homeassistant/components/abode/manifest.json
+++ b/homeassistant/components/abode/manifest.json
@@ -1,10 +1,13 @@
{
"domain": "abode",
"name": "Abode",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/abode",
"requirements": [
- "abodepy==0.15.0"
+ "abodepy==0.16.6"
],
"dependencies": [],
- "codeowners": []
-}
+ "codeowners": [
+ "@shred86"
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py
index ba28eab79c7..e25921f295f 100644
--- a/homeassistant/components/abode/sensor.py
+++ b/homeassistant/components/abode/sensor.py
@@ -1,13 +1,16 @@
"""Support for Abode Security System sensors."""
import logging
+import abodepy.helpers.constants as CONST
+
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
)
-from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
+from . import AbodeDevice
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -19,23 +22,22 @@ SENSOR_TYPES = {
}
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up a sensor for an Abode device."""
- import abodepy.helpers.constants as CONST
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Platform uses config entry setup."""
+ pass
- data = hass.data[ABODE_DOMAIN]
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up a sensor for an Abode device."""
+
+ data = hass.data[DOMAIN]
devices = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR):
- if data.is_excluded(device):
- continue
-
for sensor_type in SENSOR_TYPES:
devices.append(AbodeSensor(data, device, sensor_type))
- data.devices.extend(devices)
-
- add_entities(devices)
+ async_add_entities(devices)
class AbodeSensor(AbodeDevice):
diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json
new file mode 100644
index 00000000000..bf7e768f6e3
--- /dev/null
+++ b/homeassistant/components/abode/strings.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "title": "Abode",
+ "step": {
+ "user": {
+ "title": "Fill in your Abode login information",
+ "data": {
+ "username": "Email Address",
+ "password": "Password"
+ }
+ }
+ },
+ "error": {
+ "identifier_exists": "Account already registered.",
+ "invalid_credentials": "Invalid credentials.",
+ "connection_error": "Unable to connect to Abode."
+ },
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of Abode is allowed."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py
index 82a550df1a5..7bd7f394d30 100644
--- a/homeassistant/components/abode/switch.py
+++ b/homeassistant/components/abode/switch.py
@@ -1,41 +1,37 @@
"""Support for Abode Security System switches."""
import logging
+import abodepy.helpers.constants as CONST
+import abodepy.helpers.timeline as TIMELINE
+
from homeassistant.components.switch import SwitchDevice
-from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice
+from . import AbodeAutomation, AbodeDevice
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up Abode switch devices."""
- import abodepy.helpers.constants as CONST
- import abodepy.helpers.timeline as TIMELINE
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Platform uses config entry setup."""
+ pass
- data = hass.data[ABODE_DOMAIN]
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Abode switch devices."""
+ data = hass.data[DOMAIN]
devices = []
- # Get all regular switches that are not excluded or marked as lights
for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH):
- if data.is_excluded(device) or data.is_light(device):
- continue
-
devices.append(AbodeSwitch(data, device))
- # Get all Abode automations that can be enabled/disabled
for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION):
- if data.is_automation_excluded(automation):
- continue
-
devices.append(
AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)
)
- data.devices.extend(devices)
-
- add_entities(devices)
+ async_add_entities(devices)
class AbodeSwitch(AbodeDevice, SwitchDevice):
diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py
index 558cf84d0e1..39a79636c93 100644
--- a/homeassistant/components/acer_projector/switch.py
+++ b/homeassistant/components/acer_projector/switch.py
@@ -1,6 +1,7 @@
"""Use serial protocol of Acer projector to obtain state of the projector."""
import logging
import re
+import serial
import voluptuous as vol
@@ -73,7 +74,6 @@ class AcerSwitch(SwitchDevice):
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
"""Init of the Acer projector."""
- import serial
self.ser = serial.Serial(
port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs
@@ -90,7 +90,6 @@ class AcerSwitch(SwitchDevice):
def _write_read(self, msg):
"""Write to the projector and read the return."""
- import serial
ret = ""
# Sometimes the projector won't answer for no reason or the projector
diff --git a/homeassistant/components/adguard/.translations/ca.json b/homeassistant/components/adguard/.translations/ca.json
index 30fd509cb7a..9b7b3c39b03 100644
--- a/homeassistant/components/adguard/.translations/ca.json
+++ b/homeassistant/components/adguard/.translations/ca.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}. Actualitza el complement de Hass.io d'AdGuard Home.",
+ "adguard_home_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}.",
"existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.",
"single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'AdGuard Home."
},
diff --git a/homeassistant/components/adguard/.translations/en.json b/homeassistant/components/adguard/.translations/en.json
index 6e3b5b58503..00d048c3343 100644
--- a/homeassistant/components/adguard/.translations/en.json
+++ b/homeassistant/components/adguard/.translations/en.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.",
+ "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.",
"existing_instance_updated": "Updated existing configuration.",
"single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
},
diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json
index 5886d8e5c5b..c6946ab6120 100644
--- a/homeassistant/components/adguard/.translations/es.json
+++ b/homeassistant/components/adguard/.translations/es.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}. Por favor, actualice su complemento Hass.io AdGuard Home.",
+ "adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}.",
"existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.",
"single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
},
diff --git a/homeassistant/components/adguard/.translations/fr.json b/homeassistant/components/adguard/.translations/fr.json
index 6543ddd50bc..749ba7d9c03 100644
--- a/homeassistant/components/adguard/.translations/fr.json
+++ b/homeassistant/components/adguard/.translations/fr.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}. Veuillez mettre \u00e0 jour votre compl\u00e9ment Hass.io AdGuard Home.",
+ "adguard_home_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}.",
"existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.",
"single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e."
},
diff --git a/homeassistant/components/adguard/.translations/ko.json b/homeassistant/components/adguard/.translations/ko.json
index bb93d675103..e1f39259292 100644
--- a/homeassistant/components/adguard/.translations/ko.json
+++ b/homeassistant/components/adguard/.translations/ko.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4. Hass.io AdGuard Home \uc560\ub4dc\uc628\uc744 \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uc138\uc694.",
+ "adguard_home_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4.",
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.",
"single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
diff --git a/homeassistant/components/adguard/.translations/lb.json b/homeassistant/components/adguard/.translations/lb.json
index cc3ecf5db87..e449f668fd9 100644
--- a/homeassistant/components/adguard/.translations/lb.json
+++ b/homeassistant/components/adguard/.translations/lb.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}. Aktualis\u00e9iert w.e.g. \u00e4ren Hass.io AdGuard Home Add-on.",
+ "adguard_home_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}.",
"existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.",
"single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun AdGuard Home ass erlaabt."
},
diff --git a/homeassistant/components/adguard/.translations/nl.json b/homeassistant/components/adguard/.translations/nl.json
index 3ef86c30a3f..bd0dcc5fa43 100644
--- a/homeassistant/components/adguard/.translations/nl.json
+++ b/homeassistant/components/adguard/.translations/nl.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}. Update uw Hass.io AdGuard Home-add-on.",
+ "adguard_home_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}.",
"existing_instance_updated": "Bestaande configuratie bijgewerkt.",
"single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan."
},
diff --git a/homeassistant/components/adguard/.translations/nn.json b/homeassistant/components/adguard/.translations/nn.json
index 7c129cba3af..0e2e82437e8 100644
--- a/homeassistant/components/adguard/.translations/nn.json
+++ b/homeassistant/components/adguard/.translations/nn.json
@@ -6,6 +6,7 @@
"username": "Brukarnamn"
}
}
- }
+ },
+ "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
index 2cd6cd72f6d..22a8c23644f 100644
--- a/homeassistant/components/adguard/.translations/no.json
+++ b/homeassistant/components/adguard/.translations/no.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}. Vennligst oppdater Hass.io AdGuard Home-tillegget.",
+ "adguard_home_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}.",
"existing_instance_updated": "Oppdatert eksisterende konfigurasjon.",
"single_instance_allowed": "Kun en konfigurasjon av AdGuard Hjemer tillatt."
},
diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json
index f8f64d54260..69ba6b024e2 100644
--- a/homeassistant/components/adguard/.translations/pl.json
+++ b/homeassistant/components/adguard/.translations/pl.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}. Zaktualizuj sw\u00f3j dodatek Hass.io AdGuard Home.",
+ "adguard_home_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}.",
"existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119.",
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home."
},
diff --git a/homeassistant/components/adguard/.translations/pt.json b/homeassistant/components/adguard/.translations/pt.json
new file mode 100644
index 00000000000..f681da4210f
--- /dev/null
+++ b/homeassistant/components/adguard/.translations/pt.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Servidor",
+ "port": "Porta"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/.translations/ru.json b/homeassistant/components/adguard/.translations/ru.json
index c50d0197351..eca46d7db00 100644
--- a/homeassistant/components/adguard/.translations/ru.json
+++ b/homeassistant/components/adguard/.translations/ru.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0414\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f {minimal_version}, \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0430\u044f. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 Hass.io.",
+ "adguard_home_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u044e {minimal_version} \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e.",
"existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.",
"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."
},
diff --git a/homeassistant/components/adguard/.translations/sl.json b/homeassistant/components/adguard/.translations/sl.json
index f1ca796363d..974524c932d 100644
--- a/homeassistant/components/adguard/.translations/sl.json
+++ b/homeassistant/components/adguard/.translations/sl.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}. Prosimo posodobite va\u0161 hass.io AdGuard Home dodatek.",
+ "adguard_home_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}.",
"existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.",
"single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home."
},
diff --git a/homeassistant/components/adguard/.translations/zh-Hant.json b/homeassistant/components/adguard/.translations/zh-Hant.json
index a693652fedf..d08a5715a8e 100644
--- a/homeassistant/components/adguard/.translations/zh-Hant.json
+++ b/homeassistant/components/adguard/.translations/zh-Hant.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002\u8acb\u66f4\u65b0 Hass.io AdGuard Home \u5143\u4ef6\u3002",
+ "adguard_home_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002",
"existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002",
"single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 AdGuard Home\u3002"
},
diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py
index ba716ae0f9c..bb53d00aab8 100644
--- a/homeassistant/components/adguard/__init__.py
+++ b/homeassistant/components/adguard/__init__.py
@@ -1,8 +1,9 @@
"""Support for AdGuard Home."""
+from distutils.version import LooseVersion
import logging
from typing import Any, Dict
-from adguardhome import AdGuardHome, AdGuardHomeError
+from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
import voluptuous as vol
from homeassistant.components.adguard.const import (
@@ -10,6 +11,7 @@ from homeassistant.components.adguard.const import (
DATA_ADGUARD_CLIENT,
DATA_ADGUARD_VERION,
DOMAIN,
+ MIN_ADGUARD_HOME_VERSION,
SERVICE_ADD_URL,
SERVICE_DISABLE_URL,
SERVICE_ENABLE_URL,
@@ -27,6 +29,7 @@ from homeassistant.const import (
CONF_USERNAME,
CONF_VERIFY_SSL,
)
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
@@ -64,6 +67,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard
+ try:
+ version = await adguard.version()
+ except AdGuardHomeConnectionError as exception:
+ raise ConfigEntryNotReady from exception
+
+ if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
+ _LOGGER.error(
+ "This integration requires AdGuard Home v0.99.0 or higher to work correctly"
+ )
+ raise ConfigEntryNotReady
+
for component in "sensor", "switch":
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py
index 5a096aeceed..9f5645edb8d 100644
--- a/homeassistant/components/adguard/config_flow.py
+++ b/homeassistant/components/adguard/config_flow.py
@@ -1,11 +1,12 @@
"""Config flow to configure the AdGuard Home integration."""
+from distutils.version import LooseVersion
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.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import (
CONF_HOST,
@@ -83,11 +84,20 @@ class AdGuardHomeFlowHandler(ConfigFlow):
)
try:
- await adguard.version()
+ version = await adguard.version()
except AdGuardHomeConnectionError:
errors["base"] = "connection_error"
return await self._show_setup_form(errors)
+ if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
+ return self.async_abort(
+ reason="adguard_home_outdated",
+ description_placeholders={
+ "current_version": version,
+ "minimal_version": MIN_ADGUARD_HOME_VERSION,
+ },
+ )
+
return self.async_create_entry(
title=user_input[CONF_HOST],
data={
@@ -156,11 +166,20 @@ class AdGuardHomeFlowHandler(ConfigFlow):
)
try:
- await adguard.version()
+ version = await adguard.version()
except AdGuardHomeConnectionError:
errors["base"] = "connection_error"
return await self._show_hassio_form(errors)
+ if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
+ return self.async_abort(
+ reason="adguard_home_addon_outdated",
+ description_placeholders={
+ "current_version": version,
+ "minimal_version": MIN_ADGUARD_HOME_VERSION,
+ },
+ )
+
return self.async_create_entry(
title=self._hassio_discovery["addon"],
data={
diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py
index c77d76a70cf..eb12a9c163f 100644
--- a/homeassistant/components/adguard/const.py
+++ b/homeassistant/components/adguard/const.py
@@ -7,6 +7,8 @@ DATA_ADGUARD_VERION = "adguard_version"
CONF_FORCE = "force"
+MIN_ADGUARD_HOME_VERSION = "v0.99.0"
+
SERVICE_ADD_URL = "add_url"
SERVICE_DISABLE_URL = "disable_url"
SERVICE_ENABLE_URL = "enable_url"
diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json
index f207e6dff09..45fd21f4fc8 100644
--- a/homeassistant/components/adguard/manifest.json
+++ b/homeassistant/components/adguard/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adguard",
"requirements": [
- "adguardhome==0.2.1"
+ "adguardhome==0.3.0"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json
index b3966bca820..d33ba2b397a 100644
--- a/homeassistant/components/adguard/strings.json
+++ b/homeassistant/components/adguard/strings.json
@@ -23,8 +23,10 @@
"connection_error": "Failed to connect."
},
"abort": {
- "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed.",
- "existing_instance_updated": "Updated existing configuration."
+ "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.",
+ "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.",
+ "existing_instance_updated": "Updated existing configuration.",
+ "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
}
}
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py
index 1b4f11c7cc1..ba4762da84a 100644
--- a/homeassistant/components/ads/__init__.py
+++ b/homeassistant/components/ads/__init__.py
@@ -7,6 +7,8 @@ from collections import namedtuple
import asyncio
import async_timeout
+import pyads
+
import voluptuous as vol
from homeassistant.const import (
@@ -78,7 +80,6 @@ SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
def setup(hass, config):
"""Set up the ADS component."""
- import pyads
conf = config[DOMAIN]
@@ -161,7 +162,6 @@ class AdsHub:
def shutdown(self, *args, **kwargs):
"""Shutdown ADS connection."""
- import pyads
_LOGGER.debug("Shutting down ADS")
for notification_item in self._notification_items.values():
@@ -187,7 +187,6 @@ class AdsHub:
def write_by_name(self, name, value, plc_datatype):
"""Write a value to the device."""
- import pyads
with self._lock:
try:
@@ -197,7 +196,6 @@ class AdsHub:
def read_by_name(self, name, plc_datatype):
"""Read a value from the device."""
- import pyads
with self._lock:
try:
@@ -207,7 +205,6 @@ class AdsHub:
def add_device_notification(self, name, plc_datatype, callback):
"""Add a notification to the ADS devices."""
- import pyads
attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype))
diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py
index e54a48f7ee4..c41e5aec7b5 100644
--- a/homeassistant/components/aftership/sensor.py
+++ b/homeassistant/components/aftership/sensor.py
@@ -146,10 +146,10 @@ class AfterShipSensor(Entity):
async def async_added_to_hass(self):
"""Register callbacks."""
self.hass.helpers.dispatcher.async_dispatcher_connect(
- UPDATE_TOPIC, self.force_update
+ UPDATE_TOPIC, self._force_update
)
- async def force_update(self):
+ async def _force_update(self):
"""Force update of data."""
await self.async_update(no_throttle=True)
await self.async_update_ha_state()
diff --git a/homeassistant/components/.translations/airly.ca.json b/homeassistant/components/airly/.translations/ca.json
similarity index 100%
rename from homeassistant/components/.translations/airly.ca.json
rename to homeassistant/components/airly/.translations/ca.json
diff --git a/homeassistant/components/.translations/airly.da.json b/homeassistant/components/airly/.translations/da.json
similarity index 100%
rename from homeassistant/components/.translations/airly.da.json
rename to homeassistant/components/airly/.translations/da.json
diff --git a/homeassistant/components/airly/.translations/de.json b/homeassistant/components/airly/.translations/de.json
new file mode 100644
index 00000000000..83c23a90389
--- /dev/null
+++ b/homeassistant/components/airly/.translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "error": {
+ "auth": "Der API-Schl\u00fcssel ist nicht korrekt.",
+ "name_exists": "Name existiert bereits",
+ "wrong_location": "Keine Airly Luftmessstation an diesem Ort"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Airly API-Schl\u00fcssel",
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad",
+ "name": "Name der Integration"
+ },
+ "description": "Einrichtung der Airly-Luftqualit\u00e4t Integration. Um einen API-Schl\u00fcssel zu generieren, registriere dich auf https://developer.airly.eu/register",
+ "title": "Airly"
+ }
+ },
+ "title": "Airly"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/.translations/airly.en.json b/homeassistant/components/airly/.translations/en.json
similarity index 100%
rename from homeassistant/components/.translations/airly.en.json
rename to homeassistant/components/airly/.translations/en.json
diff --git a/homeassistant/components/.translations/airly.es.json b/homeassistant/components/airly/.translations/es.json
similarity index 100%
rename from homeassistant/components/.translations/airly.es.json
rename to homeassistant/components/airly/.translations/es.json
diff --git a/homeassistant/components/.translations/airly.fr.json b/homeassistant/components/airly/.translations/fr.json
similarity index 76%
rename from homeassistant/components/.translations/airly.fr.json
rename to homeassistant/components/airly/.translations/fr.json
index cf756a9f492..374e578eed2 100644
--- a/homeassistant/components/.translations/airly.fr.json
+++ b/homeassistant/components/airly/.translations/fr.json
@@ -13,6 +13,7 @@
"longitude": "Longitude",
"name": "Nom de l'int\u00e9gration"
},
+ "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air Airly. Pour g\u00e9n\u00e9rer une cl\u00e9 API, rendez-vous sur https://developer.airly.eu/register.",
"title": "Airly"
}
},
diff --git a/homeassistant/components/.translations/airly.it.json b/homeassistant/components/airly/.translations/it.json
similarity index 100%
rename from homeassistant/components/.translations/airly.it.json
rename to homeassistant/components/airly/.translations/it.json
diff --git a/homeassistant/components/airly/.translations/ko.json b/homeassistant/components/airly/.translations/ko.json
new file mode 100644
index 00000000000..eb20c9174b4
--- /dev/null
+++ b/homeassistant/components/airly/.translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "error": {
+ "auth": "API \ud0a4\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
+ "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4.",
+ "wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Airly API \ud0a4",
+ "latitude": "\uc704\ub3c4",
+ "longitude": "\uacbd\ub3c4",
+ "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984"
+ },
+ "description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694",
+ "title": "Airly"
+ }
+ },
+ "title": "Airly"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/.translations/airly.lb.json b/homeassistant/components/airly/.translations/lb.json
similarity index 100%
rename from homeassistant/components/.translations/airly.lb.json
rename to homeassistant/components/airly/.translations/lb.json
diff --git a/homeassistant/components/airly/.translations/nl.json b/homeassistant/components/airly/.translations/nl.json
new file mode 100644
index 00000000000..232d5d54d85
--- /dev/null
+++ b/homeassistant/components/airly/.translations/nl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "error": {
+ "auth": "API-sleutel is niet correct.",
+ "name_exists": "Naam bestaat al.",
+ "wrong_location": "Geen Airly meetstations in dit gebied."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Airly API-sleutel",
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "name": "Naam van de integratie"
+ },
+ "description": "Airly-integratie van luchtkwaliteit instellen. Ga naar https://developer.airly.eu/register om de API-sleutel te genereren",
+ "title": "Airly"
+ }
+ },
+ "title": "Airly"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/.translations/airly.nn.json b/homeassistant/components/airly/.translations/nn.json
similarity index 100%
rename from homeassistant/components/.translations/airly.nn.json
rename to homeassistant/components/airly/.translations/nn.json
diff --git a/homeassistant/components/.translations/airly.no.json b/homeassistant/components/airly/.translations/no.json
similarity index 100%
rename from homeassistant/components/.translations/airly.no.json
rename to homeassistant/components/airly/.translations/no.json
diff --git a/homeassistant/components/.translations/airly.pl.json b/homeassistant/components/airly/.translations/pl.json
similarity index 100%
rename from homeassistant/components/.translations/airly.pl.json
rename to homeassistant/components/airly/.translations/pl.json
diff --git a/homeassistant/components/airly/.translations/pt.json b/homeassistant/components/airly/.translations/pt.json
new file mode 100644
index 00000000000..d99bcb90733
--- /dev/null
+++ b/homeassistant/components/airly/.translations/pt.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ },
+ "title": ""
+ }
+ },
+ "title": ""
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/.translations/airly.ru.json b/homeassistant/components/airly/.translations/ru.json
similarity index 100%
rename from homeassistant/components/.translations/airly.ru.json
rename to homeassistant/components/airly/.translations/ru.json
diff --git a/homeassistant/components/.translations/airly.sl.json b/homeassistant/components/airly/.translations/sl.json
similarity index 100%
rename from homeassistant/components/.translations/airly.sl.json
rename to homeassistant/components/airly/.translations/sl.json
diff --git a/homeassistant/components/.translations/airly.zh-Hant.json b/homeassistant/components/airly/.translations/zh-Hant.json
similarity index 100%
rename from homeassistant/components/.translations/airly.zh-Hant.json
rename to homeassistant/components/airly/.translations/zh-Hant.json
diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py
new file mode 100644
index 00000000000..dc2323ddd4e
--- /dev/null
+++ b/homeassistant/components/airly/__init__.py
@@ -0,0 +1,114 @@
+"""The Airly component."""
+import asyncio
+import logging
+from datetime import timedelta
+
+import async_timeout
+from aiohttp.client_exceptions import ClientConnectorError
+from airly import Airly
+from airly.exceptions import AirlyError
+
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
+from homeassistant.core import Config, HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.util import Throttle
+
+from .const import (
+ ATTR_API_ADVICE,
+ ATTR_API_CAQI,
+ ATTR_API_CAQI_DESCRIPTION,
+ ATTR_API_CAQI_LEVEL,
+ DATA_CLIENT,
+ DOMAIN,
+ NO_AIRLY_SENSORS,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
+
+
+async def async_setup(hass: HomeAssistant, config: Config) -> bool:
+ """Set up configured Airly."""
+ return True
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up Airly as config entry."""
+ api_key = config_entry.data[CONF_API_KEY]
+ latitude = config_entry.data[CONF_LATITUDE]
+ longitude = config_entry.data[CONF_LONGITUDE]
+
+ websession = async_get_clientsession(hass)
+
+ airly = AirlyData(websession, api_key, latitude, longitude)
+
+ await airly.async_update()
+
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN][DATA_CLIENT] = {}
+ hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly
+
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, "air_quality")
+ )
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, "sensor")
+ )
+ return True
+
+
+async def async_unload_entry(hass, config_entry):
+ """Unload a config entry."""
+ hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
+ await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality")
+ await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
+ return True
+
+
+class AirlyData:
+ """Define an object to hold Airly data."""
+
+ def __init__(self, session, api_key, latitude, longitude):
+ """Initialize."""
+ self.latitude = latitude
+ self.longitude = longitude
+ self.airly = Airly(api_key, session)
+ self.data = {}
+
+ @Throttle(DEFAULT_SCAN_INTERVAL)
+ async def async_update(self):
+ """Update Airly data."""
+
+ try:
+ with async_timeout.timeout(10):
+ measurements = self.airly.create_measurements_session_point(
+ self.latitude, self.longitude
+ )
+ await measurements.update()
+
+ values = measurements.current["values"]
+ index = measurements.current["indexes"][0]
+ standards = measurements.current["standards"]
+
+ if index["description"] == NO_AIRLY_SENSORS:
+ _LOGGER.error("Can't retrieve data: no Airly sensors in this area")
+ return
+ for value in values:
+ self.data[value["name"]] = value["value"]
+ for standard in standards:
+ self.data[f"{standard['pollutant']}_LIMIT"] = standard["limit"]
+ self.data[f"{standard['pollutant']}_PERCENT"] = standard["percent"]
+ self.data[ATTR_API_CAQI] = index["value"]
+ self.data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ")
+ self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"]
+ self.data[ATTR_API_ADVICE] = index["advice"]
+ _LOGGER.debug("Data retrieved from Airly")
+ except (
+ ValueError,
+ AirlyError,
+ asyncio.TimeoutError,
+ ClientConnectorError,
+ ) as error:
+ _LOGGER.error(error)
+ self.data = {}
diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py
new file mode 100644
index 00000000000..082344c14e3
--- /dev/null
+++ b/homeassistant/components/airly/air_quality.py
@@ -0,0 +1,138 @@
+"""Support for the Airly air_quality service."""
+from homeassistant.components.air_quality import (
+ AirQualityEntity,
+ ATTR_AQI,
+ ATTR_PM_10,
+ ATTR_PM_2_5,
+)
+from homeassistant.const import CONF_NAME
+
+from .const import (
+ ATTR_API_ADVICE,
+ ATTR_API_CAQI,
+ ATTR_API_CAQI_DESCRIPTION,
+ ATTR_API_CAQI_LEVEL,
+ ATTR_API_PM10,
+ ATTR_API_PM10_LIMIT,
+ ATTR_API_PM10_PERCENT,
+ ATTR_API_PM25,
+ ATTR_API_PM25_LIMIT,
+ ATTR_API_PM25_PERCENT,
+ DATA_CLIENT,
+ DOMAIN,
+)
+
+ATTRIBUTION = "Data provided by Airly"
+
+LABEL_ADVICE = "advice"
+LABEL_AQI_LEVEL = f"{ATTR_AQI}_level"
+LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit"
+LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit"
+LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit"
+LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit"
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Airly air_quality entity based on a config entry."""
+ name = config_entry.data[CONF_NAME]
+
+ data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
+
+ async_add_entities([AirlyAirQuality(data, name)], True)
+
+
+def round_state(func):
+ """Round state."""
+
+ def _decorator(self):
+ res = func(self)
+ if isinstance(res, float):
+ return round(res)
+ return res
+
+ return _decorator
+
+
+class AirlyAirQuality(AirQualityEntity):
+ """Define an Airly air quality."""
+
+ def __init__(self, airly, name):
+ """Initialize."""
+ self.airly = airly
+ self.data = airly.data
+ self._name = name
+ self._pm_2_5 = None
+ self._pm_10 = None
+ self._aqi = None
+ self._icon = "mdi:blur"
+ self._attrs = {}
+
+ @property
+ def name(self):
+ """Return the name."""
+ return self._name
+
+ @property
+ def icon(self):
+ """Return the icon."""
+ return self._icon
+
+ @property
+ @round_state
+ def air_quality_index(self):
+ """Return the air quality index."""
+ return self._aqi
+
+ @property
+ @round_state
+ def particulate_matter_2_5(self):
+ """Return the particulate matter 2.5 level."""
+ return self._pm_2_5
+
+ @property
+ @round_state
+ def particulate_matter_10(self):
+ """Return the particulate matter 10 level."""
+ return self._pm_10
+
+ @property
+ def attribution(self):
+ """Return the attribution."""
+ return ATTRIBUTION
+
+ @property
+ def state(self):
+ """Return the CAQI description."""
+ return self.data[ATTR_API_CAQI_DESCRIPTION]
+
+ @property
+ def unique_id(self):
+ """Return a unique_id for this entity."""
+ return f"{self.airly.latitude}-{self.airly.longitude}"
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return bool(self.airly.data)
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ self._attrs[LABEL_ADVICE] = self.data[ATTR_API_ADVICE]
+ self._attrs[LABEL_AQI_LEVEL] = self.data[ATTR_API_CAQI_LEVEL]
+ self._attrs[LABEL_PM_2_5_LIMIT] = self.data[ATTR_API_PM25_LIMIT]
+ self._attrs[LABEL_PM_2_5_PERCENT] = round(self.data[ATTR_API_PM25_PERCENT])
+ self._attrs[LABEL_PM_10_LIMIT] = self.data[ATTR_API_PM10_LIMIT]
+ self._attrs[LABEL_PM_10_PERCENT] = round(self.data[ATTR_API_PM10_PERCENT])
+ return self._attrs
+
+ async def async_update(self):
+ """Update the entity."""
+ await self.airly.async_update()
+
+ if self.airly.data:
+ self.data = self.airly.data
+
+ self._pm_10 = self.data[ATTR_API_PM10]
+ self._pm_2_5 = self.data[ATTR_API_PM25]
+ self._aqi = self.data[ATTR_API_CAQI]
diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py
new file mode 100644
index 00000000000..b361930fa7d
--- /dev/null
+++ b/homeassistant/components/airly/config_flow.py
@@ -0,0 +1,114 @@
+"""Adds config flow for Airly."""
+import async_timeout
+import voluptuous as vol
+from airly import Airly
+from airly.exceptions import AirlyError
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant import config_entries
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.core import callback
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS
+
+
+@callback
+def configured_instances(hass):
+ """Return a set of configured Airly instances."""
+ return set(
+ entry.data[CONF_NAME] for entry in hass.config_entries.async_entries(DOMAIN)
+ )
+
+
+class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Config flow for Airly."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def __init__(self):
+ """Initialize."""
+ self._errors = {}
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ self._errors = {}
+
+ websession = async_get_clientsession(self.hass)
+
+ if user_input is not None:
+ if user_input[CONF_NAME] in configured_instances(self.hass):
+ self._errors[CONF_NAME] = "name_exists"
+ api_key_valid = await self._test_api_key(websession, user_input["api_key"])
+ if not api_key_valid:
+ self._errors["base"] = "auth"
+ else:
+ location_valid = await self._test_location(
+ websession,
+ user_input["api_key"],
+ user_input["latitude"],
+ user_input["longitude"],
+ )
+ if not location_valid:
+ self._errors["base"] = "wrong_location"
+
+ if not self._errors:
+ return self.async_create_entry(
+ title=user_input[CONF_NAME], data=user_input
+ )
+
+ return self._show_config_form(
+ name=DEFAULT_NAME,
+ api_key="",
+ latitude=self.hass.config.latitude,
+ longitude=self.hass.config.longitude,
+ )
+
+ def _show_config_form(self, name=None, api_key=None, latitude=None, longitude=None):
+ """Show the configuration form to edit data."""
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_API_KEY, default=api_key): str,
+ vol.Optional(
+ CONF_LATITUDE, default=self.hass.config.latitude
+ ): cv.latitude,
+ vol.Optional(
+ CONF_LONGITUDE, default=self.hass.config.longitude
+ ): cv.longitude,
+ vol.Optional(CONF_NAME, default=name): str,
+ }
+ ),
+ errors=self._errors,
+ )
+
+ async def _test_api_key(self, client, api_key):
+ """Return true if api_key is valid."""
+
+ with async_timeout.timeout(10):
+ airly = Airly(api_key, client)
+ measurements = airly.create_measurements_session_point(
+ latitude=52.24131, longitude=20.99101
+ )
+ try:
+ await measurements.update()
+ except AirlyError:
+ return False
+ return True
+
+ async def _test_location(self, client, api_key, latitude, longitude):
+ """Return true if location is valid."""
+
+ with async_timeout.timeout(10):
+ airly = Airly(api_key, client)
+ measurements = airly.create_measurements_session_point(
+ latitude=latitude, longitude=longitude
+ )
+
+ await measurements.update()
+ current = measurements.current
+ if current["indexes"][0]["description"] == NO_AIRLY_SENSORS:
+ return False
+ return True
diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py
new file mode 100644
index 00000000000..2040faea6b6
--- /dev/null
+++ b/homeassistant/components/airly/const.py
@@ -0,0 +1,19 @@
+"""Constants for Airly integration."""
+ATTR_API_ADVICE = "ADVICE"
+ATTR_API_CAQI = "CAQI"
+ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION"
+ATTR_API_CAQI_LEVEL = "LEVEL"
+ATTR_API_HUMIDITY = "HUMIDITY"
+ATTR_API_PM1 = "PM1"
+ATTR_API_PM10 = "PM10"
+ATTR_API_PM10_LIMIT = "PM10_LIMIT"
+ATTR_API_PM10_PERCENT = "PM10_PERCENT"
+ATTR_API_PM25 = "PM25"
+ATTR_API_PM25_LIMIT = "PM25_LIMIT"
+ATTR_API_PM25_PERCENT = "PM25_PERCENT"
+ATTR_API_PRESSURE = "PRESSURE"
+ATTR_API_TEMPERATURE = "TEMPERATURE"
+DATA_CLIENT = "client"
+DEFAULT_NAME = "Airly"
+DOMAIN = "airly"
+NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."
diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json
new file mode 100644
index 00000000000..1859f084bf1
--- /dev/null
+++ b/homeassistant/components/airly/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "airly",
+ "name": "Airly",
+ "documentation": "https://www.home-assistant.io/integrations/airly",
+ "dependencies": [],
+ "codeowners": ["@bieniu"],
+ "requirements": ["airly==0.0.2"],
+ "config_flow": true
+}
diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py
new file mode 100644
index 00000000000..bce32d64041
--- /dev/null
+++ b/homeassistant/components/airly/sensor.py
@@ -0,0 +1,150 @@
+"""Support for the Airly sensor service."""
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ ATTR_DEVICE_CLASS,
+ CONF_NAME,
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_PRESSURE,
+ DEVICE_CLASS_TEMPERATURE,
+ PRESSURE_HPA,
+ TEMP_CELSIUS,
+)
+from homeassistant.helpers.entity import Entity
+
+from .const import (
+ ATTR_API_HUMIDITY,
+ ATTR_API_PM1,
+ ATTR_API_PRESSURE,
+ ATTR_API_TEMPERATURE,
+ DATA_CLIENT,
+ DOMAIN,
+)
+
+ATTRIBUTION = "Data provided by Airly"
+
+ATTR_ICON = "icon"
+ATTR_LABEL = "label"
+ATTR_UNIT = "unit"
+
+HUMI_PERCENT = "%"
+VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m³"
+
+SENSOR_TYPES = {
+ ATTR_API_PM1: {
+ ATTR_DEVICE_CLASS: None,
+ ATTR_ICON: "mdi:blur",
+ ATTR_LABEL: ATTR_API_PM1,
+ ATTR_UNIT: VOLUME_MICROGRAMS_PER_CUBIC_METER,
+ },
+ ATTR_API_HUMIDITY: {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
+ ATTR_ICON: None,
+ ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(),
+ ATTR_UNIT: HUMI_PERCENT,
+ },
+ ATTR_API_PRESSURE: {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
+ ATTR_ICON: None,
+ ATTR_LABEL: ATTR_API_PRESSURE.capitalize(),
+ ATTR_UNIT: PRESSURE_HPA,
+ },
+ ATTR_API_TEMPERATURE: {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ ATTR_ICON: None,
+ ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(),
+ ATTR_UNIT: TEMP_CELSIUS,
+ },
+}
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Airly sensor entities based on a config entry."""
+ name = config_entry.data[CONF_NAME]
+
+ data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
+
+ sensors = []
+ for sensor in SENSOR_TYPES:
+ sensors.append(AirlySensor(data, name, sensor))
+ async_add_entities(sensors, True)
+
+
+def round_state(func):
+ """Round state."""
+
+ def _decorator(self):
+ res = func(self)
+ if isinstance(res, float):
+ return round(res)
+ return res
+
+ return _decorator
+
+
+class AirlySensor(Entity):
+ """Define an Airly sensor."""
+
+ def __init__(self, airly, name, kind):
+ """Initialize."""
+ self.airly = airly
+ self.data = airly.data
+ self._name = name
+ self.kind = kind
+ self._device_class = None
+ self._state = None
+ self._icon = None
+ self._unit_of_measurement = None
+ self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
+
+ @property
+ def name(self):
+ """Return the name."""
+ return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
+
+ @property
+ def state(self):
+ """Return the state."""
+ self._state = self.data[self.kind]
+ if self.kind in [ATTR_API_PM1, ATTR_API_PRESSURE]:
+ self._state = round(self._state)
+ if self.kind in [ATTR_API_TEMPERATURE, ATTR_API_HUMIDITY]:
+ self._state = round(self._state, 1)
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._attrs
+
+ @property
+ def icon(self):
+ """Return the icon."""
+ self._icon = SENSOR_TYPES[self.kind][ATTR_ICON]
+ return self._icon
+
+ @property
+ def device_class(self):
+ """Return the device_class."""
+ return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
+
+ @property
+ def unique_id(self):
+ """Return a unique_id for this entity."""
+ return f"{self.airly.latitude}-{self.airly.longitude}-{self.kind.lower()}"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return SENSOR_TYPES[self.kind][ATTR_UNIT]
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return bool(self.airly.data)
+
+ async def async_update(self):
+ """Update the sensor."""
+ await self.airly.async_update()
+
+ if self.airly.data:
+ self.data = self.airly.data
diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json
new file mode 100644
index 00000000000..116b6df83e6
--- /dev/null
+++ b/homeassistant/components/airly/strings.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "title": "Airly",
+ "step": {
+ "user": {
+ "title": "Airly",
+ "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register",
+ "data": {
+ "name": "Name of the integration",
+ "api_key": "Airly API key",
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ }
+ }
+ },
+ "error": {
+ "name_exists": "Name already exists.",
+ "wrong_location": "No Airly measuring stations in this area.",
+ "auth": "API key is not correct."
+ }
+ }
+}
diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py
index 20e5196c0f1..888d6ae6ec9 100644
--- a/homeassistant/components/airvisual/sensor.py
+++ b/homeassistant/components/airvisual/sensor.py
@@ -1,7 +1,9 @@
"""Support for AirVisual air quality sensors."""
-from logging import getLogger
from datetime import timedelta
+from logging import getLogger
+from pyairvisual import Client
+from pyairvisual.errors import AirVisualError
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -14,8 +16,8 @@ from homeassistant.const import (
CONF_LONGITUDE,
CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL,
- CONF_STATE,
CONF_SHOW_ON_MAP,
+ CONF_STATE,
)
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -97,7 +99,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Configure the platform and add the sensors."""
- from pyairvisual import Client
city = config.get(CONF_CITY)
state = config.get(CONF_STATE)
@@ -249,7 +250,6 @@ class AirVisualData:
async def _async_update(self):
"""Update AirVisual data."""
- from pyairvisual.errors import AirVisualError
try:
if self.city and self.state and self.country:
diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py
index b3da4fb4cbc..4cfcd5403dd 100644
--- a/homeassistant/components/aladdin_connect/cover.py
+++ b/homeassistant/components/aladdin_connect/cover.py
@@ -1,21 +1,22 @@
"""Platform for the Aladdin Connect cover component."""
import logging
+from aladdin_connect import AladdinConnectClient
import voluptuous as vol
from homeassistant.components.cover import (
- CoverDevice,
PLATFORM_SCHEMA,
- SUPPORT_OPEN,
SUPPORT_CLOSE,
+ SUPPORT_OPEN,
+ CoverDevice,
)
from homeassistant.const import (
- CONF_USERNAME,
CONF_PASSWORD,
+ CONF_USERNAME,
STATE_CLOSED,
- STATE_OPENING,
STATE_CLOSING,
STATE_OPEN,
+ STATE_OPENING,
)
import homeassistant.helpers.config_validation as cv
@@ -40,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Aladdin Connect platform."""
- from aladdin_connect import AladdinConnectClient
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
diff --git a/homeassistant/components/alarm_control_panel/.translations/ca.json b/homeassistant/components/alarm_control_panel/.translations/ca.json
new file mode 100644
index 00000000000..8d95d5f6485
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/ca.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Activa {entity_name} fora",
+ "arm_home": "Activa {entity_name} a casa",
+ "arm_night": "Activa {entity_name} nocturn",
+ "disarm": "Desactiva {entity_name}",
+ "trigger": "Dispara {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/da.json b/homeassistant/components/alarm_control_panel/.translations/da.json
new file mode 100644
index 00000000000..74e02e10de4
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/da.json
@@ -0,0 +1,7 @@
+{
+ "device_automation": {
+ "action_type": {
+ "trigger": "Udl\u00f8s {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/en.json b/homeassistant/components/alarm_control_panel/.translations/en.json
new file mode 100644
index 00000000000..b8eeb1d2e8c
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/en.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Arm {entity_name} away",
+ "arm_home": "Arm {entity_name} home",
+ "arm_night": "Arm {entity_name} night",
+ "disarm": "Disarm {entity_name}",
+ "trigger": "Trigger {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/es.json b/homeassistant/components/alarm_control_panel/.translations/es.json
new file mode 100644
index 00000000000..273efeeaba5
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/es.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Armar {entity_name} exterior",
+ "arm_home": "Armar {entity_name} modo casa",
+ "arm_night": "Armar {entity_name} por la noche",
+ "disarm": "Desarmar {entity_name}",
+ "trigger": "Lanzar {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/fr.json b/homeassistant/components/alarm_control_panel/.translations/fr.json
new file mode 100644
index 00000000000..c3ba6db0c62
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/fr.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Armer {entity_name} mode sortie",
+ "arm_home": "Armer {entity_name} mode \u00e0 la maison",
+ "arm_night": "Armer {entity_name} mode nuit",
+ "disarm": "D\u00e9sarmer {entity_name}",
+ "trigger": "D\u00e9clencheur {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/it.json b/homeassistant/components/alarm_control_panel/.translations/it.json
new file mode 100644
index 00000000000..e39967e9dac
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/it.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Armare {entity_name} uscito",
+ "arm_home": "Armare {entity_name} casa",
+ "arm_night": "Armare {entity_name} notte",
+ "disarm": "Disarmare {entity_name}",
+ "trigger": "Attivazione {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/ko.json b/homeassistant/components/alarm_control_panel/.translations/ko.json
new file mode 100644
index 00000000000..5d6caa5fe12
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/ko.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "{entity_name} \uc678\ucd9c\uacbd\ube44",
+ "arm_home": "{entity_name} \uc7ac\uc2e4\uacbd\ube44",
+ "arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44",
+ "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c",
+ "trigger": "{entity_name} \ud2b8\ub9ac\uac70"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/lb.json b/homeassistant/components/alarm_control_panel/.translations/lb.json
new file mode 100644
index 00000000000..ff265a52c38
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/lb.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "{entity_name} fir \u00ebnnerwee uschalten",
+ "arm_home": "{entity_name} fir doheem uschalten",
+ "arm_night": "{entity_name} fir Nuecht uschalten",
+ "disarm": "{entity_name} entsch\u00e4rfen",
+ "trigger": "{entity_name} ausl\u00e9isen"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json
new file mode 100644
index 00000000000..86cacad9fd6
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/nl.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "disarm": "Uitschakelen {entity_name}",
+ "trigger": "Trigger {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/no.json b/homeassistant/components/alarm_control_panel/.translations/no.json
new file mode 100644
index 00000000000..93833f33d41
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/no.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Aktiver {entity_name} borte",
+ "arm_home": "Aktiver {entity_name} hjemme",
+ "arm_night": "Aktiver {entity_name} natt",
+ "disarm": "Deaktiver {entity_name}",
+ "trigger": "Utl\u00f8ser {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/pl.json b/homeassistant/components/alarm_control_panel/.translations/pl.json
new file mode 100644
index 00000000000..a5dc326c267
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/pl.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "uzbr\u00f3j (poza domem) {entity_name}",
+ "arm_home": "uzbr\u00f3j (w domu) {entity_name}",
+ "arm_night": "uzbr\u00f3j (noc) {entity_name}",
+ "disarm": "rozbr\u00f3j {entity_name}",
+ "trigger": "wyzw\u00f3l {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json
new file mode 100644
index 00000000000..1f7c994330d
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json
@@ -0,0 +1,7 @@
+{
+ "device_automation": {
+ "action_type": {
+ "trigger": "Disparar {entidade_nome}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/pt.json b/homeassistant/components/alarm_control_panel/.translations/pt.json
new file mode 100644
index 00000000000..90b9b1d43d5
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/pt.json
@@ -0,0 +1,9 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_home": "Armar casa {entity_name}",
+ "arm_night": "Armar noite {entity_name}",
+ "disarm": "Desarmar {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json
new file mode 100644
index 00000000000..acea0ae7551
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/ru.json
@@ -0,0 +1,7 @@
+{
+ "device_automation": {
+ "action_type": {
+ "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/sl.json b/homeassistant/components/alarm_control_panel/.translations/sl.json
new file mode 100644
index 00000000000..9bf01fc62de
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/sl.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Vklju\u010di {entity_name} zdoma",
+ "arm_home": "Vklju\u010di {entity_name} doma",
+ "arm_night": "Vklju\u010di {entity_name} no\u010d",
+ "disarm": "Razoro\u017ei {entity_name}",
+ "trigger": "Spro\u017ei {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json
new file mode 100644
index 00000000000..c52288802d1
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "\u8a2d\u5b9a {entity_name} \u5916\u51fa\u6a21\u5f0f",
+ "arm_home": "\u8a2d\u5b9a {entity_name} \u8fd4\u5bb6\u6a21\u5f0f",
+ "arm_night": "\u8a2d\u5b9a {entity_name} \u591c\u9593\u6a21\u5f0f",
+ "disarm": "\u89e3\u9664 {entity_name}",
+ "trigger": "\u89f8\u767c {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py
new file mode 100644
index 00000000000..a3c2b482261
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/device_action.py
@@ -0,0 +1,126 @@
+"""Provides device automations for Alarm control panel."""
+from typing import Optional, List
+import voluptuous as vol
+
+from homeassistant.const import (
+ ATTR_CODE,
+ ATTR_ENTITY_ID,
+ CONF_CODE,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_TYPE,
+ SERVICE_ALARM_ARM_AWAY,
+ SERVICE_ALARM_ARM_HOME,
+ SERVICE_ALARM_ARM_NIGHT,
+ SERVICE_ALARM_DISARM,
+ SERVICE_ALARM_TRIGGER,
+)
+from homeassistant.core import HomeAssistant, Context
+from homeassistant.helpers import entity_registry
+import homeassistant.helpers.config_validation as cv
+from . import ATTR_CODE_ARM_REQUIRED, DOMAIN
+
+ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"}
+
+ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_TYPE): vol.In(ACTION_TYPES),
+ vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
+ vol.Optional(CONF_CODE): cv.string,
+ }
+)
+
+
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+ """List device actions for Alarm control panel devices."""
+ registry = await entity_registry.async_get_registry(hass)
+ actions = []
+
+ # Get all the integrations entities for this device
+ for entry in entity_registry.async_entries_for_device(registry, device_id):
+ if entry.domain != DOMAIN:
+ continue
+
+ # Add actions for each entity that belongs to this integration
+ actions.append(
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "arm_away",
+ }
+ )
+ actions.append(
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "arm_home",
+ }
+ )
+ actions.append(
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "arm_night",
+ }
+ )
+ actions.append(
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "disarm",
+ }
+ )
+ actions.append(
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "trigger",
+ }
+ )
+
+ return actions
+
+
+async def async_call_action_from_config(
+ hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+) -> None:
+ """Execute a device action."""
+ config = ACTION_SCHEMA(config)
+
+ service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
+ if CONF_CODE in config:
+ service_data[ATTR_CODE] = config[CONF_CODE]
+
+ if config[CONF_TYPE] == "arm_away":
+ service = SERVICE_ALARM_ARM_AWAY
+ elif config[CONF_TYPE] == "arm_home":
+ service = SERVICE_ALARM_ARM_HOME
+ elif config[CONF_TYPE] == "arm_night":
+ service = SERVICE_ALARM_ARM_NIGHT
+ elif config[CONF_TYPE] == "disarm":
+ service = SERVICE_ALARM_DISARM
+ elif config[CONF_TYPE] == "trigger":
+ service = SERVICE_ALARM_TRIGGER
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, blocking=True, context=context
+ )
+
+
+async def async_get_action_capabilities(hass, config):
+ """List action capabilities."""
+ state = hass.states.get(config[CONF_ENTITY_ID])
+ code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False
+
+ if config[CONF_TYPE] == "trigger" or (
+ config[CONF_TYPE] != "disarm" and not code_required
+ ):
+ return {}
+
+ return {"extra_fields": vol.Schema({vol.Optional(CONF_CODE): str})}
diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json
new file mode 100644
index 00000000000..f67635776dd
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/strings.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Arm {entity_name} away",
+ "arm_home": "Arm {entity_name} home",
+ "arm_night": "Arm {entity_name} night",
+ "disarm": "Disarm {entity_name}",
+ "trigger": "Trigger {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py
index e0ff80ae9fa..61cb0effe53 100644
--- a/homeassistant/components/alarmdecoder/__init__.py
+++ b/homeassistant/components/alarmdecoder/__init__.py
@@ -174,7 +174,7 @@ def setup(hass, config):
hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone)
def handle_rel_message(sender, message):
- """Handle relay message from AlarmDecoder."""
+ """Handle relay or zone expander message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message)
controller = False
@@ -195,7 +195,7 @@ def setup(hass, config):
controller.on_zone_fault += zone_fault_callback
controller.on_zone_restore += zone_restore_callback
controller.on_close += handle_closed_connection
- controller.on_relay_changed += handle_rel_message
+ controller.on_expander_message += handle_rel_message
hass.data[DATA_AD] = controller
diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py
index bbcc4fd6eae..dc3f16b7d22 100644
--- a/homeassistant/components/alarmdecoder/binary_sensor.py
+++ b/homeassistant/components/alarmdecoder/binary_sensor.py
@@ -151,10 +151,15 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
self.schedule_update_ha_state()
def _rel_message_callback(self, message):
- """Update relay state."""
+ """Update relay / expander state."""
+
if self._relay_addr == message.address and self._relay_chan == message.channel:
_LOGGER.debug(
- "Relay %d:%d value:%d", message.address, message.channel, message.value
+ "%s %d:%d value:%d",
+ "Relay" if message.type == message.RELAY else "ZoneExpander",
+ message.address,
+ message.channel,
+ message.value,
)
self._state = message.value
self.schedule_update_ha_state()
diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py
index f80e8d6eb1e..07d69960e0b 100644
--- a/homeassistant/components/alarmdotcom/alarm_control_panel.py
+++ b/homeassistant/components/alarmdotcom/alarm_control_panel.py
@@ -2,6 +2,7 @@
import logging
import re
+from pyalarmdotcom import Alarmdotcom
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
@@ -49,7 +50,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
def __init__(self, hass, name, code, username, password):
"""Initialize the Alarm.com status."""
- from pyalarmdotcom import Alarmdotcom
_LOGGER.debug("Setting up Alarm.com...")
self._hass = hass
diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py
index b8bd3841a78..deb83813dbc 100644
--- a/homeassistant/components/alexa/capabilities.py
+++ b/homeassistant/components/alexa/capabilities.py
@@ -5,6 +5,10 @@ from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
STATE_LOCKED,
STATE_OFF,
STATE_ON,
@@ -13,24 +17,26 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
import homeassistant.components.climate.const as climate
+from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER
from homeassistant.components import light, fan, cover
import homeassistant.util.color as color_util
import homeassistant.util.dt as dt_util
from .const import (
+ Catalog,
API_TEMP_UNITS,
API_THERMOSTAT_MODES,
API_THERMOSTAT_PRESETS,
DATE_FORMAT,
PERCENTAGE_FAN_MAP,
+ RANGE_FAN_MAP,
)
from .errors import UnsupportedProperty
-
_LOGGER = logging.getLogger(__name__)
-class AlexaCapibility:
+class AlexaCapability:
"""Base class for Alexa capability interfaces.
The Smart Home Skills API defines a number of "capability interfaces",
@@ -40,9 +46,10 @@ class AlexaCapibility:
https://developer.amazon.com/docs/device-apis/message-guide.html
"""
- def __init__(self, entity):
- """Initialize an Alexa capibility."""
+ def __init__(self, entity, instance=None):
+ """Initialize an Alexa capability."""
self.entity = entity
+ self.instance = instance
def name(self):
"""Return the Alexa API name of this interface."""
@@ -63,6 +70,11 @@ class AlexaCapibility:
"""Return True if properties can be retrieved."""
return False
+ @staticmethod
+ def properties_non_controllable():
+ """Return True if non controllable."""
+ return None
+
@staticmethod
def get_property(name):
"""Read and return a property.
@@ -79,23 +91,65 @@ class AlexaCapibility:
"""Applicable only to scenes."""
return None
+ @staticmethod
+ def capability_proactively_reported():
+ """Return True if the capability is proactively reported.
+
+ Set properties_proactively_reported() for proactively reported properties.
+ Applicable to DoorbellEventSource.
+ """
+ return None
+
+ @staticmethod
+ def capability_resources():
+ """Applicable to ToggleController, RangeController, and ModeController interfaces."""
+ return []
+
+ @staticmethod
+ def configuration():
+ """Return the Configuration object."""
+ return []
+
def serialize_discovery(self):
"""Serialize according to the Discovery API."""
- result = {
- "type": "AlexaInterface",
- "interface": self.name(),
- "version": "3",
- "properties": {
+ result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"}
+
+ properties_supported = self.properties_supported()
+ if properties_supported:
+ result["properties"] = {
"supported": self.properties_supported(),
"proactivelyReported": self.properties_proactively_reported(),
"retrievable": self.properties_retrievable(),
- },
- }
+ }
+
+ # pylint: disable=assignment-from-none
+ proactively_reported = self.capability_proactively_reported()
+ if proactively_reported is not None:
+ result["proactivelyReported"] = proactively_reported
+
+ # pylint: disable=assignment-from-none
+ non_controllable = self.properties_non_controllable()
+ if non_controllable is not None:
+ result["properties"]["nonControllable"] = non_controllable
# pylint: disable=assignment-from-none
supports_deactivation = self.supports_deactivation()
if supports_deactivation is not None:
result["supportsDeactivation"] = supports_deactivation
+
+ capability_resources = self.serialize_capability_resources()
+ if capability_resources:
+ result["capabilityResources"] = capability_resources
+
+ configuration = self.configuration()
+ if configuration:
+ result["configuration"] = configuration
+
+ # pylint: disable=assignment-from-none
+ instance = self.instance
+ if instance is not None:
+ result["instance"] = instance
+
return result
def serialize_properties(self):
@@ -105,16 +159,51 @@ class AlexaCapibility:
# pylint: disable=assignment-from-no-return
prop_value = self.get_property(prop_name)
if prop_value is not None:
- yield {
+ result = {
"name": prop_name,
"namespace": self.name(),
"value": prop_value,
"timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT),
"uncertaintyInMilliseconds": 0,
}
+ instance = self.instance
+ if instance is not None:
+ result["instance"] = instance
+
+ yield result
+
+ def serialize_capability_resources(self):
+ """Return capabilityResources friendlyNames serialized for an API response."""
+ resources = self.capability_resources()
+ if resources:
+ return {"friendlyNames": self.serialize_friendly_names(resources)}
+
+ return None
+
+ @staticmethod
+ def serialize_friendly_names(resources):
+ """Return capabilityResources, ModeResources, or presetResources friendlyNames serialized for an API response."""
+ friendly_names = []
+ for resource in resources:
+ if resource["type"] == Catalog.LABEL_ASSET:
+ friendly_names.append(
+ {
+ "@type": Catalog.LABEL_ASSET,
+ "value": {"assetId": resource["value"]},
+ }
+ )
+ else:
+ friendly_names.append(
+ {
+ "@type": Catalog.LABEL_TEXT,
+ "value": {"text": resource["value"], "locale": "en-US"},
+ }
+ )
+
+ return friendly_names
-class AlexaEndpointHealth(AlexaCapibility):
+class AlexaEndpointHealth(AlexaCapability):
"""Implements Alexa.EndpointHealth.
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it
@@ -151,7 +240,7 @@ class AlexaEndpointHealth(AlexaCapibility):
return {"value": "OK"}
-class AlexaPowerController(AlexaCapibility):
+class AlexaPowerController(AlexaCapability):
"""Implements Alexa.PowerController.
https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html
@@ -187,7 +276,7 @@ class AlexaPowerController(AlexaCapibility):
return "ON" if is_on else "OFF"
-class AlexaLockController(AlexaCapibility):
+class AlexaLockController(AlexaCapability):
"""Implements Alexa.LockController.
https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html
@@ -221,7 +310,7 @@ class AlexaLockController(AlexaCapibility):
return "JAMMED"
-class AlexaSceneController(AlexaCapibility):
+class AlexaSceneController(AlexaCapability):
"""Implements Alexa.SceneController.
https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html
@@ -237,7 +326,7 @@ class AlexaSceneController(AlexaCapibility):
return "Alexa.SceneController"
-class AlexaBrightnessController(AlexaCapibility):
+class AlexaBrightnessController(AlexaCapability):
"""Implements Alexa.BrightnessController.
https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html
@@ -268,7 +357,7 @@ class AlexaBrightnessController(AlexaCapibility):
return 0
-class AlexaColorController(AlexaCapibility):
+class AlexaColorController(AlexaCapability):
"""Implements Alexa.ColorController.
https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html
@@ -300,7 +389,7 @@ class AlexaColorController(AlexaCapibility):
}
-class AlexaColorTemperatureController(AlexaCapibility):
+class AlexaColorTemperatureController(AlexaCapability):
"""Implements Alexa.ColorTemperatureController.
https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html
@@ -329,7 +418,7 @@ class AlexaColorTemperatureController(AlexaCapibility):
return None
-class AlexaPercentageController(AlexaCapibility):
+class AlexaPercentageController(AlexaCapability):
"""Implements Alexa.PercentageController.
https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html
@@ -363,7 +452,7 @@ class AlexaPercentageController(AlexaCapibility):
return 0
-class AlexaSpeaker(AlexaCapibility):
+class AlexaSpeaker(AlexaCapability):
"""Implements Alexa.Speaker.
https://developer.amazon.com/docs/device-apis/alexa-speaker.html
@@ -374,7 +463,7 @@ class AlexaSpeaker(AlexaCapibility):
return "Alexa.Speaker"
-class AlexaStepSpeaker(AlexaCapibility):
+class AlexaStepSpeaker(AlexaCapability):
"""Implements Alexa.StepSpeaker.
https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html
@@ -385,7 +474,7 @@ class AlexaStepSpeaker(AlexaCapibility):
return "Alexa.StepSpeaker"
-class AlexaPlaybackController(AlexaCapibility):
+class AlexaPlaybackController(AlexaCapability):
"""Implements Alexa.PlaybackController.
https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html
@@ -396,7 +485,7 @@ class AlexaPlaybackController(AlexaCapibility):
return "Alexa.PlaybackController"
-class AlexaInputController(AlexaCapibility):
+class AlexaInputController(AlexaCapability):
"""Implements Alexa.InputController.
https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html
@@ -407,7 +496,7 @@ class AlexaInputController(AlexaCapibility):
return "Alexa.InputController"
-class AlexaTemperatureSensor(AlexaCapibility):
+class AlexaTemperatureSensor(AlexaCapability):
"""Implements Alexa.TemperatureSensor.
https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html
@@ -457,7 +546,7 @@ class AlexaTemperatureSensor(AlexaCapibility):
return {"value": temp, "scale": API_TEMP_UNITS[unit]}
-class AlexaContactSensor(AlexaCapibility):
+class AlexaContactSensor(AlexaCapability):
"""Implements Alexa.ContactSensor.
The Alexa.ContactSensor interface describes the properties and events used
@@ -499,7 +588,7 @@ class AlexaContactSensor(AlexaCapibility):
return "NOT_DETECTED"
-class AlexaMotionSensor(AlexaCapibility):
+class AlexaMotionSensor(AlexaCapability):
"""Implements Alexa.MotionSensor.
https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html
@@ -536,7 +625,7 @@ class AlexaMotionSensor(AlexaCapibility):
return "NOT_DETECTED"
-class AlexaThermostatController(AlexaCapibility):
+class AlexaThermostatController(AlexaCapability):
"""Implements Alexa.ThermostatController.
https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html
@@ -614,3 +703,378 @@ class AlexaThermostatController(AlexaCapibility):
return None
return {"value": temp, "scale": API_TEMP_UNITS[unit]}
+
+
+class AlexaPowerLevelController(AlexaCapability):
+ """Implements Alexa.PowerLevelController.
+
+ https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html
+ """
+
+ def name(self):
+ """Return the Alexa API name of this interface."""
+ return "Alexa.PowerLevelController"
+
+ def properties_supported(self):
+ """Return what properties this entity supports."""
+ return [{"name": "powerLevel"}]
+
+ def properties_proactively_reported(self):
+ """Return True if properties asynchronously reported."""
+ return True
+
+ def properties_retrievable(self):
+ """Return True if properties can be retrieved."""
+ return True
+
+ def get_property(self, name):
+ """Read and return a property."""
+ if name != "powerLevel":
+ raise UnsupportedProperty(name)
+
+ if self.entity.domain == fan.DOMAIN:
+ speed = self.entity.attributes.get(fan.ATTR_SPEED)
+
+ return PERCENTAGE_FAN_MAP.get(speed, None)
+
+ return None
+
+
+class AlexaSecurityPanelController(AlexaCapability):
+ """Implements Alexa.SecurityPanelController.
+
+ https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html
+ """
+
+ def __init__(self, hass, entity):
+ """Initialize the entity."""
+ super().__init__(entity)
+ self.hass = hass
+
+ def name(self):
+ """Return the Alexa API name of this interface."""
+ return "Alexa.SecurityPanelController"
+
+ def properties_supported(self):
+ """Return what properties this entity supports."""
+ return [{"name": "armState"}]
+
+ def properties_proactively_reported(self):
+ """Return True if properties asynchronously reported."""
+ return True
+
+ def properties_retrievable(self):
+ """Return True if properties can be retrieved."""
+ return True
+
+ def get_property(self, name):
+ """Read and return a property."""
+ if name != "armState":
+ raise UnsupportedProperty(name)
+
+ arm_state = self.entity.state
+ if arm_state == STATE_ALARM_ARMED_HOME:
+ return "ARMED_STAY"
+ if arm_state == STATE_ALARM_ARMED_AWAY:
+ return "ARMED_AWAY"
+ if arm_state == STATE_ALARM_ARMED_NIGHT:
+ return "ARMED_NIGHT"
+ if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
+ return "ARMED_STAY"
+ return "DISARMED"
+
+ def configuration(self):
+ """Return configuration object with supported authorization types."""
+ code_format = self.entity.attributes.get(ATTR_CODE_FORMAT)
+
+ if code_format == FORMAT_NUMBER:
+ return {"supportedAuthorizationTypes": [{"type": "FOUR_DIGIT_PIN"}]}
+ return None
+
+
+class AlexaModeController(AlexaCapability):
+ """Implements Alexa.ModeController.
+
+ https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html
+ """
+
+ def __init__(self, entity, instance, non_controllable=False):
+ """Initialize the entity."""
+ super().__init__(entity, instance)
+ self.properties_non_controllable = lambda: non_controllable
+
+ def name(self):
+ """Return the Alexa API name of this interface."""
+ return "Alexa.ModeController"
+
+ def properties_supported(self):
+ """Return what properties this entity supports."""
+ return [{"name": "mode"}]
+
+ def properties_proactively_reported(self):
+ """Return True if properties asynchronously reported."""
+ return True
+
+ def properties_retrievable(self):
+ """Return True if properties can be retrieved."""
+
+ def get_property(self, name):
+ """Read and return a property."""
+ if name != "mode":
+ raise UnsupportedProperty(name)
+
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
+ return self.entity.attributes.get(fan.ATTR_DIRECTION)
+
+ return None
+
+ def configuration(self):
+ """Return configuration with modeResources."""
+ return self.serialize_mode_resources()
+
+ def capability_resources(self):
+ """Return capabilityResources object."""
+ capability_resources = []
+
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
+ capability_resources = [
+ {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION}
+ ]
+
+ return capability_resources
+
+ def mode_resources(self):
+ """Return modeResources object."""
+ mode_resources = None
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
+ mode_resources = {
+ "ordered": False,
+ "resources": [
+ {
+ "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}",
+ "friendly_names": [
+ {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_FORWARD}
+ ],
+ },
+ {
+ "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}",
+ "friendly_names": [
+ {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_REVERSE}
+ ],
+ },
+ ],
+ }
+
+ return mode_resources
+
+ def serialize_mode_resources(self):
+ """Return ModeResources, friendlyNames serialized for an API response."""
+ mode_resources = []
+ resources = self.mode_resources()
+ ordered = resources["ordered"]
+ for resource in resources["resources"]:
+ mode_value = resource["value"]
+ friendly_names = resource["friendly_names"]
+ result = {
+ "value": mode_value,
+ "modeResources": {
+ "friendlyNames": self.serialize_friendly_names(friendly_names)
+ },
+ }
+ mode_resources.append(result)
+
+ return {"ordered": ordered, "supportedModes": mode_resources}
+
+
+class AlexaRangeController(AlexaCapability):
+ """Implements Alexa.RangeController.
+
+ https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html
+ """
+
+ def __init__(self, entity, instance, non_controllable=False):
+ """Initialize the entity."""
+ super().__init__(entity, instance)
+ self.properties_non_controllable = lambda: non_controllable
+
+ def name(self):
+ """Return the Alexa API name of this interface."""
+ return "Alexa.RangeController"
+
+ def properties_supported(self):
+ """Return what properties this entity supports."""
+ return [{"name": "rangeValue"}]
+
+ def properties_proactively_reported(self):
+ """Return True if properties asynchronously reported."""
+ return True
+
+ def properties_retrievable(self):
+ """Return True if properties can be retrieved."""
+ return True
+
+ def get_property(self, name):
+ """Read and return a property."""
+ if name != "rangeValue":
+ raise UnsupportedProperty(name)
+
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
+ speed = self.entity.attributes.get(fan.ATTR_SPEED)
+ return RANGE_FAN_MAP.get(speed, 0)
+
+ return None
+
+ def configuration(self):
+ """Return configuration with presetResources."""
+ return self.serialize_preset_resources()
+
+ def capability_resources(self):
+ """Return capabilityResources object."""
+ capability_resources = []
+
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
+ return [{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_FANSPEED}]
+
+ return capability_resources
+
+ def preset_resources(self):
+ """Return presetResources object."""
+ preset_resources = []
+
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
+ preset_resources = {
+ "minimumValue": 1,
+ "maximumValue": 3,
+ "precision": 1,
+ "presets": [
+ {
+ "rangeValue": 1,
+ "names": [
+ {
+ "type": Catalog.LABEL_ASSET,
+ "value": Catalog.VALUE_MINIMUM,
+ },
+ {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_LOW},
+ ],
+ },
+ {
+ "rangeValue": 2,
+ "names": [
+ {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_MEDIUM}
+ ],
+ },
+ {
+ "rangeValue": 3,
+ "names": [
+ {
+ "type": Catalog.LABEL_ASSET,
+ "value": Catalog.VALUE_MAXIMUM,
+ },
+ {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_HIGH},
+ ],
+ },
+ ],
+ }
+
+ return preset_resources
+
+ def serialize_preset_resources(self):
+ """Return PresetResources, friendlyNames serialized for an API response."""
+ preset_resources = []
+ resources = self.preset_resources()
+ for preset in resources["presets"]:
+ preset_resources.append(
+ {
+ "rangeValue": preset["rangeValue"],
+ "presetResources": {
+ "friendlyNames": self.serialize_friendly_names(preset["names"])
+ },
+ }
+ )
+
+ return {
+ "supportedRange": {
+ "minimumValue": resources["minimumValue"],
+ "maximumValue": resources["maximumValue"],
+ "precision": resources["precision"],
+ },
+ "presets": preset_resources,
+ }
+
+
+class AlexaToggleController(AlexaCapability):
+ """Implements Alexa.ToggleController.
+
+ https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html
+ """
+
+ def __init__(self, entity, instance, non_controllable=False):
+ """Initialize the entity."""
+ super().__init__(entity, instance)
+ self.properties_non_controllable = lambda: non_controllable
+
+ def name(self):
+ """Return the Alexa API name of this interface."""
+ return "Alexa.ToggleController"
+
+ def properties_supported(self):
+ """Return what properties this entity supports."""
+ return [{"name": "toggleState"}]
+
+ def properties_proactively_reported(self):
+ """Return True if properties asynchronously reported."""
+ return True
+
+ def properties_retrievable(self):
+ """Return True if properties can be retrieved."""
+ return True
+
+ def get_property(self, name):
+ """Read and return a property."""
+ if name != "toggleState":
+ raise UnsupportedProperty(name)
+
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
+ is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING))
+ return "ON" if is_on else "OFF"
+
+ return None
+
+ def capability_resources(self):
+ """Return capabilityResources object."""
+ capability_resources = []
+
+ if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
+ capability_resources = [
+ {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_OSCILLATE},
+ {"type": Catalog.LABEL_TEXT, "value": "Rotate"},
+ {"type": Catalog.LABEL_TEXT, "value": "Rotation"},
+ ]
+
+ return capability_resources
+
+
+class AlexaChannelController(AlexaCapability):
+ """Implements Alexa.ChannelController.
+
+ https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html
+ """
+
+ def name(self):
+ """Return the Alexa API name of this interface."""
+ return "Alexa.ChannelController"
+
+
+class AlexaDoorbellEventSource(AlexaCapability):
+ """Implements Alexa.DoorbellEventSource.
+
+ https://developer.amazon.com/docs/device-apis/alexa-doorbelleventsource.html
+ """
+
+ def name(self):
+ """Return the Alexa API name of this interface."""
+ return "Alexa.DoorbellEventSource"
+
+ def capability_proactively_reported(self):
+ """Return True for proactively reported capability."""
+ return True
diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py
index 83c7da41c16..8d1f0ac95a5 100644
--- a/homeassistant/components/alexa/const.py
+++ b/homeassistant/components/alexa/const.py
@@ -5,7 +5,6 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.components.climate import const as climate
from homeassistant.components import fan
-
DOMAIN = "alexa"
# Flash briefing constants
@@ -62,7 +61,26 @@ API_THERMOSTAT_MODES = OrderedDict(
)
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
-PERCENTAGE_FAN_MAP = {fan.SPEED_LOW: 33, fan.SPEED_MEDIUM: 66, fan.SPEED_HIGH: 100}
+PERCENTAGE_FAN_MAP = {
+ fan.SPEED_OFF: 0,
+ fan.SPEED_LOW: 33,
+ fan.SPEED_MEDIUM: 66,
+ fan.SPEED_HIGH: 100,
+}
+
+RANGE_FAN_MAP = {
+ fan.SPEED_OFF: 0,
+ fan.SPEED_LOW: 1,
+ fan.SPEED_MEDIUM: 2,
+ fan.SPEED_HIGH: 3,
+}
+
+SPEED_FAN_MAP = {
+ 0: fan.SPEED_OFF,
+ 1: fan.SPEED_LOW,
+ 2: fan.SPEED_MEDIUM,
+ 3: fan.SPEED_HIGH,
+}
class Cause:
@@ -96,3 +114,160 @@ class Cause:
# Indicates that the event was caused by a voice interaction with Alexa.
# For example a user speaking to their Echo device.
VOICE_INTERACTION = "VOICE_INTERACTION"
+
+
+class Catalog:
+ """The Global Alexa catalog.
+
+ https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog
+
+ You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units.
+ This catalog is localized into all the languages that Alexa supports.
+
+ You can reference the following catalog of pre-defined friendly names.
+ Each item in the following list is an asset identifier followed by its supported friendly names.
+ The first friendly name for each identifier is the one displayed in the Alexa mobile app.
+ """
+
+ LABEL_ASSET = "asset"
+ LABEL_TEXT = "text"
+
+ # Shower
+ DEVICENAME_SHOWER = "Alexa.DeviceName.Shower"
+
+ # Washer, Washing Machine
+ DEVICENAME_WASHER = "Alexa.DeviceName.Washer"
+
+ # Router, Internet Router, Network Router, Wifi Router, Net Router
+ DEVICENAME_ROUTER = "Alexa.DeviceName.Router"
+
+ # Fan, Blower
+ DEVICENAME_FAN = "Alexa.DeviceName.Fan"
+
+ # Air Purifier, Air Cleaner,Clean Air Machine
+ DEVICENAME_AIRPURIFIER = "Alexa.DeviceName.AirPurifier"
+
+ # Space Heater, Portable Heater
+ DEVICENAME_SPACEHEATER = "Alexa.DeviceName.SpaceHeater"
+
+ # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet
+ SHOWER_RAINHEAD = "Alexa.Shower.RainHead"
+
+ # Handheld Shower, Shower Wand, Hand Shower
+ SHOWER_HANDHELD = "Alexa.Shower.HandHeld"
+
+ # Water Temperature, Water Temp, Water Heat
+ SETTING_WATERTEMPERATURE = "Alexa.Setting.WaterTemperature"
+
+ # Temperature, Temp
+ SETTING_TEMPERATURE = "Alexa.Setting.Temperature"
+
+ # Wash Cycle, Wash Preset, Wash setting
+ SETTING_WASHCYCLE = "Alexa.Setting.WashCycle"
+
+ # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi
+ SETTING_2GGUESTWIFI = "Alexa.Setting.2GGuestWiFi"
+
+ # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi
+ SETTING_5GGUESTWIFI = "Alexa.Setting.5GGuestWiFi"
+
+ # Guest Wi-fi, Guest Network, Guest Net
+ SETTING_GUESTWIFI = "Alexa.Setting.GuestWiFi"
+
+ # Auto, Automatic, Automatic Mode, Auto Mode
+ SETTING_AUTO = "Alexa.Setting.Auto"
+
+ # #Night, Night Mode
+ SETTING_NIGHT = "Alexa.Setting.Night"
+
+ # Quiet, Quiet Mode, Noiseless, Silent
+ SETTING_QUIET = "Alexa.Setting.Quiet"
+
+ # Oscillate, Swivel, Oscillation, Spin, Back and forth
+ SETTING_OSCILLATE = "Alexa.Setting.Oscillate"
+
+ # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity
+ SETTING_FANSPEED = "Alexa.Setting.FanSpeed"
+
+ # Preset, Setting
+ SETTING_PRESET = "Alexa.Setting.Preset"
+
+ # Mode
+ SETTING_MODE = "Alexa.Setting.Mode"
+
+ # Direction
+ SETTING_DIRECTION = "Alexa.Setting.Direction"
+
+ # Delicates, Delicate
+ VALUE_DELICATE = "Alexa.Value.Delicate"
+
+ # Quick Wash, Fast Wash, Wash Quickly, Speed Wash
+ VALUE_QUICKWASH = "Alexa.Value.QuickWash"
+
+ # Maximum, Max
+ VALUE_MAXIMUM = "Alexa.Value.Maximum"
+
+ # Minimum, Min
+ VALUE_MINIMUM = "Alexa.Value.Minimum"
+
+ # High
+ VALUE_HIGH = "Alexa.Value.High"
+
+ # Low
+ VALUE_LOW = "Alexa.Value.Low"
+
+ # Medium, Mid
+ VALUE_MEDIUM = "Alexa.Value.Medium"
+
+
+class Unit:
+ """Alexa Units of Measure.
+
+ https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#units-of-measure
+ """
+
+ ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees"
+
+ ANGLE_RADIANS = "Alexa.Unit.Angle.Radians"
+
+ DISTANCE_FEET = "Alexa.Unit.Distance.Feet"
+
+ DISTANCE_INCHES = "Alexa.Unit.Distance.Inches"
+
+ DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers"
+
+ DISTANCE_METERS = "Alexa.Unit.Distance.Meters"
+
+ DISTANCE_MILES = "Alexa.Unit.Distance.Miles"
+
+ DISTANCE_YARDS = "Alexa.Unit.Distance.Yards"
+
+ MASS_GRAMS = "Alexa.Unit.Mass.Grams"
+
+ MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms"
+
+ PERCENT = "Alexa.Unit.Percent"
+
+ TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius"
+
+ TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees"
+
+ TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit"
+
+ TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin"
+
+ VOLUME_CUBICFEET = "Alexa.Unit.Volume.CubicFeet"
+
+ VOLUME_CUBICMETERS = "Alexa.Unit.Volume.CubicMeters"
+
+ VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons"
+
+ VOLUME_LITERS = "Alexa.Unit.Volume.Liters"
+
+ VOLUME_PINTS = "Alexa.Unit.Volume.Pints"
+
+ VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts"
+
+ WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces"
+
+ WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds"
diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py
index 55b5878f667..d84848e9aba 100644
--- a/homeassistant/components/alexa/entities.py
+++ b/homeassistant/components/alexa/entities.py
@@ -14,6 +14,7 @@ from homeassistant.const import (
from homeassistant.util.decorator import Registry
from homeassistant.components.climate import const as climate
from homeassistant.components import (
+ alarm_control_panel,
alert,
automation,
binary_sensor,
@@ -33,21 +34,28 @@ from homeassistant.components import (
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
from .capabilities import (
AlexaBrightnessController,
+ AlexaChannelController,
AlexaColorController,
AlexaColorTemperatureController,
AlexaContactSensor,
+ AlexaDoorbellEventSource,
AlexaEndpointHealth,
AlexaInputController,
AlexaLockController,
+ AlexaModeController,
AlexaMotionSensor,
AlexaPercentageController,
AlexaPlaybackController,
AlexaPowerController,
+ AlexaPowerLevelController,
+ AlexaRangeController,
AlexaSceneController,
+ AlexaSecurityPanelController,
AlexaSpeaker,
AlexaStepSpeaker,
AlexaTemperatureSensor,
AlexaThermostatController,
+ AlexaToggleController,
)
ENTITY_ADAPTERS = Registry()
@@ -77,7 +85,7 @@ class DisplayCategory:
DOOR = "DOOR"
# Indicates a doorbell.
- DOOR_BELL = "DOORBELL"
+ DOORBELL = "DOORBELL"
# Indicates a fan.
FAN = "FAN"
@@ -344,6 +352,20 @@ class FanCapabilities(AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & fan.SUPPORT_SET_SPEED:
yield AlexaPercentageController(self.entity)
+ yield AlexaPowerLevelController(self.entity)
+ yield AlexaRangeController(
+ self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
+ )
+
+ if supported & fan.SUPPORT_OSCILLATE:
+ yield AlexaToggleController(
+ self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
+ )
+ if supported & fan.SUPPORT_DIRECTION:
+ yield AlexaModeController(
+ self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"
+ )
+
yield AlexaEndpointHealth(self.hass, self.entity)
@@ -400,6 +422,9 @@ class MediaPlayerCapabilities(AlexaEntity):
if supported & media_player.SUPPORT_SELECT_SOURCE:
yield AlexaInputController(self.entity)
+ if supported & media_player.const.SUPPORT_PLAY_MEDIA:
+ yield AlexaChannelController(self.entity)
+
@ENTITY_ADAPTERS.register(scene.DOMAIN)
class SceneCapabilities(AlexaEntity):
@@ -476,6 +501,11 @@ class BinarySensorCapabilities(AlexaEntity):
elif sensor_type is self.TYPE_MOTION:
yield AlexaMotionSensor(self.hass, self.entity)
+ entity_conf = self.config.entity_config.get(self.entity.entity_id, {})
+ if CONF_DISPLAY_CATEGORIES in entity_conf:
+ if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL:
+ yield AlexaDoorbellEventSource(self.entity)
+
yield AlexaEndpointHealth(self.hass, self.entity)
def get_type(self):
@@ -485,3 +515,18 @@ class BinarySensorCapabilities(AlexaEntity):
return self.TYPE_CONTACT
if attrs.get(ATTR_DEVICE_CLASS) == "motion":
return self.TYPE_MOTION
+
+
+@ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN)
+class AlarmControlPanelCapabilities(AlexaEntity):
+ """Class to represent Alarm capabilities."""
+
+ def default_display_categories(self):
+ """Return the display categories for this entity."""
+ return [DisplayCategory.SECURITY_PANEL]
+
+ def interfaces(self):
+ """Yield the supported interfaces."""
+ if not self.entity.attributes.get("code_arm_required"):
+ yield AlexaSecurityPanelController(self.hass, self.entity)
+ yield AlexaEndpointHealth(self.hass, self.entity)
diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py
index 8c2fa692267..b0600313fc2 100644
--- a/homeassistant/components/alexa/errors.py
+++ b/homeassistant/components/alexa/errors.py
@@ -83,3 +83,31 @@ class AlexaBridgeUnreachableError(AlexaError):
namespace = "Alexa"
error_type = "BRIDGE_UNREACHABLE"
+
+
+class AlexaSecurityPanelUnauthorizedError(AlexaError):
+ """Class to represent SecurityPanelController Unauthorized errors."""
+
+ namespace = "Alexa.SecurityPanelController"
+ error_type = "UNAUTHORIZED"
+
+
+class AlexaSecurityPanelAuthorizationRequired(AlexaError):
+ """Class to represent SecurityPanelController AuthorizationRequired errors."""
+
+ namespace = "Alexa.SecurityPanelController"
+ error_type = "AUTHORIZATION_REQUIRED"
+
+
+class AlexaAlreadyInOperationError(AlexaError):
+ """Class to represent AlreadyInOperation errors."""
+
+ namespace = "Alexa"
+ error_type = "ALREADY_IN_OPERATION"
+
+
+class AlexaInvalidDirectiveError(AlexaError):
+ """Class to represent InvalidDirective errors."""
+
+ namespace = "Alexa"
+ error_type = "INVALID_DIRECTIVE"
diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py
index c72101460c4..331990dc4a4 100644
--- a/homeassistant/components/alexa/handlers.py
+++ b/homeassistant/components/alexa/handlers.py
@@ -9,6 +9,11 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
+ STATE_ALARM_DISARMED,
+ SERVICE_ALARM_ARM_AWAY,
+ SERVICE_ALARM_ARM_HOME,
+ SERVICE_ALARM_ARM_NIGHT,
+ SERVICE_ALARM_DISARM,
SERVICE_LOCK,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
@@ -31,10 +36,21 @@ import homeassistant.util.dt as dt_util
from homeassistant.util.decorator import Registry
from homeassistant.util.temperature import convert as convert_temperature
-from .const import API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause
+from .const import (
+ API_TEMP_UNITS,
+ API_THERMOSTAT_MODES,
+ API_THERMOSTAT_PRESETS,
+ Cause,
+ PERCENTAGE_FAN_MAP,
+ RANGE_FAN_MAP,
+ SPEED_FAN_MAP,
+)
from .entities import async_get_entities
from .errors import (
+ AlexaInvalidDirectiveError,
AlexaInvalidValueError,
+ AlexaSecurityPanelAuthorizationRequired,
+ AlexaSecurityPanelUnauthorizedError,
AlexaTempRangeError,
AlexaUnsupportedThermostatModeError,
)
@@ -349,15 +365,7 @@ async def async_api_adjust_percentage(hass, config, directive, context):
if entity.domain == fan.DOMAIN:
service = fan.SERVICE_SET_SPEED
speed = entity.attributes.get(fan.ATTR_SPEED)
-
- if speed == "off":
- current = 0
- elif speed == "low":
- current = 33
- elif speed == "medium":
- current = 66
- elif speed == "high":
- current = 100
+ current = PERCENTAGE_FAN_MAP.get(speed, 100)
# set percentage
percentage = max(0, percentage_delta + current)
@@ -405,7 +413,6 @@ async def async_api_lock(hass, config, directive, context):
return response
-# Not supported by Alexa yet
@HANDLERS.register(("Alexa.LockController", "Unlock"))
async def async_api_unlock(hass, config, directive, context):
"""Process an unlock request."""
@@ -418,7 +425,12 @@ async def async_api_unlock(hass, config, directive, context):
context=context,
)
- return directive.response()
+ response = directive.response()
+ response.add_context_property(
+ {"namespace": "Alexa.LockController", "name": "lockState", "value": "UNLOCKED"}
+ )
+
+ return response
@HANDLERS.register(("Alexa.Speaker", "SetVolume"))
@@ -509,20 +521,28 @@ async def async_api_adjust_volume_step(hass, config, directive, context):
"""Process an adjust volume step request."""
# media_player volume up/down service does not support specifying steps
# each component handles it differently e.g. via config.
- # For now we use the volumeSteps returned to figure out if we
- # should step up/down
- volume_step = directive.payload["volumeSteps"]
+ # This workaround will simply call the volume up/Volume down the amount of steps asked for
+ # When no steps are called in the request, Alexa sends a default of 10 steps which for most
+ # purposes is too high. The default is set 1 in this case.
entity = directive.entity
+ volume_int = int(directive.payload["volumeSteps"])
+ is_default = bool(directive.payload["volumeStepsDefault"])
+ default_steps = 1
+
+ if volume_int < 0:
+ service_volume = SERVICE_VOLUME_DOWN
+ if is_default:
+ volume_int = -default_steps
+ else:
+ service_volume = SERVICE_VOLUME_UP
+ if is_default:
+ volume_int = default_steps
data = {ATTR_ENTITY_ID: entity.entity_id}
- if volume_step > 0:
+ for _ in range(0, abs(volume_int)):
await hass.services.async_call(
- entity.domain, SERVICE_VOLUME_UP, data, blocking=False, context=context
- )
- elif volume_step < 0:
- await hass.services.async_call(
- entity.domain, SERVICE_VOLUME_DOWN, data, blocking=False, context=context
+ entity.domain, service_volume, data, blocking=False, context=context
)
return directive.response()
@@ -534,7 +554,6 @@ async def async_api_set_mute(hass, config, directive, context):
"""Process a set mute request."""
mute = bool(directive.payload["mute"])
entity = directive.entity
-
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
@@ -779,3 +798,373 @@ async def async_api_set_thermostat_mode(hass, config, directive, context):
async def async_api_reportstate(hass, config, directive, context):
"""Process a ReportState request."""
return directive.response(name="StateReport")
+
+
+@HANDLERS.register(("Alexa.PowerLevelController", "SetPowerLevel"))
+async def async_api_set_power_level(hass, config, directive, context):
+ """Process a SetPowerLevel request."""
+ entity = directive.entity
+ percentage = int(directive.payload["powerLevel"])
+ service = None
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+
+ if entity.domain == fan.DOMAIN:
+ service = fan.SERVICE_SET_SPEED
+ speed = "off"
+
+ if percentage <= 33:
+ speed = "low"
+ elif percentage <= 66:
+ speed = "medium"
+ else:
+ speed = "high"
+
+ data[fan.ATTR_SPEED] = speed
+
+ await hass.services.async_call(
+ entity.domain, service, data, blocking=False, context=context
+ )
+
+ return directive.response()
+
+
+@HANDLERS.register(("Alexa.PowerLevelController", "AdjustPowerLevel"))
+async def async_api_adjust_power_level(hass, config, directive, context):
+ """Process an AdjustPowerLevel request."""
+ entity = directive.entity
+ percentage_delta = int(directive.payload["powerLevelDelta"])
+ service = None
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+
+ if entity.domain == fan.DOMAIN:
+ service = fan.SERVICE_SET_SPEED
+ speed = entity.attributes.get(fan.ATTR_SPEED)
+ current = PERCENTAGE_FAN_MAP.get(speed, 100)
+
+ # set percentage
+ percentage = max(0, percentage_delta + current)
+ speed = "off"
+
+ if percentage <= 33:
+ speed = "low"
+ elif percentage <= 66:
+ speed = "medium"
+ else:
+ speed = "high"
+
+ data[fan.ATTR_SPEED] = speed
+
+ await hass.services.async_call(
+ entity.domain, service, data, blocking=False, context=context
+ )
+
+ return directive.response()
+
+
+@HANDLERS.register(("Alexa.SecurityPanelController", "Arm"))
+async def async_api_arm(hass, config, directive, context):
+ """Process a Security Panel Arm request."""
+ entity = directive.entity
+ service = None
+ arm_state = directive.payload["armState"]
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+
+ if entity.state != STATE_ALARM_DISARMED:
+ msg = "You must disarm the system before you can set the requested arm state."
+ raise AlexaSecurityPanelAuthorizationRequired(msg)
+
+ if arm_state == "ARMED_AWAY":
+ service = SERVICE_ALARM_ARM_AWAY
+ if arm_state == "ARMED_STAY":
+ service = SERVICE_ALARM_ARM_HOME
+ if arm_state == "ARMED_NIGHT":
+ service = SERVICE_ALARM_ARM_NIGHT
+
+ await hass.services.async_call(
+ entity.domain, service, data, blocking=False, context=context
+ )
+
+ response = directive.response(
+ name="Arm.Response", namespace="Alexa.SecurityPanelController"
+ )
+
+ response.add_context_property(
+ {
+ "name": "armState",
+ "namespace": "Alexa.SecurityPanelController",
+ "value": arm_state,
+ }
+ )
+
+ return response
+
+
+@HANDLERS.register(("Alexa.SecurityPanelController", "Disarm"))
+async def async_api_disarm(hass, config, directive, context):
+ """Process a Security Panel Disarm request."""
+ entity = directive.entity
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+
+ payload = directive.payload
+ if "authorization" in payload:
+ value = payload["authorization"]["value"]
+ if payload["authorization"]["type"] == "FOUR_DIGIT_PIN":
+ data["code"] = value
+
+ if not await hass.services.async_call(
+ entity.domain, SERVICE_ALARM_DISARM, data, blocking=True, context=context
+ ):
+ msg = "Invalid Code"
+ raise AlexaSecurityPanelUnauthorizedError(msg)
+
+ response = directive.response()
+ response.add_context_property(
+ {
+ "name": "armState",
+ "namespace": "Alexa.SecurityPanelController",
+ "value": "DISARMED",
+ }
+ )
+
+ return response
+
+
+@HANDLERS.register(("Alexa.ModeController", "SetMode"))
+async def async_api_set_mode(hass, config, directive, context):
+ """Process a next request."""
+ entity = directive.entity
+ instance = directive.instance
+ domain = entity.domain
+ service = None
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+ mode = directive.payload["mode"]
+
+ if domain != fan.DOMAIN:
+ msg = "Entity does not support directive"
+ raise AlexaInvalidDirectiveError(msg)
+
+ if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
+ mode, direction = mode.split(".")
+ if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]:
+ service = fan.SERVICE_SET_DIRECTION
+ data[fan.ATTR_DIRECTION] = direction
+
+ await hass.services.async_call(
+ domain, service, data, blocking=False, context=context
+ )
+
+ return directive.response()
+
+
+@HANDLERS.register(("Alexa.ModeController", "AdjustMode"))
+async def async_api_adjust_mode(hass, config, directive, context):
+ """Process a AdjustMode request.
+
+ Requires modeResources to be ordered.
+ Only modes that are ordered support the adjustMode directive.
+ """
+ entity = directive.entity
+ instance = directive.instance
+ domain = entity.domain
+
+ if domain != fan.DOMAIN:
+ msg = "Entity does not support directive"
+ raise AlexaInvalidDirectiveError(msg)
+
+ if instance is None:
+ msg = "Entity does not support directive"
+ raise AlexaInvalidDirectiveError(msg)
+
+ # No modeResources are currently ordered to support this request.
+
+ return directive.response()
+
+
+@HANDLERS.register(("Alexa.ToggleController", "TurnOn"))
+async def async_api_toggle_on(hass, config, directive, context):
+ """Process a toggle on request."""
+ entity = directive.entity
+ instance = directive.instance
+ domain = entity.domain
+ service = None
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+
+ if domain != fan.DOMAIN:
+ msg = "Entity does not support directive"
+ raise AlexaInvalidDirectiveError(msg)
+
+ if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
+ service = fan.SERVICE_OSCILLATE
+ data[fan.ATTR_OSCILLATING] = True
+
+ await hass.services.async_call(
+ domain, service, data, blocking=False, context=context
+ )
+
+ return directive.response()
+
+
+@HANDLERS.register(("Alexa.ToggleController", "TurnOff"))
+async def async_api_toggle_off(hass, config, directive, context):
+ """Process a toggle off request."""
+ entity = directive.entity
+ instance = directive.instance
+ domain = entity.domain
+ service = None
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+
+ if domain != fan.DOMAIN:
+ msg = "Entity does not support directive"
+ raise AlexaInvalidDirectiveError(msg)
+
+ if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
+ service = fan.SERVICE_OSCILLATE
+ data[fan.ATTR_OSCILLATING] = False
+
+ await hass.services.async_call(
+ domain, service, data, blocking=False, context=context
+ )
+
+ return directive.response()
+
+
+@HANDLERS.register(("Alexa.RangeController", "SetRangeValue"))
+async def async_api_set_range(hass, config, directive, context):
+ """Process a next request."""
+ entity = directive.entity
+ instance = directive.instance
+ domain = entity.domain
+ service = None
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+ range_value = int(directive.payload["rangeValue"])
+
+ if domain != fan.DOMAIN:
+ msg = "Entity does not support directive"
+ raise AlexaInvalidDirectiveError(msg)
+
+ if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
+ service = fan.SERVICE_SET_SPEED
+ speed = SPEED_FAN_MAP.get(range_value, None)
+
+ if not speed:
+ msg = "Entity does not support value"
+ raise AlexaInvalidValueError(msg)
+
+ if speed == fan.SPEED_OFF:
+ service = fan.SERVICE_TURN_OFF
+
+ data[fan.ATTR_SPEED] = speed
+
+ await hass.services.async_call(
+ domain, service, data, blocking=False, context=context
+ )
+
+ return directive.response()
+
+
+@HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue"))
+async def async_api_adjust_range(hass, config, directive, context):
+ """Process a next request."""
+ entity = directive.entity
+ instance = directive.instance
+ domain = entity.domain
+ service = None
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+ range_delta = int(directive.payload["rangeValueDelta"])
+
+ if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
+ service = fan.SERVICE_SET_SPEED
+
+ # adjust range
+ current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0)
+ speed = SPEED_FAN_MAP.get(max(0, range_delta + current_range), fan.SPEED_OFF)
+
+ if speed == fan.SPEED_OFF:
+ service = fan.SERVICE_TURN_OFF
+
+ data[fan.ATTR_SPEED] = speed
+
+ await hass.services.async_call(
+ domain, service, data, blocking=False, context=context
+ )
+
+ return directive.response()
+
+
+@HANDLERS.register(("Alexa.ChannelController", "ChangeChannel"))
+async def async_api_changechannel(hass, config, directive, context):
+ """Process a change channel request."""
+ channel = "0"
+ entity = directive.entity
+ payload = directive.payload["channel"]
+ payload_name = "number"
+
+ if "number" in payload:
+ channel = payload["number"]
+ payload_name = "number"
+ elif "callSign" in payload:
+ channel = payload["callSign"]
+ payload_name = "callSign"
+ elif "affiliateCallSign" in payload:
+ channel = payload["affiliateCallSign"]
+ payload_name = "affiliateCallSign"
+ elif "uri" in payload:
+ channel = payload["uri"]
+ payload_name = "uri"
+
+ data = {
+ ATTR_ENTITY_ID: entity.entity_id,
+ media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
+ media_player.const.ATTR_MEDIA_CONTENT_TYPE: media_player.const.MEDIA_TYPE_CHANNEL,
+ }
+
+ await hass.services.async_call(
+ entity.domain,
+ media_player.const.SERVICE_PLAY_MEDIA,
+ data,
+ blocking=False,
+ context=context,
+ )
+
+ response = directive.response()
+
+ response.add_context_property(
+ {
+ "namespace": "Alexa.ChannelController",
+ "name": "channel",
+ "value": {payload_name: channel},
+ }
+ )
+
+ return response
+
+
+@HANDLERS.register(("Alexa.ChannelController", "SkipChannels"))
+async def async_api_skipchannel(hass, config, directive, context):
+ """Process a skipchannel request."""
+ channel = int(directive.payload["channelCount"])
+ entity = directive.entity
+
+ data = {ATTR_ENTITY_ID: entity.entity_id}
+
+ if channel < 0:
+ service_media = SERVICE_MEDIA_PREVIOUS_TRACK
+ else:
+ service_media = SERVICE_MEDIA_NEXT_TRACK
+
+ for _ in range(0, abs(channel)):
+ await hass.services.async_call(
+ entity.domain, service_media, data, blocking=False, context=context
+ )
+
+ response = directive.response()
+
+ response.add_context_property(
+ {
+ "namespace": "Alexa.ChannelController",
+ "name": "channel",
+ "value": {"number": ""},
+ }
+ )
+
+ return response
diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json
index 9db7e270e61..ad0f1c33d49 100644
--- a/homeassistant/components/alexa/manifest.json
+++ b/homeassistant/components/alexa/manifest.json
@@ -4,5 +4,8 @@
"documentation": "https://www.home-assistant.io/integrations/alexa",
"requirements": [],
"dependencies": ["http"],
- "codeowners": ["@home-assistant/cloud"]
+ "codeowners": [
+ "@home-assistant/cloud",
+ "@ochlocracy"
+ ]
}
diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py
index 3195656ed09..cb78f269f8f 100644
--- a/homeassistant/components/alexa/messages.py
+++ b/homeassistant/components/alexa/messages.py
@@ -28,7 +28,7 @@ class AlexaDirective:
self.payload = self._directive[API_PAYLOAD]
self.has_endpoint = API_ENDPOINT in self._directive
- self.entity = self.entity_id = self.endpoint = None
+ self.entity = self.entity_id = self.endpoint = self.instance = None
def load_entity(self, hass, config):
"""Set attributes related to the entity for this request.
@@ -38,6 +38,7 @@ class AlexaDirective:
- entity
- entity_id
- endpoint
+ - instance (when header includes instance property)
Behavior when self.has_endpoint is False is undefined.
@@ -52,6 +53,8 @@ class AlexaDirective:
raise AlexaInvalidEndpointError(_endpoint_id)
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity)
+ if "instance" in self._directive[API_HEADER]:
+ self.instance = self._directive[API_HEADER]["instance"]
def response(self, name="Response", namespace="Alexa", payload=None):
"""Create an API formatted response.
diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py
index 42c16919a45..b5e1b741f0c 100644
--- a/homeassistant/components/alexa/state_report.py
+++ b/homeassistant/components/alexa/state_report.py
@@ -6,7 +6,8 @@ import logging
import aiohttp
import async_timeout
-from homeassistant.const import MATCH_ALL
+import homeassistant.util.dt as dt_util
+from homeassistant.const import MATCH_ALL, STATE_ON
from .const import API_CHANGE, Cause
from .entities import ENTITY_ADAPTERS
@@ -45,6 +46,14 @@ async def async_enable_proactive_mode(hass, smart_home_config):
hass, smart_home_config, alexa_changed_entity
)
return
+ if (
+ interface.name() == "Alexa.DoorbellEventSource"
+ and new_state.state == STATE_ON
+ ):
+ await async_send_doorbell_event_message(
+ hass, smart_home_config, alexa_changed_entity
+ )
+ return
return hass.helpers.event.async_track_state_change(
MATCH_ALL, async_entity_state_listener
@@ -184,3 +193,58 @@ async def async_send_delete_message(hass, config, entity_ids):
return await session.post(
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
)
+
+
+async def async_send_doorbell_event_message(hass, config, alexa_entity):
+ """Send a DoorbellPress event message for an Alexa entity.
+
+ https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html
+ """
+ token = await config.async_get_access_token()
+
+ headers = {"Authorization": f"Bearer {token}"}
+
+ endpoint = alexa_entity.alexa_id()
+
+ message = AlexaResponse(
+ name="DoorbellPress",
+ namespace="Alexa.DoorbellEventSource",
+ payload={
+ "cause": {"type": Cause.PHYSICAL_INTERACTION},
+ "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z",
+ },
+ )
+
+ message.set_endpoint_full(token, endpoint)
+
+ message_serialized = message.serialize()
+ session = hass.helpers.aiohttp_client.async_get_clientsession()
+
+ try:
+ with async_timeout.timeout(DEFAULT_TIMEOUT):
+ response = await session.post(
+ config.endpoint,
+ headers=headers,
+ json=message_serialized,
+ allow_redirects=True,
+ )
+
+ except (asyncio.TimeoutError, aiohttp.ClientError):
+ _LOGGER.error("Timeout sending report to Alexa.")
+ return
+
+ response_text = await response.text()
+
+ _LOGGER.debug("Sent: %s", json.dumps(message_serialized))
+ _LOGGER.debug("Received (%s): %s", response.status, response_text)
+
+ if response.status == 202:
+ return
+
+ response_json = json.loads(response_text)
+
+ _LOGGER.error(
+ "Error when sending DoorbellPress event to Alexa: %s: %s",
+ response_json["payload"]["code"],
+ response_json["payload"]["description"],
+ )
diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json
index 9ac8d1ea1e0..1213bb12e74 100644
--- a/homeassistant/components/alpha_vantage/manifest.json
+++ b/homeassistant/components/alpha_vantage/manifest.json
@@ -3,7 +3,7 @@
"name": "Alpha vantage",
"documentation": "https://www.home-assistant.io/integrations/alpha_vantage",
"requirements": [
- "alpha_vantage==2.1.0"
+ "alpha_vantage==2.1.1"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py
index 188567e4cf4..da29e4e25e1 100644
--- a/homeassistant/components/alpha_vantage/sensor.py
+++ b/homeassistant/components/alpha_vantage/sensor.py
@@ -3,6 +3,8 @@ from datetime import timedelta
import logging
import voluptuous as vol
+from alpha_vantage.timeseries import TimeSeries
+from alpha_vantage.foreignexchange import ForeignExchange
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME
@@ -62,15 +64,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Alpha Vantage sensor."""
- from alpha_vantage.timeseries import TimeSeries
- from alpha_vantage.foreignexchange import ForeignExchange
-
api_key = config.get(CONF_API_KEY)
symbols = config.get(CONF_SYMBOLS, [])
conversions = config.get(CONF_FOREIGN_EXCHANGE, [])
if not symbols and not conversions:
- msg = "Warning: No symbols or currencies configured."
+ msg = "No symbols or currencies configured."
hass.components.persistent_notification.create(msg, "Sensor alpha_vantage")
_LOGGER.warning(msg)
return
diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py
index 64b8b71457c..3acfd472320 100644
--- a/homeassistant/components/amazon_polly/tts.py
+++ b/homeassistant/components/amazon_polly/tts.py
@@ -1,5 +1,6 @@
"""Support for the Amazon Polly text to speech service."""
import logging
+import boto3
import voluptuous as vol
@@ -156,8 +157,6 @@ def get_engine(hass, config):
config[CONF_SAMPLE_RATE] = sample_rate
- import boto3
-
profile = config.get(CONF_PROFILE_NAME)
if profile is not None:
diff --git a/homeassistant/components/ambiclimate/.translations/nn.json b/homeassistant/components/ambiclimate/.translations/nn.json
new file mode 100644
index 00000000000..ce8a3ed9db6
--- /dev/null
+++ b/homeassistant/components/ambiclimate/.translations/nn.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Ambiclimate"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json
index 5a816bce140..ba667ea7b9a 100644
--- a/homeassistant/components/ambiclimate/.translations/ru.json
+++ b/homeassistant/components/ambiclimate/.translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.",
- "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.",
+ "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.",
"no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambiclimate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
diff --git a/homeassistant/components/ambient_station/.translations/nn.json b/homeassistant/components/ambient_station/.translations/nn.json
new file mode 100644
index 00000000000..0f878b363c9
--- /dev/null
+++ b/homeassistant/components/ambient_station/.translations/nn.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Ambient PWS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json
index 2d7964f18eb..3a7c405ea4c 100644
--- a/homeassistant/components/ambient_station/.translations/ru.json
+++ b/homeassistant/components/ambient_station/.translations/ru.json
@@ -2,8 +2,8 @@
"config": {
"error": {
"identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.",
- "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f",
- "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b"
+ "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.",
+ "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b."
},
"step": {
"user": {
diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py
index f915872abf0..d49104a0b26 100644
--- a/homeassistant/components/amcrest/__init__.py
+++ b/homeassistant/components/amcrest/__init__.py
@@ -167,6 +167,8 @@ class AmcrestChecker(Http):
offline = not self.available
if offline and was_online:
_LOGGER.error("%s camera offline: Too many errors", self._wrap_name)
+ with self._token_lock:
+ self._token = None
dispatcher_send(
self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)
)
diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py
index f75a5adbe9c..e9e1e2b5f84 100644
--- a/homeassistant/components/amcrest/camera.py
+++ b/homeassistant/components/amcrest/camera.py
@@ -2,19 +2,20 @@
import asyncio
from datetime import timedelta
import logging
-from urllib3.exceptions import HTTPError
from amcrest import AmcrestError
+from haffmpeg.camera import CameraMjpeg
+from urllib3.exceptions import HTTPError
import voluptuous as vol
from homeassistant.components.camera import (
- Camera,
CAMERA_SERVICE_SCHEMA,
SUPPORT_ON_OFF,
SUPPORT_STREAM,
+ Camera,
)
from homeassistant.components.ffmpeg import DATA_FFMPEG
-from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF
+from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream,
async_aiohttp_proxy_web,
@@ -159,7 +160,6 @@ class AmcrestCam(Camera):
return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
# streaming via ffmpeg
- from haffmpeg.camera import CameraMjpeg
streaming_url = self._rtsp_url
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py
index e63f59839a8..c925909a9a8 100644
--- a/homeassistant/components/ampio/air_quality.py
+++ b/homeassistant/components/ampio/air_quality.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+from asmog import AmpioSmog
import voluptuous as vol
from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity
@@ -23,7 +24,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Ampio Smog air quality platform."""
- from asmog import AmpioSmog
name = config.get(CONF_NAME)
station_id = config[CONF_STATION_ID]
diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py
index 33362bd37cc..1f9df527c28 100644
--- a/homeassistant/components/android_ip_webcam/__init__.py
+++ b/homeassistant/components/android_ip_webcam/__init__.py
@@ -1,34 +1,35 @@
"""Support for Android IP Webcam."""
import asyncio
-import logging
from datetime import timedelta
+import logging
+from pydroid_ipcam import PyDroidIPCam
import voluptuous as vol
-from homeassistant.core import callback
+from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL
from homeassistant.const import (
- CONF_NAME,
CONF_HOST,
- CONF_PORT,
- CONF_USERNAME,
+ CONF_NAME,
CONF_PASSWORD,
+ CONF_PLATFORM,
+ CONF_PORT,
+ CONF_SCAN_INTERVAL,
CONF_SENSORS,
CONF_SWITCHES,
CONF_TIMEOUT,
- CONF_SCAN_INTERVAL,
- CONF_PLATFORM,
+ CONF_USERNAME,
)
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.core import callback
from homeassistant.helpers import discovery
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
- async_dispatcher_send,
async_dispatcher_connect,
+ async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
-from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL
_LOGGER = logging.getLogger(__name__)
@@ -187,7 +188,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Set up the IP Webcam component."""
- from pydroid_ipcam import PyDroidIPCam
webcams = hass.data[DATA_IP_WEBCAM] = {}
websession = async_get_clientsession(hass)
diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json
index e84ed35c763..9ec993b9f91 100644
--- a/homeassistant/components/androidtv/manifest.json
+++ b/homeassistant/components/androidtv/manifest.json
@@ -3,8 +3,8 @@
"name": "Androidtv",
"documentation": "https://www.home-assistant.io/integrations/androidtv",
"requirements": [
- "adb-shell==0.0.4",
- "androidtv==0.0.30"
+ "adb-shell==0.0.7",
+ "androidtv==0.0.32"
],
"dependencies": [],
"codeowners": ["@JeffLIrion"]
diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py
index fcf4950f5e2..62ae93f96e4 100644
--- a/homeassistant/components/androidtv/media_player.py
+++ b/homeassistant/components/androidtv/media_player.py
@@ -1,8 +1,10 @@
"""Support for functionality to interact with Android TV / Fire TV devices."""
import functools
import logging
+import os
import voluptuous as vol
+from adb_shell.auth.keygen import keygen
from adb_shell.exceptions import (
InvalidChecksumError,
InvalidCommandError,
@@ -40,6 +42,7 @@ from homeassistant.const import (
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.storage import STORAGE_DIR
ANDROIDTV_DOMAIN = "androidtv"
@@ -133,27 +136,39 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if CONF_ADB_SERVER_IP not in config:
# Use "adb_shell" (Python ADB implementation)
- adb_log = "using Python ADB implementation " + (
- f"with adbkey='{config[CONF_ADBKEY]}'"
- if CONF_ADBKEY in config
- else "without adbkey authentication"
- )
- if CONF_ADBKEY in config:
+ if CONF_ADBKEY not in config:
+ # Generate ADB key files (if they don't exist)
+ adbkey = hass.config.path(STORAGE_DIR, "androidtv_adbkey")
+ if not os.path.isfile(adbkey):
+ keygen(adbkey)
+
+ adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
+
+ aftv = setup(
+ host,
+ adbkey,
+ device_class=config[CONF_DEVICE_CLASS],
+ state_detection_rules=config[CONF_STATE_DETECTION_RULES],
+ auth_timeout_s=10.0,
+ )
+
+ else:
+ adb_log = (
+ f"using Python ADB implementation with adbkey='{config[CONF_ADBKEY]}'"
+ )
+
aftv = setup(
host,
config[CONF_ADBKEY],
device_class=config[CONF_DEVICE_CLASS],
state_detection_rules=config[CONF_STATE_DETECTION_RULES],
+ auth_timeout_s=10.0,
)
- else:
- aftv = setup(
- host,
- device_class=config[CONF_DEVICE_CLASS],
- state_detection_rules=config[CONF_STATE_DETECTION_RULES],
- )
else:
# Use "pure-python-adb" (communicate with ADB server)
+ adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}"
+
aftv = setup(
host,
adb_server_ip=config[CONF_ADB_SERVER_IP],
@@ -161,7 +176,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
device_class=config[CONF_DEVICE_CLASS],
state_detection_rules=config[CONF_STATE_DETECTION_RULES],
)
- adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}"
if not aftv.available:
# Determine the name that will be used for the device in the log
@@ -257,7 +271,7 @@ def adb_decorator(override_available=False):
"establishing attempt in the next update. Error: %s",
err,
)
- self.aftv.adb.close()
+ self.aftv.adb_close()
self._available = False # pylint: disable=protected-access
return None
@@ -429,7 +443,7 @@ class AndroidTVDevice(ADBDevice):
# Check if device is disconnected.
if not self._available:
# Try to connect
- self._available = self.aftv.connect(always_log_errors=False)
+ self._available = self.aftv.adb_connect(always_log_errors=False)
# To be safe, wait until the next update to run ADB commands if
# using the Python ADB implementation.
@@ -508,7 +522,7 @@ class FireTVDevice(ADBDevice):
# Check if device is disconnected.
if not self._available:
# Try to connect
- self._available = self.aftv.connect(always_log_errors=False)
+ self._available = self.aftv.adb_connect(always_log_errors=False)
# To be safe, wait until the next update to run ADB commands if
# using the Python ADB implementation.
diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py
index 6184465ef16..3c181d7d04b 100644
--- a/homeassistant/components/anel_pwrctrl/switch.py
+++ b/homeassistant/components/anel_pwrctrl/switch.py
@@ -1,13 +1,14 @@
"""Support for ANEL PwrCtrl switches."""
+from datetime import timedelta
import logging
import socket
-from datetime import timedelta
+from anel_pwrctrl import DeviceMaster
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
+from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
+import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -36,8 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
port_recv = config.get(CONF_PORT_RECV)
port_send = config.get(CONF_PORT_SEND)
- from anel_pwrctrl import DeviceMaster
-
try:
master = DeviceMaster(
username=username,
diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py
index a033470e5c9..d472af6104e 100644
--- a/homeassistant/components/anthemav/media_player.py
+++ b/homeassistant/components/anthemav/media_player.py
@@ -1,6 +1,8 @@
"""Support for Anthem Network Receivers and Processors."""
import logging
+import anthemav
+
import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
@@ -46,7 +48,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up our socket to the AVR."""
- import anthemav
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py
index 512bd01b72a..71f25f04387 100644
--- a/homeassistant/components/apcupsd/__init__.py
+++ b/homeassistant/components/apcupsd/__init__.py
@@ -1,7 +1,8 @@
"""Support for APCUPSd via its Network Information Server (NIS)."""
-import logging
from datetime import timedelta
+import logging
+from apcaccess import status
import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PORT
@@ -64,7 +65,6 @@ class APCUPSdData:
def __init__(self, host, port):
"""Initialize the data object."""
- from apcaccess import status
self._host = host
self._port = port
diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py
index 837e6e45c6c..255eb1624ff 100644
--- a/homeassistant/components/apcupsd/sensor.py
+++ b/homeassistant/components/apcupsd/sensor.py
@@ -1,12 +1,13 @@
"""Support for APCUPSd sensors."""
import logging
+from apcaccess.status import ALL_UNITS
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-import homeassistant.helpers.config_validation as cv
from homeassistant.components import apcupsd
-from homeassistant.const import TEMP_CELSIUS, CONF_RESOURCES, POWER_WATT
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_RESOURCES, POWER_WATT, TEMP_CELSIUS
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -135,7 +136,6 @@ def infer_unit(value):
Split the unit off the end of the value and return the value, unit tuple
pair. Else return the original value and None as the unit.
"""
- from apcaccess.status import ALL_UNITS
for unit in ALL_UNITS:
if value.endswith(unit):
diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py
index dbd45013a3c..c24c9cc1605 100644
--- a/homeassistant/components/apns/notify.py
+++ b/homeassistant/components/apns/notify.py
@@ -1,14 +1,11 @@
"""APNS Notification platform."""
import logging
+from apns2.client import APNsClient
+from apns2.errors import Unregistered
+from apns2.payload import Payload
import voluptuous as vol
-from homeassistant.config import load_yaml_config_file
-from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM
-from homeassistant.helpers import template as template_helper
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.event import track_state_change
-
from homeassistant.components.notify import (
ATTR_DATA,
ATTR_TARGET,
@@ -16,6 +13,11 @@ from homeassistant.components.notify import (
PLATFORM_SCHEMA,
BaseNotificationService,
)
+from homeassistant.config import load_yaml_config_file
+from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM
+from homeassistant.helpers import template as template_helper
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.event import track_state_change
APNS_DEVICES = "apns.yaml"
CONF_CERTFILE = "cert_file"
@@ -213,9 +215,6 @@ class ApnsNotificationService(BaseNotificationService):
def send_message(self, message=None, **kwargs):
"""Send push message to registered devices."""
- from apns2.client import APNsClient
- from apns2.payload import Payload
- from apns2.errors import Unregistered
apns = APNsClient(
self.certificate, use_sandbox=self.sandbox, use_alternative_port=False
diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py
index 51c2ee7e1a5..38d520f73da 100644
--- a/homeassistant/components/apple_tv/__init__.py
+++ b/homeassistant/components/apple_tv/__init__.py
@@ -3,13 +3,15 @@ import asyncio
import logging
from typing import Sequence, TypeVar, Union
+from pyatv import AppleTVDevice, connect_to_apple_tv, scan_for_apple_tvs
+from pyatv.exceptions import DeviceAuthenticationError
import voluptuous as vol
+import homeassistant.helpers.config_validation as cv
from homeassistant.components.discovery import SERVICE_APPLE_TV
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -80,7 +82,6 @@ def request_configuration(hass, config, atv, credentials):
async def configuration_callback(callback_data):
"""Handle the submitted configuration."""
- from pyatv import exceptions
pin = callback_data.get("pin")
@@ -93,7 +94,7 @@ def request_configuration(hass, config, atv, credentials):
title=NOTIFICATION_AUTH_TITLE,
notification_id=NOTIFICATION_AUTH_ID,
)
- except exceptions.DeviceAuthenticationError as ex:
+ except DeviceAuthenticationError as ex:
hass.components.persistent_notification.async_create(
"Authentication failed! Did you enter correct PIN?
"
"Details: {0}".format(ex),
@@ -112,11 +113,10 @@ def request_configuration(hass, config, atv, credentials):
)
-async def scan_for_apple_tvs(hass):
+async def scan_apple_tvs(hass):
"""Scan for devices and present a notification of the ones found."""
- import pyatv
- atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
+ atvs = await scan_for_apple_tvs(hass.loop, timeout=3)
devices = []
for atv in atvs:
@@ -149,7 +149,7 @@ async def async_setup(hass, config):
entity_ids = service.data.get(ATTR_ENTITY_ID)
if service.service == SERVICE_SCAN:
- hass.async_add_job(scan_for_apple_tvs, hass)
+ hass.async_add_job(scan_apple_tvs, hass)
return
if entity_ids:
@@ -207,7 +207,6 @@ async def async_setup(hass, config):
async def _setup_atv(hass, hass_config, atv_config):
"""Set up an Apple TV."""
- import pyatv
name = atv_config.get(CONF_NAME)
host = atv_config.get(CONF_HOST)
@@ -218,9 +217,9 @@ async def _setup_atv(hass, hass_config, atv_config):
if host in hass.data[DATA_APPLE_TV]:
return
- details = pyatv.AppleTVDevice(name, host, login_id)
+ details = AppleTVDevice(name, host, login_id)
session = async_get_clientsession(hass)
- atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
+ atv = connect_to_apple_tv(details, hass.loop, session=session)
if credentials:
await atv.airplay.load_credentials(credentials)
diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py
index 9ac5ba77f98..c816be52259 100644
--- a/homeassistant/components/apple_tv/media_player.py
+++ b/homeassistant/components/apple_tv/media_player.py
@@ -1,6 +1,8 @@
"""Support for Apple TV media player."""
import logging
+import pyatv.const as atv_const
+
from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
@@ -112,22 +114,21 @@ class AppleTvDevice(MediaPlayerDevice):
return STATE_OFF
if self._playing:
- from pyatv import const
state = self._playing.play_state
if state in (
- const.PLAY_STATE_IDLE,
- const.PLAY_STATE_NO_MEDIA,
- const.PLAY_STATE_LOADING,
+ atv_const.PLAY_STATE_IDLE,
+ atv_const.PLAY_STATE_NO_MEDIA,
+ atv_const.PLAY_STATE_LOADING,
):
return STATE_IDLE
- if state == const.PLAY_STATE_PLAYING:
+ if state == atv_const.PLAY_STATE_PLAYING:
return STATE_PLAYING
if state in (
- const.PLAY_STATE_PAUSED,
- const.PLAY_STATE_FAST_FORWARD,
- const.PLAY_STATE_FAST_BACKWARD,
- const.PLAY_STATE_STOPPED,
+ atv_const.PLAY_STATE_PAUSED,
+ atv_const.PLAY_STATE_FAST_FORWARD,
+ atv_const.PLAY_STATE_FAST_BACKWARD,
+ atv_const.PLAY_STATE_STOPPED,
):
# Catch fast forward/backward here so "play" is default action
return STATE_PAUSED
@@ -156,14 +157,13 @@ class AppleTvDevice(MediaPlayerDevice):
def media_content_type(self):
"""Content type of current playing media."""
if self._playing:
- from pyatv import const
media_type = self._playing.media_type
- if media_type == const.MEDIA_TYPE_VIDEO:
+ if media_type == atv_const.MEDIA_TYPE_VIDEO:
return MEDIA_TYPE_VIDEO
- if media_type == const.MEDIA_TYPE_MUSIC:
+ if media_type == atv_const.MEDIA_TYPE_MUSIC:
return MEDIA_TYPE_MUSIC
- if media_type == const.MEDIA_TYPE_TV:
+ if media_type == atv_const.MEDIA_TYPE_TV:
return MEDIA_TYPE_TVSHOW
@property
diff --git a/homeassistant/components/apprise/__init__.py b/homeassistant/components/apprise/__init__.py
new file mode 100644
index 00000000000..6ffdaf690d9
--- /dev/null
+++ b/homeassistant/components/apprise/__init__.py
@@ -0,0 +1 @@
+"""The apprise component."""
diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json
new file mode 100644
index 00000000000..3e971a96e7e
--- /dev/null
+++ b/homeassistant/components/apprise/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "apprise",
+ "name": "Apprise",
+ "documentation": "https://www.home-assistant.io/components/apprise",
+ "requirements": [
+ "apprise==0.8.1"
+ ],
+ "dependencies": [],
+ "codeowners": [
+ "@caronc"
+ ]
+}
diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py
new file mode 100644
index 00000000000..662cc9c1ab6
--- /dev/null
+++ b/homeassistant/components/apprise/notify.py
@@ -0,0 +1,73 @@
+"""Apprise platform for notify component."""
+import logging
+
+import voluptuous as vol
+
+import apprise
+
+import homeassistant.helpers.config_validation as cv
+
+from homeassistant.components.notify import (
+ ATTR_TARGET,
+ ATTR_TITLE,
+ ATTR_TITLE_DEFAULT,
+ PLATFORM_SCHEMA,
+ BaseNotificationService,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_FILE = "config"
+CONF_URL = "url"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_URL): vol.All(cv.ensure_list, [str]),
+ vol.Optional(CONF_FILE): cv.string,
+ }
+)
+
+
+def get_service(hass, config, discovery_info=None):
+ """Get the Apprise notification service."""
+
+ # Create our object
+ a_obj = apprise.Apprise()
+
+ if config.get(CONF_FILE):
+ # Sourced from a Configuration File
+ a_config = apprise.AppriseConfig()
+ if not a_config.add(config[CONF_FILE]):
+ _LOGGER.error("Invalid Apprise config url provided")
+ return None
+
+ if not a_obj.add(a_config):
+ _LOGGER.error("Invalid Apprise config url provided")
+ return None
+
+ if config.get(CONF_URL):
+ # Ordered list of URLs
+ if not a_obj.add(config[CONF_URL]):
+ _LOGGER.error("Invalid Apprise URL(s) supplied")
+ return None
+
+ return AppriseNotificationService(a_obj)
+
+
+class AppriseNotificationService(BaseNotificationService):
+ """Implement the notification service for Apprise."""
+
+ def __init__(self, a_obj):
+ """Initialize the service."""
+ self.apprise = a_obj
+
+ def send_message(self, message="", **kwargs):
+ """Send a message to a specified target.
+
+ If no target/tags are specified, then services are notified as is
+ However, if any tags are specified, then they will be applied
+ to the notification causing filtering (if set up that way).
+ """
+ targets = kwargs.get(ATTR_TARGET)
+ title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
+ self.apprise.notify(body=message, title=title, tag=targets)
diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py
index 86b0b6f48af..0d23cedb4ee 100644
--- a/homeassistant/components/aprs/device_tracker.py
+++ b/homeassistant/components/aprs/device_tracker.py
@@ -3,6 +3,11 @@
import logging
import threading
+import geopy.distance
+import aprslib
+from aprslib import ConnectionError as AprsConnectionError
+from aprslib import LoginError
+
import voluptuous as vol
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
@@ -59,7 +64,6 @@ def make_filter(callsigns: list) -> str:
def gps_accuracy(gps, posambiguity: int) -> int:
"""Calculate the GPS accuracy based on APRS posambiguity."""
- import geopy.distance
pos_a_map = {0: 0, 1: 1 / 600, 2: 1 / 60, 3: 1 / 6, 4: 1}
if posambiguity in pos_a_map:
@@ -115,8 +119,6 @@ class AprsListenerThread(threading.Thread):
"""Initialize the class."""
super().__init__()
- import aprslib
-
self.callsign = callsign
self.host = host
self.start_event = threading.Event()
@@ -138,8 +140,6 @@ class AprsListenerThread(threading.Thread):
def run(self):
"""Connect to APRS and listen for data."""
self.ais.set_filter(self.server_filter)
- from aprslib import ConnectionError as AprsConnectionError
- from aprslib import LoginError
try:
_LOGGER.info(
diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py
index cabe00b6c6d..9f693966382 100644
--- a/homeassistant/components/aqualogic/__init__.py
+++ b/homeassistant/components/aqualogic/__init__.py
@@ -1,9 +1,10 @@
"""Support for AquaLogic devices."""
from datetime import timedelta
import logging
-import time
import threading
+import time
+from aqualogic.core import AquaLogic
import voluptuous as vol
from homeassistant.const import (
@@ -71,7 +72,6 @@ class AquaLogicProcessor(threading.Thread):
def run(self):
"""Event thread."""
- from aqualogic.core import AquaLogic
while True:
self._panel = AquaLogic()
diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py
index b5a7a409647..74f1a9d9f9a 100644
--- a/homeassistant/components/aqualogic/switch.py
+++ b/homeassistant/components/aqualogic/switch.py
@@ -1,6 +1,7 @@
"""Support for AquaLogic switches."""
import logging
+from aqualogic.core import States
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
@@ -50,7 +51,6 @@ class AquaLogicSwitch(SwitchDevice):
def __init__(self, processor, switch_type):
"""Initialize switch."""
- from aqualogic.core import States
self._processor = processor
self._type = switch_type
diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py
index 016db478fc9..d8770592c9f 100644
--- a/homeassistant/components/aquostv/media_player.py
+++ b/homeassistant/components/aquostv/media_player.py
@@ -1,6 +1,8 @@
"""Support for interface with an Aquos TV."""
import logging
+import sharp_aquos_rc
+
import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
@@ -77,7 +79,6 @@ SOURCES = {
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Sharp Aquos TV platform."""
- import sharp_aquos_rc
name = config.get(CONF_NAME)
port = config.get(CONF_PORT)
diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py
index 4dcde93e749..f973ec136e3 100644
--- a/homeassistant/components/arduino/__init__.py
+++ b/homeassistant/components/arduino/__init__.py
@@ -1,8 +1,11 @@
"""Support for Arduino boards running with the Firmata firmware."""
import logging
+import serial
import voluptuous as vol
+from PyMata.pymata import PyMata
+
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.const import CONF_PORT
import homeassistant.helpers.config_validation as cv
@@ -20,7 +23,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Arduino component."""
- import serial
port = config[DOMAIN][CONF_PORT]
@@ -59,7 +61,6 @@ class ArduinoBoard:
def __init__(self, port):
"""Initialize the board."""
- from PyMata.pymata import PyMata
self._port = port
self._board = PyMata(self._port, verbose=False)
diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json
index 3567ce71cd1..a29f65700ff 100644
--- a/homeassistant/components/arduino/manifest.json
+++ b/homeassistant/components/arduino/manifest.json
@@ -3,7 +3,7 @@
"name": "Arduino",
"documentation": "https://www.home-assistant.io/integrations/arduino",
"requirements": [
- "PyMata==2.14"
+ "PyMata==2.20"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/arlo/__init__.py b/homeassistant/components/arlo/__init__.py
index 80fa37b6787..df24bdd1a92 100644
--- a/homeassistant/components/arlo/__init__.py
+++ b/homeassistant/components/arlo/__init__.py
@@ -1,14 +1,15 @@
"""Support for Netgear Arlo IP cameras."""
-import logging
from datetime import timedelta
+import logging
+from pyarlo import PyArlo
+from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
-from requests.exceptions import HTTPError, ConnectTimeout
+from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from homeassistant.helpers import config_validation as cv
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL
-from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers.event import track_time_interval
_LOGGER = logging.getLogger(__name__)
@@ -47,7 +48,6 @@ def setup(hass, config):
scan_interval = conf.get(CONF_SCAN_INTERVAL)
try:
- from pyarlo import PyArlo
arlo = PyArlo(username, password, preload=False)
if not arlo.is_connected:
diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py
index a05dc40a9ef..958c383765a 100644
--- a/homeassistant/components/arlo/camera.py
+++ b/homeassistant/components/arlo/camera.py
@@ -1,6 +1,7 @@
"""Support for Netgear Arlo IP cameras."""
import logging
+from haffmpeg.camera import CameraMjpeg
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
@@ -77,7 +78,6 @@ class ArloCam(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
- from haffmpeg.camera import CameraMjpeg
video = self._camera.last_video
if not video:
diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py
index f93533b6beb..485c731ff6a 100644
--- a/homeassistant/components/aruba/device_tracker.py
+++ b/homeassistant/components/aruba/device_tracker.py
@@ -2,15 +2,16 @@
import logging
import re
+import pexpect
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN,
PLATFORM_SCHEMA,
DeviceScanner,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -82,7 +83,6 @@ class ArubaDeviceScanner(DeviceScanner):
def get_aruba_data(self):
"""Retrieve data from Aruba Access Point and return parsed result."""
- import pexpect
connect = "ssh {}@{}"
ssh = pexpect.spawn(connect.format(self.username, self.host))
diff --git a/homeassistant/components/asterisk_mbox/__init__.py b/homeassistant/components/asterisk_mbox/__init__.py
index 6c9412d07d8..1ecba9f4c8f 100644
--- a/homeassistant/components/asterisk_mbox/__init__.py
+++ b/homeassistant/components/asterisk_mbox/__init__.py
@@ -1,6 +1,12 @@
"""Support for Asterisk Voicemail interface."""
import logging
+from asterisk_mbox import Client as asteriskClient
+from asterisk_mbox.commands import (
+ CMD_MESSAGE_CDR,
+ CMD_MESSAGE_CDR_AVAILABLE,
+ CMD_MESSAGE_LIST,
+)
import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
@@ -51,7 +57,6 @@ class AsteriskData:
def __init__(self, hass, host, port, password, config):
"""Init the Asterisk data object."""
- from asterisk_mbox import Client as asteriskClient
self.hass = hass
self.config = config
@@ -76,11 +81,6 @@ class AsteriskData:
@callback
def handle_data(self, command, msg):
"""Handle changes to the mailbox."""
- from asterisk_mbox.commands import (
- CMD_MESSAGE_LIST,
- CMD_MESSAGE_CDR_AVAILABLE,
- CMD_MESSAGE_CDR,
- )
if command == CMD_MESSAGE_LIST:
_LOGGER.debug("AsteriskVM sent updated message list: Len %d", len(msg))
diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py
index 4d3c255fd5b..3cd6fe059b6 100644
--- a/homeassistant/components/asterisk_mbox/mailbox.py
+++ b/homeassistant/components/asterisk_mbox/mailbox.py
@@ -1,6 +1,8 @@
"""Support for the Asterisk Voicemail interface."""
import logging
+from asterisk_mbox import ServerError
+
from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -50,7 +52,6 @@ class AsteriskMailbox(Mailbox):
async def async_get_media(self, msgid):
"""Return the media blob for the msgid."""
- from asterisk_mbox import ServerError
client = self.hass.data[ASTERISK_DOMAIN].client
try:
diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py
index 93b5ec6ec78..468e6e429a7 100644
--- a/homeassistant/components/august/__init__.py
+++ b/homeassistant/components/august/__init__.py
@@ -1,18 +1,20 @@
"""Support for August devices."""
-import logging
from datetime import timedelta
+import logging
+from august.api import Api
+from august.authenticator import AuthenticationState, Authenticator, ValidationResult
+from requests import RequestException, Session
import voluptuous as vol
-from requests import RequestException
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_PASSWORD,
- CONF_USERNAME,
CONF_TIMEOUT,
+ CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers import discovery
+import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -62,7 +64,6 @@ def request_configuration(hass, config, api, authenticator):
def august_configuration_callback(data):
"""Run when the configuration callback is called."""
- from august.authenticator import ValidationResult
result = authenticator.validate_verification_code(data.get("verification_code"))
@@ -94,7 +95,6 @@ def request_configuration(hass, config, api, authenticator):
def setup_august(hass, config, api, authenticator):
"""Set up the August component."""
- from august.authenticator import AuthenticationState
authentication = None
try:
@@ -134,9 +134,6 @@ def setup_august(hass, config, api, authenticator):
def setup(hass, config):
"""Set up the August component."""
- from august.api import Api
- from august.authenticator import Authenticator
- from requests import Session
conf = config[DOMAIN]
api_http_session = None
diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py
index d68582d30c5..14d03189c92 100644
--- a/homeassistant/components/august/binary_sensor.py
+++ b/homeassistant/components/august/binary_sensor.py
@@ -2,6 +2,9 @@
from datetime import datetime, timedelta
import logging
+from august.activity import ActivityType
+from august.lock import LockDoorStatus
+
from homeassistant.components.binary_sensor import BinarySensorDevice
from . import DATA_AUGUST
@@ -26,7 +29,6 @@ def _retrieve_online_state(data, doorbell):
def _retrieve_motion_state(data, doorbell):
- from august.activity import ActivityType
return _activity_time_based_state(
data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
@@ -34,7 +36,6 @@ def _retrieve_motion_state(data, doorbell):
def _retrieve_ding_state(data, doorbell):
- from august.activity import ActivityType
return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING])
@@ -65,8 +66,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = hass.data[DATA_AUGUST]
devices = []
- from august.lock import LockDoorStatus
-
for door in data.locks:
for sensor_type in SENSOR_TYPES_DOOR:
state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
@@ -136,8 +135,6 @@ class AugustDoorBinarySensor(BinarySensorDevice):
self._state = state_provider(self._data, self._door)
self._available = self._state is not None
- from august.lock import LockDoorStatus
-
self._state = self._state == LockDoorStatus.OPEN
@property
diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py
index 8b8c019eb2d..a541be67097 100644
--- a/homeassistant/components/august/lock.py
+++ b/homeassistant/components/august/lock.py
@@ -2,6 +2,9 @@
from datetime import timedelta
import logging
+from august.activity import ActivityType
+from august.lock import LockStatus
+
from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
@@ -51,8 +54,6 @@ class AugustLock(LockDevice):
self._lock_detail = self._data.get_lock_detail(self._lock.device_id)
- from august.activity import ActivityType
-
activity = self._data.get_latest_device_activity(
self._lock.device_id, ActivityType.LOCK_OPERATION
)
@@ -73,7 +74,6 @@ class AugustLock(LockDevice):
@property
def is_locked(self):
"""Return true if device is on."""
- from august.lock import LockStatus
return self._lock_status is LockStatus.LOCKED
diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json
index 1cb70519b20..6c2e8988d83 100644
--- a/homeassistant/components/auth/.translations/ko.json
+++ b/homeassistant/components/auth/.translations/ko.json
@@ -25,7 +25,7 @@
},
"step": {
"init": {
- "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [\uad6c\uae00 OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
+ "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
"title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
}
},
diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py
index 0f5da5d7527..d6844396ce7 100644
--- a/homeassistant/components/auth/login_flow.py
+++ b/homeassistant/components/auth/login_flow.py
@@ -68,10 +68,15 @@ associate with an credential if "type" set to "link_user" in
"""
from aiohttp import web
import voluptuous as vol
+import voluptuous_serialize
from homeassistant import data_entry_flow
from homeassistant.components.http import KEY_REAL_IP
-from homeassistant.components.http.ban import process_wrong_login, log_invalid_auth
+from homeassistant.components.http.ban import (
+ process_wrong_login,
+ process_success_login,
+ log_invalid_auth,
+)
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from . import indieauth
@@ -120,8 +125,6 @@ def _prepare_result_json(result):
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
return result
- import voluptuous_serialize
-
data = result.copy()
schema = data["data_schema"]
@@ -186,6 +189,7 @@ class LoginFlowIndexView(HomeAssistantView):
return self.json_message("Handler does not support init", 400)
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
+ await process_success_login(request)
result.pop("data")
result["result"] = self._store_result(data["client_id"], result["result"])
return self.json(result)
diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py
index 42dab7ebb5a..271e9ae1634 100644
--- a/homeassistant/components/auth/mfa_setup_flow.py
+++ b/homeassistant/components/auth/mfa_setup_flow.py
@@ -2,6 +2,7 @@
import logging
import voluptuous as vol
+import voluptuous_serialize
from homeassistant import data_entry_flow
from homeassistant.components import websocket_api
@@ -134,8 +135,6 @@ def _prepare_result_json(result):
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
return result
- import voluptuous_serialize
-
data = result.copy()
schema = data["data_schema"]
diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py
index 09cf3f67114..fbb823dd329 100644
--- a/homeassistant/components/automatic/device_tracker.py
+++ b/homeassistant/components/automatic/device_tracker.py
@@ -6,6 +6,8 @@ import logging
import os
from aiohttp import web
+import aioautomatic
+
import voluptuous as vol
from homeassistant.components.device_tracker import (
@@ -82,7 +84,6 @@ def _write_refresh_token_to_file(hass, filename, refresh_token):
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Validate the configuration and return an Automatic scanner."""
- import aioautomatic
hass.http.register_view(AutomaticAuthCallbackView())
@@ -215,7 +216,6 @@ class AutomaticData:
@asyncio.coroutine
def handle_event(self, name, event):
"""Coroutine to update state for a real time event."""
- import aioautomatic
self.hass.bus.async_fire(EVENT_AUTOMATIC_UPDATE, event.data)
@@ -261,7 +261,6 @@ class AutomaticData:
@asyncio.coroutine
def ws_connect(self, now=None):
"""Open the websocket connection."""
- import aioautomatic
self.ws_close_requested = False
@@ -321,7 +320,6 @@ class AutomaticData:
@asyncio.coroutine
def get_vehicle_info(self, vehicle):
"""Fetch the latest vehicle info from automatic."""
- import aioautomatic
name = vehicle.display_name
if name is None:
diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index f669d415854..3409ce832dd 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -7,9 +7,6 @@ from typing import Any, Awaitable, Callable
import voluptuous as vol
-from homeassistant.components.device_automation.exceptions import (
- InvalidDeviceAutomationConfig,
-)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_NAME,
@@ -476,10 +473,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
for conf in trigger_configs:
platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__)
- try:
- remove = await platform.async_attach_trigger(hass, conf, action, info)
- except InvalidDeviceAutomationConfig:
- remove = False
+ remove = await platform.async_attach_trigger(hass, conf, action, info)
if not remove:
_LOGGER.error("Error setting up trigger %s", name)
diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py
index ebbd1771e84..5733cd2e83e 100644
--- a/homeassistant/components/automation/config.py
+++ b/homeassistant/components/automation/config.py
@@ -4,6 +4,9 @@ import importlib
import voluptuous as vol
+from homeassistant.components.device_automation.exceptions import (
+ InvalidDeviceAutomationConfig,
+)
from homeassistant.const import CONF_PLATFORM
from homeassistant.config import async_log_exception, config_without_domain
from homeassistant.exceptions import HomeAssistantError
@@ -52,7 +55,12 @@ async def _try_async_validate_config_item(hass, config, full_config=None):
"""Validate config item."""
try:
config = await async_validate_config_item(hass, config, full_config)
- except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex:
+ except (
+ vol.Invalid,
+ HomeAssistantError,
+ IntegrationNotFound,
+ InvalidDeviceAutomationConfig,
+ ) as ex:
async_log_exception(ex, DOMAIN, full_config or config, hass)
return None
diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py
index dc65008c3fb..ced8f65cbf5 100644
--- a/homeassistant/components/automation/device.py
+++ b/homeassistant/components/automation/device.py
@@ -18,6 +18,9 @@ async def async_validate_trigger_config(hass, config):
platform = await async_get_device_automation_platform(
hass, config[CONF_DOMAIN], "trigger"
)
+ if hasattr(platform, "async_validate_trigger_config"):
+ return await getattr(platform, "async_validate_trigger_config")(hass, config)
+
return platform.TRIGGER_SCHEMA(config)
diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py
new file mode 100644
index 00000000000..553d6871087
--- /dev/null
+++ b/homeassistant/components/automation/reproduce_state.py
@@ -0,0 +1,61 @@
+"""Reproduce an Automation state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ STATE_ON,
+ STATE_OFF,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+)
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+VALID_STATES = {STATE_ON, STATE_OFF}
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if state.state not in VALID_STATES:
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state:
+ return
+
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+
+ if state.state == STATE_ON:
+ service = SERVICE_TURN_ON
+ elif state.state == STATE_OFF:
+ service = SERVICE_TURN_OFF
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Automation states."""
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py
index c899e009796..f15e4a80e36 100644
--- a/homeassistant/components/awair/sensor.py
+++ b/homeassistant/components/awair/sensor.py
@@ -4,6 +4,7 @@ from datetime import timedelta
import logging
import math
+from python_awair import AwairClient
import voluptuous as vol
from homeassistant.const import (
@@ -105,7 +106,6 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
# used at this time is the `uuid` value.
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Connect to the Awair API and find devices."""
- from python_awair import AwairClient
token = config[CONF_ACCESS_TOKEN]
client = AwairClient(token, session=async_get_clientsession(hass))
diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py
index 1959cc05e80..780a65b2d47 100644
--- a/homeassistant/components/aws/__init__.py
+++ b/homeassistant/components/aws/__init__.py
@@ -3,6 +3,8 @@ import asyncio
import logging
from collections import OrderedDict
+import aiobotocore
+
import voluptuous as vol
from homeassistant import config_entries
@@ -151,7 +153,6 @@ async def async_setup_entry(hass, entry):
async def _validate_aws_credentials(hass, credential):
"""Validate AWS credential config."""
- import aiobotocore
aws_config = credential.copy()
del aws_config[CONF_NAME]
diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py
index fa1cf3fa363..2afa9a3a402 100644
--- a/homeassistant/components/aws/notify.py
+++ b/homeassistant/components/aws/notify.py
@@ -4,6 +4,8 @@ import base64
import json
import logging
+import aiobotocore
+
from homeassistant.components.notify import (
ATTR_TARGET,
ATTR_TITLE,
@@ -26,7 +28,6 @@ _LOGGER = logging.getLogger(__name__)
async def get_available_regions(hass, service):
"""Get available regions for a service."""
- import aiobotocore
session = aiobotocore.get_session()
# get_available_regions is not a coroutine since it does not perform
@@ -41,8 +42,6 @@ async def async_get_service(hass, config, discovery_info=None):
_LOGGER.error("Please config aws notify platform in aws component")
return None
- import aiobotocore
-
session = None
conf = discovery_info
diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json
index 75dd89ef9c1..3458dcc4529 100644
--- a/homeassistant/components/axis/.translations/ca.json
+++ b/homeassistant/components/axis/.translations/ca.json
@@ -12,6 +12,7 @@
"device_unavailable": "El dispositiu no est\u00e0 disponible",
"faulty_credentials": "Credencials d'usuari incorrectes"
},
+ "flow_title": "Dispositiu d'eix: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/da.json b/homeassistant/components/axis/.translations/da.json
index 2d728468fc7..c169f85f280 100644
--- a/homeassistant/components/axis/.translations/da.json
+++ b/homeassistant/components/axis/.translations/da.json
@@ -12,6 +12,7 @@
"device_unavailable": "Enheden er ikke tilg\u00e6ngelig",
"faulty_credentials": "Ugyldige legitimationsoplysninger"
},
+ "flow_title": "Axis enhed: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json
index 5fd5d9be565..c7d84aa8cc3 100644
--- a/homeassistant/components/axis/.translations/en.json
+++ b/homeassistant/components/axis/.translations/en.json
@@ -12,6 +12,7 @@
"device_unavailable": "Device is not available",
"faulty_credentials": "Bad user credentials"
},
+ "flow_title": "Axis device: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json
index d29481a3be9..3f7db674fdf 100644
--- a/homeassistant/components/axis/.translations/es.json
+++ b/homeassistant/components/axis/.translations/es.json
@@ -12,6 +12,7 @@
"device_unavailable": "El dispositivo no est\u00e1 disponible",
"faulty_credentials": "Credenciales de usuario incorrectas"
},
+ "flow_title": "Dispositivo Axis: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json
index 24afb4a226c..608e12d020a 100644
--- a/homeassistant/components/axis/.translations/fr.json
+++ b/homeassistant/components/axis/.translations/fr.json
@@ -12,6 +12,7 @@
"device_unavailable": "L'appareil n'est pas disponible",
"faulty_credentials": "Mauvaises informations d'identification de l'utilisateur"
},
+ "flow_title": "Appareil Axis: {name} ( {host} )",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json
index e979af08836..3f303140c68 100644
--- a/homeassistant/components/axis/.translations/it.json
+++ b/homeassistant/components/axis/.translations/it.json
@@ -12,6 +12,7 @@
"device_unavailable": "Il dispositivo non \u00e8 disponibile",
"faulty_credentials": "Credenziali utente non valide"
},
+ "flow_title": "Dispositivo Axis: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/ko.json b/homeassistant/components/axis/.translations/ko.json
index 5ceaa082810..f02b7cdcefa 100644
--- a/homeassistant/components/axis/.translations/ko.json
+++ b/homeassistant/components/axis/.translations/ko.json
@@ -12,6 +12,7 @@
"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"
},
+ "flow_title": "Axis \uae30\uae30: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/lb.json b/homeassistant/components/axis/.translations/lb.json
index 281eaa7c881..24ee0e24125 100644
--- a/homeassistant/components/axis/.translations/lb.json
+++ b/homeassistant/components/axis/.translations/lb.json
@@ -12,6 +12,7 @@
"device_unavailable": "Apparat ass net erreechbar",
"faulty_credentials": "Ong\u00eblteg Login Informatioune"
},
+ "flow_title": "Axis Apparat: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/nl.json b/homeassistant/components/axis/.translations/nl.json
index 83395283404..10fc8c02d66 100644
--- a/homeassistant/components/axis/.translations/nl.json
+++ b/homeassistant/components/axis/.translations/nl.json
@@ -12,6 +12,7 @@
"device_unavailable": "Apparaat is niet beschikbaar",
"faulty_credentials": "Ongeldige gebruikersreferenties"
},
+ "flow_title": "Axis apparaat: {naam} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json
index 29022e39745..190737e5a76 100644
--- a/homeassistant/components/axis/.translations/no.json
+++ b/homeassistant/components/axis/.translations/no.json
@@ -12,6 +12,7 @@
"device_unavailable": "Enheten er ikke tilgjengelig",
"faulty_credentials": "Ugyldig brukerlegitimasjon"
},
+ "flow_title": "Akse-enhet: {Name} ({Host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json
index 88e80360536..4ca87310f48 100644
--- a/homeassistant/components/axis/.translations/pl.json
+++ b/homeassistant/components/axis/.translations/pl.json
@@ -12,6 +12,7 @@
"device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne",
"faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce"
},
+ "flow_title": "Urz\u0105dzenie Axis: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json
index 951263d53f9..0345862b865 100644
--- a/homeassistant/components/axis/.translations/ru.json
+++ b/homeassistant/components/axis/.translations/ru.json
@@ -1,17 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438",
- "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f",
- "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis"
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.",
+ "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.",
+ "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis."
},
"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_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.",
- "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"
+ "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."
},
+ "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/sl.json b/homeassistant/components/axis/.translations/sl.json
index 205e901553e..5ffa02e19f7 100644
--- a/homeassistant/components/axis/.translations/sl.json
+++ b/homeassistant/components/axis/.translations/sl.json
@@ -12,6 +12,7 @@
"device_unavailable": "Naprava ni na voljo",
"faulty_credentials": "Napa\u010dni uporabni\u0161ki podatki"
},
+ "flow_title": "OS naprava: {Name} ({Host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json
index c0d0df02135..6c78fc2166c 100644
--- a/homeassistant/components/axis/.translations/zh-Hant.json
+++ b/homeassistant/components/axis/.translations/zh-Hant.json
@@ -12,6 +12,7 @@
"device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528",
"faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548"
},
+ "flow_title": "Axis \u8a2d\u5099\uff1a{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py
index 3b5efe96760..5eb4f9daddd 100644
--- a/homeassistant/components/axis/config_flow.py
+++ b/homeassistant/components/axis/config_flow.py
@@ -171,7 +171,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if discovery_info[CONF_HOST].startswith("169.254"):
return self.async_abort(reason="link_local_address")
- # pylint: disable=unsupported-assignment-operation
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["macaddress"] = serialnumber
if any(
@@ -191,6 +191,12 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
load_json, self.hass.config.path(CONFIG_FILE)
)
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ self.context["title_placeholders"] = {
+ "name": discovery_info["hostname"][:-7],
+ "host": discovery_info[CONF_HOST],
+ }
+
if serialnumber not in config_file:
self.discovery_schema = {
vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str,
@@ -198,6 +204,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int,
}
+
return await self.async_step_user()
try:
diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py
index 3b91f7e1474..e42a758f3c4 100644
--- a/homeassistant/components/axis/device.py
+++ b/homeassistant/components/axis/device.py
@@ -3,6 +3,9 @@
import asyncio
import async_timeout
+import axis
+from axis.streammanager import SIGNAL_PLAYING
+
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
@@ -140,7 +143,6 @@ class AxisNetworkDevice:
This is called on every RTSP keep-alive message.
Only signal state change if state change is true.
"""
- from axis.streammanager import SIGNAL_PLAYING
if self.available != (status == SIGNAL_PLAYING):
self.available = not self.available
@@ -198,7 +200,6 @@ class AxisNetworkDevice:
async def get_device(hass, config):
"""Create a Axis device."""
- import axis
device = axis.AxisDevice(
loop=hass.loop,
diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json
index 29fe09b7e5b..2dc23f3e466 100644
--- a/homeassistant/components/axis/strings.json
+++ b/homeassistant/components/axis/strings.json
@@ -1,6 +1,7 @@
{
"config": {
"title": "Axis device",
+ "flow_title": "Axis device: {name} ({host})",
"step": {
"user": {
"title": "Set up Axis device",
diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py
index 85737d1affd..8d753753e5a 100644
--- a/homeassistant/components/baidu/tts.py
+++ b/homeassistant/components/baidu/tts.py
@@ -1,6 +1,7 @@
"""Support for Baidu speech service."""
import logging
+from aip import AipSpeech
import voluptuous as vol
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
@@ -106,7 +107,6 @@ class BaiduTTSProvider(Provider):
def get_tts_audio(self, message, language, options=None):
"""Load TTS from BaiduTTS."""
- from aip import AipSpeech
aip_speech = AipSpeech(
self._app_data["appid"],
diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py
index acefc5a3b26..ffa13a6288c 100644
--- a/homeassistant/components/bayesian/binary_sensor.py
+++ b/homeassistant/components/bayesian/binary_sensor.py
@@ -250,7 +250,7 @@ class BayesianBinarySensor(BinarySensorDevice):
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
- ATTR_OBSERVATIONS: [val for val in self.current_obs.values()],
+ ATTR_OBSERVATIONS: list(self.current_obs.values()),
ATTR_PROBABILITY: round(self.probability, 2),
ATTR_PROBABILITY_THRESHOLD: self._probability_threshold,
}
diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py
index bfaa2a7c50d..e68633c0688 100644
--- a/homeassistant/components/bbb_gpio/__init__.py
+++ b/homeassistant/components/bbb_gpio/__init__.py
@@ -1,6 +1,8 @@
"""Support for controlling GPIO pins of a Beaglebone Black."""
import logging
+from Adafruit_BBIO import GPIO # pylint: disable=import-error
+
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
_LOGGER = logging.getLogger(__name__)
@@ -11,7 +13,6 @@ DOMAIN = "bbb_gpio"
def setup(hass, config):
"""Set up the BeagleBone Black GPIO component."""
# pylint: disable=import-error
- from Adafruit_BBIO import GPIO
def cleanup_gpio(event):
"""Stuff to do before stopping."""
@@ -27,39 +28,29 @@ def setup(hass, config):
def setup_output(pin):
"""Set up a GPIO as output."""
- # pylint: disable=import-error
- from Adafruit_BBIO import GPIO
GPIO.setup(pin, GPIO.OUT)
def setup_input(pin, pull_mode):
"""Set up a GPIO as input."""
- # pylint: disable=import-error
- from Adafruit_BBIO import GPIO
GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP)
def write_output(pin, value):
"""Write a value to a GPIO."""
- # pylint: disable=import-error
- from Adafruit_BBIO import GPIO
GPIO.output(pin, value)
def read_input(pin):
"""Read a value from a GPIO."""
- # pylint: disable=import-error
- from Adafruit_BBIO import GPIO
return GPIO.input(pin) is GPIO.HIGH
def edge_detect(pin, event_callback, bounce):
"""Add detection for RISING and FALLING events."""
- # pylint: disable=import-error
- from Adafruit_BBIO import GPIO
GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py
index 89449aeab45..122016ecf96 100644
--- a/homeassistant/components/bbox/device_tracker.py
+++ b/homeassistant/components/bbox/device_tracker.py
@@ -4,6 +4,8 @@ from datetime import timedelta
import logging
from typing import List
+import pybbox
+
import voluptuous as vol
from homeassistant.components.device_tracker import (
@@ -75,8 +77,6 @@ class BboxDeviceScanner(DeviceScanner):
"""
_LOGGER.info("Scanning...")
- import pybbox
-
box = pybbox.Bbox(ip=self.host)
result = box.get_all_connected_devices()
diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py
index ba38f8d2607..ad6bcc39796 100644
--- a/homeassistant/components/bbox/sensor.py
+++ b/homeassistant/components/bbox/sensor.py
@@ -3,6 +3,8 @@ import logging
from datetime import timedelta
import requests
+import pybbox
+
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -136,7 +138,6 @@ class BboxData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from the Bbox."""
- import pybbox
try:
box = pybbox.Bbox()
diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py
index 0a305c21adb..cc91fa48bae 100644
--- a/homeassistant/components/bh1750/sensor.py
+++ b/homeassistant/components/bh1750/sensor.py
@@ -2,6 +2,9 @@
from functools import partial
import logging
+import smbus # pylint: disable=import-error
+from i2csense.bh1750 import BH1750 # pylint: disable=import-error
+
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -60,8 +63,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the BH1750 sensor."""
- import smbus # pylint: disable=import-error
- from i2csense.bh1750 import BH1750 # pylint: disable=import-error
name = config.get(CONF_NAME)
bus_number = config.get(CONF_I2C_BUS)
diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json
index de7d837b12c..8bbd19a0d45 100644
--- a/homeassistant/components/binary_sensor/.translations/ca.json
+++ b/homeassistant/components/binary_sensor/.translations/ca.json
@@ -53,6 +53,7 @@
"hot": "{entity_name} es torna calent",
"light": "{entity_name} ha comen\u00e7at a detectar llum",
"locked": "{entity_name} est\u00e0 bloquejat",
+ "moist": "{entity_name} es torna humit",
"moist\u00a7": "{entity_name} es torna humit",
"motion": "{entity_name} ha comen\u00e7at a detectar moviment",
"moving": "{entity_name} ha comen\u00e7at a moure's",
@@ -71,6 +72,7 @@
"not_moist": "{entity_name} es torna sec",
"not_moving": "{entity_name} ha parat de moure's",
"not_occupied": "{entity_name} es desocupa",
+ "not_opened": "{entity_name} es tanca",
"not_plugged_in": "{entity_name} desendollat",
"not_powered": "{entity_name} no est\u00e0 alimentat",
"not_present": "{entity_name} no est\u00e0 present",
diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json
index 56822c2365c..f7bd834561c 100644
--- a/homeassistant/components/binary_sensor/.translations/da.json
+++ b/homeassistant/components/binary_sensor/.translations/da.json
@@ -39,6 +39,7 @@
"closed": "{entity_name} lukket",
"cold": "{entity_name} blev kold",
"connected": "{entity_name} tilsluttet",
+ "moist": "{entity_name} blev fugtig",
"moist\u00a7": "{entity_name} blev fugtig",
"motion": "{entity_name} begyndte at registrere bev\u00e6gelse",
"moving": "{entity_name} begyndte at bev\u00e6ge sig",
@@ -53,6 +54,7 @@
"not_hot": "{entity_name} blev ikke varm",
"not_locked": "{entity_name} l\u00e5st op",
"not_moist": "{entity_name} blev t\u00f8r",
+ "not_opened": "{entity_name} lukket",
"not_present": "{entity_name} ikke til stede",
"not_unsafe": "{entity_name} blev sikker",
"occupied": "{entity_name} blev optaget",
diff --git a/homeassistant/components/binary_sensor/.translations/de.json b/homeassistant/components/binary_sensor/.translations/de.json
new file mode 100644
index 00000000000..e246198864b
--- /dev/null
+++ b/homeassistant/components/binary_sensor/.translations/de.json
@@ -0,0 +1,94 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "{entity_name} Batterie ist schwach",
+ "is_cold": "{entity_name} ist kalt",
+ "is_connected": "{entity_name} ist verbunden",
+ "is_gas": "{entity_name} erkennt Gas",
+ "is_hot": "{entity_name} ist hei\u00df",
+ "is_light": "{entity_name} erkennt Licht",
+ "is_locked": "{entity_name} ist gesperrt",
+ "is_moist": "{entity_name} ist feucht",
+ "is_motion": "{entity_name} erkennt Bewegung",
+ "is_moving": "{entity_name} bewegt sich",
+ "is_no_gas": "{entity_name} erkennt kein Gas",
+ "is_no_light": "{entity_name} erkennt kein Licht",
+ "is_no_motion": "{entity_name} erkennt keine Bewegung",
+ "is_no_problem": "{entity_name} erkennt kein Problem",
+ "is_no_smoke": "{entity_name} erkennt keinen Rauch",
+ "is_no_sound": "{entity_name} erkennt keine Ger\u00e4usche",
+ "is_no_vibration": "{entity_name} erkennt keine Vibrationen",
+ "is_not_bat_low": "{entity_name} Batterie ist normal",
+ "is_not_cold": "{entity_name} ist nicht kalt",
+ "is_not_connected": "{entity_name} ist nicht verbunden",
+ "is_not_hot": "{entity_name} ist nicht hei\u00df",
+ "is_not_locked": "{entity_name} ist entsperrt",
+ "is_not_moist": "{entity_name} ist trocken",
+ "is_not_moving": "{entity_name} bewegt sich nicht",
+ "is_not_occupied": "{entity_name} ist nicht besch\u00e4ftigt / besetzt",
+ "is_not_open": "{entity_name} ist geschlossen",
+ "is_not_plugged_in": "{entity_name} ist nicht angeschlossen",
+ "is_not_powered": "{entity_name} wird nicht mit Strom versorgt",
+ "is_not_present": "{entity_name} ist nicht vorhanden",
+ "is_not_unsafe": "{entity_name} ist sicher",
+ "is_occupied": "{entity_name} ist besch\u00e4ftigt / besetzt",
+ "is_off": "{entity_name} ist ausgeschaltet",
+ "is_on": "{entity_name} ist eingeschaltet",
+ "is_open": "{entity_name} ist offen",
+ "is_plugged_in": "{entity_name} ist eingesteckt",
+ "is_powered": "{entity_name} wird mit Strom versorgt",
+ "is_present": "{entity_name} ist vorhanden",
+ "is_problem": "{entity_name} hat ein Problem festgestellt",
+ "is_smoke": "{entity_name} hat Rauch detektiert",
+ "is_sound": "{entity_name} hat Ger\u00e4usche detektiert",
+ "is_unsafe": "{entity_name} ist unsicher",
+ "is_vibration": "{entity_name} erkennt Vibrationen."
+ },
+ "trigger_type": {
+ "bat_low": "{entity_name} Batterie schwach",
+ "closed": "{entity_name} geschlossen",
+ "cold": "{entity_name} wurde kalt",
+ "connected": "{entity_name} verbunden",
+ "gas": "{entity_name} hat Gas detektiert",
+ "hot": "{entity_name} wurde hei\u00df",
+ "light": "{entity_name} hat Licht detektiert",
+ "locked": "{entity_name} gesperrt",
+ "moist": "{entity_name} wurde feucht",
+ "moist\u00a7": "{entity_name} wurde feucht",
+ "motion": "{entity_name} hat Bewegungen detektiert",
+ "moving": "{entity_name} hat angefangen sich zu bewegen",
+ "no_gas": "{entity_name} hat kein Gas mehr erkannt",
+ "no_light": "{entity_name} hat kein Licht mehr erkannt",
+ "no_motion": "{entity_name} hat keine Bewegung mehr erkannt",
+ "no_problem": "{entity_name} hat kein Problem mehr erkannt",
+ "no_smoke": "{entity_name} hat keinen Rauch mehr erkannt",
+ "no_sound": "{entity_name} hat keine Ger\u00e4usche mehr erkannt",
+ "no_vibration": "{entity_name}hat keine Vibrationen mehr erkannt",
+ "not_bat_low": "{entity_name} Batterie normal",
+ "not_cold": "{entity_name} w\u00e4rmte auf",
+ "not_connected": "{entity_name} getrennt",
+ "not_hot": "{entity_name} k\u00fchlte ab",
+ "not_locked": "{entity_name} entsperrt",
+ "not_moist": "{entity_name} wurde trocken",
+ "not_moving": "{entity_name} bewegt sich nicht mehr",
+ "not_occupied": "{entity_name} wurde frei / inaktiv",
+ "not_opened": "{entity_name} geschlossen",
+ "not_plugged_in": "{entity_name} ist nicht angeschlossen",
+ "not_powered": "{entity_name} nicht mit Strom versorgt",
+ "not_present": "{entity_name} nicht anwesend",
+ "not_unsafe": "{entity_name} wurde sicher",
+ "occupied": "{entity_name} wurde besch\u00e4ftigt / besetzt",
+ "opened": "{entity_name} ge\u00f6ffnet",
+ "plugged_in": "{entity_name} eingesteckt",
+ "powered": "{entity_name} wird mit Strom versorgt",
+ "present": "{entity_name} anwesend",
+ "problem": "{entity_name} hat ein Problem festgestellt",
+ "smoke": "{entity_name} detektiert Rauch",
+ "sound": "{entity_name} detektiert Ger\u00e4usche",
+ "turned_off": "{entity_name} ausgeschaltet",
+ "turned_on": "{entity_name} eingeschaltet",
+ "unsafe": "{entity_name} ist unsicher",
+ "vibration": "{entity_name} detektiert Vibrationen"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json
index 8e2d326d9d3..756a370ca3c 100644
--- a/homeassistant/components/binary_sensor/.translations/es.json
+++ b/homeassistant/components/binary_sensor/.translations/es.json
@@ -53,6 +53,7 @@
"hot": "{entity_name} se est\u00e1 calentando",
"light": "{entity_name} empez\u00f3 a detectar la luz",
"locked": "{entity_name} bloqueado",
+ "moist": "{entity_name} se humedece",
"moist\u00a7": "{entity_name} se humedeci\u00f3",
"motion": "{entity_name} comenz\u00f3 a detectar movimiento",
"moving": "{entity_name} empez\u00f3 a moverse",
@@ -71,6 +72,7 @@
"not_moist": "{entity_name} se sec\u00f3",
"not_moving": "{entity_name} dej\u00f3 de moverse",
"not_occupied": "{entity_name} no est\u00e1 ocupado",
+ "not_opened": "{nombre_de_la_entidad} cerrado",
"not_plugged_in": "{entity_name} desconectado",
"not_powered": "{entity_name} no est\u00e1 activado",
"not_present": "{entity_name} no est\u00e1 presente",
diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json
index 80792f16635..4d9bcefbe66 100644
--- a/homeassistant/components/binary_sensor/.translations/fr.json
+++ b/homeassistant/components/binary_sensor/.translations/fr.json
@@ -9,7 +9,7 @@
"is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re",
"is_locked": "{entity_name} est verrouill\u00e9",
"is_moist": "{entity_name} est humide",
- "is_motion": "{entity_name} d\u00e9tecte un mouvement",
+ "is_motion": "{entity_name} d\u00e9tecte du mouvement",
"is_moving": "{entity_name} se d\u00e9place",
"is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz",
"is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re",
@@ -40,12 +40,52 @@
"is_present": "{entity_name} est pr\u00e9sent",
"is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me",
"is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e",
- "is_sound": "{entity_name} d\u00e9tecte du son"
+ "is_sound": "{entity_name} d\u00e9tecte du son",
+ "is_unsafe": "{entity_name} est dangereux",
+ "is_vibration": "{entity_name} d\u00e9tecte des vibrations"
},
"trigger_type": {
+ "bat_low": "{entity_name} batterie faible",
+ "closed": "{entity_name} ferm\u00e9",
+ "cold": "{entity_name} est devenu froid",
+ "connected": "{entity_name} connect\u00e9",
+ "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz",
+ "hot": "{entity_name} est devenu chaud",
+ "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter la lumi\u00e8re",
+ "locked": "{entity_name} verrouill\u00e9",
+ "moist": "{entity_name} est devenu humide",
+ "moist\u00a7": "{entity_name} est devenu humide",
+ "motion": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du mouvement",
+ "moving": "{entity_name} a commenc\u00e9 \u00e0 se d\u00e9placer",
+ "no_gas": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le gaz",
+ "no_light": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter la lumi\u00e8re",
+ "no_motion": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le mouvement",
+ "no_problem": "{entity_name} a cess\u00e9 de d\u00e9tecter un probl\u00e8me",
+ "no_smoke": "{entity_name} a cess\u00e9 de d\u00e9tecter de la fum\u00e9e",
+ "no_sound": "{entity_name} a cess\u00e9 de d\u00e9tecter du bruit",
+ "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter des vibrations",
+ "not_bat_low": "{entity_name} batterie normale",
+ "not_cold": "{entity_name} n'est plus froid",
+ "not_connected": "{entity_name} d\u00e9connect\u00e9",
+ "not_hot": "{entity_name} n'est plus chaud",
+ "not_locked": "{entity_name} d\u00e9verrouill\u00e9",
+ "not_moist": "{entity_name} est devenu sec",
+ "not_moving": "{entity_name} a cess\u00e9 de bouger",
+ "not_occupied": "{entity_name} est devenu non occup\u00e9",
+ "not_opened": "{entity_name} ferm\u00e9",
+ "not_plugged_in": "{entity_name} d\u00e9branch\u00e9",
+ "not_powered": "{entity_name} non aliment\u00e9",
+ "not_present": "{entity_name} non pr\u00e9sent",
+ "not_unsafe": "{entity_name} est devenu s\u00fbr",
+ "occupied": "{entity_name} est devenu occup\u00e9",
+ "opened": "{entity_name} ouvert",
+ "plugged_in": "{entity_name} branch\u00e9",
+ "powered": "{entity_name} aliment\u00e9",
+ "present": "{entity_name} pr\u00e9sent",
+ "problem": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter un probl\u00e8me",
"smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e",
"sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son",
- "turned_off": "{entity_name} d\u00e9sactiv\u00e9",
+ "turned_off": "{entity_name} est d\u00e9sactiv\u00e9",
"turned_on": "{entity_name} activ\u00e9",
"unsafe": "{entity_name} est devenu dangereux",
"vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations"
diff --git a/homeassistant/components/binary_sensor/.translations/it.json b/homeassistant/components/binary_sensor/.translations/it.json
index 0583a4d4f74..c69f5a07a41 100644
--- a/homeassistant/components/binary_sensor/.translations/it.json
+++ b/homeassistant/components/binary_sensor/.translations/it.json
@@ -53,6 +53,7 @@
"hot": "{entity_name} \u00e8 diventato caldo",
"light": "{entity_name} ha iniziato a rilevare la luce",
"locked": "{entity_name} bloccato",
+ "moist": "{entity_name} diventato umido",
"moist\u00a7": "{entity_name} \u00e8 diventato umido",
"motion": "{entity_name} ha iniziato a rilevare il movimento",
"moving": "{entity_name} ha iniziato a muoversi",
@@ -71,6 +72,7 @@
"not_moist": "{entity_name} \u00e8 diventato asciutto",
"not_moving": "{entity_name} ha smesso di muoversi",
"not_occupied": "{entity_name} non \u00e8 occupato",
+ "not_opened": "{entity_name} chiuso",
"not_plugged_in": "{entity_name} \u00e8 scollegato",
"not_powered": "{entity_name} non \u00e8 alimentato",
"not_present": "{entity_name} non \u00e8 presente",
diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json
index 3c12eabe8ff..167708c2cf1 100644
--- a/homeassistant/components/binary_sensor/.translations/ko.json
+++ b/homeassistant/components/binary_sensor/.translations/ko.json
@@ -1,7 +1,7 @@
{
"device_automation": {
"condition_type": {
- "is_bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4",
+ "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4",
"is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4",
"is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4",
@@ -18,20 +18,20 @@
"is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
- "is_not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4",
+ "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4",
"is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
"is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4",
"is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
"is_not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud569\ub2c8\ub2e4",
"is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
- "is_not_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
+ "is_not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
"is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4",
"is_not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud614\uc2b5\ub2c8\ub2e4",
"is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
"is_not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc2b5\ub2c8\ub2e4",
"is_not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud569\ub2c8\ub2e4",
- "is_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4",
+ "is_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4",
"is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4",
"is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4",
"is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4",
@@ -45,8 +45,50 @@
"is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4"
},
"trigger_type": {
- "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871",
- "closed": "{entity_name} \ub2eb\ud798"
+ "bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871",
+ "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798",
+ "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9d0",
+ "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub428",
+ "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud568",
+ "hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6cc\uc9d0",
+ "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud568",
+ "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae40",
+ "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0",
+ "moist\u00a7": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0",
+ "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud568",
+ "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784",
+ "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0 \ubabb\ud568",
+ "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0 \ubabb\ud568",
+ "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0 \ubabb\ud568",
+ "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0 \ubabb\ud568",
+ "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0 \ubabb\ud568",
+ "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0 \ubabb\ud568",
+ "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0 \ubabb\ud568",
+ "not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc815\uc0c1",
+ "not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc74c",
+ "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9d0",
+ "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc74c",
+ "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub428",
+ "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9d0",
+ "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc74c",
+ "not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc74c",
+ "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud798",
+ "not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud798",
+ "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc74c",
+ "not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc74c",
+ "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9d0",
+ "occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911",
+ "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc",
+ "plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud798",
+ "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub428",
+ "present": "{entity_name} \uc774(\uac00) \uc788\uc74c",
+ "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud568",
+ "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud568",
+ "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud568",
+ "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9d0",
+ "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9d0",
+ "unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc74c",
+ "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud568"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/.translations/lb.json b/homeassistant/components/binary_sensor/.translations/lb.json
index 0b10e1f51a5..c65ae94396b 100644
--- a/homeassistant/components/binary_sensor/.translations/lb.json
+++ b/homeassistant/components/binary_sensor/.translations/lb.json
@@ -53,6 +53,7 @@
"hot": "{entity_name} gouf waarm",
"light": "{entity_name} huet ugefange Luucht z'entdecken",
"locked": "{entity_name} gespaart",
+ "moist": "{entity_name} gouf fiicht",
"moist\u00a7": "{entity_name} gouf fiicht",
"motion": "{entity_name} huet ugefaange Beweegung z'entdecken",
"moving": "{entity_name} huet ugefaangen sech ze beweegen",
@@ -71,6 +72,7 @@
"not_moist": "{entity_name} gouf dr\u00e9chen",
"not_moving": "{entity_name} huet opgehale sech ze beweegen",
"not_occupied": "{entity_name} gouf fr\u00e4i",
+ "not_opened": "{entity_name} gouf zougemaach",
"not_plugged_in": "{entity_name} net ugeschloss",
"not_powered": "{entity_name} net aliment\u00e9iert",
"not_present": "{entity_name} net pr\u00e4sent",
diff --git a/homeassistant/components/binary_sensor/.translations/lv.json b/homeassistant/components/binary_sensor/.translations/lv.json
new file mode 100644
index 00000000000..7668dfa5ac8
--- /dev/null
+++ b/homeassistant/components/binary_sensor/.translations/lv.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "turned_off": "{entity_name} tika izsl\u0113gta",
+ "turned_on": "{entity_name} tika iesl\u0113gta"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/.translations/nl.json b/homeassistant/components/binary_sensor/.translations/nl.json
new file mode 100644
index 00000000000..508a06b38a2
--- /dev/null
+++ b/homeassistant/components/binary_sensor/.translations/nl.json
@@ -0,0 +1,94 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "{entity_name} batterij is bijna leeg",
+ "is_cold": "{entity_name} is koud",
+ "is_connected": "{entity_name} is verbonden",
+ "is_gas": "{entity_name} detecteert gas",
+ "is_hot": "{entity_name} is hot",
+ "is_light": "{entity_name} detecteert licht",
+ "is_locked": "{entity_name} is vergrendeld",
+ "is_moist": "{entity_name} is vochtig",
+ "is_motion": "{entity_name} detecteert beweging",
+ "is_moving": "{entity_name} is in beweging",
+ "is_no_gas": "{entity_name} detecteert geen gas",
+ "is_no_light": "{entity_name} detecteert geen licht",
+ "is_no_motion": "{entity_name} detecteert geen beweging",
+ "is_no_problem": "{entity_name} detecteert geen probleem",
+ "is_no_smoke": "{entity_name} detecteert geen rook",
+ "is_no_sound": "{entity_name} detecteert geen geluid",
+ "is_no_vibration": "{entity_name} detecteert geen trillingen",
+ "is_not_bat_low": "{entity_name} batterij is normaal",
+ "is_not_cold": "{entity_name} is niet koud",
+ "is_not_connected": "{entity_name} is niet verbonden",
+ "is_not_hot": "{entity_name} is niet heet",
+ "is_not_locked": "{entity_name} is ontgrendeld",
+ "is_not_moist": "{entity_name} is droog",
+ "is_not_moving": "{entity_name} beweegt niet",
+ "is_not_occupied": "{entity_name} is niet bezet",
+ "is_not_open": "{entity_name} is gesloten",
+ "is_not_plugged_in": "{entity_name} is niet aangesloten",
+ "is_not_powered": "{entity_name} is niet van stroom voorzien...",
+ "is_not_present": "{entity_name} is niet aanwezig",
+ "is_not_unsafe": "{entity_name} is veilig",
+ "is_occupied": "{entity_name} bezet is",
+ "is_off": "{entity_name} is uitgeschakeld",
+ "is_on": "{entity_name} is ingeschakeld",
+ "is_open": "{entity_name} is open",
+ "is_plugged_in": "{entity_name} is aangesloten",
+ "is_powered": "{entity_name} is van stroom voorzien....",
+ "is_present": "{entity_name} is aanwezig",
+ "is_problem": "{entity_name} detecteert een probleem",
+ "is_smoke": "{entity_name} detecteert rook",
+ "is_sound": "{entity_name} detecteert geluid",
+ "is_unsafe": "{entity_name} is onveilig",
+ "is_vibration": "{entity_name} detecteert trillingen"
+ },
+ "trigger_type": {
+ "bat_low": "{entity_name} batterij bijna leeg",
+ "closed": "{entity_name} gesloten",
+ "cold": "{entity_name} werd koud",
+ "connected": "{entity_name} verbonden",
+ "gas": "{entity_name} begon gas te detecteren",
+ "hot": "{entity_name} werd heet",
+ "light": "{entity_name} begon licht te detecteren",
+ "locked": "{entity_name} vergrendeld",
+ "moist": "{entity_name} werd vochtig",
+ "moist\u00a7": "{entity_name} werd vochtig",
+ "motion": "{entity_name} begon beweging te detecteren",
+ "moving": "{entity_name} begon te bewegen",
+ "no_gas": "{entity_name} is gestopt met het detecteren van gas",
+ "no_light": "{entity_name} gestopt met het detecteren van licht",
+ "no_motion": "{entity_name} gestopt met het detecteren van beweging",
+ "no_problem": "{entity_name} gestopt met het detecteren van het probleem",
+ "no_smoke": "{entity_name} gestopt met het detecteren van rook",
+ "no_sound": "{entity_name} gestopt met het detecteren van geluid",
+ "no_vibration": "{entity_name} gestopt met het detecteren van trillingen",
+ "not_bat_low": "{entity_name} batterij normaal",
+ "not_cold": "{entity_name} werd niet koud",
+ "not_connected": "{entity_name} verbroken",
+ "not_hot": "{entity_name} werd niet warm",
+ "not_locked": "{entity_name} ontgrendeld",
+ "not_moist": "{entity_name} werd droog",
+ "not_moving": "{entity_name} gestopt met bewegen",
+ "not_occupied": "{entity_name} werd niet bezet",
+ "not_opened": "{entity_name} gesloten",
+ "not_plugged_in": "{entity_name} niet verbonden",
+ "not_powered": "{entity_name} niet ingeschakeld",
+ "not_present": "{entity_name} is niet aanwezig",
+ "not_unsafe": "{entity_name} werd veilig",
+ "occupied": "{entity_name} werd bezet",
+ "opened": "{entity_name} geopend",
+ "plugged_in": "{entity_name} aangesloten",
+ "powered": "{entity_name} heeft vermogen",
+ "present": "{entity_name} aanwezig",
+ "problem": "{entity_name} begonnen met het detecteren van een probleem",
+ "smoke": "{entity_name} begon rook te detecteren",
+ "sound": "{entity_name} begon geluid te detecteren",
+ "turned_off": "{entity_name} uitgeschakeld",
+ "turned_on": "{entity_name} ingeschakeld",
+ "unsafe": "{entity_name} werd onveilig",
+ "vibration": "{entity_name} begon trillingen te detecteren"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/.translations/no.json b/homeassistant/components/binary_sensor/.translations/no.json
index 5a1916bce59..4194102948b 100644
--- a/homeassistant/components/binary_sensor/.translations/no.json
+++ b/homeassistant/components/binary_sensor/.translations/no.json
@@ -53,6 +53,7 @@
"hot": "{entity_name} ble varm",
"light": "{entity_name} begynte \u00e5 registrere lys",
"locked": "{entity_name} l\u00e5st",
+ "moist": "{entity_name} ble fuktig",
"moist\u00a7": "{entity_name} ble fuktig",
"motion": "{entity_name} begynte \u00e5 registrere bevegelse",
"moving": "{entity_name} begynte \u00e5 bevege seg",
@@ -71,6 +72,7 @@
"not_moist": "{entity_name} ble t\u00f8rr",
"not_moving": "{entity_name} sluttet \u00e5 bevege seg",
"not_occupied": "{entity_name} ble ledig",
+ "not_opened": "{entity_name} stengt",
"not_plugged_in": "{entity_name} koblet fra",
"not_powered": "{entity_name} spenningsl\u00f8s",
"not_present": "{entity_name} ikke til stede",
diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json
index a7f0bd516a0..bc474e3d514 100644
--- a/homeassistant/components/binary_sensor/.translations/pl.json
+++ b/homeassistant/components/binary_sensor/.translations/pl.json
@@ -45,14 +45,15 @@
"is_vibration": "sensor {entity_name} wykrywa wibracje"
},
"trigger_type": {
- "bat_low": "bateria {entity_name} stanie si\u0119 roz\u0142adowana",
- "closed": "zamkni\u0119cie {entity_name}",
+ "bat_low": "nast\u0105pi roz\u0142adowanie baterii {entity_name}",
+ "closed": "nast\u0105pi zamkni\u0119cie {entity_name}",
"cold": "sensor {entity_name} wykryje zimno",
- "connected": "pod\u0142\u0105czenie {entity_name}",
+ "connected": "nast\u0105pi pod\u0142\u0105czenie {entity_name}",
"gas": "sensor {entity_name} wykryje gaz",
"hot": "sensor {entity_name} wykryje gor\u0105co",
"light": "sensor {entity_name} wykryje \u015bwiat\u0142o",
- "locked": "zamkni\u0119cie {entity_name}",
+ "locked": "nast\u0105pi zamkni\u0119cie {entity_name}",
+ "moist": "nast\u0105pi wykrycie wilgoci {entity_name}",
"moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107",
"motion": "sensor {entity_name} wykryje ruch",
"moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119",
@@ -63,28 +64,29 @@
"no_smoke": "sensor {entity_name} przestanie wykrywa\u0107 dym",
"no_sound": "sensor {entity_name} przestanie wykrywa\u0107 d\u017awi\u0119k",
"no_vibration": "sensor {entity_name} przestanie wykrywa\u0107 wibracje",
- "not_bat_low": "bateria {entity_name} staje si\u0119 na\u0142adowana",
+ "not_bat_low": "nast\u0105pi na\u0142adowanie baterii {entity_name}",
"not_cold": "sensor {entity_name} przestanie wykrywa\u0107 zimno",
- "not_connected": "roz\u0142\u0105czenie {entity_name}",
+ "not_connected": "nast\u0105pi roz\u0142\u0105czenie {entity_name}",
"not_hot": "sensor {entity_name} przestanie wykrywa\u0107 gor\u0105co",
- "not_locked": "otwarcie {entity_name}",
+ "not_locked": "nast\u0105pi otwarcie {entity_name}",
"not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107",
"not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119",
- "not_occupied": "sensor {entity_name} przesta\u0142 by\u0107 zaj\u0119ty",
- "not_plugged_in": "od\u0142\u0105czenie {entity_name}",
- "not_powered": "od\u0142\u0105czenie zasilania {entity_name}",
+ "not_occupied": "sensor {entity_name} przestanie by\u0107 zaj\u0119ty",
+ "not_opened": "nast\u0105pi zamkni\u0119cie {entity_name}",
+ "not_plugged_in": "nast\u0105pi od\u0142\u0105czenie {entity_name}",
+ "not_powered": "nast\u0105pi od\u0142\u0105czenie zasilania {entity_name}",
"not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107",
"not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 niebezpiecze\u0144stwo",
- "occupied": "sensor {entity_name} sta\u0142 si\u0119 zaj\u0119ty",
- "opened": "otwarcie {entity_name}",
- "plugged_in": "pod\u0142\u0105czenie {entity_name}",
- "powered": "pod\u0142\u0105czenie zasilenia {entity_name}",
+ "occupied": "sensor {entity_name} stanie si\u0119 zaj\u0119ty",
+ "opened": "nast\u0105pi otwarcie {entity_name}",
+ "plugged_in": "nast\u0105pi pod\u0142\u0105czenie {entity_name}",
+ "powered": "nast\u0105pi pod\u0142\u0105czenie zasilenia {entity_name}",
"present": "sensor {entity_name} wykryje obecno\u015b\u0107",
"problem": "sensor {entity_name} wykryje problem",
"smoke": "sensor {entity_name} wykryje dym",
"sound": "sensor {entity_name} wykryje d\u017awi\u0119k",
- "turned_off": "wy\u0142\u0105czenie {entity_name}",
- "turned_on": "w\u0142\u0105czenie {entity_name}",
+ "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}",
+ "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}",
"unsafe": "sensor {entity_name} wykryje niebezpiecze\u0144stwo",
"vibration": "sensor {entity_name} wykryje wibracje"
}
diff --git a/homeassistant/components/binary_sensor/.translations/pt.json b/homeassistant/components/binary_sensor/.translations/pt.json
new file mode 100644
index 00000000000..aa16576d2c1
--- /dev/null
+++ b/homeassistant/components/binary_sensor/.translations/pt.json
@@ -0,0 +1,41 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "a bateria {entity_name} est\u00e1 baixa",
+ "is_cold": "{entity_name} est\u00e1 frio",
+ "is_connected": "{entity_name} est\u00e1 ligado",
+ "is_gas": "{entity_name} est\u00e1 a detectar g\u00e1s",
+ "is_hot": "{entity_name} est\u00e1 quente",
+ "is_light": "{entity_name} est\u00e1 a detectar luz",
+ "is_locked": "{entity_name} est\u00e1 fechado",
+ "is_moist": "{entity_name} est\u00e1 h\u00famido",
+ "is_motion": "{entity_name} est\u00e1 a detectar movimento",
+ "is_moving": "{entity_name} est\u00e1 a mexer",
+ "is_not_open": "{entity_name} est\u00e1 fechada",
+ "is_off": "{entity_name} est\u00e1 desligado",
+ "is_on": "{entity_name} est\u00e1 ligado",
+ "is_vibration": "{entity_name} est\u00e1 a detectar vibra\u00e7\u00f5es"
+ },
+ "trigger_type": {
+ "closed": "{entity_name} est\u00e1 fechado",
+ "moist": "ficou h\u00famido {entity_name}",
+ "not_opened": "fechado {entity_name}",
+ "not_plugged_in": "{entity_name} desligado",
+ "not_powered": "{entity_name} n\u00e3o alimentado",
+ "not_present": "ausente {entity_name}",
+ "not_unsafe": "ficou seguro {entity_name}",
+ "occupied": "ficou ocupado {entity_name}",
+ "opened": "{entity_name} aberto",
+ "plugged_in": "{entity_name} ligado",
+ "powered": "{entity_name} alimentado",
+ "present": "{entity_name} presente",
+ "problem": "foi detectado problema em {entity_name}",
+ "smoke": "foi detectado fumo em {entity_name}",
+ "sound": "foram detectadas sons em {entity_name}",
+ "turned_off": "foi desligado {entity_name}",
+ "turned_on": "foi ligado {entity_name}",
+ "unsafe": "ficou inseguro {entity_name}",
+ "vibration": "foram detectadas vibra\u00e7\u00f5es em {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json
index 7d73cb8d4aa..cce765c8d84 100644
--- a/homeassistant/components/binary_sensor/.translations/ru.json
+++ b/homeassistant/components/binary_sensor/.translations/ru.json
@@ -1,15 +1,94 @@
{
"device_automation": {
"condition_type": {
- "is_bat_low": "{entity_name}: \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434",
- "is_cold": "{entity_name}: \u0445\u043e\u043b\u043e\u0434\u043d\u043e",
- "is_connected": "{entity_name}: \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
- "is_gas": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0433\u0430\u0437",
- "is_hot": "{entity_name}: \u0433\u043e\u0440\u044f\u0447\u043e",
- "is_light": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0432\u0435\u0442",
- "is_locked": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e",
- "is_moist": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0432\u043b\u0430\u0433\u0430",
- "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435"
+ "is_bat_low": "{entity_name} \u0432 \u0440\u0430\u0437\u0440\u044f\u0436\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_cold": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435",
+ "is_connected": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
+ "is_gas": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437",
+ "is_hot": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432",
+ "is_light": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442",
+ "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_moist": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443",
+ "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
+ "is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f",
+ "is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437",
+ "is_no_light": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442",
+ "is_no_motion": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
+ "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443",
+ "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c",
+ "is_no_sound": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a",
+ "is_no_vibration": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e",
+ "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_not_cold": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435",
+ "is_not_connected": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
+ "is_not_hot": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432",
+ "is_not_locked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_not_moist": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443",
+ "is_not_moving": "{entity_name} \u043d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f",
+ "is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
+ "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
+ "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e",
+ "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
+ "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
+ "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_plugged_in": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
+ "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e",
+ "is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
+ "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443",
+ "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c",
+ "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a",
+ "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e"
+ },
+ "trigger_type": {
+ "bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434",
+ "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
+ "cold": "{entity_name} \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0435\u0442\u0441\u044f",
+ "connected": "{entity_name} \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "gas": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437",
+ "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f",
+ "light": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442",
+ "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f",
+ "moist": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443",
+ "moist\u00a7": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443",
+ "motion": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
+ "moving": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435",
+ "no_gas": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437",
+ "no_light": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442",
+ "no_motion": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
+ "no_problem": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443",
+ "no_smoke": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c",
+ "no_sound": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a",
+ "no_vibration": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e",
+ "not_bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u0440\u044f\u0434",
+ "not_cold": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0442\u044c\u0441\u044f",
+ "not_connected": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "not_hot": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u044c\u0441\u044f",
+ "not_locked": "{entity_name} \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f",
+ "not_moist": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443",
+ "not_moving": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435",
+ "not_occupied": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
+ "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
+ "not_plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
+ "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f",
+ "not_present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
+ "not_unsafe": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c",
+ "occupied": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
+ "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
+ "plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
+ "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f",
+ "present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
+ "problem": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443",
+ "smoke": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c",
+ "sound": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a",
+ "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c",
+ "vibration": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/.translations/sl.json b/homeassistant/components/binary_sensor/.translations/sl.json
index 6b4e144d9a6..2004caeb342 100644
--- a/homeassistant/components/binary_sensor/.translations/sl.json
+++ b/homeassistant/components/binary_sensor/.translations/sl.json
@@ -53,6 +53,7 @@
"hot": "{entity_name} je postal vro\u010d",
"light": "{entity_name} za\u010del zaznavati svetlobo",
"locked": "{entity_name} zaklenjen",
+ "moist": "{entity_name} postal vla\u017een",
"moist\u00a7": "{entity_name} postal vla\u017een",
"motion": "{entity_name} za\u010del zaznavati gibanje",
"moving": "{entity_name} se je za\u010del premikati",
@@ -71,6 +72,7 @@
"not_moist": "{entity_name} je postalo suh",
"not_moving": "{entity_name} se je prenehal premikati",
"not_occupied": "{entity_name} ni zaseden",
+ "not_opened": "{entity_name} zaprto",
"not_plugged_in": "{entity_name} odklopljen",
"not_powered": "{entity_name} ni napajan",
"not_present": "{entity_name} ni prisoten",
diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hant.json b/homeassistant/components/binary_sensor/.translations/zh-Hant.json
index 36c72dcb9e6..046b999cb8c 100644
--- a/homeassistant/components/binary_sensor/.translations/zh-Hant.json
+++ b/homeassistant/components/binary_sensor/.translations/zh-Hant.json
@@ -53,6 +53,7 @@
"hot": "{entity_name} \u5df2\u8b8a\u71b1",
"light": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda",
"locked": "{entity_name} \u5df2\u4e0a\u9396",
+ "moist": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5",
"moist\u00a7": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5",
"motion": "{entity_name} \u5df2\u5075\u6e2c\u5230\u52d5\u4f5c",
"moving": "{entity_name} \u958b\u59cb\u79fb\u52d5",
@@ -71,6 +72,7 @@
"not_moist": "{entity_name} \u5df2\u8b8a\u4e7e",
"not_moving": "{entity_name} \u505c\u6b62\u79fb\u52d5",
"not_occupied": "{entity_name} \u672a\u6709\u4eba",
+ "not_opened": "{entity_name} \u5df2\u95dc\u9589",
"not_plugged_in": "{entity_name} \u672a\u63d2\u5165",
"not_powered": "{entity_name} \u672a\u901a\u96fb",
"not_present": "{entity_name} \u672a\u51fa\u73fe",
diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py
index 1749ea91c5b..0766d82c727 100644
--- a/homeassistant/components/binary_sensor/device_condition.py
+++ b/homeassistant/components/binary_sensor/device_condition.py
@@ -1,10 +1,10 @@
"""Implemenet device conditions for binary sensor."""
-from typing import List
+from typing import Dict, List
import voluptuous as vol
from homeassistant.core import HomeAssistant
from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON
-from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE
+from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.entity_registry import (
async_entries_for_device,
@@ -188,13 +188,16 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)
-async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_conditions(
+ hass: HomeAssistant, device_id: str
+) -> List[Dict[str, str]]:
"""List device conditions."""
- conditions: List[dict] = []
+ conditions: List[Dict[str, str]] = []
entity_registry = await async_get_registry(hass)
entries = [
entry
@@ -244,5 +247,16 @@ def async_condition_from_config(
condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
condition.CONF_STATE: stat,
}
+ if CONF_FOR in config:
+ state_config[CONF_FOR] = config[CONF_FOR]
- return condition.state_from_config(state_config, config_validation)
+ return condition.state_from_config(state_config)
+
+
+async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict:
+ """List condition capabilities."""
+ return {
+ "extra_fields": vol.Schema(
+ {vol.Optional(CONF_FOR): cv.positive_time_period_dict}
+ )
+ }
diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py
index f138bcfd5a8..c51b9749288 100644
--- a/homeassistant/components/binary_sensor/device_trigger.py
+++ b/homeassistant/components/binary_sensor/device_trigger.py
@@ -191,6 +191,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
to_state = "off"
state_config = {
+ state_automation.CONF_PLATFORM: "state",
state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
state_automation.CONF_FROM: from_state,
state_automation.CONF_TO: to_state,
@@ -198,6 +199,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
if CONF_FOR in config:
state_config[CONF_FOR] = config[CONF_FOR]
+ state_config = state_automation.TRIGGER_SCHEMA(state_config)
return await state_automation.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
)
@@ -240,7 +242,7 @@ async def async_get_triggers(hass, device_id):
return triggers
-async def async_get_trigger_capabilities(hass, trigger):
+async def async_get_trigger_capabilities(hass, config):
"""List trigger capabilities."""
return {
"extra_fields": vol.Schema(
diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py
index 4d8d5643826..b62bb434e85 100644
--- a/homeassistant/components/bitcoin/sensor.py
+++ b/homeassistant/components/bitcoin/sensor.py
@@ -1,11 +1,12 @@
"""Bitcoin information service that uses blockchain.info."""
-import logging
from datetime import timedelta
+import logging
+from blockchain import exchangerates, statistics
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_DISPLAY_OPTIONS, ATTR_ATTRIBUTION, CONF_CURRENCY
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_OPTIONS
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -55,7 +56,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Bitcoin sensors."""
- from blockchain import exchangerates
currency = config.get(CONF_CURRENCY)
@@ -169,7 +169,6 @@ class BitcoinData:
def update(self):
"""Get the latest data from blockchain.info."""
- from blockchain import statistics, exchangerates
self.stats = statistics.get()
self.ticker = exchangerates.get_ticker()
diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py
index eca7fa84f50..e1aa7200c07 100644
--- a/homeassistant/components/blackbird/media_player.py
+++ b/homeassistant/components/blackbird/media_player.py
@@ -2,9 +2,11 @@
import logging
import socket
+from pyblackbird import get_blackbird
+from serial import SerialException
import voluptuous as vol
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
DOMAIN,
SUPPORT_SELECT_SOURCE,
@@ -72,9 +74,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
port = config.get(CONF_PORT)
host = config.get(CONF_HOST)
- from pyblackbird import get_blackbird
- from serial import SerialException
-
connection = None
if port is not None:
try:
diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py
index bd11572ba1c..e233a8b21d8 100644
--- a/homeassistant/components/blink/__init__.py
+++ b/homeassistant/components/blink/__init__.py
@@ -1,22 +1,24 @@
"""Support for Blink Home Camera System."""
-import logging
from datetime import timedelta
+import logging
+
+from blinkpy import blinkpy
import voluptuous as vol
-from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.const import (
- CONF_USERNAME,
- CONF_PASSWORD,
- CONF_NAME,
- CONF_SCAN_INTERVAL,
CONF_BINARY_SENSORS,
- CONF_SENSORS,
CONF_FILENAME,
- CONF_MONITORED_CONDITIONS,
CONF_MODE,
+ CONF_MONITORED_CONDITIONS,
+ CONF_NAME,
CONF_OFFSET,
+ CONF_PASSWORD,
+ CONF_SCAN_INTERVAL,
+ CONF_SENSORS,
+ CONF_USERNAME,
TEMP_FAHRENHEIT,
)
+from homeassistant.helpers import config_validation as cv, discovery
_LOGGER = logging.getLogger(__name__)
@@ -97,7 +99,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up Blink System."""
- from blinkpy import blinkpy
conf = config[BLINK_DATA]
username = conf[CONF_USERNAME]
diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json
index a38ba0bd613..47cded00cc0 100644
--- a/homeassistant/components/blink/manifest.json
+++ b/homeassistant/components/blink/manifest.json
@@ -3,7 +3,7 @@
"name": "Blink",
"documentation": "https://www.home-assistant.io/integrations/blink",
"requirements": [
- "blinkpy==0.14.1"
+ "blinkpy==0.14.2"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py
index 5f3cb7ebfd1..197213f7473 100644
--- a/homeassistant/components/blinksticklight/light.py
+++ b/homeassistant/components/blinksticklight/light.py
@@ -1,15 +1,16 @@
"""Support for Blinkstick lights."""
import logging
+from blinkstick import blinkstick
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_HS_COLOR,
+ PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
Light,
- PLATFORM_SCHEMA,
)
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
@@ -33,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Blinkstick device specified by serial number."""
- from blinkstick import blinkstick
name = config.get(CONF_NAME)
serial = config.get(CONF_SERIAL)
diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py
index c95ccb3fed3..6d17484bdd7 100644
--- a/homeassistant/components/blockchain/sensor.py
+++ b/homeassistant/components/blockchain/sensor.py
@@ -1,12 +1,13 @@
"""Support for Blockchain.info sensors."""
-import logging
from datetime import timedelta
+import logging
+from pyblockchain import get_balance, validate_address
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -31,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Blockchain.info sensors."""
- from pyblockchain import validate_address
addresses = config.get(CONF_ADDRESSES)
name = config.get(CONF_NAME)
@@ -81,6 +81,5 @@ class BlockchainSensor(Entity):
def update(self):
"""Get the latest state of the sensor."""
- from pyblockchain import get_balance
self._state = get_balance(self.addresses)
diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py
index bf0568aed16..702cf5ddc30 100644
--- a/homeassistant/components/bluesound/media_player.py
+++ b/homeassistant/components/bluesound/media_player.py
@@ -3,14 +3,16 @@ import asyncio
from asyncio.futures import CancelledError
from datetime import timedelta
import logging
+from urllib import parse
import aiohttp
from aiohttp.client_exceptions import ClientError
from aiohttp.hdrs import CONNECTION, KEEP_ALIVE
import async_timeout
import voluptuous as vol
+import xmltodict
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
ATTR_MEDIA_ENQUEUE,
DOMAIN,
@@ -329,7 +331,6 @@ class BluesoundPlayer(MediaPlayerDevice):
self, method, raise_timeout=False, allow_offline=False
):
"""Send command to the player."""
- import xmltodict
if not self._is_online and not allow_offline:
return
@@ -370,7 +371,6 @@ class BluesoundPlayer(MediaPlayerDevice):
async def async_update_status(self):
"""Use the poll session to always get the status of the player."""
- import xmltodict
response = None
@@ -690,7 +690,6 @@ class BluesoundPlayer(MediaPlayerDevice):
@property
def source(self):
"""Name of the current input source."""
- from urllib import parse
if self._status is None or (self.is_grouped and not self.is_master):
return None
diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py
index 29eecdfd077..18edd750639 100644
--- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py
+++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py
@@ -2,6 +2,8 @@
import asyncio
import logging
+import pygatt # pylint: disable=import-error
+
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker.legacy import (
YAML_DEVICES,
@@ -26,8 +28,6 @@ MIN_SEEN_NEW = 5
def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Bluetooth LE Scanner."""
- # pylint: disable=import-error
- import pygatt
new_devices = {}
hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None})
diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json
index 30ed924a9dc..d9f4cb0a2b5 100644
--- a/homeassistant/components/bluetooth_le_tracker/manifest.json
+++ b/homeassistant/components/bluetooth_le_tracker/manifest.json
@@ -3,7 +3,7 @@
"name": "Bluetooth le tracker",
"documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker",
"requirements": [
- "pygatt[GATTTOOL]==4.0.1"
+ "pygatt[GATTTOOL]==4.0.5"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py
index ee4e1731156..b9bc18e6abf 100644
--- a/homeassistant/components/bme280/sensor.py
+++ b/homeassistant/components/bme280/sensor.py
@@ -3,6 +3,9 @@ from datetime import timedelta
from functools import partial
import logging
+import smbus # pylint: disable=import-error
+from i2csense.bme280 import BME280 # pylint: disable=import-error
+
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -76,8 +79,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the BME280 sensor."""
- import smbus # pylint: disable=import-error
- from i2csense.bme280 import BME280 # pylint: disable=import-error
SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py
index a36b35ea9d4..5a1e9fd120f 100644
--- a/homeassistant/components/bme680/sensor.py
+++ b/homeassistant/components/bme680/sensor.py
@@ -1,14 +1,15 @@
"""Support for BME680 Sensor over SMBus."""
-import importlib
import logging
+import threading
+from time import sleep, time
-from time import time, sleep
-
+from smbus import SMBus # pylint: disable=import-error
+import bme680 # pylint: disable=import-error
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_FAHRENHEIT
import homeassistant.helpers.config_validation as cv
-from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS
from homeassistant.helpers.entity import Entity
from homeassistant.util.temperature import celsius_to_fahrenheit
@@ -121,9 +122,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
def _setup_bme680(config):
"""Set up and configure the BME680 sensor."""
- from smbus import SMBus # pylint: disable=import-error
-
- bme680 = importlib.import_module("bme680")
sensor_handler = None
sensor = None
@@ -224,7 +222,6 @@ class BME680Handler:
self._gas_baseline = None
if gas_measurement:
- import threading
threading.Thread(
target=self._run_gas_sensor,
diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py
index 8e67da86dc3..455d821e669 100644
--- a/homeassistant/components/bmw_connected_drive/__init__.py
+++ b/homeassistant/components/bmw_connected_drive/__init__.py
@@ -1,12 +1,14 @@
"""Reads vehicle status from BMW connected drive portal."""
import logging
+from bimmer_connected.account import ConnectedDriveAccount
+from bimmer_connected.country_selector import get_region_from_name
import voluptuous as vol
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import discovery
-from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.event import track_utc_time_change
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -118,8 +120,6 @@ class BMWConnectedDriveAccount:
self, username: str, password: str, region_str: str, name: str, read_only
) -> None:
"""Constructor."""
- from bimmer_connected.account import ConnectedDriveAccount
- from bimmer_connected.country_selector import get_region_from_name
region = get_region_from_name(region_str)
diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py
index c13de455984..8163ae4eae3 100644
--- a/homeassistant/components/bmw_connected_drive/binary_sensor.py
+++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py
@@ -1,6 +1,8 @@
"""Reads vehicle status from BMW connected drive portal."""
import logging
+from bimmer_connected.state import ChargingState, LockState
+
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import LENGTH_KILOMETERS
@@ -141,8 +143,6 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
def update(self):
"""Read new state data from the library."""
- from bimmer_connected.state import LockState
- from bimmer_connected.state import ChargingState
vehicle_state = self._vehicle.state
diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py
index 2055b442dcd..5323e94c1c3 100644
--- a/homeassistant/components/bmw_connected_drive/lock.py
+++ b/homeassistant/components/bmw_connected_drive/lock.py
@@ -1,6 +1,8 @@
"""Support for BMW car locks with BMW ConnectedDrive."""
import logging
+from bimmer_connected.state import LockState
+
from homeassistant.components.lock import LockDevice
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
@@ -87,7 +89,6 @@ class BMWLock(LockDevice):
def update(self):
"""Update state of the lock."""
- from bimmer_connected.state import LockState
_LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute)
vehicle_state = self._vehicle.state
diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py
index 28a4e853f2c..f919bba6b95 100644
--- a/homeassistant/components/bmw_connected_drive/sensor.py
+++ b/homeassistant/components/bmw_connected_drive/sensor.py
@@ -1,6 +1,8 @@
"""Support for reading vehicle status from BMW connected drive portal."""
import logging
+from bimmer_connected.state import ChargingState
+
from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL,
LENGTH_KILOMETERS,
@@ -97,7 +99,6 @@ class BMWConnectedDriveSensor(Entity):
@property
def icon(self):
"""Icon to use in the frontend, if any."""
- from bimmer_connected.state import ChargingState
vehicle_state = self._vehicle.state
charging_state = vehicle_state.charging_status in [ChargingState.CHARGING]
diff --git a/homeassistant/components/bom/camera.py b/homeassistant/components/bom/camera.py
index f417cf769a4..7460b84f734 100644
--- a/homeassistant/components/bom/camera.py
+++ b/homeassistant/components/bom/camera.py
@@ -1,4 +1,5 @@
"""Provide animated GIF loops of BOM radar imagery."""
+from bomradarloop import BOMRadarLoop
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
@@ -119,7 +120,6 @@ class BOMRadarCam(Camera):
def __init__(self, name, location, radar_id, delta, frames, outfile):
"""Initialize the component."""
- from bomradarloop import BOMRadarLoop
super().__init__()
self._name = name
diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json
index d77c32966b1..c2c128909cc 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/integrations/broadlink",
"requirements": [
- "broadlink==0.11.1"
+ "broadlink==0.12.0"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py
index 98988965ca0..6374f35c503 100644
--- a/homeassistant/components/broadlink/sensor.py
+++ b/homeassistant/components/broadlink/sensor.py
@@ -3,6 +3,8 @@ import binascii
import logging
from datetime import timedelta
+import broadlink
+
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -128,7 +130,6 @@ class BroadlinkData:
_LOGGER.warning("Failed to connect to device")
def _connect(self):
- import broadlink
self._device = broadlink.a1((self.ip_addr, 80), self.mac_addr, None)
self._device.timeout = self.timeout
diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py
index d60331aaa44..bfb6dc4f42e 100644
--- a/homeassistant/components/broadlink/switch.py
+++ b/homeassistant/components/broadlink/switch.py
@@ -4,6 +4,8 @@ from datetime import timedelta
import logging
import socket
+import broadlink
+
import voluptuous as vol
from homeassistant.components.switch import (
@@ -91,7 +93,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Broadlink switches."""
- import broadlink
devices = config.get(CONF_SWITCHES)
slots = config.get("slots", {})
diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py
index 5a3f72c3ef2..d8592f44fff 100644
--- a/homeassistant/components/brottsplatskartan/sensor.py
+++ b/homeassistant/components/brottsplatskartan/sensor.py
@@ -4,6 +4,8 @@ from datetime import timedelta
import logging
import uuid
+import brottsplatskartan
+
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -60,7 +62,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Brottsplatskartan platform."""
- import brottsplatskartan
area = config.get(CONF_AREA)
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
@@ -105,7 +106,6 @@ class BrottsplatskartanSensor(Entity):
def update(self):
"""Update device state."""
- import brottsplatskartan
incident_counts = defaultdict(int)
incidents = self._brottsplatskartan.get_incidents()
diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py
index b163f16a5c4..b7612def701 100644
--- a/homeassistant/components/browser/__init__.py
+++ b/homeassistant/components/browser/__init__.py
@@ -1,4 +1,5 @@
"""Support for launching a web browser on the host machine."""
+import webbrowser
import voluptuous as vol
ATTR_URL = "url"
@@ -18,7 +19,6 @@ SERVICE_BROWSE_URL_SCHEMA = vol.Schema(
def setup(hass, config):
"""Listen for browse_url events."""
- import webbrowser
hass.services.register(
DOMAIN,
diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml
index e69de29bb2d..460def22dc1 100644
--- a/homeassistant/components/browser/services.yaml
+++ b/homeassistant/components/browser/services.yaml
@@ -0,0 +1,6 @@
+browse_url:
+ description: Open a URL in the default browser on the host machine of Home Assistant.
+ fields:
+ url:
+ description: The URL to open.
+ example: "https://www.home-assistant.io"
diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py
index af809cc7878..7d4279cf5b2 100644
--- a/homeassistant/components/brunt/cover.py
+++ b/homeassistant/components/brunt/cover.py
@@ -2,17 +2,18 @@
import logging
+from brunt import BruntAPI
import voluptuous as vol
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME
from homeassistant.components.cover import (
ATTR_POSITION,
- CoverDevice,
PLATFORM_SCHEMA,
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
+ CoverDevice,
)
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -36,7 +37,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the brunt platform."""
# pylint: disable=no-name-in-module
- from brunt import BruntAPI
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py
index 0a068a3981f..20ad909c44e 100644
--- a/homeassistant/components/bt_home_hub_5/device_tracker.py
+++ b/homeassistant/components/bt_home_hub_5/device_tracker.py
@@ -1,6 +1,8 @@
"""Support for BT Home Hub 5."""
import logging
+import bthomehub5_devicelist
+
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -32,7 +34,6 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialise the scanner."""
- import bthomehub5_devicelist
_LOGGER.info("Initialising BT Home Hub 5")
self.host = config[CONF_HOST]
@@ -61,7 +62,6 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
def update_info(self):
"""Ensure the information from the BT Home Hub 5 is up to date."""
- import bthomehub5_devicelist
_LOGGER.info("Scanning")
diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py
index 58f409c2d4b..45b18b963c5 100644
--- a/homeassistant/components/bt_smarthub/device_tracker.py
+++ b/homeassistant/components/bt_smarthub/device_tracker.py
@@ -1,15 +1,16 @@
"""Support for BT Smart Hub (Sometimes referred to as BT Home Hub 6)."""
import logging
+import btsmarthub_devicelist
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN,
PLATFORM_SCHEMA,
DeviceScanner,
)
from homeassistant.const import CONF_HOST
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -69,12 +70,11 @@ class BTSmartHubScanner(DeviceScanner):
_LOGGER.warning("Error scanning devices")
return
- clients = [client for client in data.values()]
+ clients = list(data.values())
self.last_results = clients
def get_bt_smarthub_data(self):
"""Retrieve data from BT Smart Hub and return parsed result."""
- import btsmarthub_devicelist
# Request data from bt smarthub into a list of dicts.
data = btsmarthub_devicelist.get_devicelist(
diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py
new file mode 100644
index 00000000000..b91d2497d77
--- /dev/null
+++ b/homeassistant/components/buienradar/const.py
@@ -0,0 +1,7 @@
+"""Constants for buienradar component."""
+DEFAULT_TIMEFRAME = 60
+
+"""Schedule next call after (minutes)."""
+SCHEDULE_OK = 10
+"""When an error occurred, new call after (minutes)."""
+SCHEDULE_NOK = 2
diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py
index ef65db74f16..5fe97b6fb38 100644
--- a/homeassistant/components/buienradar/sensor.py
+++ b/homeassistant/components/buienradar/sensor.py
@@ -1,10 +1,23 @@
"""Support for Buienradar.nl weather service."""
-import asyncio
-from datetime import datetime, timedelta
import logging
-import aiohttp
-import async_timeout
+from buienradar.constants import (
+ ATTRIBUTION,
+ CONDCODE,
+ CONDITION,
+ DETAILED,
+ EXACT,
+ EXACTNL,
+ FORECAST,
+ IMAGE,
+ MEASURED,
+ PRECIPITATION_FORECAST,
+ STATIONNAME,
+ TIMEFRAME,
+ VISIBILITY,
+ WINDGUST,
+ WINDSPEED,
+)
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -16,12 +29,15 @@ from homeassistant.const import (
CONF_NAME,
TEMP_CELSIUS,
)
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util
+
+from .const import DEFAULT_TIMEFRAME
+from .util import BrData
+
+
_LOGGER = logging.getLogger(__name__)
MEASURED_LABEL = "Measured"
@@ -183,7 +199,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Create the buienradar sensor."""
- from .weather import DEFAULT_TIMEFRAME
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
@@ -216,7 +231,6 @@ class BrSensor(Entity):
def __init__(self, sensor_type, client_name, coordinates):
"""Initialize the sensor."""
- from buienradar.constants import PRECIPITATION_FORECAST, CONDITION
self.client_name = client_name
self._name = SENSOR_TYPES[sensor_type][0]
@@ -247,23 +261,6 @@ class BrSensor(Entity):
def load_data(self, data):
"""Load the sensor with relevant data."""
# Find sensor
- from buienradar.constants import (
- ATTRIBUTION,
- CONDITION,
- CONDCODE,
- DETAILED,
- EXACT,
- EXACTNL,
- FORECAST,
- IMAGE,
- MEASURED,
- PRECIPITATION_FORECAST,
- STATIONNAME,
- TIMEFRAME,
- VISIBILITY,
- WINDGUST,
- WINDSPEED,
- )
# Check if we have a new measurement,
# otherwise we do not have to update the sensor
@@ -421,7 +418,6 @@ class BrSensor(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- from buienradar.constants import PRECIPITATION_FORECAST
if self.type.startswith(PRECIPITATION_FORECAST):
result = {ATTR_ATTRIBUTION: self._attribution}
@@ -455,208 +451,3 @@ class BrSensor(Entity):
def force_update(self):
"""Return true for continuous sensors, false for discrete sensors."""
return self._force_update
-
-
-class BrData:
- """Get the latest data and updates the states."""
-
- def __init__(self, hass, coordinates, timeframe, devices):
- """Initialize the data object."""
- self.devices = devices
- self.data = {}
- self.hass = hass
- self.coordinates = coordinates
- self.timeframe = timeframe
-
- async def update_devices(self):
- """Update all devices/sensors."""
- if self.devices:
- tasks = []
- # Update all devices
- for dev in self.devices:
- if dev.load_data(self.data):
- tasks.append(dev.async_update_ha_state())
-
- if tasks:
- await asyncio.wait(tasks)
-
- async def schedule_update(self, minute=1):
- """Schedule an update after minute minutes."""
- _LOGGER.debug("Scheduling next update in %s minutes.", minute)
- nxt = dt_util.utcnow() + timedelta(minutes=minute)
- async_track_point_in_utc_time(self.hass, self.async_update, nxt)
-
- async def get_data(self, url):
- """Load data from specified url."""
- from buienradar.constants import CONTENT, MESSAGE, STATUS_CODE, SUCCESS
-
- _LOGGER.debug("Calling url: %s...", url)
- result = {SUCCESS: False, MESSAGE: None}
- resp = None
- try:
- websession = async_get_clientsession(self.hass)
- with async_timeout.timeout(10):
- resp = await websession.get(url)
-
- result[STATUS_CODE] = resp.status
- result[CONTENT] = await resp.text()
- if resp.status == 200:
- result[SUCCESS] = True
- else:
- result[MESSAGE] = "Got http statuscode: %d" % (resp.status)
-
- return result
- except (asyncio.TimeoutError, aiohttp.ClientError) as err:
- result[MESSAGE] = "%s" % err
- return result
- finally:
- if resp is not None:
- await resp.release()
-
- async def async_update(self, *_):
- """Update the data from buienradar."""
- from buienradar.constants import CONTENT, DATA, MESSAGE, STATUS_CODE, SUCCESS
- from buienradar.buienradar import parse_data
- from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url
-
- content = await self.get_data(JSON_FEED_URL)
-
- if content.get(SUCCESS) is not True:
- # unable to get the data
- _LOGGER.warning(
- "Unable to retrieve json data from Buienradar."
- "(Msg: %s, status: %s,)",
- content.get(MESSAGE),
- content.get(STATUS_CODE),
- )
- # schedule new call
- await self.schedule_update(SCHEDULE_NOK)
- return
-
- # rounding coordinates prevents unnecessary redirects/calls
- lat = self.coordinates[CONF_LATITUDE]
- lon = self.coordinates[CONF_LONGITUDE]
- rainurl = json_precipitation_forecast_url(lat, lon)
- raincontent = await self.get_data(rainurl)
-
- if raincontent.get(SUCCESS) is not True:
- # unable to get the data
- _LOGGER.warning(
- "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)",
- raincontent.get(MESSAGE),
- raincontent.get(STATUS_CODE),
- )
- # schedule new call
- await self.schedule_update(SCHEDULE_NOK)
- return
-
- result = parse_data(
- content.get(CONTENT),
- raincontent.get(CONTENT),
- self.coordinates[CONF_LATITUDE],
- self.coordinates[CONF_LONGITUDE],
- self.timeframe,
- False,
- )
-
- _LOGGER.debug("Buienradar parsed data: %s", result)
- if result.get(SUCCESS) is not True:
- if int(datetime.now().strftime("%H")) > 0:
- _LOGGER.warning(
- "Unable to parse data from Buienradar." "(Msg: %s)",
- result.get(MESSAGE),
- )
- await self.schedule_update(SCHEDULE_NOK)
- return
-
- self.data = result.get(DATA)
- await self.update_devices()
- await self.schedule_update(SCHEDULE_OK)
-
- @property
- def attribution(self):
- """Return the attribution."""
- from buienradar.constants import ATTRIBUTION
-
- return self.data.get(ATTRIBUTION)
-
- @property
- def stationname(self):
- """Return the name of the selected weatherstation."""
- from buienradar.constants import STATIONNAME
-
- return self.data.get(STATIONNAME)
-
- @property
- def condition(self):
- """Return the condition."""
- from buienradar.constants import CONDITION
-
- return self.data.get(CONDITION)
-
- @property
- def temperature(self):
- """Return the temperature, or None."""
- from buienradar.constants import TEMPERATURE
-
- try:
- return float(self.data.get(TEMPERATURE))
- except (ValueError, TypeError):
- return None
-
- @property
- def pressure(self):
- """Return the pressure, or None."""
- from buienradar.constants import PRESSURE
-
- try:
- return float(self.data.get(PRESSURE))
- except (ValueError, TypeError):
- return None
-
- @property
- def humidity(self):
- """Return the humidity, or None."""
- from buienradar.constants import HUMIDITY
-
- try:
- return int(self.data.get(HUMIDITY))
- except (ValueError, TypeError):
- return None
-
- @property
- def visibility(self):
- """Return the visibility, or None."""
- from buienradar.constants import VISIBILITY
-
- try:
- return int(self.data.get(VISIBILITY))
- except (ValueError, TypeError):
- return None
-
- @property
- def wind_speed(self):
- """Return the windspeed, or None."""
- from buienradar.constants import WINDSPEED
-
- try:
- return float(self.data.get(WINDSPEED))
- except (ValueError, TypeError):
- return None
-
- @property
- def wind_bearing(self):
- """Return the wind bearing, or None."""
- from buienradar.constants import WINDAZIMUTH
-
- try:
- return int(self.data.get(WINDAZIMUTH))
- except (ValueError, TypeError):
- return None
-
- @property
- def forecast(self):
- """Return the forecast data."""
- from buienradar.constants import FORECAST
-
- return self.data.get(FORECAST)
diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py
new file mode 100644
index 00000000000..579b3418271
--- /dev/null
+++ b/homeassistant/components/buienradar/util.py
@@ -0,0 +1,228 @@
+"""Shared utilities for different supported platforms."""
+import asyncio
+from datetime import datetime, timedelta
+import logging
+
+import aiohttp
+import async_timeout
+
+from buienradar.buienradar import parse_data
+from buienradar.constants import (
+ ATTRIBUTION,
+ CONDITION,
+ CONTENT,
+ DATA,
+ FORECAST,
+ HUMIDITY,
+ MESSAGE,
+ PRESSURE,
+ STATIONNAME,
+ STATUS_CODE,
+ SUCCESS,
+ TEMPERATURE,
+ VISIBILITY,
+ WINDAZIMUTH,
+ WINDSPEED,
+)
+from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url
+
+from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.event import async_track_point_in_utc_time
+from homeassistant.util import dt as dt_util
+
+
+from .const import SCHEDULE_OK, SCHEDULE_NOK
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class BrData:
+ """Get the latest data and updates the states."""
+
+ def __init__(self, hass, coordinates, timeframe, devices):
+ """Initialize the data object."""
+ self.devices = devices
+ self.data = {}
+ self.hass = hass
+ self.coordinates = coordinates
+ self.timeframe = timeframe
+
+ async def update_devices(self):
+ """Update all devices/sensors."""
+ if self.devices:
+ tasks = []
+ # Update all devices
+ for dev in self.devices:
+ if dev.load_data(self.data):
+ tasks.append(dev.async_update_ha_state())
+
+ if tasks:
+ await asyncio.wait(tasks)
+
+ async def schedule_update(self, minute=1):
+ """Schedule an update after minute minutes."""
+ _LOGGER.debug("Scheduling next update in %s minutes.", minute)
+ nxt = dt_util.utcnow() + timedelta(minutes=minute)
+ async_track_point_in_utc_time(self.hass, self.async_update, nxt)
+
+ async def get_data(self, url):
+ """Load data from specified url."""
+ _LOGGER.debug("Calling url: %s...", url)
+ result = {SUCCESS: False, MESSAGE: None}
+ resp = None
+ try:
+ websession = async_get_clientsession(self.hass)
+ with async_timeout.timeout(10):
+ resp = await websession.get(url)
+
+ result[STATUS_CODE] = resp.status
+ result[CONTENT] = await resp.text()
+ if resp.status == 200:
+ result[SUCCESS] = True
+ else:
+ result[MESSAGE] = "Got http statuscode: %d" % (resp.status)
+
+ return result
+ except (asyncio.TimeoutError, aiohttp.ClientError) as err:
+ result[MESSAGE] = "%s" % err
+ return result
+ finally:
+ if resp is not None:
+ await resp.release()
+
+ async def async_update(self, *_):
+ """Update the data from buienradar."""
+
+ content = await self.get_data(JSON_FEED_URL)
+
+ if content.get(SUCCESS) is not True:
+ # unable to get the data
+ _LOGGER.warning(
+ "Unable to retrieve json data from Buienradar."
+ "(Msg: %s, status: %s,)",
+ content.get(MESSAGE),
+ content.get(STATUS_CODE),
+ )
+ # schedule new call
+ await self.schedule_update(SCHEDULE_NOK)
+ return
+
+ # rounding coordinates prevents unnecessary redirects/calls
+ lat = self.coordinates[CONF_LATITUDE]
+ lon = self.coordinates[CONF_LONGITUDE]
+ rainurl = json_precipitation_forecast_url(lat, lon)
+ raincontent = await self.get_data(rainurl)
+
+ if raincontent.get(SUCCESS) is not True:
+ # unable to get the data
+ _LOGGER.warning(
+ "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)",
+ raincontent.get(MESSAGE),
+ raincontent.get(STATUS_CODE),
+ )
+ # schedule new call
+ await self.schedule_update(SCHEDULE_NOK)
+ return
+
+ result = parse_data(
+ content.get(CONTENT),
+ raincontent.get(CONTENT),
+ self.coordinates[CONF_LATITUDE],
+ self.coordinates[CONF_LONGITUDE],
+ self.timeframe,
+ False,
+ )
+
+ _LOGGER.debug("Buienradar parsed data: %s", result)
+ if result.get(SUCCESS) is not True:
+ if int(datetime.now().strftime("%H")) > 0:
+ _LOGGER.warning(
+ "Unable to parse data from Buienradar." "(Msg: %s)",
+ result.get(MESSAGE),
+ )
+ await self.schedule_update(SCHEDULE_NOK)
+ return
+
+ self.data = result.get(DATA)
+ await self.update_devices()
+ await self.schedule_update(SCHEDULE_OK)
+
+ @property
+ def attribution(self):
+ """Return the attribution."""
+
+ return self.data.get(ATTRIBUTION)
+
+ @property
+ def stationname(self):
+ """Return the name of the selected weatherstation."""
+
+ return self.data.get(STATIONNAME)
+
+ @property
+ def condition(self):
+ """Return the condition."""
+
+ return self.data.get(CONDITION)
+
+ @property
+ def temperature(self):
+ """Return the temperature, or None."""
+
+ try:
+ return float(self.data.get(TEMPERATURE))
+ except (ValueError, TypeError):
+ return None
+
+ @property
+ def pressure(self):
+ """Return the pressure, or None."""
+
+ try:
+ return float(self.data.get(PRESSURE))
+ except (ValueError, TypeError):
+ return None
+
+ @property
+ def humidity(self):
+ """Return the humidity, or None."""
+
+ try:
+ return int(self.data.get(HUMIDITY))
+ except (ValueError, TypeError):
+ return None
+
+ @property
+ def visibility(self):
+ """Return the visibility, or None."""
+
+ try:
+ return int(self.data.get(VISIBILITY))
+ except (ValueError, TypeError):
+ return None
+
+ @property
+ def wind_speed(self):
+ """Return the windspeed, or None."""
+
+ try:
+ return float(self.data.get(WINDSPEED))
+ except (ValueError, TypeError):
+ return None
+
+ @property
+ def wind_bearing(self):
+ """Return the wind bearing, or None."""
+
+ try:
+ return int(self.data.get(WINDAZIMUTH))
+ except (ValueError, TypeError):
+ return None
+
+ @property
+ def forecast(self):
+ """Return the forecast data."""
+
+ return self.data.get(FORECAST)
diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py
index d8ae448c981..c95e57807c4 100644
--- a/homeassistant/components/buienradar/weather.py
+++ b/homeassistant/components/buienradar/weather.py
@@ -1,30 +1,40 @@
"""Support for Buienradar.nl weather service."""
import logging
+from buienradar.constants import (
+ CONDCODE,
+ CONDITION,
+ DATETIME,
+ MAX_TEMP,
+ MIN_TEMP,
+ RAIN,
+ WINDAZIMUTH,
+ WINDSPEED,
+)
import voluptuous as vol
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
- PLATFORM_SCHEMA,
- WeatherEntity,
- ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
+ PLATFORM_SCHEMA,
+ WeatherEntity,
)
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS
from homeassistant.helpers import config_validation as cv
# Reuse data and API logic from the sensor implementation
-from .sensor import BrData
+from .util import BrData
+from .const import DEFAULT_TIMEFRAME
_LOGGER = logging.getLogger(__name__)
DATA_CONDITION = "buienradar_condition"
-DEFAULT_TIMEFRAME = 60
CONF_FORECAST = "forecast"
@@ -110,7 +120,6 @@ class BrWeather(WeatherEntity):
@property
def condition(self):
"""Return the current condition."""
- from buienradar.constants import CONDCODE
if self._data and self._data.condition:
ccode = self._data.condition.get(CONDCODE)
@@ -161,16 +170,6 @@ class BrWeather(WeatherEntity):
@property
def forecast(self):
"""Return the forecast array."""
- from buienradar.constants import (
- CONDITION,
- CONDCODE,
- RAIN,
- DATETIME,
- MIN_TEMP,
- MAX_TEMP,
- WINDAZIMUTH,
- WINDSPEED,
- )
if not self._forecast:
return None
diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py
index 6251679b225..ad9dac1f727 100644
--- a/homeassistant/components/caldav/calendar.py
+++ b/homeassistant/components/caldav/calendar.py
@@ -4,6 +4,7 @@ from datetime import datetime, timedelta
import logging
import re
+import caldav
import voluptuous as vol
from homeassistant.components.calendar import (
@@ -62,8 +63,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_entities, disc_info=None):
"""Set up the WebDav Calendar platform."""
- import caldav
-
url = config[CONF_URL]
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
@@ -279,6 +278,10 @@ class WebDavCalendarData:
def to_datetime(obj):
"""Return a datetime."""
if isinstance(obj, datetime):
+ if obj.tzinfo is None:
+ # floating value, not bound to any time zone in particular
+ # represent same time regardless of which time zone is currently being observed
+ return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE)
return obj
return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min))
diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py
index f23b6ad46c9..a8a45f5b946 100644
--- a/homeassistant/components/canary/__init__.py
+++ b/homeassistant/components/canary/__init__.py
@@ -1,13 +1,14 @@
"""Support for Canary devices."""
-import logging
from datetime import timedelta
+import logging
-import voluptuous as vol
+from canary.api import Api
from requests import ConnectTimeout, HTTPError
+import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
+from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.helpers import discovery
+import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -67,7 +68,6 @@ class CanaryData:
def __init__(self, username, password, timeout):
"""Init the Canary data object."""
- from canary.api import Api
self._api = Api(username, password, timeout)
diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py
index 42b5048bc1d..856ecb9f3a2 100644
--- a/homeassistant/components/canary/alarm_control_panel.py
+++ b/homeassistant/components/canary/alarm_control_panel.py
@@ -1,6 +1,8 @@
"""Support for Canary alarm."""
import logging
+from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT
+
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
@@ -42,11 +44,6 @@ class CanaryAlarm(AlarmControlPanel):
@property
def state(self):
"""Return the state of the device."""
- from canary.api import (
- LOCATION_MODE_AWAY,
- LOCATION_MODE_HOME,
- LOCATION_MODE_NIGHT,
- )
location = self._data.get_location(self._location_id)
@@ -75,18 +72,15 @@ class CanaryAlarm(AlarmControlPanel):
def alarm_arm_home(self, code=None):
"""Send arm home command."""
- from canary.api import LOCATION_MODE_HOME
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
- from canary.api import LOCATION_MODE_AWAY
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
def alarm_arm_night(self, code=None):
"""Send arm night command."""
- from canary.api import LOCATION_MODE_NIGHT
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py
index 8a6d27b8916..7ed1e62ab8a 100644
--- a/homeassistant/components/canary/camera.py
+++ b/homeassistant/components/canary/camera.py
@@ -3,6 +3,8 @@ import asyncio
from datetime import timedelta
import logging
+from haffmpeg.camera import CameraMjpeg
+from haffmpeg.tools import IMAGE_JPEG, ImageFrame
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
@@ -81,8 +83,6 @@ class CanaryCamera(Camera):
"""Return a still image response from the camera."""
self.renew_live_stream_session()
- from haffmpeg.tools import ImageFrame, IMAGE_JPEG
-
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
image = await asyncio.shield(
ffmpeg.get_image(
@@ -98,8 +98,6 @@ class CanaryCamera(Camera):
if self._live_stream_session is None:
return
- from haffmpeg.camera import CameraMjpeg
-
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
await stream.open_camera(
self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments
diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json
index 71dee3afec5..1374372aa24 100644
--- a/homeassistant/components/cast/.translations/ko.json
+++ b/homeassistant/components/cast/.translations/ko.json
@@ -1,15 +1,15 @@
{
"config": {
"abort": {
- "no_devices_found": "\uad6c\uae00 \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 \uad6c\uae00 \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "Google \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
+ "single_instance_allowed": "\ud558\ub098\uc758 Google \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
- "description": "\uad6c\uae00 \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8"
+ "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Google \uce90\uc2a4\ud2b8"
}
},
- "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8"
+ "title": "Google \uce90\uc2a4\ud2b8"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py
index c3c21944d02..5c2b6dca932 100644
--- a/homeassistant/components/cast/config_flow.py
+++ b/homeassistant/components/cast/config_flow.py
@@ -1,12 +1,14 @@
"""Config flow for Cast."""
-from homeassistant.helpers import config_entry_flow
+from pychromecast.discovery import discover_chromecasts
+
from homeassistant import config_entries
+from homeassistant.helpers import config_entry_flow
+
from .const import DOMAIN
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
- from pychromecast.discovery import discover_chromecasts
return await hass.async_add_executor_job(discover_chromecasts)
diff --git a/homeassistant/components/cert_expiry/.translations/ca.json b/homeassistant/components/cert_expiry/.translations/ca.json
index 25c0b26fafc..f1df9a06be1 100644
--- a/homeassistant/components/cert_expiry/.translations/ca.json
+++ b/homeassistant/components/cert_expiry/.translations/ca.json
@@ -4,15 +4,17 @@
"host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada"
},
"error": {
+ "certificate_error": "El certificat no ha pogut ser validat",
"certificate_fetch_failed": "No s'ha pogut obtenir el certificat des d'aquesta combinaci\u00f3 d'amfitri\u00f3 i port",
"connection_timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 amb l'amfitri\u00f3.",
"host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada",
- "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3"
+ "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3",
+ "wrong_host": "El certificat no coincideix amb el nom de l'amfitri\u00f3"
},
"step": {
"user": {
"data": {
- "host": "Nom d'amfitri\u00f3 del certificat",
+ "host": "Nom de l'amfitri\u00f3 del certificat",
"name": "Nom del certificat",
"port": "Port del certificat"
},
diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json
index 667ab5fa4e3..c95a56320c9 100644
--- a/homeassistant/components/cert_expiry/.translations/da.json
+++ b/homeassistant/components/cert_expiry/.translations/da.json
@@ -4,10 +4,12 @@
"host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret"
},
"error": {
+ "certificate_error": "Certifikatet kunne ikke valideres",
"certificate_fetch_failed": "Kan ikke hente certifikat fra denne v\u00e6rt- og portkombination",
"connection_timeout": "Timeout ved tilslutning til denne v\u00e6rt",
"host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret",
- "resolve_failed": "V\u00e6rten kunne ikke findes"
+ "resolve_failed": "V\u00e6rten kunne ikke findes",
+ "wrong_host": "Certifikatet stemmer ikke overens med v\u00e6rtsnavnet"
},
"step": {
"user": {
diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json
index 873dfee9a92..19e237a6d05 100644
--- a/homeassistant/components/cert_expiry/.translations/en.json
+++ b/homeassistant/components/cert_expiry/.translations/en.json
@@ -4,10 +4,12 @@
"host_port_exists": "This host and port combination is already configured"
},
"error": {
+ "certificate_error": "Certificate could not be validated",
"certificate_fetch_failed": "Can not fetch certificate from this host and port combination",
"connection_timeout": "Timeout when connecting to this host",
"host_port_exists": "This host and port combination is already configured",
- "resolve_failed": "This host can not be resolved"
+ "resolve_failed": "This host can not be resolved",
+ "wrong_host": "Certificate does not match hostname"
},
"step": {
"user": {
diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json
index a3536902c76..9e7df5564a2 100644
--- a/homeassistant/components/cert_expiry/.translations/fr.json
+++ b/homeassistant/components/cert_expiry/.translations/fr.json
@@ -4,10 +4,12 @@
"host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e"
},
"error": {
+ "certificate_error": "Le certificat n'a pas pu \u00eatre valid\u00e9",
"certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port",
"connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te",
"host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e",
- "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu"
+ "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu",
+ "wrong_host": "Le certificat ne correspond pas au nom d'h\u00f4te"
},
"step": {
"user": {
diff --git a/homeassistant/components/cert_expiry/.translations/nl.json b/homeassistant/components/cert_expiry/.translations/nl.json
index d2fe3c76e85..0544c8c02c1 100644
--- a/homeassistant/components/cert_expiry/.translations/nl.json
+++ b/homeassistant/components/cert_expiry/.translations/nl.json
@@ -4,10 +4,12 @@
"host_port_exists": "Deze combinatie van host en poort is al geconfigureerd"
},
"error": {
+ "certificate_error": "Certificaat kon niet worden gevalideerd",
"certificate_fetch_failed": "Kan certificaat niet ophalen van deze combinatie van host en poort",
- "connection_timeout": "Timeout bij verbinding maken met deze host",
+ "connection_timeout": "Time-out bij verbinding maken met deze host",
"host_port_exists": "Deze combinatie van host en poort is al geconfigureerd",
- "resolve_failed": "Deze host kon niet gevonden worden"
+ "resolve_failed": "Deze host kon niet gevonden worden",
+ "wrong_host": "Certificaat komt niet overeen met hostnaam"
},
"step": {
"user": {
diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json
index 73e899106c1..fc2e98b725d 100644
--- a/homeassistant/components/cert_expiry/.translations/no.json
+++ b/homeassistant/components/cert_expiry/.translations/no.json
@@ -4,10 +4,12 @@
"host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert"
},
"error": {
+ "certificate_error": "Sertifikatet kunne ikke valideres",
"certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen",
"connection_timeout": "Tidsavbrudd n\u00e5r du kobler til denne verten",
"host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert",
- "resolve_failed": "Denne verten kan ikke l\u00f8ses"
+ "resolve_failed": "Denne verten kan ikke l\u00f8ses",
+ "wrong_host": "Sertifikatet samsvarer ikke med vertsnavn"
},
"step": {
"user": {
diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json
index d962c793121..8c0f230382a 100644
--- a/homeassistant/components/cert_expiry/.translations/ru.json
+++ b/homeassistant/components/cert_expiry/.translations/ru.json
@@ -4,19 +4,21 @@
"host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430."
},
"error": {
- "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430",
- "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443",
+ "certificate_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442.",
+ "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430.",
+ "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443.",
"host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.",
- "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442"
+ "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442.",
+ "wrong_host": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u043c\u0443 \u0438\u043c\u0435\u043d\u0438."
},
"step": {
"user": {
"data": {
- "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430",
- "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430",
- "port": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430"
+ "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
+ "port": "\u041f\u043e\u0440\u0442"
},
- "title": "C\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f"
+ "title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430"
}
},
"title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430"
diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py
index 7c7efea7333..28a79a3e505 100644
--- a/homeassistant/components/cert_expiry/__init__.py
+++ b/homeassistant/components/cert_expiry/__init__.py
@@ -15,3 +15,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ return await hass.config_entries.async_forward_entry_unload(entry, "sensor")
diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py
index d73762ce882..78450d247b9 100644
--- a/homeassistant/components/cert_expiry/config_flow.py
+++ b/homeassistant/components/cert_expiry/config_flow.py
@@ -1,15 +1,18 @@
"""Config flow for the Cert Expiry platform."""
+import logging
import socket
+import ssl
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST
from homeassistant.core import HomeAssistant, callback
-from homeassistant.util import slugify
from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME
from .helper import get_cert
+_LOGGER = logging.getLogger(__name__)
+
@callback
def certexpiry_entries(hass: HomeAssistant):
@@ -40,17 +43,28 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _test_connection(self, user_input=None):
"""Test connection to the server and try to get the certtificate."""
+ host = user_input[CONF_HOST]
try:
await self.hass.async_add_executor_job(
- get_cert, user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT)
+ get_cert, host, user_input.get(CONF_PORT, DEFAULT_PORT)
)
return True
except socket.gaierror:
+ _LOGGER.error("Host cannot be resolved: %s", host)
self._errors[CONF_HOST] = "resolve_failed"
except socket.timeout:
+ _LOGGER.error("Timed out connecting to %s", host)
self._errors[CONF_HOST] = "connection_timeout"
- except OSError:
- self._errors[CONF_HOST] = "certificate_fetch_failed"
+ except ssl.CertificateError as err:
+ if "doesn't match" in err.args[0]:
+ _LOGGER.error("Certificate does not match host: %s", host)
+ self._errors[CONF_HOST] = "wrong_host"
+ else:
+ _LOGGER.error("Certificate could not be validated: %s", host)
+ self._errors[CONF_HOST] = "certificate_error"
+ except ssl.SSLError:
+ _LOGGER.error("Certificate could not be validated: %s", host)
+ self._errors[CONF_HOST] = "certificate_error"
return False
async def async_step_user(self, user_input=None):
@@ -62,11 +76,12 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._errors[CONF_HOST] = "host_port_exists"
else:
if await self._test_connection(user_input):
- host = user_input[CONF_HOST]
- name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME))
- prt = user_input.get(CONF_PORT, DEFAULT_PORT)
return self.async_create_entry(
- title=name, data={CONF_HOST: host, CONF_PORT: prt}
+ title=user_input.get(CONF_NAME, DEFAULT_NAME),
+ data={
+ CONF_HOST: user_input[CONF_HOST],
+ CONF_PORT: user_input.get(CONF_PORT, DEFAULT_PORT),
+ },
)
else:
user_input = {}
diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py
index 9c10887293a..cd49588ec89 100644
--- a/homeassistant/components/cert_expiry/helper.py
+++ b/homeassistant/components/cert_expiry/helper.py
@@ -11,5 +11,6 @@ def get_cert(host, port):
address = (host, port)
with socket.create_connection(address, timeout=TIMEOUT) as sock:
with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock:
- cert = ssock.getpeercert()
+ # pylint disable: https://github.com/PyCQA/pylint/issues/3166
+ cert = ssock.getpeercert() # pylint: disable=no-member
return cert
diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json
index 97f72f2ad11..48816809bbd 100644
--- a/homeassistant/components/cert_expiry/manifest.json
+++ b/homeassistant/components/cert_expiry/manifest.json
@@ -5,5 +5,8 @@
"requirements": [],
"config_flow": true,
"dependencies": [],
- "codeowners": ["@cereal2nd"]
+ "codeowners": [
+ "@Cereal2nd",
+ "@jjlawren"
+ ]
}
diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py
index a5b879e5661..3022c7bd42b 100644
--- a/homeassistant/components/cert_expiry/sensor.py
+++ b/homeassistant/components/cert_expiry/sensor.py
@@ -70,12 +70,18 @@ class SSLCertificate(Entity):
self._name = sensor_name
self._state = None
self._available = False
+ self._valid = False
@property
def name(self):
"""Return the name of the sensor."""
return self._name
+ @property
+ def unique_id(self):
+ """Return a unique id for the sensor."""
+ return f"{self.server_name}:{self.server_port}"
+
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
@@ -117,16 +123,17 @@ class SSLCertificate(Entity):
except socket.gaierror:
_LOGGER.error("Cannot resolve hostname: %s", self.server_name)
self._available = False
+ self._valid = False
return
except socket.timeout:
_LOGGER.error("Connection timeout with server: %s", self.server_name)
self._available = False
+ self._valid = False
return
- except OSError:
- _LOGGER.error(
- "Cannot fetch certificate from %s", self.server_name, exc_info=1
- )
- self._available = False
+ except (ssl.CertificateError, ssl.SSLError):
+ self._available = True
+ self._state = 0
+ self._valid = False
return
ts_seconds = ssl.cert_time_to_seconds(cert["notAfter"])
@@ -134,3 +141,11 @@ class SSLCertificate(Entity):
expiry = timestamp - datetime.today()
self._available = True
self._state = expiry.days
+ self._valid = True
+
+ @property
+ def device_state_attributes(self):
+ """Return additional sensor state attributes."""
+ attr = {"is_valid": self._valid}
+
+ return attr
diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json
index 3e2fea2342e..e5e670d214f 100644
--- a/homeassistant/components/cert_expiry/strings.json
+++ b/homeassistant/components/cert_expiry/strings.json
@@ -15,7 +15,8 @@
"host_port_exists": "This host and port combination is already configured",
"resolve_failed": "This host can not be resolved",
"connection_timeout": "Timeout when connecting to this host",
- "certificate_fetch_failed": "Can not fetch certificate from this host and port combination"
+ "certificate_error": "Certificate could not be validated",
+ "wrong_host": "Certificate does not match hostname"
},
"abort": {
"host_port_exists": "This host and port combination is already configured"
diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py
index 6c3e18cdb05..6d978a5451e 100644
--- a/homeassistant/components/channels/media_player.py
+++ b/homeassistant/components/channels/media_player.py
@@ -1,9 +1,10 @@
"""Support for interfacing with an instance of getchannels.com."""
import logging
+from pychannels import Channels
import voluptuous as vol
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
DOMAIN,
MEDIA_TYPE_CHANNEL,
@@ -124,7 +125,6 @@ class ChannelsPlayer(MediaPlayerDevice):
def __init__(self, name, host, port):
"""Initialize the Channels app."""
- from pychannels import Channels
self._name = name
self._host = host
diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py
index b442b24feb4..5a42ef1c8b8 100644
--- a/homeassistant/components/cisco_ios/device_tracker.py
+++ b/homeassistant/components/cisco_ios/device_tracker.py
@@ -1,15 +1,17 @@
"""Support for Cisco IOS Routers."""
import logging
+import re
+from pexpect import pxssh
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN,
PLATFORM_SCHEMA,
DeviceScanner,
)
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -100,8 +102,6 @@ class CiscoDeviceScanner(DeviceScanner):
def _get_arp_data(self):
"""Open connection to the router and get arp entries."""
- from pexpect import pxssh
- import re
try:
cisco_ssh = pxssh.pxssh()
diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py
index ca24fcb5c52..702ebdfa611 100644
--- a/homeassistant/components/cisco_mobility_express/device_tracker.py
+++ b/homeassistant/components/cisco_mobility_express/device_tracker.py
@@ -1,9 +1,9 @@
"""Support for Cisco Mobility Express."""
import logging
+from ciscomobilityexpress.ciscome import CiscoMobilityExpress
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN,
PLATFORM_SCHEMA,
@@ -11,11 +11,12 @@ from homeassistant.components.device_tracker import (
)
from homeassistant.const import (
CONF_HOST,
- CONF_USERNAME,
CONF_PASSWORD,
CONF_SSL,
+ CONF_USERNAME,
CONF_VERIFY_SSL,
)
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -35,7 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_scanner(hass, config):
"""Validate the configuration and return a Cisco ME scanner."""
- from ciscomobilityexpress.ciscome import CiscoMobilityExpress
config = config[DOMAIN]
diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py
index a77f5673df7..6f80fa138d4 100644
--- a/homeassistant/components/cisco_webex_teams/notify.py
+++ b/homeassistant/components/cisco_webex_teams/notify.py
@@ -2,11 +2,12 @@
import logging
import voluptuous as vol
+from webexteamssdk import ApiError, WebexTeamsAPI, exceptions
from homeassistant.components.notify import (
+ ATTR_TITLE,
PLATFORM_SCHEMA,
BaseNotificationService,
- ATTR_TITLE,
)
from homeassistant.const import CONF_TOKEN
import homeassistant.helpers.config_validation as cv
@@ -22,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the CiscoWebexTeams notification service."""
- from webexteamssdk import WebexTeamsAPI, exceptions
client = WebexTeamsAPI(access_token=config[CONF_TOKEN])
try:
@@ -45,7 +45,6 @@ class CiscoWebexTeamsNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
- from webexteamssdk import ApiError
title = ""
if kwargs.get(ATTR_TITLE) is not None:
diff --git a/homeassistant/components/ciscospark/notify.py b/homeassistant/components/ciscospark/notify.py
index 67609766366..e765aff05f6 100644
--- a/homeassistant/components/ciscospark/notify.py
+++ b/homeassistant/components/ciscospark/notify.py
@@ -1,16 +1,16 @@
"""Cisco Spark platform for notify component."""
import logging
+from ciscosparkapi import CiscoSparkAPI, SparkApiError
import voluptuous as vol
-from homeassistant.const import CONF_TOKEN
-import homeassistant.helpers.config_validation as cv
-
from homeassistant.components.notify import (
ATTR_TITLE,
PLATFORM_SCHEMA,
BaseNotificationService,
)
+from homeassistant.const import CONF_TOKEN
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -33,7 +33,6 @@ class CiscoSparkNotificationService(BaseNotificationService):
def __init__(self, token, default_room):
"""Initialize the service."""
- from ciscosparkapi import CiscoSparkAPI
self._default_room = default_room
self._token = token
@@ -41,7 +40,6 @@ class CiscoSparkNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
- from ciscosparkapi import SparkApiError
try:
title = ""
diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py
index 71550fc37b1..a2c79fdc0a7 100644
--- a/homeassistant/components/cloud/__init__.py
+++ b/homeassistant/components/cloud/__init__.py
@@ -1,6 +1,7 @@
"""Component to integrate the Home Assistant cloud."""
import logging
+from hass_nabucasa import Cloud
import voluptuous as vol
from homeassistant.auth.const import GROUP_ID_ADMIN
@@ -20,25 +21,26 @@ from homeassistant.loader import bind_hass
from homeassistant.util.aiohttp import MockRequest
from . import http_api
+from .client import CloudClient
from .const import (
CONF_ACME_DIRECTORY_SERVER,
CONF_ALEXA,
+ CONF_ALEXA_ACCESS_TOKEN_URL,
CONF_ALIASES,
CONF_CLOUDHOOK_CREATE_URL,
CONF_COGNITO_CLIENT_ID,
CONF_ENTITY_CONFIG,
CONF_FILTER,
CONF_GOOGLE_ACTIONS,
+ CONF_GOOGLE_ACTIONS_REPORT_STATE_URL,
CONF_GOOGLE_ACTIONS_SYNC_URL,
CONF_RELAYER,
CONF_REMOTE_API_URL,
CONF_SUBSCRIPTION_INFO_URL,
CONF_USER_POOL_ID,
- CONF_GOOGLE_ACTIONS_REPORT_STATE_URL,
DOMAIN,
MODE_DEV,
MODE_PROD,
- CONF_ALEXA_ACCESS_TOKEN_URL,
)
from .prefs import CloudPreferences
@@ -166,8 +168,6 @@ def is_cloudhook_request(request):
async def async_setup(hass, config):
"""Initialize the Home Assistant cloud."""
- from hass_nabucasa import Cloud
- from .client import CloudClient
# Process configs
if DOMAIN in config:
diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py
index 38ae09ced93..c7626777943 100644
--- a/homeassistant/components/cloud/client.py
+++ b/homeassistant/components/cloud/client.py
@@ -110,14 +110,17 @@ class CloudClient(Interface):
if not self.cloud.is_logged_in:
return
- if self.alexa_config.should_report_state:
+ if self.alexa_config.enabled and self.alexa_config.should_report_state:
try:
await self.alexa_config.async_enable_proactive_mode()
except alexa_errors.NoTokenAvailable:
pass
- if self.google_config.should_report_state:
- self.google_config.async_enable_report_state()
+ if self.google_config.enabled:
+ self.google_config.async_enable_local_sdk()
+
+ if self.google_config.should_report_state:
+ self.google_config.async_enable_report_state()
async def cleanups(self) -> None:
"""Cleanup some stuff after logout."""
diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py
index e28d75f017d..6495cba23b7 100644
--- a/homeassistant/components/cloud/const.py
+++ b/homeassistant/components/cloud/const.py
@@ -16,6 +16,7 @@ PREF_OVERRIDE_NAME = "override_name"
PREF_DISABLE_2FA = "disable_2fa"
PREF_ALIASES = "aliases"
PREF_SHOULD_EXPOSE = "should_expose"
+PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id"
DEFAULT_SHOULD_EXPOSE = True
DEFAULT_DISABLE_2FA = False
DEFAULT_ALEXA_REPORT_STATE = False
diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py
index 38e4aec56e0..582fa007550 100644
--- a/homeassistant/components/cloud/google_config.py
+++ b/homeassistant/components/cloud/google_config.py
@@ -63,6 +63,19 @@ class CloudGoogleConfig(AbstractConfig):
"""Return if states should be proactively reported."""
return self._prefs.google_report_state
+ @property
+ def local_sdk_webhook_id(self):
+ """Return the local SDK webhook.
+
+ Return None to disable the local SDK.
+ """
+ return self._prefs.google_local_webhook_id
+
+ @property
+ def local_sdk_user_id(self):
+ """Return the user ID to be used for actions received via the local SDK."""
+ return self._prefs.cloud_user
+
def should_expose(self, state):
"""If a state object should be exposed."""
return self._should_expose_entity_id(state.entity_id)
@@ -131,17 +144,19 @@ class CloudGoogleConfig(AbstractConfig):
# State reporting is reported as a property on entities.
# So when we change it, we need to sync all entities.
await self.async_sync_entities()
- return
# If entity prefs are the same or we have filter in config.yaml,
# don't sync.
- if (
- self._cur_entity_prefs is prefs.google_entity_configs
- or not self._config["filter"].empty_filter
+ elif (
+ self._cur_entity_prefs is not prefs.google_entity_configs
+ and self._config["filter"].empty_filter
):
- return
+ self.async_schedule_google_sync()
- self.async_schedule_google_sync()
+ if self.enabled and not self.is_local_sdk_active:
+ self.async_enable_local_sdk()
+ elif not self.enabled and self.is_local_sdk_active:
+ self.async_disable_local_sdk()
async def _handle_entity_registry_updated(self, event):
"""Handle when entity registry updated."""
diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index f243eab8fd0..97c96b0a3e8 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -3,33 +3,34 @@ import asyncio
from functools import wraps
import logging
-import attr
import aiohttp
import async_timeout
+import attr
+from hass_nabucasa import Cloud, auth
+from hass_nabucasa.const import STATE_DISCONNECTED
import voluptuous as vol
-from hass_nabucasa import Cloud
-from homeassistant.core import callback
-from homeassistant.components.http import HomeAssistantView
-from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components import websocket_api
-from homeassistant.components.websocket_api import const as ws_const
from homeassistant.components.alexa import (
entities as alexa_entities,
errors as alexa_errors,
)
from homeassistant.components.google_assistant import helpers as google_helpers
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.components.http.data_validator import RequestDataValidator
+from homeassistant.components.websocket_api import const as ws_const
+from homeassistant.core import callback
from .const import (
DOMAIN,
- REQUEST_TIMEOUT,
+ PREF_ALEXA_REPORT_STATE,
PREF_ENABLE_ALEXA,
PREF_ENABLE_GOOGLE,
+ PREF_GOOGLE_REPORT_STATE,
PREF_GOOGLE_SECURE_DEVICES_PIN,
+ REQUEST_TIMEOUT,
InvalidTrustedNetworks,
InvalidTrustedProxies,
- PREF_ALEXA_REPORT_STATE,
- PREF_GOOGLE_REPORT_STATE,
RequireRelink,
)
@@ -104,8 +105,6 @@ async def async_setup(hass):
hass.http.register_view(CloudResendConfirmView)
hass.http.register_view(CloudForgotPasswordView)
- from hass_nabucasa import auth
-
_CLOUD_ERRORS.update(
{
auth.UserNotFound: (400, "User does not exist."),
@@ -320,7 +319,6 @@ def _require_cloud_login(handler):
@websocket_api.async_response
async def websocket_subscription(hass, connection, msg):
"""Handle request for account info."""
- from hass_nabucasa.const import STATE_DISCONNECTED
cloud = hass.data[DOMAIN]
@@ -417,7 +415,6 @@ async def websocket_hook_delete(hass, connection, msg):
def _account_data(cloud):
"""Generate the auth data JSON response."""
- from hass_nabucasa.const import STATE_DISCONNECTED
if not cloud.is_logged_in:
return {"logged_in": False, "cloud": STATE_DISCONNECTED}
diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py
index a8ff775a227..0599b00a8bd 100644
--- a/homeassistant/components/cloud/prefs.py
+++ b/homeassistant/components/cloud/prefs.py
@@ -21,6 +21,7 @@ from .const import (
PREF_ALEXA_REPORT_STATE,
DEFAULT_ALEXA_REPORT_STATE,
PREF_GOOGLE_REPORT_STATE,
+ PREF_GOOGLE_LOCAL_WEBHOOK_ID,
DEFAULT_GOOGLE_REPORT_STATE,
InvalidTrustedNetworks,
InvalidTrustedProxies,
@@ -59,6 +60,14 @@ class CloudPreferences:
self._prefs = prefs
+ if PREF_GOOGLE_LOCAL_WEBHOOK_ID not in self._prefs:
+ await self._save_prefs(
+ {
+ **self._prefs,
+ PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(),
+ }
+ )
+
@callback
def async_listen_updates(self, listener):
"""Listen for updates to the preferences."""
@@ -79,6 +88,8 @@ class CloudPreferences:
google_report_state=_UNDEF,
):
"""Update user preferences."""
+ prefs = {**self._prefs}
+
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
(PREF_ENABLE_ALEXA, alexa_enabled),
@@ -92,20 +103,17 @@ class CloudPreferences:
(PREF_GOOGLE_REPORT_STATE, google_report_state),
):
if value is not _UNDEF:
- self._prefs[key] = value
+ prefs[key] = value
if remote_enabled is True and self._has_local_trusted_network:
- self._prefs[PREF_ENABLE_REMOTE] = False
+ prefs[PREF_ENABLE_REMOTE] = False
raise InvalidTrustedNetworks
if remote_enabled is True and self._has_local_trusted_proxies:
- self._prefs[PREF_ENABLE_REMOTE] = False
+ prefs[PREF_ENABLE_REMOTE] = False
raise InvalidTrustedProxies
- await self._store.async_save(self._prefs)
-
- for listener in self._listeners:
- self._hass.async_create_task(async_create_catching_coro(listener(self)))
+ await self._save_prefs(prefs)
async def async_update_google_entity_config(
self,
@@ -216,6 +224,11 @@ class CloudPreferences:
"""Return Google Entity configurations."""
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
+ @property
+ def google_local_webhook_id(self):
+ """Return Google webhook ID to receive local messages."""
+ return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID]
+
@property
def alexa_entity_configs(self):
"""Return Alexa Entity configurations."""
@@ -262,3 +275,11 @@ class CloudPreferences:
return True
return False
+
+ async def _save_prefs(self, prefs):
+ """Save preferences to disk."""
+ self._prefs = prefs
+ await self._store.async_save(self._prefs)
+
+ for listener in self._listeners:
+ self._hass.async_create_task(async_create_catching_coro(listener(self)))
diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py
index 26feff069da..265621b6250 100644
--- a/homeassistant/components/cloudflare/__init__.py
+++ b/homeassistant/components/cloudflare/__init__.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+from pycfdns import CloudflareUpdater
import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_EMAIL, CONF_ZONE
@@ -33,7 +34,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Cloudflare component."""
- from pycfdns import CloudflareUpdater
cfupdate = CloudflareUpdater()
email = config[DOMAIN][CONF_EMAIL]
diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py
index dbaa763c461..3daf0bac828 100644
--- a/homeassistant/components/cmus/media_player.py
+++ b/homeassistant/components/cmus/media_player.py
@@ -1,9 +1,10 @@
"""Support for interacting with and controlling the cmus music player."""
import logging
+from pycmus import exceptions, remote
import voluptuous as vol
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_PLAYLIST,
@@ -57,7 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discover_info=None):
"""Set up the CMUS platform."""
- from pycmus import exceptions
host = config.get(CONF_HOST)
password = config.get(CONF_PASSWORD)
@@ -78,7 +78,6 @@ class CmusDevice(MediaPlayerDevice):
# pylint: disable=no-member
def __init__(self, server, password, port, name):
"""Initialize the CMUS device."""
- from pycmus import remote
if server:
self.cmus = remote.PyCmus(server=server, password=password, port=port)
diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py
index 9098a053fff..7160d140b3f 100644
--- a/homeassistant/components/co2signal/sensor.py
+++ b/homeassistant/components/co2signal/sensor.py
@@ -1,16 +1,17 @@
"""Support for the CO2signal platform."""
import logging
+import CO2Signal
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
+from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_ATTRIBUTION,
- CONF_TOKEN,
CONF_LATITUDE,
CONF_LONGITUDE,
+ CONF_TOKEN,
)
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
CONF_COUNTRY_CODE = "country_code"
@@ -97,7 +98,6 @@ class CO2Sensor(Entity):
def update(self):
"""Get the latest data and updates the states."""
- import CO2Signal
_LOGGER.debug("Update data for %s", self._friendly_name)
diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py
index 6eca0616ca8..67869e6b88c 100644
--- a/homeassistant/components/coinbase/__init__.py
+++ b/homeassistant/components/coinbase/__init__.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+from coinbase.wallet.client import Client
+from coinbase.wallet.error import AuthenticationError
import voluptuous as vol
from homeassistant.const import CONF_API_KEY
@@ -79,7 +81,6 @@ class CoinbaseData:
def __init__(self, api_key, api_secret):
"""Init the coinbase data object."""
- from coinbase.wallet.client import Client
self.client = Client(api_key, api_secret)
self.update()
@@ -87,7 +88,6 @@ class CoinbaseData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from coinbase."""
- from coinbase.wallet.error import AuthenticationError
try:
self.accounts = self.client.get_accounts()
diff --git a/homeassistant/components/coinmarketcap/sensor.py b/homeassistant/components/coinmarketcap/sensor.py
index fbe05187684..ca166aa793a 100644
--- a/homeassistant/components/coinmarketcap/sensor.py
+++ b/homeassistant/components/coinmarketcap/sensor.py
@@ -1,13 +1,14 @@
"""Details about crypto currencies from CoinMarketCap."""
-import logging
from datetime import timedelta
+import logging
from urllib.error import HTTPError
+from coinmarketcap import Market
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -159,6 +160,5 @@ class CoinMarketCapData:
def update(self):
"""Get the latest data from coinmarketcap.com."""
- from coinmarketcap import Market
self.ticker = Market().ticker(self.currency_id, convert=self.display_currency)
diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py
index 22e9d95bbd8..aef4bf1deeb 100644
--- a/homeassistant/components/comfoconnect/__init__.py
+++ b/homeassistant/components/comfoconnect/__init__.py
@@ -1,6 +1,12 @@
"""Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit."""
import logging
+from pycomfoconnect import (
+ SENSOR_TEMPERATURE_EXTRACT,
+ SENSOR_TEMPERATURE_OUTDOOR,
+ Bridge,
+ ComfoConnect,
+)
import voluptuous as vol
from homeassistant.const import (
@@ -56,7 +62,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the ComfoConnect bridge."""
- from pycomfoconnect import Bridge
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
@@ -97,7 +102,6 @@ class ComfoConnectBridge:
def __init__(self, hass, bridge, name, token, friendly_name, pin):
"""Initialize the ComfoConnect bridge."""
- from pycomfoconnect import ComfoConnect
self.data = {}
self.name = name
@@ -125,11 +129,6 @@ class ComfoConnectBridge:
"""Call function for sensor updates."""
_LOGGER.debug("Got value from bridge: %d = %d", var, value)
- from pycomfoconnect import (
- SENSOR_TEMPERATURE_EXTRACT,
- SENSOR_TEMPERATURE_OUTDOOR,
- )
-
if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]:
self.data[var] = value / 10
else:
diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py
index 6c90ab8cba1..bbb4b0176bf 100644
--- a/homeassistant/components/comfoconnect/fan.py
+++ b/homeassistant/components/comfoconnect/fan.py
@@ -1,6 +1,14 @@
"""Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit."""
import logging
+from pycomfoconnect import (
+ CMD_FAN_MODE_AWAY,
+ CMD_FAN_MODE_HIGH,
+ CMD_FAN_MODE_LOW,
+ CMD_FAN_MODE_MEDIUM,
+ SENSOR_FAN_SPEED_MODE,
+)
+
from homeassistant.components.fan import (
SPEED_HIGH,
SPEED_LOW,
@@ -30,7 +38,6 @@ class ComfoConnectFan(FanEntity):
def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None:
"""Initialize the ComfoConnect fan."""
- from pycomfoconnect import SENSOR_FAN_SPEED_MODE
self._ccb = ccb
self._name = name
@@ -64,7 +71,6 @@ class ComfoConnectFan(FanEntity):
@property
def speed(self):
"""Return the current fan mode."""
- from pycomfoconnect import SENSOR_FAN_SPEED_MODE
try:
speed = self._ccb.data[SENSOR_FAN_SPEED_MODE]
@@ -91,13 +97,6 @@ class ComfoConnectFan(FanEntity):
"""Set fan speed."""
_LOGGER.debug("Changing fan speed to %s.", speed)
- from pycomfoconnect import (
- CMD_FAN_MODE_AWAY,
- CMD_FAN_MODE_LOW,
- CMD_FAN_MODE_MEDIUM,
- CMD_FAN_MODE_HIGH,
- )
-
if speed == SPEED_OFF:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY)
elif speed == SPEED_LOW:
diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py
index 4099804d413..06d0506e2cf 100644
--- a/homeassistant/components/comfoconnect/sensor.py
+++ b/homeassistant/components/comfoconnect/sensor.py
@@ -1,6 +1,15 @@
"""Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit."""
import logging
+from pycomfoconnect import (
+ SENSOR_FAN_EXHAUST_FLOW,
+ SENSOR_FAN_SUPPLY_FLOW,
+ SENSOR_HUMIDITY_EXTRACT,
+ SENSOR_HUMIDITY_OUTDOOR,
+ SENSOR_TEMPERATURE_EXTRACT,
+ SENSOR_TEMPERATURE_OUTDOOR,
+)
+
from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS
from homeassistant.helpers.dispatcher import dispatcher_connect
from homeassistant.helpers.entity import Entity
@@ -24,14 +33,6 @@ SENSOR_TYPES = {}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ComfoConnect fan platform."""
- from pycomfoconnect import (
- SENSOR_TEMPERATURE_EXTRACT,
- SENSOR_HUMIDITY_EXTRACT,
- SENSOR_TEMPERATURE_OUTDOOR,
- SENSOR_HUMIDITY_OUTDOOR,
- SENSOR_FAN_SUPPLY_FLOW,
- SENSOR_FAN_EXHAUST_FLOW,
- )
global SENSOR_TYPES
SENSOR_TYPES = {
diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py
index e86ec02040e..37bbf052838 100644
--- a/homeassistant/components/concord232/alarm_control_panel.py
+++ b/homeassistant/components/concord232/alarm_control_panel.py
@@ -2,22 +2,23 @@
import datetime
import logging
+from concord232 import client as concord232_client
import requests
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
+ CONF_CODE,
CONF_HOST,
+ CONF_MODE,
CONF_NAME,
CONF_PORT,
- CONF_CODE,
- CONF_MODE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -60,7 +61,6 @@ class Concord232Alarm(alarm.AlarmControlPanel):
def __init__(self, url, name, code, mode):
"""Initialize the Concord232 alarm panel."""
- from concord232 import client as concord232_client
self._state = None
self._name = name
diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py
index 1a406d743b7..2d119e2cf86 100644
--- a/homeassistant/components/concord232/binary_sensor.py
+++ b/homeassistant/components/concord232/binary_sensor.py
@@ -2,13 +2,14 @@
import datetime
import logging
+from concord232 import client as concord232_client
import requests
import voluptuous as vol
from homeassistant.components.binary_sensor import (
- BinarySensorDevice,
- PLATFORM_SCHEMA,
DEVICE_CLASSES,
+ PLATFORM_SCHEMA,
+ BinarySensorDevice,
)
from homeassistant.const import CONF_HOST, CONF_PORT
import homeassistant.helpers.config_validation as cv
@@ -42,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Concord232 binary sensor platform."""
- from concord232 import client as concord232_client
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py
index 97ddf1e0714..0e9b4053b7b 100644
--- a/homeassistant/components/config/automation.py
+++ b/homeassistant/components/config/automation.py
@@ -5,12 +5,11 @@ import uuid
from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA
from homeassistant.components.automation.config import async_validate_config_item
from homeassistant.const import CONF_ID, SERVICE_RELOAD
+from homeassistant.config import AUTOMATION_CONFIG_PATH
import homeassistant.helpers.config_validation as cv
from . import EditIdBasedConfigView
-CONFIG_PATH = "automations.yaml"
-
async def async_setup(hass):
"""Set up the Automation config API."""
@@ -23,7 +22,7 @@ async def async_setup(hass):
EditAutomationConfigView(
DOMAIN,
"config",
- CONFIG_PATH,
+ AUTOMATION_CONFIG_PATH,
cv.string,
PLATFORM_SCHEMA,
post_write_hook=hook,
diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
index b21991a8479..81065665e34 100644
--- a/homeassistant/components/config/config_entries.py
+++ b/homeassistant/components/config/config_entries.py
@@ -1,6 +1,7 @@
"""Http views to control the config manager."""
import aiohttp.web_exceptions
import voluptuous as vol
+import voluptuous_serialize
from homeassistant import config_entries, data_entry_flow
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES
@@ -41,8 +42,6 @@ def _prepare_json(result):
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
return result
- import voluptuous_serialize
-
data = result.copy()
schema = data["data_schema"]
diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py
index 371bd98cf08..d104cd2e1df 100644
--- a/homeassistant/components/config/group.py
+++ b/homeassistant/components/config/group.py
@@ -1,12 +1,11 @@
"""Provide configuration end points for Groups."""
from homeassistant.components.group import DOMAIN, GROUP_SCHEMA
from homeassistant.const import SERVICE_RELOAD
+from homeassistant.config import GROUP_CONFIG_PATH
import homeassistant.helpers.config_validation as cv
from . import EditKeyBasedConfigView
-CONFIG_PATH = "groups.yaml"
-
async def async_setup(hass):
"""Set up the Group config API."""
@@ -17,7 +16,12 @@ async def async_setup(hass):
hass.http.register_view(
EditKeyBasedConfigView(
- "group", "config", CONFIG_PATH, cv.slug, GROUP_SCHEMA, post_write_hook=hook
+ "group",
+ "config",
+ GROUP_CONFIG_PATH,
+ cv.slug,
+ GROUP_SCHEMA,
+ post_write_hook=hook,
)
)
return True
diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py
index 8ce163745f1..e63651d8f2a 100644
--- a/homeassistant/components/config/script.py
+++ b/homeassistant/components/config/script.py
@@ -1,12 +1,11 @@
"""Provide configuration end points for scripts."""
from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA
from homeassistant.const import SERVICE_RELOAD
+from homeassistant.config import SCRIPT_CONFIG_PATH
import homeassistant.helpers.config_validation as cv
from . import EditKeyBasedConfigView
-CONFIG_PATH = "scripts.yaml"
-
async def async_setup(hass):
"""Set up the script config API."""
@@ -19,7 +18,7 @@ async def async_setup(hass):
EditKeyBasedConfigView(
"script",
"config",
- CONFIG_PATH,
+ SCRIPT_CONFIG_PATH,
cv.slug,
SCRIPT_ENTRY_SCHEMA,
post_write_hook=hook,
diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py
index 9d7d510b10e..798fc926e0f 100644
--- a/homeassistant/components/conversation/__init__.py
+++ b/homeassistant/components/conversation/__init__.py
@@ -6,15 +6,12 @@ import voluptuous as vol
from homeassistant import core
from homeassistant.components import http
-from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER
from homeassistant.components.http.data_validator import RequestDataValidator
-from homeassistant.const import EVENT_COMPONENT_LOADED
-from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv, intent
from homeassistant.loader import bind_hass
-from homeassistant.setup import ATTR_COMPONENT
-from .util import create_matcher
+from .agent import AbstractConversationAgent
+from .default_agent import async_register, DefaultAgent
_LOGGER = logging.getLogger(__name__)
@@ -22,15 +19,8 @@ ATTR_TEXT = "text"
DOMAIN = "conversation"
-REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)")
REGEX_TYPE = type(re.compile(""))
-
-UTTERANCES = {
- "cover": {
- INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"],
- INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"],
- }
-}
+DATA_AGENT = "conversation_agent"
SERVICE_PROCESS = "process"
@@ -50,137 +40,64 @@ CONFIG_SCHEMA = vol.Schema(
)
+async_register = bind_hass(async_register) # pylint: disable=invalid-name
+
+
@core.callback
@bind_hass
-def async_register(hass, intent_type, utterances):
- """Register utterances and any custom intents.
-
- Registrations don't require conversations to be loaded. They will become
- active once the conversation component is loaded.
- """
- intents = hass.data.get(DOMAIN)
-
- if intents is None:
- intents = hass.data[DOMAIN] = {}
-
- conf = intents.get(intent_type)
-
- if conf is None:
- conf = intents[intent_type] = []
-
- for utterance in utterances:
- if isinstance(utterance, REGEX_TYPE):
- conf.append(utterance)
- else:
- conf.append(create_matcher(utterance))
+def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent):
+ """Set the agent to handle the conversations."""
+ hass.data[DATA_AGENT] = agent
async def async_setup(hass, config):
"""Register the process service."""
- config = config.get(DOMAIN, {})
- intents = hass.data.get(DOMAIN)
- if intents is None:
- intents = hass.data[DOMAIN] = {}
+ async def process(hass, text):
+ """Process a line of text."""
+ agent = hass.data.get(DATA_AGENT)
- for intent_type, utterances in config.get("intents", {}).items():
- conf = intents.get(intent_type)
+ if agent is None:
+ agent = hass.data[DATA_AGENT] = DefaultAgent(hass)
+ await agent.async_initialize(config)
- if conf is None:
- conf = intents[intent_type] = []
+ return await agent.async_process(text)
- conf.extend(create_matcher(utterance) for utterance in utterances)
-
- async def process(service):
+ async def handle_service(service):
"""Parse text into commands."""
text = service.data[ATTR_TEXT]
_LOGGER.debug("Processing: <%s>", text)
try:
- await _process(hass, text)
+ await process(hass, text)
except intent.IntentHandleError as err:
_LOGGER.error("Error processing %s: %s", text, err)
hass.services.async_register(
- DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA
+ DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA
)
- hass.http.register_view(ConversationProcessView)
-
- # We strip trailing 's' from name because our state matcher will fail
- # if a letter is not there. By removing 's' we can match singular and
- # plural names.
-
- async_register(
- hass,
- intent.INTENT_TURN_ON,
- ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"],
- )
- async_register(
- hass,
- intent.INTENT_TURN_OFF,
- ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"],
- )
- async_register(
- hass,
- intent.INTENT_TOGGLE,
- ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"],
- )
-
- @callback
- def register_utterances(component):
- """Register utterances for a component."""
- if component not in UTTERANCES:
- return
- for intent_type, sentences in UTTERANCES[component].items():
- async_register(hass, intent_type, sentences)
-
- @callback
- def component_loaded(event):
- """Handle a new component loaded."""
- register_utterances(event.data[ATTR_COMPONENT])
-
- hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
-
- # Check already loaded components.
- for component in hass.config.components:
- register_utterances(component)
+ hass.http.register_view(ConversationProcessView(process))
return True
-async def _process(hass, text):
- """Process a line of text."""
- intents = hass.data.get(DOMAIN, {})
-
- for intent_type, matchers in intents.items():
- for matcher in matchers:
- match = matcher.match(text)
-
- if not match:
- continue
-
- response = await hass.helpers.intent.async_handle(
- DOMAIN,
- intent_type,
- {key: {"value": value} for key, value in match.groupdict().items()},
- text,
- )
- return response
-
-
class ConversationProcessView(http.HomeAssistantView):
"""View to retrieve shopping list content."""
url = "/api/conversation/process"
name = "api:conversation:process"
+ def __init__(self, process):
+ """Initialize the conversation process view."""
+ self._process = process
+
@RequestDataValidator(vol.Schema({vol.Required("text"): str}))
async def post(self, request, data):
"""Send a request for processing."""
hass = request.app["hass"]
try:
- intent_result = await _process(hass, data["text"])
+ intent_result = await self._process(hass, data["text"])
except intent.IntentHandleError as err:
intent_result = intent.IntentResponse()
intent_result.async_set_speech(str(err))
diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py
new file mode 100644
index 00000000000..eae6402530c
--- /dev/null
+++ b/homeassistant/components/conversation/agent.py
@@ -0,0 +1,12 @@
+"""Agent foundation for conversation integration."""
+from abc import ABC, abstractmethod
+
+from homeassistant.helpers import intent
+
+
+class AbstractConversationAgent(ABC):
+ """Abstract conversation agent."""
+
+ @abstractmethod
+ async def async_process(self, text: str) -> intent.IntentResponse:
+ """Process a sentence."""
diff --git a/homeassistant/components/conversation/const.py b/homeassistant/components/conversation/const.py
new file mode 100644
index 00000000000..04bfa373061
--- /dev/null
+++ b/homeassistant/components/conversation/const.py
@@ -0,0 +1,3 @@
+"""Const for conversation integration."""
+
+DOMAIN = "conversation"
diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py
new file mode 100644
index 00000000000..e93afcfaf65
--- /dev/null
+++ b/homeassistant/components/conversation/default_agent.py
@@ -0,0 +1,127 @@
+"""Standard conversastion implementation for Home Assistant."""
+import logging
+import re
+
+from homeassistant import core
+from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER
+from homeassistant.components.shopping_list import INTENT_ADD_ITEM, INTENT_LAST_ITEMS
+from homeassistant.const import EVENT_COMPONENT_LOADED
+from homeassistant.core import callback
+from homeassistant.helpers import intent
+from homeassistant.setup import ATTR_COMPONENT
+
+from .agent import AbstractConversationAgent
+from .const import DOMAIN
+from .util import create_matcher
+
+_LOGGER = logging.getLogger(__name__)
+
+REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)")
+REGEX_TYPE = type(re.compile(""))
+
+UTTERANCES = {
+ "cover": {
+ INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"],
+ INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"],
+ },
+ "shopping_list": {
+ INTENT_ADD_ITEM: ["Add [the] [a] [an] {item} to my shopping list"],
+ INTENT_LAST_ITEMS: ["What is on my shopping list"],
+ },
+}
+
+
+@core.callback
+def async_register(hass, intent_type, utterances):
+ """Register utterances and any custom intents for the default agent.
+
+ Registrations don't require conversations to be loaded. They will become
+ active once the conversation component is loaded.
+ """
+ intents = hass.data.setdefault(DOMAIN, {})
+ conf = intents.setdefault(intent_type, [])
+
+ for utterance in utterances:
+ if isinstance(utterance, REGEX_TYPE):
+ conf.append(utterance)
+ else:
+ conf.append(create_matcher(utterance))
+
+
+class DefaultAgent(AbstractConversationAgent):
+ """Default agent for conversation agent."""
+
+ def __init__(self, hass: core.HomeAssistant):
+ """Initialize the default agent."""
+ self.hass = hass
+
+ async def async_initialize(self, config):
+ """Initialize the default agent."""
+ config = config.get(DOMAIN, {})
+ intents = self.hass.data.setdefault(DOMAIN, {})
+
+ for intent_type, utterances in config.get("intents", {}).items():
+ conf = intents.get(intent_type)
+
+ if conf is None:
+ conf = intents[intent_type] = []
+
+ conf.extend(create_matcher(utterance) for utterance in utterances)
+
+ # We strip trailing 's' from name because our state matcher will fail
+ # if a letter is not there. By removing 's' we can match singular and
+ # plural names.
+
+ async_register(
+ self.hass,
+ intent.INTENT_TURN_ON,
+ ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"],
+ )
+ async_register(
+ self.hass,
+ intent.INTENT_TURN_OFF,
+ ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"],
+ )
+ async_register(
+ self.hass,
+ intent.INTENT_TOGGLE,
+ ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"],
+ )
+
+ @callback
+ def component_loaded(event):
+ """Handle a new component loaded."""
+ self.register_utterances(event.data[ATTR_COMPONENT])
+
+ self.hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
+
+ # Check already loaded components.
+ for component in self.hass.config.components:
+ self.register_utterances(component)
+
+ @callback
+ def register_utterances(self, component):
+ """Register utterances for a component."""
+ if component not in UTTERANCES:
+ return
+ for intent_type, sentences in UTTERANCES[component].items():
+ async_register(self.hass, intent_type, sentences)
+
+ async def async_process(self, text) -> intent.IntentResponse:
+ """Process a sentence."""
+ intents = self.hass.data[DOMAIN]
+
+ for intent_type, matchers in intents.items():
+ for matcher in matchers:
+ match = matcher.match(text)
+
+ if not match:
+ continue
+
+ return await intent.async_handle(
+ self.hass,
+ DOMAIN,
+ intent_type,
+ {key: {"value": value} for key, value in match.groupdict().items()},
+ text,
+ )
diff --git a/homeassistant/components/coolmaster/.translations/en.json b/homeassistant/components/coolmaster/.translations/en.json
new file mode 100644
index 00000000000..6c30efc594a
--- /dev/null
+++ b/homeassistant/components/coolmaster/.translations/en.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "error": {
+ "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.",
+ "no_units": "Could not find any HVAC units in CoolMasterNet host."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "cool": "Support cool mode",
+ "dry": "Support dry mode",
+ "fan_only": "Support fan only mode",
+ "heat": "Support heat mode",
+ "heat_cool": "Support automatic heat/cool mode",
+ "host": "Host",
+ "off": "Can be turned off"
+ },
+ "title": "Setup your CoolMasterNet connection details."
+ }
+ },
+ "title": "CoolMasterNet"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py
index b27ae5f25b4..530427d33ad 100644
--- a/homeassistant/components/coolmaster/__init__.py
+++ b/homeassistant/components/coolmaster/__init__.py
@@ -1 +1,22 @@
-"""The coolmaster component."""
+"""The Coolmaster integration."""
+
+
+async def async_setup(hass, config):
+ """Set up Coolmaster components."""
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up Coolmaster from a config entry."""
+ hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, "climate"))
+
+ return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a Coolmaster config entry."""
+ await hass.async_add_job(
+ hass.config_entries.async_forward_entry_unload(entry, "climate")
+ )
+
+ return True
diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py
index 8a319c655f6..a52431dd89b 100644
--- a/homeassistant/components/coolmaster/climate.py
+++ b/homeassistant/components/coolmaster/climate.py
@@ -2,16 +2,16 @@
import logging
-import voluptuous as vol
+from pycoolmasternet import CoolMasterNet
-from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
+from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
- HVAC_MODE_OFF,
- HVAC_MODE_HEAT_COOL,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
+ HVAC_MODE_HEAT_COOL,
+ HVAC_MODE_OFF,
SUPPORT_FAN_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
@@ -22,21 +22,11 @@ from homeassistant.const import (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
-import homeassistant.helpers.config_validation as cv
+
+from .const import CONF_SUPPORTED_MODES, DOMAIN
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
-DEFAULT_PORT = 10102
-
-AVAILABLE_MODES = [
- HVAC_MODE_OFF,
- HVAC_MODE_HEAT,
- HVAC_MODE_COOL,
- HVAC_MODE_DRY,
- HVAC_MODE_HEAT_COOL,
- HVAC_MODE_FAN_ONLY,
-]
-
CM_TO_HA_STATE = {
"heat": HVAC_MODE_HEAT,
"cool": HVAC_MODE_COOL,
@@ -49,17 +39,6 @@ HA_STATE_TO_CM = {value: key for key, value in CM_TO_HA_STATE.items()}
FAN_MODES = ["low", "med", "high", "auto"]
-CONF_SUPPORTED_MODES = "supported_modes"
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_SUPPORTED_MODES, default=AVAILABLE_MODES): vol.All(
- cv.ensure_list, [vol.In(AVAILABLE_MODES)]
- ),
- }
-)
-
_LOGGER = logging.getLogger(__name__)
@@ -68,19 +47,17 @@ def _build_entity(device, supported_modes):
return CoolmasterClimate(device, supported_modes)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the CoolMasterNet climate platform."""
- from pycoolmasternet import CoolMasterNet
-
- supported_modes = config.get(CONF_SUPPORTED_MODES)
- host = config[CONF_HOST]
- port = config[CONF_PORT]
+ supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES)
+ host = config_entry.data[CONF_HOST]
+ port = config_entry.data[CONF_PORT]
cool = CoolMasterNet(host, port=port)
- devices = cool.devices()
+ devices = await hass.async_add_executor_job(cool.devices)
all_devices = [_build_entity(device, supported_modes) for device in devices]
- add_entities(all_devices, True)
+ async_add_devices(all_devices, True)
class CoolmasterClimate(ClimateDevice):
@@ -118,6 +95,16 @@ class CoolmasterClimate(ClimateDevice):
else:
self._unit = TEMP_FAHRENHEIT
+ @property
+ def device_info(self):
+ """Return device info for this device."""
+ return {
+ "identifiers": {(DOMAIN, self.unique_id)},
+ "name": self.name,
+ "manufacturer": "CoolAutomation",
+ "model": "CoolMasterNet",
+ }
+
@property
def unique_id(self):
"""Return unique ID for this device."""
diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py
new file mode 100644
index 00000000000..543b4c239c8
--- /dev/null
+++ b/homeassistant/components/coolmaster/config_flow.py
@@ -0,0 +1,64 @@
+"""Config flow to configure Coolmaster."""
+
+from pycoolmasternet import CoolMasterNet
+import voluptuous as vol
+
+from homeassistant import core, config_entries
+from homeassistant.const import CONF_HOST, CONF_PORT
+
+# pylint: disable=unused-import
+from .const import AVAILABLE_MODES, CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN
+
+MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES}
+
+DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA})
+
+
+async def validate_connection(hass: core.HomeAssistant, host):
+ """Validate that we can connect to the Coolmaster instance."""
+ cool = CoolMasterNet(host, port=DEFAULT_PORT)
+ devices = await hass.async_add_executor_job(cool.devices)
+ return len(devices) > 0
+
+
+class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a Coolmaster config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def _async_get_entry(self, data):
+ supported_modes = [
+ key for (key, value) in data.items() if key in AVAILABLE_MODES and value
+ ]
+ return self.async_create_entry(
+ title=data[CONF_HOST],
+ data={
+ CONF_HOST: data[CONF_HOST],
+ CONF_PORT: DEFAULT_PORT,
+ CONF_SUPPORTED_MODES: supported_modes,
+ },
+ )
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ if user_input is None:
+ return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
+
+ errors = {}
+
+ host = user_input[CONF_HOST]
+
+ try:
+ result = await validate_connection(self.hass, host)
+ if not result:
+ errors["base"] = "no_units"
+ except (ConnectionRefusedError, TimeoutError):
+ errors["base"] = "connection_error"
+
+ if errors:
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
+
+ return self._async_get_entry(user_input)
diff --git a/homeassistant/components/coolmaster/const.py b/homeassistant/components/coolmaster/const.py
new file mode 100644
index 00000000000..d4cfea73820
--- /dev/null
+++ b/homeassistant/components/coolmaster/const.py
@@ -0,0 +1,25 @@
+"""Constants for the Coolmaster integration."""
+
+from homeassistant.components.climate.const import (
+ HVAC_MODE_COOL,
+ HVAC_MODE_DRY,
+ HVAC_MODE_FAN_ONLY,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_HEAT_COOL,
+ HVAC_MODE_OFF,
+)
+
+DOMAIN = "coolmaster"
+
+DEFAULT_PORT = 10102
+
+CONF_SUPPORTED_MODES = "supported_modes"
+
+AVAILABLE_MODES = [
+ HVAC_MODE_OFF,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_COOL,
+ HVAC_MODE_DRY,
+ HVAC_MODE_HEAT_COOL,
+ HVAC_MODE_FAN_ONLY,
+]
diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json
index 69ab8ee3c4b..124a1e4a5b9 100644
--- a/homeassistant/components/coolmaster/manifest.json
+++ b/homeassistant/components/coolmaster/manifest.json
@@ -1,6 +1,7 @@
{
"domain": "coolmaster",
"name": "Coolmaster",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/coolmaster",
"requirements": [
"pycoolmasternet==0.0.4"
diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json
new file mode 100644
index 00000000000..d309f8c9c93
--- /dev/null
+++ b/homeassistant/components/coolmaster/strings.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "title": "CoolMasterNet",
+ "step": {
+ "user": {
+ "title": "Setup your CoolMasterNet connection details.",
+ "data": {
+ "host": "Host",
+ "off": "Can be turned off",
+ "heat": "Support heat mode",
+ "cool": "Support cool mode",
+ "heat_cool": "Support automatic heat/cool mode",
+ "dry": "Support dry mode",
+ "fan_only": "Support fan only mode"
+ }
+ }
+ },
+ "error": {
+ "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.",
+ "no_units": "Could not find any HVAC units in CoolMasterNet host."
+ }
+ }
+}
diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py
index 79877d63f14..aca3461b4f7 100644
--- a/homeassistant/components/counter/__init__.py
+++ b/homeassistant/components/counter/__init__.py
@@ -16,6 +16,7 @@ ATTR_INITIAL = "initial"
ATTR_STEP = "step"
ATTR_MINIMUM = "minimum"
ATTR_MAXIMUM = "maximum"
+VALUE = "value"
CONF_INITIAL = "initial"
CONF_RESTORE = "restore"
@@ -37,6 +38,8 @@ SERVICE_SCHEMA_CONFIGURE = ENTITY_SERVICE_SCHEMA.extend(
vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)),
vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)),
vol.Optional(ATTR_STEP): cv.positive_int,
+ vol.Optional(ATTR_INITIAL): cv.positive_int,
+ vol.Optional(VALUE): cv.positive_int,
}
)
@@ -171,6 +174,10 @@ class Counter(RestoreEntity):
state = await self.async_get_last_state()
if state is not None:
self._state = self.compute_next_state(int(state.state))
+ self._initial = state.attributes.get(ATTR_INITIAL)
+ self._max = state.attributes.get(ATTR_MAXIMUM)
+ self._min = state.attributes.get(ATTR_MINIMUM)
+ self._step = state.attributes.get(ATTR_STEP)
async def async_decrement(self):
"""Decrement the counter."""
@@ -195,6 +202,10 @@ class Counter(RestoreEntity):
self._max = kwargs[CONF_MAXIMUM]
if CONF_STEP in kwargs:
self._step = kwargs[CONF_STEP]
+ if CONF_INITIAL in kwargs:
+ self._initial = kwargs[CONF_INITIAL]
+ if VALUE in kwargs:
+ self._state = kwargs[VALUE]
self._state = self.compute_next_state(self._state)
await self.async_update_ha_state()
diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py
new file mode 100644
index 00000000000..ac5045d68e7
--- /dev/null
+++ b/homeassistant/components/counter/reproduce_state.py
@@ -0,0 +1,71 @@
+"""Reproduce an Counter state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ ATTR_INITIAL,
+ ATTR_MAXIMUM,
+ ATTR_MINIMUM,
+ ATTR_STEP,
+ VALUE,
+ DOMAIN,
+ SERVICE_CONFIGURE,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if not state.state.isdigit():
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if (
+ cur_state.state == state.state
+ and cur_state.attributes.get(ATTR_INITIAL) == state.attributes.get(ATTR_INITIAL)
+ and cur_state.attributes.get(ATTR_MAXIMUM) == state.attributes.get(ATTR_MAXIMUM)
+ and cur_state.attributes.get(ATTR_MINIMUM) == state.attributes.get(ATTR_MINIMUM)
+ and cur_state.attributes.get(ATTR_STEP) == state.attributes.get(ATTR_STEP)
+ ):
+ return
+
+ service_data = {ATTR_ENTITY_ID: state.entity_id, VALUE: state.state}
+ service = SERVICE_CONFIGURE
+ if ATTR_INITIAL in state.attributes:
+ service_data[ATTR_INITIAL] = state.attributes[ATTR_INITIAL]
+ if ATTR_MAXIMUM in state.attributes:
+ service_data[ATTR_MAXIMUM] = state.attributes[ATTR_MAXIMUM]
+ if ATTR_MINIMUM in state.attributes:
+ service_data[ATTR_MINIMUM] = state.attributes[ATTR_MINIMUM]
+ if ATTR_STEP in state.attributes:
+ service_data[ATTR_STEP] = state.attributes[ATTR_STEP]
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Counter states."""
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml
index fc3f0ad36cb..449ae6841ff 100644
--- a/homeassistant/components/counter/services.yaml
+++ b/homeassistant/components/counter/services.yaml
@@ -33,3 +33,9 @@ configure:
step:
description: New value for step
example: 2
+ initial:
+ description: New value for initial
+ example: 6
+ value:
+ description: New state value
+ example: 3
diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json
new file mode 100644
index 00000000000..ffa9ca1a927
--- /dev/null
+++ b/homeassistant/components/cover/.translations/ca.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} est\u00e0 tancat/da",
+ "is_closing": "{entity_name} est\u00e0 tancan't-se",
+ "is_open": "{entity_name} est\u00e0 obert/a",
+ "is_opening": "{entity_name} s'est\u00e0 obrint"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/da.json b/homeassistant/components/cover/.translations/da.json
new file mode 100644
index 00000000000..e603723b564
--- /dev/null
+++ b/homeassistant/components/cover/.translations/da.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} er lukket",
+ "is_closing": "{entity_name} lukker",
+ "is_open": "{entity_name} er \u00e5ben",
+ "is_opening": "{entity_name} \u00e5bnes"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/de.json b/homeassistant/components/cover/.translations/de.json
new file mode 100644
index 00000000000..e9ed497ccc2
--- /dev/null
+++ b/homeassistant/components/cover/.translations/de.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} ist geschlossen",
+ "is_closing": "{entity_name} wird geschlossen",
+ "is_open": "{entity_name} ist offen",
+ "is_opening": "{entity_name} wird ge\u00f6ffnet"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/en.json b/homeassistant/components/cover/.translations/en.json
new file mode 100644
index 00000000000..f9f47be3104
--- /dev/null
+++ b/homeassistant/components/cover/.translations/en.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} is closed",
+ "is_closing": "{entity_name} is closing",
+ "is_open": "{entity_name} is open",
+ "is_opening": "{entity_name} is opening"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/es.json b/homeassistant/components/cover/.translations/es.json
new file mode 100644
index 00000000000..d0193b939a5
--- /dev/null
+++ b/homeassistant/components/cover/.translations/es.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} est\u00e1 cerrado",
+ "is_closing": "{entity_name} se est\u00e1 cerrando",
+ "is_open": "{entity_name} est\u00e1 abierto",
+ "is_opening": "{entity_name} se est\u00e1 abriendo"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/fr.json b/homeassistant/components/cover/.translations/fr.json
new file mode 100644
index 00000000000..95978ed0fa5
--- /dev/null
+++ b/homeassistant/components/cover/.translations/fr.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} est ferm\u00e9",
+ "is_closing": "{entity_name} se ferme",
+ "is_open": "{entity_name} est ouvert",
+ "is_opening": "{entity_name} est en train de s'ouvrir"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/it.json b/homeassistant/components/cover/.translations/it.json
new file mode 100644
index 00000000000..6a25c0f3f2f
--- /dev/null
+++ b/homeassistant/components/cover/.translations/it.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} \u00e8 chiuso",
+ "is_closing": "{entity_name} si sta chiudendo",
+ "is_open": "{entity_name} \u00e8 aperto",
+ "is_opening": "{entity_name} si sta aprendo"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json
new file mode 100644
index 00000000000..02f900a8fe5
--- /dev/null
+++ b/homeassistant/components/cover/.translations/ko.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4",
+ "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4",
+ "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4",
+ "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/lb.json b/homeassistant/components/cover/.translations/lb.json
new file mode 100644
index 00000000000..b0c9e1d0d4c
--- /dev/null
+++ b/homeassistant/components/cover/.translations/lb.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} ass zou",
+ "is_closing": "{entity_name} g\u00ebtt zougemaach",
+ "is_open": "{entity_name} ass op",
+ "is_opening": "{entity_name} g\u00ebtt opgemaach"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/nl.json b/homeassistant/components/cover/.translations/nl.json
new file mode 100644
index 00000000000..93015afbfdd
--- /dev/null
+++ b/homeassistant/components/cover/.translations/nl.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} is gesloten",
+ "is_closing": "{entity_name} wordt gesloten",
+ "is_open": "{entity_name} is open",
+ "is_opening": "{entity_name} wordt geopend"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json
new file mode 100644
index 00000000000..af567bcfcfc
--- /dev/null
+++ b/homeassistant/components/cover/.translations/no.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} er stengt",
+ "is_closing": "{entity_name} stenges",
+ "is_open": "{entity_name} er \u00e5pen",
+ "is_opening": "{entity_name} \u00e5pnes"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/pl.json b/homeassistant/components/cover/.translations/pl.json
new file mode 100644
index 00000000000..4adc0c17b54
--- /dev/null
+++ b/homeassistant/components/cover/.translations/pl.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "pokrywa {entity_name} jest zamkni\u0119ta",
+ "is_closing": "{entity_name} si\u0119 zamyka",
+ "is_open": "pokrywa {entity_name} jest otwarta",
+ "is_opening": "{entity_name} si\u0119 otwiera"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/pt.json b/homeassistant/components/cover/.translations/pt.json
new file mode 100644
index 00000000000..cb9f85c4a93
--- /dev/null
+++ b/homeassistant/components/cover/.translations/pt.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} est\u00e1 fechada",
+ "is_closing": "{entity_name} est\u00e1 a fechar",
+ "is_open": "{entity_name} est\u00e1 aberta",
+ "is_opening": "{entity_name} est\u00e1 a abrir"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json
new file mode 100644
index 00000000000..46456bb9464
--- /dev/null
+++ b/homeassistant/components/cover/.translations/ru.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
+ "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/sl.json b/homeassistant/components/cover/.translations/sl.json
new file mode 100644
index 00000000000..cb5109b5cb0
--- /dev/null
+++ b/homeassistant/components/cover/.translations/sl.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} je/so zaprt/a",
+ "is_closing": "{entity_name} se zapira/jo",
+ "is_open": "{entity_name} je odprt/a/o",
+ "is_opening": "{entity_name} se odpira/jo"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json
new file mode 100644
index 00000000000..9723d1a0dd6
--- /dev/null
+++ b/homeassistant/components/cover/.translations/zh-Hant.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} \u5df2\u95dc\u9589",
+ "is_closing": "{entity_name} \u6b63\u5728\u95dc\u9589",
+ "is_open": "{entity_name} \u5df2\u958b\u555f",
+ "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py
index 8d2b4430fe1..cfac143a5d8 100644
--- a/homeassistant/components/cover/__init__.py
+++ b/homeassistant/components/cover/__init__.py
@@ -34,7 +34,7 @@ from homeassistant.const import (
)
-# mypy: allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py
new file mode 100644
index 00000000000..129462047e4
--- /dev/null
+++ b/homeassistant/components/cover/device_condition.py
@@ -0,0 +1,103 @@
+"""Provides device automations for Cover."""
+from typing import Any, Dict, List
+import voluptuous as vol
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_CONDITION,
+ CONF_DOMAIN,
+ CONF_TYPE,
+ CONF_DEVICE_ID,
+ CONF_ENTITY_ID,
+ STATE_OPEN,
+ STATE_CLOSED,
+ STATE_OPENING,
+ STATE_CLOSING,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import condition, config_validation as cv, entity_registry
+from homeassistant.helpers.typing import ConfigType, TemplateVarsType
+from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
+from . import DOMAIN
+
+CONDITION_TYPES = {"is_open", "is_closed", "is_opening", "is_closing"}
+
+CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES),
+ }
+)
+
+
+async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]:
+ """List device conditions for Cover devices."""
+ registry = await entity_registry.async_get_registry(hass)
+ conditions: List[Dict[str, Any]] = []
+
+ # Get all the integrations entities for this device
+ for entry in entity_registry.async_entries_for_device(registry, device_id):
+ if entry.domain != DOMAIN:
+ continue
+
+ # Add conditions for each entity that belongs to this integration
+ conditions.append(
+ {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "is_open",
+ }
+ )
+ conditions.append(
+ {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "is_closed",
+ }
+ )
+ conditions.append(
+ {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "is_opening",
+ }
+ )
+ conditions.append(
+ {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "is_closing",
+ }
+ )
+
+ return conditions
+
+
+def async_condition_from_config(
+ config: ConfigType, config_validation: bool
+) -> condition.ConditionCheckerType:
+ """Create a function to test a device condition."""
+ if config_validation:
+ config = CONDITION_SCHEMA(config)
+ if config[CONF_TYPE] == "is_open":
+ state = STATE_OPEN
+ elif config[CONF_TYPE] == "is_closed":
+ state = STATE_CLOSED
+ elif config[CONF_TYPE] == "is_opening":
+ state = STATE_OPENING
+ elif config[CONF_TYPE] == "is_closing":
+ state = STATE_CLOSING
+
+ def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
+ """Test if an entity is a certain state."""
+ return condition.state(hass, config[ATTR_ENTITY_ID], state)
+
+ return test_is_state
diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py
new file mode 100644
index 00000000000..64ea410ce93
--- /dev/null
+++ b/homeassistant/components/cover/reproduce_state.py
@@ -0,0 +1,117 @@
+"""Reproduce an Cover state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.components.cover import (
+ ATTR_CURRENT_POSITION,
+ ATTR_CURRENT_TILT_POSITION,
+ ATTR_POSITION,
+ ATTR_TILT_POSITION,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_CLOSE_COVER,
+ SERVICE_CLOSE_COVER_TILT,
+ SERVICE_OPEN_COVER,
+ SERVICE_OPEN_COVER_TILT,
+ SERVICE_SET_COVER_POSITION,
+ SERVICE_SET_COVER_TILT_POSITION,
+ STATE_CLOSED,
+ STATE_CLOSING,
+ STATE_OPEN,
+ STATE_OPENING,
+)
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+VALID_STATES = {STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING}
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if state.state not in VALID_STATES:
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if (
+ cur_state.state == state.state
+ and cur_state.attributes.get(ATTR_CURRENT_POSITION)
+ == state.attributes.get(ATTR_CURRENT_POSITION)
+ and cur_state.attributes.get(ATTR_CURRENT_TILT_POSITION)
+ == state.attributes.get(ATTR_CURRENT_TILT_POSITION)
+ ):
+ return
+
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+ service_data_tilting = {ATTR_ENTITY_ID: state.entity_id}
+
+ if cur_state.state != state.state or cur_state.attributes.get(
+ ATTR_CURRENT_POSITION
+ ) != state.attributes.get(ATTR_CURRENT_POSITION):
+ # Open/Close
+ if state.state == STATE_CLOSED or state.state == STATE_CLOSING:
+ service = SERVICE_CLOSE_COVER
+ elif state.state == STATE_OPEN or state.state == STATE_OPENING:
+ if (
+ ATTR_CURRENT_POSITION in cur_state.attributes
+ and ATTR_CURRENT_POSITION in state.attributes
+ ):
+ service = SERVICE_SET_COVER_POSITION
+ service_data[ATTR_POSITION] = state.attributes[ATTR_CURRENT_POSITION]
+ else:
+ service = SERVICE_OPEN_COVER
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+ if (
+ ATTR_CURRENT_TILT_POSITION in state.attributes
+ and ATTR_CURRENT_TILT_POSITION in cur_state.attributes
+ and cur_state.attributes.get(ATTR_CURRENT_TILT_POSITION)
+ != state.attributes.get(ATTR_CURRENT_TILT_POSITION)
+ ):
+ # Tilt position
+ if state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 100:
+ service_tilting = SERVICE_OPEN_COVER_TILT
+ elif state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 0:
+ service_tilting = SERVICE_CLOSE_COVER_TILT
+ else:
+ service_tilting = SERVICE_SET_COVER_TILT_POSITION
+ service_data_tilting[ATTR_TILT_POSITION] = state.attributes[
+ ATTR_CURRENT_TILT_POSITION
+ ]
+
+ await hass.services.async_call(
+ DOMAIN,
+ service_tilting,
+ service_data_tilting,
+ context=context,
+ blocking=True,
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Cover states."""
+ # Reproduce states in parallel.
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json
new file mode 100644
index 00000000000..db3ccf9119f
--- /dev/null
+++ b/homeassistant/components/cover/strings.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_open": "{entity_name} is open",
+ "is_closed": "{entity_name} is closed",
+ "is_opening": "{entity_name} is opening",
+ "is_closing": "{entity_name} is closing"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py
index c1c62a26dd9..1bb723091d4 100644
--- a/homeassistant/components/cppm_tracker/device_tracker.py
+++ b/homeassistant/components/cppm_tracker/device_tracker.py
@@ -1,15 +1,17 @@
"""Support for ClearPass Policy Manager."""
-import logging
from datetime import timedelta
+import logging
+from clearpasspy import ClearPass
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
+
from homeassistant.components.device_tracker import (
+ DOMAIN,
PLATFORM_SCHEMA,
DeviceScanner,
- DOMAIN,
)
-from homeassistant.const import CONF_HOST, CONF_API_KEY
+from homeassistant.const import CONF_API_KEY, CONF_HOST
+import homeassistant.helpers.config_validation as cv
SCAN_INTERVAL = timedelta(seconds=120)
@@ -30,7 +32,6 @@ _LOGGER = logging.getLogger(__name__)
def get_scanner(hass, config):
"""Initialize Scanner."""
- from clearpasspy import ClearPass
data = {
"server": config[DOMAIN][CONF_HOST],
diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py
index 9484e770998..53598e24c70 100644
--- a/homeassistant/components/cpuspeed/sensor.py
+++ b/homeassistant/components/cpuspeed/sensor.py
@@ -1,11 +1,12 @@
"""Support for displaying the current CPU speed."""
import logging
+from cpuinfo import cpuinfo
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -75,7 +76,6 @@ class CpuSpeedSensor(Entity):
def update(self):
"""Get the latest data and updates the state."""
- from cpuinfo import cpuinfo
self.info = cpuinfo.get_cpu_info()
if HZ_ACTUAL_RAW in self.info:
diff --git a/homeassistant/components/crimereports/sensor.py b/homeassistant/components/crimereports/sensor.py
index 6295125b7ca..cf5b2e374e2 100644
--- a/homeassistant/components/crimereports/sensor.py
+++ b/homeassistant/components/crimereports/sensor.py
@@ -3,27 +3,28 @@ from collections import defaultdict
from datetime import timedelta
import logging
+import crimereports
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_INCLUDE,
- CONF_EXCLUDE,
- CONF_NAME,
- CONF_LATITUDE,
- CONF_LONGITUDE,
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
ATTR_LONGITUDE,
+ CONF_EXCLUDE,
+ CONF_INCLUDE,
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_NAME,
CONF_RADIUS,
LENGTH_KILOMETERS,
LENGTH_METERS,
)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
from homeassistant.util.distance import convert
from homeassistant.util.dt import now
-import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -65,8 +66,6 @@ class CrimeReportsSensor(Entity):
def __init__(self, hass, name, latitude, longitude, radius, include, exclude):
"""Initialize the Crime Reports sensor."""
- import crimereports
-
self._hass = hass
self._name = name
self._include = include
@@ -113,8 +112,6 @@ class CrimeReportsSensor(Entity):
def update(self):
"""Update device state."""
- import crimereports
-
incident_counts = defaultdict(int)
incidents = self._crimereports.get_incidents(
now().date(), include=self._include, exclude=self._exclude
diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py
index f6a5133d8a9..4af51e911a1 100644
--- a/homeassistant/components/cups/sensor.py
+++ b/homeassistant/components/cups/sensor.py
@@ -276,11 +276,11 @@ class MarkerSensor(Entity):
if self._attributes is None:
return None
- high_level = self._attributes[self._printer]["marker-high-levels"]
+ high_level = self._attributes[self._printer].get("marker-high-levels")
if isinstance(high_level, list):
high_level = high_level[self._index]
- low_level = self._attributes[self._printer]["marker-low-levels"]
+ low_level = self._attributes[self._printer].get("marker-low-levels")
if isinstance(low_level, list):
low_level = low_level[self._index]
diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json
index 98ab98e6b17..00a517f701f 100644
--- a/homeassistant/components/daikin/.translations/ru.json
+++ b/homeassistant/components/daikin/.translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.",
"device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py
index d4e7e7ec63a..cd8417e3e84 100644
--- a/homeassistant/components/darksky/sensor.py
+++ b/homeassistant/components/darksky/sensor.py
@@ -2,6 +2,7 @@
import logging
from datetime import timedelta
+import forecastio
import voluptuous as vol
from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout
@@ -797,8 +798,6 @@ class DarkSkyData:
def _update(self):
"""Get the latest data from Dark Sky."""
- import forecastio
-
try:
self.data = forecastio.load_forecast(
self._api_key,
diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py
index 5296f346626..41f063399c1 100644
--- a/homeassistant/components/darksky/weather.py
+++ b/homeassistant/components/darksky/weather.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+import forecastio
from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout
import voluptuous as vol
@@ -102,6 +103,11 @@ class DarkSkyWeather(WeatherEntity):
self._ds_hourly = None
self._ds_daily = None
+ @property
+ def available(self):
+ """Return if weather data is available from Dark Sky."""
+ return self._ds_data is not None
+
@property
def attribution(self):
"""Return the attribution."""
@@ -215,7 +221,8 @@ class DarkSkyWeather(WeatherEntity):
self._dark_sky.update()
self._ds_data = self._dark_sky.data
- self._ds_currently = self._dark_sky.currently.d
+ currently = self._dark_sky.currently
+ self._ds_currently = currently.d if currently else {}
self._ds_hourly = self._dark_sky.hourly
self._ds_daily = self._dark_sky.daily
@@ -238,8 +245,6 @@ class DarkSkyData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from Dark Sky."""
- import forecastio
-
try:
self.data = forecastio.load_forecast(
self._api_key, self.latitude, self.longitude, units=self.requested_units
diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py
index 4e661376719..bd2728d03dc 100644
--- a/homeassistant/components/ddwrt/device_tracker.py
+++ b/homeassistant/components/ddwrt/device_tracker.py
@@ -165,4 +165,4 @@ class DdWrtDeviceScanner(DeviceScanner):
def _parse_ddwrt_response(data_str):
"""Parse the DD-WRT data format."""
- return {key: val for key, val in _DDWRT_DATA_REGEX.findall(data_str)}
+ return dict(_DDWRT_DATA_REGEX.findall(data_str))
diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json
index d36de4acc1e..a2facf0d7c2 100644
--- a/homeassistant/components/deconz/.translations/ca.json
+++ b/homeassistant/components/deconz/.translations/ca.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "No s'ha pogut obtenir una clau API"
},
+ "flow_title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee ({host})",
"step": {
"hassio_confirm": {
"data": {
diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json
index 6b74c09107a..ec9c4dc35b1 100644
--- a/homeassistant/components/deconz/.translations/da.json
+++ b/homeassistant/components/deconz/.translations/da.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Kunne ikke f\u00e5 en API-n\u00f8gle"
},
+ "flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"hassio_confirm": {
"data": {
diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json
index 830ae0fd13f..2bf0667cadb 100644
--- a/homeassistant/components/deconz/.translations/de.json
+++ b/homeassistant/components/deconz/.translations/de.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden"
},
+ "flow_title": "deCONZ Zigbee Gateway",
"step": {
"hassio_confirm": {
"data": {
@@ -43,12 +44,32 @@
},
"device_automation": {
"trigger_subtype": {
+ "both_buttons": "Beide Tasten",
+ "button_1": "Erste Taste",
+ "button_2": "Zweite Taste",
+ "button_3": "Dritte Taste",
+ "button_4": "Vierte Taste",
"close": "Schlie\u00dfen",
+ "dim_down": "Dimmer runter",
+ "dim_up": "Dimmer hoch",
"left": "Links",
"open": "Offen",
"right": "Rechts",
"turn_off": "Ausschalten",
"turn_on": "Einschalten"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt",
+ "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt",
+ "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen",
+ "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt",
+ "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt",
+ "remote_button_rotated": "Button gedreht \"{subtype}\".",
+ "remote_button_rotation_stopped": "Die Tastendrehung \"{subtype}\" wurde gestoppt",
+ "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt",
+ "remote_button_short_release": "\"{subtype}\" Taste losgelassen",
+ "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt",
+ "remote_gyro_activated": "Ger\u00e4t ersch\u00fcttert"
}
},
"options": {
diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json
index c00bfca3564..e9c64ffe5fa 100644
--- a/homeassistant/components/deconz/.translations/en.json
+++ b/homeassistant/components/deconz/.translations/en.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Couldn't get an API key"
},
+ "flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"hassio_confirm": {
"data": {
diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json
index 04a08d185b3..d4f8de9f282 100644
--- a/homeassistant/components/deconz/.translations/es.json
+++ b/homeassistant/components/deconz/.translations/es.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "No se pudo obtener una clave API"
},
+ "flow_title": "pasarela deCONZ Zigbee ({host})",
"step": {
"hassio_confirm": {
"data": {
diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json
index 3729f7f556a..d1fc7fa7286 100644
--- a/homeassistant/components/deconz/.translations/fr.json
+++ b/homeassistant/components/deconz/.translations/fr.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Impossible d'obtenir une cl\u00e9 d'API"
},
+ "flow_title": "Passerelle deCONZ Zigbee ({host})",
"step": {
"hassio_confirm": {
"data": {
diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json
index 1f0b344a32d..975d69a450f 100644
--- a/homeassistant/components/deconz/.translations/it.json
+++ b/homeassistant/components/deconz/.translations/it.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Impossibile ottenere una API key"
},
+ "flow_title": "Gateway Zigbee deCONZ ({host})",
"step": {
"hassio_confirm": {
"data": {
diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json
index 923a2beb2ff..61725316b13 100644
--- a/homeassistant/components/deconz/.translations/ko.json
+++ b/homeassistant/components/deconz/.translations/ko.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
},
+ "flow_title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774 ({host})",
"step": {
"hassio_confirm": {
"data": {
@@ -64,6 +65,7 @@
"remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984",
"remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984",
"remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804",
+ "remote_button_rotation_stopped": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804 \uc815\uc9c0",
"remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984",
"remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc",
"remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984",
diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json
index f5f41a28a32..49394eb9773 100644
--- a/homeassistant/components/deconz/.translations/lb.json
+++ b/homeassistant/components/deconz/.translations/lb.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien"
},
+ "flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"hassio_confirm": {
"data": {
diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json
index 116f6254b37..7f690f11f1d 100644
--- a/homeassistant/components/deconz/.translations/nl.json
+++ b/homeassistant/components/deconz/.translations/nl.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Kon geen API-sleutel ophalen"
},
+ "flow_title": "deCONZ Zigbee gateway ( {host} )",
"step": {
"hassio_confirm": {
"data": {
@@ -64,6 +65,7 @@
"remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt",
"remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt",
"remote_button_rotated": "Knop gedraaid \" {subtype} \"",
+ "remote_button_rotation_stopped": "Knoprotatie \" {subtype} \" gestopt",
"remote_button_short_press": "\" {subtype} \" knop ingedrukt",
"remote_button_short_release": "\"{subtype}\" knop losgelaten",
"remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt",
diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json
index 7d05a366cf2..7db8f3f118d 100644
--- a/homeassistant/components/deconz/.translations/no.json
+++ b/homeassistant/components/deconz/.translations/no.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel"
},
+ "flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"hassio_confirm": {
"data": {
@@ -64,7 +65,7 @@
"remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket",
"remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket",
"remote_button_rotated": "Knappen roterte \"{subtype}\"",
- "remote_button_rotation_stopped": "Knappe rotasjon \"{subtype}\" stoppet",
+ "remote_button_rotation_stopped": "Knapperotasjon \"{subtype}\" stoppet",
"remote_button_short_press": "\"{subtype}\" -knappen ble trykket",
"remote_button_short_release": "\"{subtype}\"-knappen sluppet",
"remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket",
diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json
index 11a1beb10d6..ac9f06f1f17 100644
--- a/homeassistant/components/deconz/.translations/pl.json
+++ b/homeassistant/components/deconz/.translations/pl.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Nie mo\u017cna uzyska\u0107 klucza API"
},
+ "flow_title": "Bramka deCONZ Zigbee ({host})",
"step": {
"hassio_confirm": {
"data": {
@@ -48,26 +49,27 @@
"button_2": "drugi przycisk",
"button_3": "trzeci przycisk",
"button_4": "czwarty przycisk",
- "close": "zamkni\u0119cie",
- "dim_down": "zmniejszenie jasno\u015bci",
- "dim_up": "zwi\u0119kszenie jasno\u015bci",
+ "close": "nast\u0105pi zamkni\u0119cie",
+ "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci",
+ "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci",
"left": "w lewo",
"open": "otwarcie",
"right": "w prawo",
- "turn_off": "wy\u0142\u0105czenie",
- "turn_on": "wy\u0142\u0105czenie"
+ "turn_off": "nast\u0105pi wy\u0142\u0105czenie",
+ "turn_on": "nast\u0105pi w\u0142\u0105czenie"
},
"trigger_type": {
- "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty",
- "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
- "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
- "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty",
- "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty",
- "remote_button_rotated": "przycisk obr\u00f3cony \"{subtype}\"",
- "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty",
- "remote_button_short_release": "przycisk \"{subtype}\" zwolniony",
- "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty",
- "remote_gyro_activated": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem"
+ "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty",
+ "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
+ "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
+ "remote_button_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty",
+ "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty",
+ "remote_button_rotated": "przycisk zostanie obr\u00f3cony \"{subtype}\"",
+ "remote_button_rotation_stopped": "nast\u0105pi zatrzymanie obrotu przycisku \"{subtype}\"",
+ "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty",
+ "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony",
+ "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty",
+ "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem"
}
},
"options": {
diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json
index 47f5bb7db59..63a66595ace 100644
--- a/homeassistant/components/deconz/.translations/pt.json
+++ b/homeassistant/components/deconz/.translations/pt.json
@@ -29,5 +29,10 @@
}
},
"title": "Gateway Zigbee deCONZ"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "left": "Esquerda"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json
index 558fd9e5897..2dc3df17aa9 100644
--- a/homeassistant/components/deconz/.translations/ru.json
+++ b/homeassistant/components/deconz/.translations/ru.json
@@ -1,16 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "\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.",
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.",
- "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"
+ "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."
},
"error": {
- "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API"
+ "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API."
},
+ "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})",
"step": {
"hassio_confirm": {
"data": {
@@ -28,7 +29,7 @@
"title": "deCONZ"
},
"link": {
- "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb",
+ "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.",
"title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ"
},
"options": {
@@ -48,26 +49,27 @@
"button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
"button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
"button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
- "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e",
- "dim_down": "\u0423\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c",
- "dim_up": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c",
+ "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
+ "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f",
+ "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f",
"left": "\u041d\u0430\u043b\u0435\u0432\u043e",
- "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e",
+ "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
"right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e",
- "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
- "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e"
+ "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f"
},
"trigger_type": {
- "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b",
+ "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430",
"remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430",
- "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f",
+ "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f",
"remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430",
"remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437",
"remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430",
+ "remote_button_rotation_stopped": "\u041f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \"{subtype}\"",
"remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430",
"remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430",
- "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b",
- "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438"
+ "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430",
+ "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438"
}
},
"options": {
diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json
index 0717bcfc39f..217007c07d4 100644
--- a/homeassistant/components/deconz/.translations/sl.json
+++ b/homeassistant/components/deconz/.translations/sl.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Klju\u010da API ni mogo\u010de dobiti"
},
+ "flow_title": "deCONZ Zigbee prehod ({host})",
"step": {
"hassio_confirm": {
"data": {
diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json
index 2ad613cde68..975600a5745 100644
--- a/homeassistant/components/deconz/.translations/zh-Hant.json
+++ b/homeassistant/components/deconz/.translations/zh-Hant.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "\u7121\u6cd5\u53d6\u5f97 API key"
},
+ "flow_title": "deCONZ Zigbee \u9598\u9053\u5668\uff08{host}\uff09",
"step": {
"hassio_confirm": {
"data": {
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index 66df687047f..5ede8e715b9 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -187,8 +187,9 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
):
return self.async_abort(reason="already_in_progress")
- # pylint: disable=unsupported-assignment-operation
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context[CONF_BRIDGEID] = bridgeid
+ self.context["title_placeholders"] = {"host": discovery_info[CONF_HOST]}
self.deconz_config = {
CONF_HOST: discovery_info[CONF_HOST],
diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py
index badbe8b8651..2d097d30c0b 100644
--- a/homeassistant/components/deconz/device_trigger.py
+++ b/homeassistant/components/deconz/device_trigger.py
@@ -204,8 +204,10 @@ def _get_deconz_event_from_device_id(hass, device_id):
return None
-async def async_attach_trigger(hass, config, action, automation_info):
- """Listen for state changes based on configuration."""
+async def async_validate_trigger_config(hass, config):
+ """Validate config."""
+ config = TRIGGER_SCHEMA(config)
+
device_registry = await hass.helpers.device_registry.async_get_registry()
device = device_registry.async_get(config[CONF_DEVICE_ID])
@@ -214,6 +216,16 @@ async def async_attach_trigger(hass, config, action, automation_info):
if device.model not in REMOTES or trigger not in REMOTES[device.model]:
raise InvalidDeviceAutomationConfig
+ return config
+
+
+async def async_attach_trigger(hass, config, action, automation_info):
+ """Listen for state changes based on configuration."""
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(config[CONF_DEVICE_ID])
+
+ trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
+
trigger = REMOTES[device.model][trigger]
deconz_event = _get_deconz_event_from_device_id(hass, device.id)
@@ -222,13 +234,15 @@ async def async_attach_trigger(hass, config, action, automation_info):
event_id = deconz_event.serial
- state_config = {
+ event_config = {
+ event.CONF_PLATFORM: "event",
event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT,
event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger},
}
+ event_config = event.TRIGGER_SCHEMA(event_config)
return await event.async_attach_trigger(
- hass, state_config, action, automation_info, platform_type="device"
+ hass, event_config, action, automation_info, platform_type="device"
)
diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index 1e5cd414425..63ab17d001a 100644
--- a/homeassistant/components/deconz/manifest.json
+++ b/homeassistant/components/deconz/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
"requirements": [
- "pydeconz==63"
+ "pydeconz==64"
],
"ssdp": {
"manufacturer": [
diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json
index db43c022822..3571a9e1207 100644
--- a/homeassistant/components/deconz/strings.json
+++ b/homeassistant/components/deconz/strings.json
@@ -1,6 +1,7 @@
{
"config": {
"title": "deCONZ Zigbee gateway",
+ "flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"init": {
"title": "Define deCONZ gateway",
diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py
index cad98f9d8a4..6ca427f2476 100644
--- a/homeassistant/components/decora/light.py
+++ b/homeassistant/components/decora/light.py
@@ -1,30 +1,49 @@
"""Support for Decora dimmers."""
-import importlib
-import logging
+import copy
from functools import wraps
+import logging
import time
+from bluepy.btle import BTLEException # pylint: disable=import-error, no-member
+import decora # pylint: disable=import-error, no-member
import voluptuous as vol
-from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
+ PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
Light,
- PLATFORM_SCHEMA,
)
+from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
import homeassistant.helpers.config_validation as cv
+import homeassistant.util as util
_LOGGER = logging.getLogger(__name__)
SUPPORT_DECORA_LED = SUPPORT_BRIGHTNESS
+
+def _name_validator(config):
+ """Validate the name."""
+ config = copy.deepcopy(config)
+ for address, device_config in config[CONF_DEVICES].items():
+ if CONF_NAME not in device_config:
+ device_config[CONF_NAME] = util.slugify(address)
+
+ return config
+
+
DEVICE_SCHEMA = vol.Schema(
{vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string}
)
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}}
+PLATFORM_SCHEMA = vol.Schema(
+ vol.All(
+ PLATFORM_SCHEMA.extend(
+ {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}}
+ ),
+ _name_validator,
+ )
)
@@ -34,9 +53,6 @@ def retry(method):
@wraps(method)
def wrapper_retry(device, *args, **kwargs):
"""Try send command and retry on error."""
- # pylint: disable=import-error, no-member
- import decora
- import bluepy
initial = time.monotonic()
while True:
@@ -44,7 +60,7 @@ def retry(method):
return None
try:
return method(device, *args, **kwargs)
- except (decora.decoraException, AttributeError, bluepy.btle.BTLEException):
+ except (decora.decoraException, AttributeError, BTLEException):
_LOGGER.warning(
"Decora connect error for device %s. " "Reconnecting...",
device.name,
@@ -74,8 +90,6 @@ class DecoraLight(Light):
def __init__(self, device):
"""Initialize the light."""
- # pylint: disable=no-member
- decora = importlib.import_module("decora")
self._name = device["name"]
self._address = device["address"]
diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py
index 51fc890c873..1725b2d105c 100644
--- a/homeassistant/components/denonavr/media_player.py
+++ b/homeassistant/components/denonavr/media_player.py
@@ -3,9 +3,10 @@
from collections import namedtuple
import logging
+import denonavr
import voluptuous as vol
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_MUSIC,
@@ -88,7 +89,6 @@ NewHost = namedtuple("NewHost", ["host", "name"])
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Denon platform."""
- import denonavr
# Initialize list with receivers to be started
receivers = []
diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py
index fbe0efa15ac..ad7b40f78db 100644
--- a/homeassistant/components/deutsche_bahn/sensor.py
+++ b/homeassistant/components/deutsche_bahn/sensor.py
@@ -4,6 +4,8 @@ import logging
import voluptuous as vol
+import schiene
+
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -89,7 +91,6 @@ class SchieneData:
def __init__(self, start, goal, offset, only_direct):
"""Initialize the sensor."""
- import schiene
self.start = start
self.goal = goal
diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py
index fa6deac40ba..80e64033295 100644
--- a/homeassistant/components/device_automation/__init__.py
+++ b/homeassistant/components/device_automation/__init__.py
@@ -59,6 +59,12 @@ async def async_setup(hass, config):
hass.components.websocket_api.async_register_command(
websocket_device_automation_list_triggers
)
+ hass.components.websocket_api.async_register_command(
+ websocket_device_automation_get_action_capabilities
+ )
+ hass.components.websocket_api.async_register_command(
+ websocket_device_automation_get_condition_capabilities
+ )
hass.components.websocket_api.async_register_command(
websocket_device_automation_get_trigger_capabilities
)
@@ -150,7 +156,11 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom
# The device automation has no capabilities
return {}
- capabilities = await getattr(platform, function_name)(hass, automation)
+ try:
+ capabilities = await getattr(platform, function_name)(hass, automation)
+ except InvalidDeviceAutomationConfig:
+ return {}
+
capabilities = capabilities.copy()
extra_fields = capabilities.get("extra_fields")
@@ -206,6 +216,38 @@ async def websocket_device_automation_list_triggers(hass, connection, msg):
connection.send_result(msg["id"], triggers)
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "device_automation/action/capabilities",
+ vol.Required("action"): dict,
+ }
+)
+async def websocket_device_automation_get_action_capabilities(hass, connection, msg):
+ """Handle request for device action capabilities."""
+ action = msg["action"]
+ capabilities = await _async_get_device_automation_capabilities(
+ hass, "action", action
+ )
+ connection.send_result(msg["id"], capabilities)
+
+
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "device_automation/condition/capabilities",
+ vol.Required("condition"): dict,
+ }
+)
+async def websocket_device_automation_get_condition_capabilities(hass, connection, msg):
+ """Handle request for device condition capabilities."""
+ condition = msg["condition"]
+ capabilities = await _async_get_device_automation_capabilities(
+ hass, "condition", condition
+ )
+ connection.send_result(msg["id"], capabilities)
+
+
@websocket_api.async_response
@websocket_api.websocket_command(
{
diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py
index c9588c1efa7..5f01f4d9d71 100644
--- a/homeassistant/components/device_automation/toggle_entity.py
+++ b/homeassistant/components/device_automation/toggle_entity.py
@@ -3,7 +3,10 @@ from typing import Any, Dict, List
import voluptuous as vol
from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE
-from homeassistant.components.automation import state, AutomationActionType
+from homeassistant.components.automation import (
+ state as state_automation,
+ AutomationActionType,
+)
from homeassistant.components.device_automation.const import (
CONF_IS_OFF,
CONF_IS_ON,
@@ -14,6 +17,7 @@ from homeassistant.components.device_automation.const import (
CONF_TURNED_ON,
)
from homeassistant.const import (
+ ATTR_ENTITY_ID,
CONF_CONDITION,
CONF_ENTITY_ID,
CONF_FOR,
@@ -21,7 +25,7 @@ from homeassistant.const import (
CONF_TYPE,
)
from homeassistant.helpers.entity_registry import async_entries_for_device
-from homeassistant.helpers import condition, config_validation as cv, service
+from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import TRIGGER_BASE_SCHEMA
@@ -80,6 +84,7 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)
@@ -108,19 +113,14 @@ async def async_call_action_from_config(
else:
action = "toggle"
- service_action = {
- service.CONF_SERVICE: "{}.{}".format(domain, action),
- CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- }
+ service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
- await service.async_call_from_config(
- hass, service_action, blocking=True, variables=variables, context=context
+ await hass.services.async_call(
+ domain, action, service_data, blocking=True, context=context
)
-def async_condition_from_config(
- config: ConfigType, config_validation: bool
-) -> condition.ConditionCheckerType:
+def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
"""Evaluate state based on configuration."""
condition_type = config[CONF_TYPE]
if condition_type == CONF_IS_ON:
@@ -132,8 +132,10 @@ def async_condition_from_config(
condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
condition.CONF_STATE: stat,
}
+ if CONF_FOR in config:
+ state_config[CONF_FOR] = config[CONF_FOR]
- return condition.state_from_config(state_config, config_validation)
+ return condition.state_from_config(state_config)
async def async_attach_trigger(
@@ -151,14 +153,16 @@ async def async_attach_trigger(
from_state = "on"
to_state = "off"
state_config = {
- state.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- state.CONF_FROM: from_state,
- state.CONF_TO: to_state,
+ state_automation.CONF_PLATFORM: "state",
+ state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ state_automation.CONF_FROM: from_state,
+ state_automation.CONF_TO: to_state,
}
if CONF_FOR in config:
state_config[CONF_FOR] = config[CONF_FOR]
- return await state.async_attach_trigger(
+ state_config = state_automation.TRIGGER_SCHEMA(state_config)
+ return await state_automation.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
)
@@ -201,7 +205,7 @@ async def async_get_actions(
async def async_get_conditions(
hass: HomeAssistant, device_id: str, domain: str
-) -> List[dict]:
+) -> List[Dict[str, str]]:
"""List device conditions."""
return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain)
@@ -213,7 +217,16 @@ async def async_get_triggers(
return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain)
-async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict:
+async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict:
+ """List condition capabilities."""
+ return {
+ "extra_fields": vol.Schema(
+ {vol.Optional(CONF_FOR): cv.positive_time_period_dict}
+ )
+ }
+
+
+async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List trigger capabilities."""
return {
"extra_fields": vol.Schema(
diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py
index 5c186cc12a1..ad7ff3fe3f5 100644
--- a/homeassistant/components/device_tracker/legacy.py
+++ b/homeassistant/components/device_tracker/legacy.py
@@ -1,11 +1,12 @@
"""Legacy device tracker classes."""
import asyncio
from datetime import timedelta
+import hashlib
from typing import Any, List, Sequence
import voluptuous as vol
-from homeassistant.core import callback
+from homeassistant import util
from homeassistant.components import zone
from homeassistant.components.group import (
ATTR_ADD_ENTITIES,
@@ -16,16 +17,7 @@ from homeassistant.components.group import (
SERVICE_SET,
)
from homeassistant.components.zone import async_active_zone
-from homeassistant.config import load_yaml_config_file, async_log_exception
-from homeassistant.exceptions import HomeAssistantError
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity_registry import async_get_registry
-from homeassistant.helpers.restore_state import RestoreEntity
-from homeassistant.helpers.typing import GPSType, HomeAssistantType
-from homeassistant import util
-import homeassistant.util.dt as dt_util
-from homeassistant.util.yaml import dump
-
+from homeassistant.config import async_log_exception, load_yaml_config_file
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_GPS_ACCURACY,
@@ -37,9 +29,17 @@ from homeassistant.const import (
CONF_MAC,
CONF_NAME,
DEVICE_DEFAULT_NAME,
- STATE_NOT_HOME,
STATE_HOME,
+ STATE_NOT_HOME,
)
+from homeassistant.core import callback
+from homeassistant.exceptions import HomeAssistantError
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity_registry import async_get_registry
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.typing import GPSType, HomeAssistantType
+import homeassistant.util.dt as dt_util
+from homeassistant.util.yaml import dump
from .const import (
ATTR_BATTERY,
@@ -635,7 +635,6 @@ def get_gravatar_for_email(email: str):
Async friendly.
"""
- import hashlib
url = "https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar"
return url.format(hashlib.md5(email.encode("utf-8").lower()).hexdigest())
diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py
index aadb6b2d4cb..648e0e1ed72 100644
--- a/homeassistant/components/dht/sensor.py
+++ b/homeassistant/components/dht/sensor.py
@@ -2,6 +2,7 @@
import logging
from datetime import timedelta
+import Adafruit_DHT # pylint: disable=import-error
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -50,7 +51,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the DHT sensor."""
- import Adafruit_DHT # pylint: disable=import-error
SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit
available_sensors = {
diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py
index 18dfb49365a..bdb0c348803 100644
--- a/homeassistant/components/digital_ocean/__init__.py
+++ b/homeassistant/components/digital_ocean/__init__.py
@@ -2,6 +2,7 @@
import logging
from datetime import timedelta
+import digitalocean
import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN
@@ -38,7 +39,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Digital Ocean component."""
- import digitalocean
conf = config[DOMAIN]
access_token = conf.get(CONF_ACCESS_TOKEN)
@@ -63,7 +63,6 @@ class DigitalOcean:
def __init__(self, access_token):
"""Initialize the Digital Ocean connection."""
- import digitalocean
self._access_token = access_token
self.data = None
diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py
index 9983ccc93fa..10c8ce73a47 100644
--- a/homeassistant/components/digitalloggers/switch.py
+++ b/homeassistant/components/digitalloggers/switch.py
@@ -2,6 +2,7 @@
import logging
from datetime import timedelta
+import dlipower
import voluptuous as vol
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
@@ -45,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Find and return DIN III Relay switch."""
- import dlipower
host = config.get(CONF_HOST)
controller_name = config.get(CONF_NAME)
diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py
index 64528f4ca5e..b3f29fbe75b 100644
--- a/homeassistant/components/discogs/sensor.py
+++ b/homeassistant/components/discogs/sensor.py
@@ -3,6 +3,7 @@ from datetime import timedelta
import logging
import random
+import discogs_client
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -65,19 +66,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Discogs sensor."""
- import discogs_client
token = config[CONF_TOKEN]
name = config[CONF_NAME]
try:
- discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token)
+ _discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token)
discogs_data = {
- "user": discogs_client.identity().name,
- "folders": discogs_client.identity().collection_folders,
- "collection_count": discogs_client.identity().num_collection,
- "wantlist_count": discogs_client.identity().num_wantlist,
+ "user": _discogs_client.identity().name,
+ "folders": _discogs_client.identity().collection_folders,
+ "collection_count": _discogs_client.identity().num_collection,
+ "wantlist_count": _discogs_client.identity().num_wantlist,
}
except discogs_client.exceptions.HTTPError:
_LOGGER.error("API token is not valid")
diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py
index a3cd87bc895..67b9f1b39ba 100644
--- a/homeassistant/components/discord/__init__.py
+++ b/homeassistant/components/discord/__init__.py
@@ -1 +1 @@
-"""The discord component."""
+"""The discord integration."""
diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json
index 50c03bad25d..d00d26d2b5e 100644
--- a/homeassistant/components/discord/manifest.json
+++ b/homeassistant/components/discord/manifest.json
@@ -3,7 +3,7 @@
"name": "Discord",
"documentation": "https://www.home-assistant.io/integrations/discord",
"requirements": [
- "discord.py==1.2.3"
+ "discord.py==1.2.4"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py
index 17ff0a192d0..f35cf5b0ce9 100644
--- a/homeassistant/components/discord/notify.py
+++ b/homeassistant/components/discord/notify.py
@@ -2,6 +2,7 @@
import logging
import os.path
+import discord
import voluptuous as vol
from homeassistant.const import CONF_TOKEN
@@ -44,7 +45,6 @@ class DiscordNotificationService(BaseNotificationService):
async def async_send_message(self, message, **kwargs):
"""Login to Discord, send message to channel(s) and log out."""
- import discord
discord.VoiceClient.warn_nacl = False
discord_bot = discord.Client()
diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py
index 749e536e2e8..cdd8bc101b5 100644
--- a/homeassistant/components/dlib_face_detect/image_processing.py
+++ b/homeassistant/components/dlib_face_detect/image_processing.py
@@ -1,17 +1,19 @@
"""Component that will help set the Dlib face detect processing."""
-import logging
import io
+import logging
-from homeassistant.core import split_entity_id
+import face_recognition # pylint: disable=import-error
+
+from homeassistant.components.image_processing import (
+ CONF_ENTITY_ID,
+ CONF_NAME,
+ CONF_SOURCE,
+ ImageProcessingFaceEntity,
+)
# pylint: disable=unused-import
from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa
-from homeassistant.components.image_processing import (
- ImageProcessingFaceEntity,
- CONF_SOURCE,
- CONF_ENTITY_ID,
- CONF_NAME,
-)
+from homeassistant.core import split_entity_id
_LOGGER = logging.getLogger(__name__)
@@ -55,7 +57,6 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity):
def process_image(self, image):
"""Process image."""
- import face_recognition # pylint: disable=import-error
fak_file = io.BytesIO(image)
fak_file.name = "snapshot.jpg"
diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py
index d4851be28c8..d5b55b6a68c 100644
--- a/homeassistant/components/dlib_face_identify/image_processing.py
+++ b/homeassistant/components/dlib_face_identify/image_processing.py
@@ -2,6 +2,8 @@
import logging
import io
+# pylint: disable=import-error
+import face_recognition
import voluptuous as vol
from homeassistant.core import split_entity_id
@@ -49,8 +51,6 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity):
def __init__(self, camera_entity, faces, name, tolerance):
"""Initialize Dlib face identify entry."""
- # pylint: disable=import-error
- import face_recognition
super().__init__()
@@ -83,8 +83,6 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity):
def process_image(self, image):
"""Process image."""
- # pylint: disable=import-error
- import face_recognition
fak_file = io.BytesIO(image)
fak_file.name = "snapshot.jpg"
diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py
index 0053d5a95ea..fb57040f2c2 100644
--- a/homeassistant/components/dnsip/sensor.py
+++ b/homeassistant/components/dnsip/sensor.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+import aiodns
+from aiodns.error import DNSError
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -58,7 +60,6 @@ class WanIpSensor(Entity):
def __init__(self, hass, name, hostname, resolver, ipv6):
"""Initialize the DNS IP sensor."""
- import aiodns
self.hass = hass
self._name = name
@@ -80,7 +81,6 @@ class WanIpSensor(Entity):
async def async_update(self):
"""Get the current DNS IP address for hostname."""
- from aiodns.error import DNSError
try:
response = await self.resolver.query(self.hostname, self.querytype)
diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py
index 3eec85b3e53..02d7ce26f1c 100644
--- a/homeassistant/components/doods/image_processing.py
+++ b/homeassistant/components/doods/image_processing.py
@@ -7,6 +7,7 @@ import voluptuous as vol
from PIL import Image, ImageDraw
from pydoods import PyDOODS
+from homeassistant.const import CONF_TIMEOUT
from homeassistant.components.image_processing import (
CONF_CONFIDENCE,
CONF_ENTITY_ID,
@@ -31,6 +32,7 @@ CONF_AUTH_KEY = "auth_key"
CONF_DETECTOR = "detector"
CONF_LABELS = "labels"
CONF_AREA = "area"
+CONF_COVERS = "covers"
CONF_TOP = "top"
CONF_BOTTOM = "bottom"
CONF_RIGHT = "right"
@@ -43,6 +45,7 @@ AREA_SCHEMA = vol.Schema(
vol.Optional(CONF_LEFT, default=0): cv.small_float,
vol.Optional(CONF_RIGHT, default=1): cv.small_float,
vol.Optional(CONF_TOP, default=0): cv.small_float,
+ vol.Optional(CONF_COVERS, default=True): cv.boolean,
}
)
@@ -50,7 +53,7 @@ LABEL_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_AREA): AREA_SCHEMA,
- vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100),
+ vol.Optional(CONF_CONFIDENCE): vol.Range(min=0, max=100),
}
)
@@ -58,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_URL): cv.string,
vol.Required(CONF_DETECTOR): cv.string,
+ vol.Required(CONF_TIMEOUT, default=90): cv.positive_int,
vol.Optional(CONF_AUTH_KEY, default=""): cv.string,
vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]),
vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100),
@@ -74,8 +78,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
url = config[CONF_URL]
auth_key = config[CONF_AUTH_KEY]
detector_name = config[CONF_DETECTOR]
+ timeout = config[CONF_TIMEOUT]
- doods = PyDOODS(url, auth_key)
+ doods = PyDOODS(url, auth_key, timeout)
response = doods.get_detectors()
if not isinstance(response, dict):
_LOGGER.warning("Could not connect to doods server: %s", url)
@@ -140,6 +145,7 @@ class Doods(ImageProcessingEntity):
# handle labels and specific detection areas
labels = config[CONF_LABELS]
self._label_areas = {}
+ self._label_covers = {}
for label in labels:
if isinstance(label, dict):
label_name = label[CONF_NAME]
@@ -147,14 +153,17 @@ class Doods(ImageProcessingEntity):
_LOGGER.warning("Detector does not support label %s", label_name)
continue
- # Label Confidence
- label_confidence = label[CONF_CONFIDENCE]
+ # If label confidence is not specified, use global confidence
+ label_confidence = label.get(CONF_CONFIDENCE)
+ if not label_confidence:
+ label_confidence = confidence
if label_name not in dconfig or dconfig[label_name] > label_confidence:
dconfig[label_name] = label_confidence
# Label area
label_area = label.get(CONF_AREA)
self._label_areas[label_name] = [0, 0, 1, 1]
+ self._label_covers[label_name] = True
if label_area:
self._label_areas[label_name] = [
label_area[CONF_TOP],
@@ -162,6 +171,7 @@ class Doods(ImageProcessingEntity):
label_area[CONF_BOTTOM],
label_area[CONF_RIGHT],
]
+ self._label_covers[label_name] = label_area[CONF_COVERS]
else:
if label not in detector["labels"] and label != "*":
_LOGGER.warning("Detector does not support label %s", label)
@@ -175,6 +185,7 @@ class Doods(ImageProcessingEntity):
# Handle global detection area
self._area = [0, 0, 1, 1]
+ self._covers = True
area_config = config.get(CONF_AREA)
if area_config:
self._area = [
@@ -183,6 +194,7 @@ class Doods(ImageProcessingEntity):
area_config[CONF_BOTTOM],
area_config[CONF_RIGHT],
]
+ self._covers = area_config[CONF_COVERS]
template.attach(hass, self._file_out)
@@ -308,22 +320,41 @@ class Doods(ImageProcessingEntity):
continue
# Exclude matches outside global area definition
- if (
- boxes[0] < self._area[0]
- or boxes[1] < self._area[1]
- or boxes[2] > self._area[2]
- or boxes[3] > self._area[3]
- ):
- continue
+ if self._covers:
+ if (
+ boxes[0] < self._area[0]
+ or boxes[1] < self._area[1]
+ or boxes[2] > self._area[2]
+ or boxes[3] > self._area[3]
+ ):
+ continue
+ else:
+ if (
+ boxes[0] > self._area[2]
+ or boxes[1] > self._area[3]
+ or boxes[2] < self._area[0]
+ or boxes[3] < self._area[1]
+ ):
+ continue
# Exclude matches outside label specific area definition
- if self._label_areas and (
- boxes[0] < self._label_areas[label][0]
- or boxes[1] < self._label_areas[label][1]
- or boxes[2] > self._label_areas[label][2]
- or boxes[3] > self._label_areas[label][3]
- ):
- continue
+ if self._label_areas.get(label):
+ if self._label_covers[label]:
+ if (
+ boxes[0] < self._label_areas[label][0]
+ or boxes[1] < self._label_areas[label][1]
+ or boxes[2] > self._label_areas[label][2]
+ or boxes[3] > self._label_areas[label][3]
+ ):
+ continue
+ else:
+ if (
+ boxes[0] > self._label_areas[label][2]
+ or boxes[1] > self._label_areas[label][3]
+ or boxes[2] < self._label_areas[label][0]
+ or boxes[3] < self._label_areas[label][1]
+ ):
+ continue
if label not in matches:
matches[label] = []
diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py
index 29f0cc59392..a13c49cc61a 100644
--- a/homeassistant/components/dovado/__init__.py
+++ b/homeassistant/components/dovado/__init__.py
@@ -2,6 +2,7 @@
import logging
from datetime import timedelta
+import dovado
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -32,7 +33,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
def setup(hass, config):
"""Set up the Dovado component."""
- import dovado
hass.data[DOMAIN] = DovadoData(
dovado.Dovado(
diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml
index e69de29bb2d..d16b2788c70 100644
--- a/homeassistant/components/downloader/services.yaml
+++ b/homeassistant/components/downloader/services.yaml
@@ -0,0 +1,15 @@
+download_file:
+ description: Downloads a file to the download location.
+ fields:
+ url:
+ description: The URL of the file to download.
+ example: 'http://example.org/myfile'
+ subdir:
+ description: Download into subdirectory.
+ example: 'download_dir'
+ filename:
+ description: Determine the filename.
+ example: 'my_file_name'
+ overwrite:
+ description: Whether to overwrite the file or not.
+ example: 'false'
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py
index 82a81118dbd..253e8409f1b 100644
--- a/homeassistant/components/dsmr/sensor.py
+++ b/homeassistant/components/dsmr/sensor.py
@@ -4,6 +4,9 @@ from datetime import timedelta
from functools import partial
import logging
+from dsmr_parser import obis_references as obis_ref
+from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
+import serial
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -52,10 +55,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
# Suppress logging
logging.getLogger("dsmr_parser").setLevel(logging.ERROR)
- from dsmr_parser import obis_references as obis_ref
- from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
- import serial
-
dsmr_version = config[CONF_DSMR_VERSION]
# Define list of name,obis mappings to generate entities
@@ -212,11 +211,9 @@ class DSMREntity(Entity):
@property
def state(self):
"""Return the state of sensor, if available, translate if needed."""
- from dsmr_parser import obis_references as obis
-
value = self.get_dsmr_object_attr("value")
- if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF:
+ if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF:
return self.translate_tariff(value)
try:
diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py
index b904d004c61..aa822da0d6a 100644
--- a/homeassistant/components/dte_energy_bridge/sensor.py
+++ b/homeassistant/components/dte_energy_bridge/sensor.py
@@ -1,12 +1,13 @@
"""Support for monitoring energy usage using the DTE energy bridge."""
import logging
+import requests
import voluptuous as vol
-from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -78,8 +79,6 @@ class DteEnergyBridgeSensor(Entity):
def update(self):
"""Get the energy usage data from the DTE energy bridge."""
- import requests
-
try:
response = requests.get(self._url, timeout=5)
except (requests.exceptions.RequestException, ValueError):
diff --git a/homeassistant/components/dweet/__init__.py b/homeassistant/components/dweet/__init__.py
index bf1298479c3..db985e57a41 100644
--- a/homeassistant/components/dweet/__init__.py
+++ b/homeassistant/components/dweet/__init__.py
@@ -1,7 +1,8 @@
"""Support for sending data to Dweet.io."""
-import logging
from datetime import timedelta
+import logging
+import dweepy
import voluptuous as vol
from homeassistant.const import (
@@ -10,8 +11,8 @@ from homeassistant.const import (
EVENT_STATE_CHANGED,
STATE_UNKNOWN,
)
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import state as state_helper
+import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -69,8 +70,6 @@ def setup(hass, config):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def send_data(name, msg):
"""Send the collected data to Dweet.io."""
- import dweepy
-
try:
dweepy.dweet_for(name, msg)
except dweepy.DweepyError:
diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py
index 937de9b030a..f3f604ff369 100644
--- a/homeassistant/components/dweet/sensor.py
+++ b/homeassistant/components/dweet/sensor.py
@@ -1,18 +1,19 @@
"""Support for showing values from Dweet.io."""
+from datetime import timedelta
import json
import logging
-from datetime import timedelta
+import dweepy
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_NAME,
- CONF_VALUE_TEMPLATE,
- CONF_UNIT_OF_MEASUREMENT,
CONF_DEVICE,
+ CONF_NAME,
+ CONF_UNIT_OF_MEASUREMENT,
+ CONF_VALUE_TEMPLATE,
)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -33,8 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Dweet sensor."""
- import dweepy
-
name = config.get(CONF_NAME)
device = config.get(CONF_DEVICE)
value_template = config.get(CONF_VALUE_TEMPLATE)
@@ -107,8 +106,6 @@ class DweetData:
def update(self):
"""Get the latest data from Dweet.io."""
- import dweepy
-
try:
self.data = dweepy.get_latest_dweet_for(self._device)
except dweepy.DweepyError:
diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py
index e11de446e40..e4d0bdbcdb1 100644
--- a/homeassistant/components/ebusd/__init__.py
+++ b/homeassistant/components/ebusd/__init__.py
@@ -3,13 +3,14 @@ from datetime import timedelta
import logging
import socket
+import ebusdpy
import voluptuous as vol
from homeassistant.const import (
- CONF_NAME,
CONF_HOST,
- CONF_PORT,
CONF_MONITORED_CONDITIONS,
+ CONF_NAME,
+ CONF_PORT,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
@@ -66,7 +67,6 @@ def setup(hass, config):
try:
_LOGGER.debug("Ebusd integration setup started")
- import ebusdpy
ebusdpy.init(server_address)
hass.data[DOMAIN] = EbusdData(server_address, circuit)
@@ -98,8 +98,6 @@ class EbusdData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, name, stype):
"""Call the Ebusd API to update the data."""
- import ebusdpy
-
try:
_LOGGER.debug("Opening socket to ebusd %s", name)
command_result = ebusdpy.read(
@@ -116,8 +114,6 @@ class EbusdData:
def write(self, call):
"""Call write methon on ebusd."""
- import ebusdpy
-
name = call.data.get("name")
value = call.data.get("value")
diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py
index db79d81736e..ec097a153c9 100644
--- a/homeassistant/components/ebusd/const.py
+++ b/homeassistant/components/ebusd/const.py
@@ -1,5 +1,5 @@
"""Constants for ebus component."""
-from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, PRESSURE_BAR
+from homeassistant.const import ENERGY_KILO_WATT_HOUR, PRESSURE_BAR, TEMP_CELSIUS
DOMAIN = "ebusd"
TIME_SECONDS = "seconds"
diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py
index 4bc79e7bd39..63f72a89ccd 100644
--- a/homeassistant/components/ebusd/sensor.py
+++ b/homeassistant/components/ebusd/sensor.py
@@ -1,6 +1,6 @@
"""Support for Ebusd sensors."""
-import logging
import datetime
+import logging
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json
index 1959f769d3a..33d493f6db0 100644
--- a/homeassistant/components/ecobee/.translations/de.json
+++ b/homeassistant/components/ecobee/.translations/de.json
@@ -1,11 +1,25 @@
{
"config": {
+ "abort": {
+ "one_instance_only": "Diese Integration unterst\u00fctzt derzeit nur eine Ecobee-Instanz."
+ },
+ "error": {
+ "pin_request_failed": "Fehler beim Anfordern der PIN von ecobee; Bitte \u00fcberpr\u00fcfe, ob der API-Schl\u00fcssel korrekt ist.",
+ "token_request_failed": "Fehler beim Anfordern eines Token von ecobee; Bitte versuche es erneut."
+ },
"step": {
+ "authorize": {
+ "description": "Bitte autorisiere diese App unter https://www.ecobee.com/consumerportal/index.html mit Pincode:\n\n{pin}\n\nDr\u00fccke dann auf Senden.",
+ "title": "App auf ecobee.com autorisieren"
+ },
"user": {
"data": {
"api_key": "API Key"
- }
+ },
+ "description": "Bitte geben Sie den von ecobee.com erhaltenen API-Schl\u00fcssel ein.",
+ "title": "ecobee API-Schl\u00fcssel"
}
- }
+ },
+ "title": "ecobee"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/.translations/fr.json b/homeassistant/components/ecobee/.translations/fr.json
index 85da5b3a4ec..7f308fdf3a3 100644
--- a/homeassistant/components/ecobee/.translations/fr.json
+++ b/homeassistant/components/ecobee/.translations/fr.json
@@ -9,6 +9,7 @@
},
"step": {
"authorize": {
+ "description": "Veuillez autoriser cette application \u00e0 https://www.ecobee.com/consumerportal/index.html avec un code PIN :\n\n{pin}\n\nEnsuite, appuyez sur Soumettre.",
"title": "Autoriser l'application sur ecobee.com"
},
"user": {
diff --git a/homeassistant/components/ecobee/.translations/ko.json b/homeassistant/components/ecobee/.translations/ko.json
new file mode 100644
index 00000000000..2fea66a9d38
--- /dev/null
+++ b/homeassistant/components/ecobee/.translations/ko.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_only": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ud604\uc7ac \ud558\ub098\uc758 ecobee \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4."
+ },
+ "error": {
+ "pin_request_failed": "ecobee \ub85c\ubd80\ud130 PIN \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; API \ud0a4\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "token_request_failed": "ecobee \ub85c\ubd80\ud130 \ud1a0\ud070 \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
+ },
+ "step": {
+ "authorize": {
+ "description": "https://www.ecobee.com/consumerportal/index.html \uc5d0\uc11c PIN \ucf54\ub4dc\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc774 \uc571\uc744 \uc2b9\uc778\ud574\uc8fc\uc138\uc694:\n\n {pin} \n \n \uadf8\ub7f0 \ub2e4\uc74c Submit \uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.",
+ "title": "ecobee.com \uc5d0\uc11c \uc571 \uc2b9\uc778\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "api_key": "API \ud0a4"
+ },
+ "description": "ecobee.com \uc5d0\uc11c \uc5bb\uc740 API \ud0a4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "ecobee API \ud0a4"
+ }
+ },
+ "title": "ecobee"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/.translations/nl.json b/homeassistant/components/ecobee/.translations/nl.json
new file mode 100644
index 00000000000..56bb3ace26f
--- /dev/null
+++ b/homeassistant/components/ecobee/.translations/nl.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_only": "Deze integratie ondersteunt momenteel slechts \u00e9\u00e9n ecobee-instantie."
+ },
+ "error": {
+ "pin_request_failed": "Fout bij het aanvragen van pincode bij ecobee; Controleer of de API-sleutel correct is.",
+ "token_request_failed": "Fout bij het aanvragen van tokens bij ecobee; probeer het opnieuw."
+ },
+ "step": {
+ "authorize": {
+ "description": "Autoriseer deze app op https://www.ecobee.com/consumerportal/index.html met pincode: \n\n {pin} \n \nDruk vervolgens op Submit.",
+ "title": "Autoriseer app op ecobee.com"
+ },
+ "user": {
+ "data": {
+ "api_key": "API-sleutel"
+ },
+ "description": "Voer de API-sleutel in die u van ecobee.com hebt gekregen.",
+ "title": "ecobee API-sleutel"
+ }
+ },
+ "title": "ecobee"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/.translations/no.json b/homeassistant/components/ecobee/.translations/no.json
index 2bf141f6489..efaa566c424 100644
--- a/homeassistant/components/ecobee/.translations/no.json
+++ b/homeassistant/components/ecobee/.translations/no.json
@@ -1,15 +1,15 @@
{
"config": {
"abort": {
- "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare en ecobee-forekomst."
+ "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare \u00e9n ecobee-forekomst."
},
"error": {
"pin_request_failed": "Feil under foresp\u00f8rsel om PIN-kode fra ecobee. Kontroller at API-n\u00f8kkelen er riktig.",
- "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee; Pr\u00f8v p\u00e5 nytt."
+ "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee: Pr\u00f8v p\u00e5 nytt."
},
"step": {
"authorize": {
- "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nDeretter, trykk p\u00e5 Send.",
+ "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nTrykk deretter p\u00e5 Send.",
"title": "Autoriser app p\u00e5 ecobee.com"
},
"user": {
diff --git a/homeassistant/components/ecobee/.translations/pl.json b/homeassistant/components/ecobee/.translations/pl.json
index 5c51d86fee4..bd4e7aa1ddc 100644
--- a/homeassistant/components/ecobee/.translations/pl.json
+++ b/homeassistant/components/ecobee/.translations/pl.json
@@ -5,7 +5,7 @@
},
"error": {
"pin_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania kodu PIN od ecobee; sprawd\u017a, czy klucz API jest poprawny.",
- "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee; prosz\u0119 spr\u00f3buj ponownie."
+ "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee. Spr\u00f3buj ponownie."
},
"step": {
"authorize": {
diff --git a/homeassistant/components/ecobee/.translations/pt.json b/homeassistant/components/ecobee/.translations/pt.json
new file mode 100644
index 00000000000..20bba0ede4b
--- /dev/null
+++ b/homeassistant/components/ecobee/.translations/pt.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chave da API"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py
index 375146b96e8..06289572aea 100644
--- a/homeassistant/components/ecobee/binary_sensor.py
+++ b/homeassistant/components/ecobee/binary_sensor.py
@@ -4,7 +4,7 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASS_OCCUPANCY,
)
-from .const import DOMAIN
+from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -53,6 +53,44 @@ class EcobeeBinarySensor(BinarySensorDevice):
thermostat = self.data.ecobee.get_thermostat(self.index)
return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}"
+ @property
+ def device_info(self):
+ """Return device information for this sensor."""
+ identifier = None
+ model = None
+ for sensor in self.data.ecobee.get_remote_sensors(self.index):
+ if sensor["name"] != self.sensor_name:
+ continue
+ if "code" in sensor:
+ identifier = sensor["code"]
+ model = "ecobee Room Sensor"
+ else:
+ thermostat = self.data.ecobee.get_thermostat(self.index)
+ identifier = thermostat["identifier"]
+ try:
+ model = (
+ f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat"
+ )
+ except KeyError:
+ _LOGGER.error(
+ "Model number for ecobee thermostat %s not recognized. "
+ "Please visit this link and provide the following information: "
+ "https://github.com/home-assistant/home-assistant/issues/27172 "
+ "Unrecognized model number: %s",
+ thermostat["name"],
+ thermostat["modelNumber"],
+ )
+ break
+
+ if identifier is not None and model is not None:
+ return {
+ "identifiers": {(DOMAIN, identifier)},
+ "name": self.sensor_name,
+ "manufacturer": MANUFACTURER,
+ "model": model,
+ }
+ return None
+
@property
def is_on(self):
"""Return the status of the sensor."""
diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py
index f930282ba7b..e29e2381008 100644
--- a/homeassistant/components/ecobee/climate.py
+++ b/homeassistant/components/ecobee/climate.py
@@ -36,7 +36,7 @@ from homeassistant.const import (
from homeassistant.util.temperature import convert
import homeassistant.helpers.config_validation as cv
-from .const import DOMAIN, _LOGGER
+from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER
from .util import ecobee_date, ecobee_time
ATTR_COOL_TEMP = "cool_temp"
@@ -264,6 +264,7 @@ class Thermostat(ClimateDevice):
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
self._name = self.thermostat["name"]
self.vacation = None
+ self._last_active_hvac_mode = HVAC_MODE_AUTO
self._operation_list = []
if self.thermostat["settings"]["heatStages"]:
@@ -289,6 +290,8 @@ class Thermostat(ClimateDevice):
else:
await self.data.update()
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
+ if self.hvac_mode is not HVAC_MODE_OFF:
+ self._last_active_hvac_mode = self.hvac_mode
@property
def available(self):
@@ -310,6 +313,29 @@ class Thermostat(ClimateDevice):
"""Return a unique identifier for this ecobee thermostat."""
return self.thermostat["identifier"]
+ @property
+ def device_info(self):
+ """Return device information for this ecobee thermostat."""
+ try:
+ model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat"
+ except KeyError:
+ _LOGGER.error(
+ "Model number for ecobee thermostat %s not recognized. "
+ "Please visit this link and provide the following information: "
+ "https://github.com/home-assistant/home-assistant/issues/27172 "
+ "Unrecognized model number: %s",
+ self.name,
+ self.thermostat["modelNumber"],
+ )
+ return None
+
+ return {
+ "identifiers": {(DOMAIN, self.thermostat["identifier"])},
+ "name": self.name,
+ "manufacturer": MANUFACTURER,
+ "model": model,
+ }
+
@property
def temperature_unit(self):
"""Return the unit of measurement."""
@@ -677,3 +703,12 @@ class Thermostat(ClimateDevice):
vacation_name,
)
self.data.ecobee.delete_vacation(self.thermostat_index, vacation_name)
+
+ def turn_on(self):
+ """Set the thermostat to the last active HVAC mode."""
+ _LOGGER.debug(
+ "Turning on ecobee thermostat %s in %s mode",
+ self.name,
+ self._last_active_hvac_mode,
+ )
+ self.set_hvac_mode(self._last_active_hvac_mode)
diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py
index c3a23099b8a..5022cb71903 100644
--- a/homeassistant/components/ecobee/const.py
+++ b/homeassistant/components/ecobee/const.py
@@ -9,4 +9,47 @@ DATA_ECOBEE_CONFIG = "ecobee_config"
CONF_INDEX = "index"
CONF_REFRESH_TOKEN = "refresh_token"
+ECOBEE_MODEL_TO_NAME = {
+ "idtSmart": "ecobee Smart",
+ "idtEms": "ecobee Smart EMS",
+ "siSmart": "ecobee Si Smart",
+ "siEms": "ecobee Si EMS",
+ "athenaSmart": "ecobee3 Smart",
+ "athenaEms": "ecobee3 EMS",
+ "corSmart": "Carrier/Bryant Cor",
+ "nikeSmart": "ecobee3 lite Smart",
+ "nikeEms": "ecobee3 lite EMS",
+ "apolloSmart": "ecobee4 Smart",
+}
+
ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"]
+
+MANUFACTURER = "ecobee"
+
+# Translates ecobee API weatherSymbol to HASS usable names
+# https://www.ecobee.com/home/developer/api/documentation/v1/objects/WeatherForecast.shtml
+ECOBEE_WEATHER_SYMBOL_TO_HASS = {
+ 0: "sunny",
+ 1: "partlycloudy",
+ 2: "partlycloudy",
+ 3: "cloudy",
+ 4: "cloudy",
+ 5: "cloudy",
+ 6: "rainy",
+ 7: "snowy-rainy",
+ 8: "pouring",
+ 9: "hail",
+ 10: "snowy",
+ 11: "snowy",
+ 12: "snowy-rainy",
+ 13: "snowy-heavy",
+ 14: "hail",
+ 15: "lightning-rainy",
+ 16: "windy",
+ 17: "tornado",
+ 18: "fog",
+ 19: "hazy",
+ 20: "hazy",
+ 21: "hazy",
+ -2: None,
+}
diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py
index 48a616a1d1f..76945080bfa 100644
--- a/homeassistant/components/ecobee/sensor.py
+++ b/homeassistant/components/ecobee/sensor.py
@@ -8,7 +8,7 @@ from homeassistant.const import (
)
from homeassistant.helpers.entity import Entity
-from .const import DOMAIN
+from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER
SENSOR_TYPES = {
"temperature": ["Temperature", TEMP_FAHRENHEIT],
@@ -64,6 +64,44 @@ class EcobeeSensor(Entity):
thermostat = self.data.ecobee.get_thermostat(self.index)
return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}"
+ @property
+ def device_info(self):
+ """Return device information for this sensor."""
+ identifier = None
+ model = None
+ for sensor in self.data.ecobee.get_remote_sensors(self.index):
+ if sensor["name"] != self.sensor_name:
+ continue
+ if "code" in sensor:
+ identifier = sensor["code"]
+ model = "ecobee Room Sensor"
+ else:
+ thermostat = self.data.ecobee.get_thermostat(self.index)
+ identifier = thermostat["identifier"]
+ try:
+ model = (
+ f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat"
+ )
+ except KeyError:
+ _LOGGER.error(
+ "Model number for ecobee thermostat %s not recognized. "
+ "Please visit this link and provide the following information: "
+ "https://github.com/home-assistant/home-assistant/issues/27172 "
+ "Unrecognized model number: %s",
+ thermostat["name"],
+ thermostat["modelNumber"],
+ )
+ break
+
+ if identifier is not None and model is not None:
+ return {
+ "identifiers": {(DOMAIN, identifier)},
+ "name": self.sensor_name,
+ "manufacturer": MANUFACTURER,
+ "model": model,
+ }
+ return None
+
@property
def device_class(self):
"""Return the device class of the sensor."""
diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py
index 6175405638e..7b057f09a0c 100644
--- a/homeassistant/components/ecobee/weather.py
+++ b/homeassistant/components/ecobee/weather.py
@@ -8,17 +8,19 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
WeatherEntity,
)
from homeassistant.const import TEMP_FAHRENHEIT
-from .const import DOMAIN
-
-ATTR_FORECAST_TEMP_HIGH = "temphigh"
-ATTR_FORECAST_PRESSURE = "pressure"
-ATTR_FORECAST_VISIBILITY = "visibility"
-ATTR_FORECAST_HUMIDITY = "humidity"
+from .const import (
+ DOMAIN,
+ ECOBEE_MODEL_TO_NAME,
+ ECOBEE_WEATHER_SYMBOL_TO_HASS,
+ MANUFACTURER,
+ _LOGGER,
+)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -66,11 +68,35 @@ class EcobeeWeather(WeatherEntity):
"""Return a unique identifier for the weather platform."""
return self.data.ecobee.get_thermostat(self._index)["identifier"]
+ @property
+ def device_info(self):
+ """Return device information for the ecobee weather platform."""
+ thermostat = self.data.ecobee.get_thermostat(self._index)
+ try:
+ model = f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat"
+ except KeyError:
+ _LOGGER.error(
+ "Model number for ecobee thermostat %s not recognized. "
+ "Please visit this link and provide the following information: "
+ "https://github.com/home-assistant/home-assistant/issues/27172 "
+ "Unrecognized model number: %s",
+ thermostat["name"],
+ thermostat["modelNumber"],
+ )
+ return None
+
+ return {
+ "identifiers": {(DOMAIN, thermostat["identifier"])},
+ "name": self.name,
+ "manufacturer": MANUFACTURER,
+ "model": model,
+ }
+
@property
def condition(self):
"""Return the current condition."""
try:
- return self.get_forecast(0, "condition")
+ return ECOBEE_WEATHER_SYMBOL_TO_HASS[self.get_forecast(0, "weatherSymbol")]
except ValueError:
return None
@@ -107,7 +133,7 @@ class EcobeeWeather(WeatherEntity):
def visibility(self):
"""Return the visibility."""
try:
- return int(self.get_forecast(0, "visibility"))
+ return int(self.get_forecast(0, "visibility")) / 1000
except ValueError:
return None
@@ -130,45 +156,59 @@ class EcobeeWeather(WeatherEntity):
@property
def attribution(self):
"""Return the attribution."""
- if self.weather:
- station = self.weather.get("weatherStation", "UNKNOWN")
- time = self.weather.get("timestamp", "UNKNOWN")
- return f"Ecobee weather provided by {station} at {time}"
- return None
+ if not self.weather:
+ return None
+
+ station = self.weather.get("weatherStation", "UNKNOWN")
+ time = self.weather.get("timestamp", "UNKNOWN")
+ return f"Ecobee weather provided by {station} at {time} UTC"
@property
def forecast(self):
"""Return the forecast array."""
- try:
- forecasts = []
- for day in self.weather["forecasts"]:
- date_time = datetime.strptime(
- day["dateTime"], "%Y-%m-%d %H:%M:%S"
- ).isoformat()
- forecast = {
- ATTR_FORECAST_TIME: date_time,
- ATTR_FORECAST_CONDITION: day["condition"],
- ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10,
- }
- if day["tempHigh"] == ECOBEE_STATE_UNKNOWN:
- break
- if day["tempLow"] != ECOBEE_STATE_UNKNOWN:
- forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10
- if day["pressure"] != ECOBEE_STATE_UNKNOWN:
- forecast[ATTR_FORECAST_PRESSURE] = int(day["pressure"])
- if day["windSpeed"] != ECOBEE_STATE_UNKNOWN:
- forecast[ATTR_FORECAST_WIND_SPEED] = int(day["windSpeed"])
- if day["visibility"] != ECOBEE_STATE_UNKNOWN:
- forecast[ATTR_FORECAST_WIND_SPEED] = int(day["visibility"])
- if day["relativeHumidity"] != ECOBEE_STATE_UNKNOWN:
- forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"])
- forecasts.append(forecast)
- return forecasts
- except (ValueError, IndexError, KeyError):
+ if "forecasts" not in self.weather:
return None
+ forecasts = list()
+ for day in range(1, 5):
+ forecast = _process_forecast(self.weather["forecasts"][day])
+ if forecast is None:
+ continue
+ forecasts.append(forecast)
+
+ if forecasts:
+ return forecasts
+ return None
+
async def async_update(self):
"""Get the latest weather data."""
await self.data.update()
thermostat = self.data.ecobee.get_thermostat(self._index)
self.weather = thermostat.get("weather", None)
+
+
+def _process_forecast(json):
+ """Process a single ecobee API forecast to return expected values."""
+ forecast = dict()
+ try:
+ forecast[ATTR_FORECAST_TIME] = datetime.strptime(
+ json["dateTime"], "%Y-%m-%d %H:%M:%S"
+ ).isoformat()
+ forecast[ATTR_FORECAST_CONDITION] = ECOBEE_WEATHER_SYMBOL_TO_HASS[
+ json["weatherSymbol"]
+ ]
+ if json["tempHigh"] != ECOBEE_STATE_UNKNOWN:
+ forecast[ATTR_FORECAST_TEMP] = float(json["tempHigh"]) / 10
+ if json["tempLow"] != ECOBEE_STATE_UNKNOWN:
+ forecast[ATTR_FORECAST_TEMP_LOW] = float(json["tempLow"]) / 10
+ if json["windBearing"] != ECOBEE_STATE_UNKNOWN:
+ forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"])
+ if json["windSpeed"] != ECOBEE_STATE_UNKNOWN:
+ forecast[ATTR_FORECAST_WIND_SPEED] = int(json["windSpeed"])
+
+ except (ValueError, IndexError, KeyError):
+ return None
+
+ if forecast:
+ return forecast
+ return None
diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py
index 1f21263a4d6..b3d56e42325 100644
--- a/homeassistant/components/eliqonline/sensor.py
+++ b/homeassistant/components/eliqonline/sensor.py
@@ -1,15 +1,16 @@
"""Monitors home energy use for the ELIQ Online service."""
+import asyncio
from datetime import timedelta
import logging
-import asyncio
+import eliqonline
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT
-from homeassistant.helpers.entity import Entity
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -34,8 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the ELIQ Online sensor."""
- import eliqonline
-
access_token = config.get(CONF_ACCESS_TOKEN)
name = config.get(CONF_NAME, DEFAULT_NAME)
channel_id = config.get(CONF_CHANNEL_ID)
diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py
index d15399df67b..d257c46839c 100644
--- a/homeassistant/components/elkm1/__init__.py
+++ b/homeassistant/components/elkm1/__init__.py
@@ -2,7 +2,10 @@
import logging
import re
+import elkm1_lib as elkm1
+from elkm1_lib.const import Max
import voluptuous as vol
+
from homeassistant.const import (
CONF_EXCLUDE,
CONF_HOST,
@@ -12,8 +15,7 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback # noqa
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers import discovery
+from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType # noqa
@@ -125,9 +127,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Elk M1 platform."""
- from elkm1_lib.const import Max
- import elkm1_lib as elkm1
-
devices = {}
elk_datas = {}
diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py
index 927ed53115e..38519ab5b3f 100644
--- a/homeassistant/components/elkm1/alarm_control_panel.py
+++ b/homeassistant/components/elkm1/alarm_control_panel.py
@@ -1,4 +1,5 @@
"""Each ElkM1 area will be created as a separate alarm_control_panel."""
+from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
@@ -93,8 +94,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
def _arm_services():
- from elkm1_lib.const import ArmLevel
-
return {
"elkm1_alarm_arm_vacation": ArmLevel.ARMED_VACATION.value,
"elkm1_alarm_arm_home_instant": ArmLevel.ARMED_STAY_INSTANT.value,
@@ -147,8 +146,6 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel):
@property
def device_state_attributes(self):
"""Attributes of the area."""
- from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState
-
attrs = self.initial_attrs()
elmt = self._element
attrs["is_exit"] = elmt.is_exit
@@ -164,8 +161,6 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel):
return attrs
def _element_changed(self, element, changeset):
- from elkm1_lib.const import ArmedStatus
-
elk_state_to_hass_state = {
ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED,
ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY,
@@ -191,8 +186,6 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel):
return self._element.timer1 > 0 or self._element.timer2 > 0
def _area_is_in_alarm_state(self):
- from elkm1_lib.const import AlarmState
-
return self._element.alarm_state >= AlarmState.FIRE_ALARM.value
async def async_alarm_disarm(self, code=None):
@@ -201,20 +194,14 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel):
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
- from elkm1_lib.const import ArmLevel
-
self._element.arm(ArmLevel.ARMED_STAY.value, int(code))
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
- from elkm1_lib.const import ArmLevel
-
self._element.arm(ArmLevel.ARMED_AWAY.value, int(code))
async def async_alarm_arm_night(self, code=None):
"""Send arm night command."""
- from elkm1_lib.const import ArmLevel
-
self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code))
async def _arm_service(self, arm_level, code):
diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py
index 58273e71222..abc9dc0933c 100644
--- a/homeassistant/components/elkm1/climate.py
+++ b/homeassistant/components/elkm1/climate.py
@@ -1,4 +1,6 @@
"""Support for control of Elk-M1 connected thermostats."""
+from elkm1_lib.const import ThermostatFan, ThermostatMode, ThermostatSetting
+
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
ATTR_TARGET_TEMP_HIGH,
@@ -16,7 +18,6 @@ from homeassistant.const import PRECISION_WHOLE, STATE_ON
from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities
-
SUPPORT_HVAC = [
HVAC_MODE_OFF,
HVAC_MODE_HEAT,
@@ -67,8 +68,6 @@ class ElkThermostat(ElkEntity, ClimateDevice):
@property
def target_temperature(self):
"""Return the temperature we are trying to reach."""
- from elkm1_lib.const import ThermostatMode
-
if (self._element.mode == ThermostatMode.HEAT.value) or (
self._element.mode == ThermostatMode.EMERGENCY_HEAT.value
):
@@ -115,8 +114,6 @@ class ElkThermostat(ElkEntity, ClimateDevice):
@property
def is_aux_heat(self):
"""Return if aux heater is on."""
- from elkm1_lib.const import ThermostatMode
-
return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value
@property
@@ -132,8 +129,6 @@ class ElkThermostat(ElkEntity, ClimateDevice):
@property
def fan_mode(self):
"""Return the fan setting."""
- from elkm1_lib.const import ThermostatFan
-
if self._element.fan == ThermostatFan.AUTO.value:
return HVAC_MODE_AUTO
if self._element.fan == ThermostatFan.ON.value:
@@ -141,8 +136,6 @@ class ElkThermostat(ElkEntity, ClimateDevice):
return None
def _elk_set(self, mode, fan):
- from elkm1_lib.const import ThermostatSetting
-
if mode is not None:
self._element.set(ThermostatSetting.MODE.value, mode)
if fan is not None:
@@ -150,8 +143,6 @@ class ElkThermostat(ElkEntity, ClimateDevice):
async def async_set_hvac_mode(self, hvac_mode):
"""Set thermostat operation mode."""
- from elkm1_lib.const import ThermostatFan, ThermostatMode
-
settings = {
HVAC_MODE_OFF: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value),
HVAC_MODE_HEAT: (ThermostatMode.HEAT.value, None),
@@ -163,14 +154,10 @@ class ElkThermostat(ElkEntity, ClimateDevice):
async def async_turn_aux_heat_on(self):
"""Turn auxiliary heater on."""
- from elkm1_lib.const import ThermostatMode
-
self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None)
async def async_turn_aux_heat_off(self):
"""Turn auxiliary heater off."""
- from elkm1_lib.const import ThermostatMode
-
self._elk_set(ThermostatMode.HEAT.value, None)
@property
@@ -180,8 +167,6 @@ class ElkThermostat(ElkEntity, ClimateDevice):
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
- from elkm1_lib.const import ThermostatFan
-
if fan_mode == HVAC_MODE_AUTO:
self._elk_set(None, ThermostatFan.AUTO.value)
elif fan_mode == STATE_ON:
@@ -189,8 +174,6 @@ class ElkThermostat(ElkEntity, ClimateDevice):
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
- from elkm1_lib.const import ThermostatSetting
-
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if low_temp is not None:
@@ -199,8 +182,6 @@ class ElkThermostat(ElkEntity, ClimateDevice):
self._element.set(ThermostatSetting.COOL_SETPOINT.value, round(high_temp))
def _element_changed(self, element, changeset):
- from elkm1_lib.const import ThermostatFan, ThermostatMode
-
mode_to_state = {
ThermostatMode.OFF.value: HVAC_MODE_OFF,
ThermostatMode.COOL.value: HVAC_MODE_COOL,
diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py
index 3f524b778db..3ed5356f4de 100644
--- a/homeassistant/components/elkm1/sensor.py
+++ b/homeassistant/components/elkm1/sensor.py
@@ -1,4 +1,12 @@
"""Support for control of ElkM1 sensors."""
+from elkm1_lib.const import (
+ SettingFormat,
+ ZoneLogicalStatus,
+ ZonePhysicalStatus,
+ ZoneType,
+)
+from elkm1_lib.util import pretty_const, username
+
from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities
@@ -79,8 +87,6 @@ class ElkKeypad(ElkSensor):
@property
def device_state_attributes(self):
"""Attributes of the sensor."""
- from elkm1_lib.util import username
-
attrs = self.initial_attrs()
attrs["area"] = self._element.area + 1
attrs["temperature"] = self._element.temperature
@@ -140,8 +146,6 @@ class ElkSetting(ElkSensor):
@property
def device_state_attributes(self):
"""Attributes of the sensor."""
- from elkm1_lib.const import SettingFormat
-
attrs = self.initial_attrs()
attrs["value_format"] = SettingFormat(self._element.value_format).name.lower()
return attrs
@@ -153,8 +157,6 @@ class ElkZone(ElkSensor):
@property
def icon(self):
"""Icon to use in the frontend."""
- from elkm1_lib.const import ZoneType
-
zone_icons = {
ZoneType.FIRE_ALARM.value: "fire",
ZoneType.FIRE_VERIFIED.value: "fire",
@@ -181,8 +183,6 @@ class ElkZone(ElkSensor):
@property
def device_state_attributes(self):
"""Attributes of the sensor."""
- from elkm1_lib.const import ZoneLogicalStatus, ZonePhysicalStatus, ZoneType
-
attrs = self.initial_attrs()
attrs["physical_status"] = ZonePhysicalStatus(
self._element.physical_status
@@ -199,8 +199,6 @@ class ElkZone(ElkSensor):
@property
def temperature_unit(self):
"""Return the temperature unit."""
- from elkm1_lib.const import ZoneType
-
if self._element.definition == ZoneType.TEMPERATURE.value:
return self._temperature_unit
return None
@@ -208,8 +206,6 @@ class ElkZone(ElkSensor):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
- from elkm1_lib.const import ZoneType
-
if self._element.definition == ZoneType.TEMPERATURE.value:
return self._temperature_unit
if self._element.definition == ZoneType.ANALOG_ZONE.value:
@@ -217,9 +213,6 @@ class ElkZone(ElkSensor):
return None
def _element_changed(self, element, changeset):
- from elkm1_lib.const import ZoneLogicalStatus, ZoneType
- from elkm1_lib.util import pretty_const
-
if self._element.definition == ZoneType.TEMPERATURE.value:
self._state = temperature_to_state(self._element.temperature, -60)
elif self._element.definition == ZoneType.ANALOG_ZONE.value:
diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json
index c62e1e356b6..4d1a8094663 100644
--- a/homeassistant/components/environment_canada/manifest.json
+++ b/homeassistant/components/environment_canada/manifest.json
@@ -3,7 +3,7 @@
"name": "Environment Canada",
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"requirements": [
- "env_canada==0.0.25"
+ "env_canada==0.0.27"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py
index 76d6a7e369c..6cdedf89744 100644
--- a/homeassistant/components/envisalink/__init__.py
+++ b/homeassistant/components/envisalink/__init__.py
@@ -141,9 +141,12 @@ async def async_setup(hass, config):
@callback
def connection_fail_callback(data):
"""Network failure callback."""
- _LOGGER.error("Could not establish a connection with the Envisalink")
+ _LOGGER.error(
+ "Could not establish a connection with the Envisalink- retrying..."
+ )
if not sync_connect.done():
- sync_connect.set_result(False)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
+ sync_connect.set_result(True)
@callback
def connection_success_callback(data):
diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py
index 81e656708c5..663f19c8ed5 100644
--- a/homeassistant/components/envisalink/alarm_control_panel.py
+++ b/homeassistant/components/envisalink/alarm_control_panel.py
@@ -8,6 +8,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
@@ -126,6 +127,8 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
if self._info["status"]["alarm"]:
state = STATE_ALARM_TRIGGERED
+ elif self._info["status"]["armed_zero_entry_delay"]:
+ state = STATE_ALARM_ARMED_NIGHT
elif self._info["status"]["armed_away"]:
state = STATE_ALARM_ARMED_AWAY
elif self._info["status"]["armed_stay"]:
@@ -173,6 +176,12 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Alarm trigger command. Will be used to trigger a panic alarm."""
self.hass.data[DATA_EVL].panic_alarm(self._panic_type)
+ async def async_alarm_arm_night(self, code=None):
+ """Send arm night command."""
+ self.hass.data[DATA_EVL].arm_night_partition(
+ str(code) if code else str(self._code), self._partition_number
+ )
+
@callback
def async_alarm_keypress(self, keypress=None):
"""Send custom keypress."""
diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json
index 6c5405c75ea..52303c18413 100644
--- a/homeassistant/components/envisalink/manifest.json
+++ b/homeassistant/components/envisalink/manifest.json
@@ -3,8 +3,8 @@
"name": "Envisalink",
"documentation": "https://www.home-assistant.io/integrations/envisalink",
"requirements": [
- "pyenvisalink==3.8"
+ "pyenvisalink==4.0"
],
"dependencies": [],
"codeowners": []
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py
index 435ef582da8..638f012ac7a 100644
--- a/homeassistant/components/epson/media_player.py
+++ b/homeassistant/components/epson/media_player.py
@@ -3,6 +3,30 @@ import logging
import voluptuous as vol
+from epson_projector.const import (
+ BACK,
+ BUSY,
+ CMODE,
+ CMODE_LIST,
+ CMODE_LIST_SET,
+ DEFAULT_SOURCES,
+ EPSON_CODES,
+ FAST,
+ INV_SOURCES,
+ MUTE,
+ PAUSE,
+ PLAY,
+ POWER,
+ SOURCE,
+ SOURCE_LIST,
+ TURN_ON,
+ TURN_OFF,
+ VOLUME,
+ VOL_DOWN,
+ VOL_UP,
+)
+import epson_projector as epson
+
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
from homeassistant.components.media_player.const import (
DOMAIN,
@@ -61,8 +85,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Epson media player platform."""
- from epson_projector.const import CMODE_LIST_SET
-
if DATA_EPSON not in hass.data:
hass.data[DATA_EPSON] = []
@@ -71,12 +93,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
port = config.get(CONF_PORT)
ssl = config.get(CONF_SSL)
- epson = EpsonProjector(
+ epson_proj = EpsonProjector(
async_get_clientsession(hass, verify_ssl=False), name, host, port, ssl
)
- hass.data[DATA_EPSON].append(epson)
- async_add_entities([epson], update_before_add=True)
+ hass.data[DATA_EPSON].append(epson_proj)
+ async_add_entities([epson_proj], update_before_add=True)
async def async_service_handler(service):
"""Handle for services."""
@@ -108,9 +130,6 @@ class EpsonProjector(MediaPlayerDevice):
def __init__(self, websession, name, host, port, encryption):
"""Initialize entity to control Epson projector."""
- import epson_projector as epson
- from epson_projector.const import DEFAULT_SOURCES
-
self._name = name
self._projector = epson.Projector(host, websession=websession, port=port)
self._cmode = None
@@ -121,17 +140,6 @@ class EpsonProjector(MediaPlayerDevice):
async def async_update(self):
"""Update state of device."""
- from epson_projector.const import (
- EPSON_CODES,
- POWER,
- CMODE,
- CMODE_LIST,
- SOURCE,
- VOLUME,
- BUSY,
- SOURCE_LIST,
- )
-
is_turned_on = await self._projector.get_property(POWER)
_LOGGER.debug("Project turn on/off status: %s", is_turned_on)
if is_turned_on and is_turned_on == EPSON_CODES[POWER]:
@@ -165,15 +173,11 @@ class EpsonProjector(MediaPlayerDevice):
async def async_turn_on(self):
"""Turn on epson."""
- from epson_projector.const import TURN_ON
-
if self._state == STATE_OFF:
await self._projector.send_command(TURN_ON)
async def async_turn_off(self):
"""Turn off epson."""
- from epson_projector.const import TURN_OFF
-
if self._state == STATE_ON:
await self._projector.send_command(TURN_OFF)
@@ -194,57 +198,39 @@ class EpsonProjector(MediaPlayerDevice):
async def select_cmode(self, cmode):
"""Set color mode in Epson."""
- from epson_projector.const import CMODE_LIST_SET
-
await self._projector.send_command(CMODE_LIST_SET[cmode])
async def async_select_source(self, source):
"""Select input source."""
- from epson_projector.const import INV_SOURCES
-
selected_source = INV_SOURCES[source]
await self._projector.send_command(selected_source)
async def async_mute_volume(self, mute):
"""Mute (true) or unmute (false) sound."""
- from epson_projector.const import MUTE
-
await self._projector.send_command(MUTE)
async def async_volume_up(self):
"""Increase volume."""
- from epson_projector.const import VOL_UP
-
await self._projector.send_command(VOL_UP)
async def async_volume_down(self):
"""Decrease volume."""
- from epson_projector.const import VOL_DOWN
-
await self._projector.send_command(VOL_DOWN)
async def async_media_play(self):
"""Play media via Epson."""
- from epson_projector.const import PLAY
-
await self._projector.send_command(PLAY)
async def async_media_pause(self):
"""Pause media via Epson."""
- from epson_projector.const import PAUSE
-
await self._projector.send_command(PAUSE)
async def async_media_next_track(self):
"""Skip to next."""
- from epson_projector.const import FAST
-
await self._projector.send_command(FAST)
async def async_media_previous_track(self):
"""Skip to previous."""
- from epson_projector.const import BACK
-
await self._projector.send_command(BACK)
@property
diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py
index 99e2723bf4a..b310376e5cc 100644
--- a/homeassistant/components/epsonworkforce/sensor.py
+++ b/homeassistant/components/epsonworkforce/sensor.py
@@ -10,8 +10,6 @@ from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ["epsonprinter==0.0.9"]
-
_LOGGER = logging.getLogger(__name__)
MONITORED_CONDITIONS = {
"black": ["Ink level Black", "%", "mdi:water"],
diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json
index 62d24662ab6..27d223012c0 100644
--- a/homeassistant/components/esphome/.translations/ru.json
+++ b/homeassistant/components/esphome/.translations/ru.json
@@ -6,7 +6,7 @@
"error": {
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.",
"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"
+ "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": {
diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py
index bc06aba94ea..a669726ca38 100644
--- a/homeassistant/components/esphome/__init__.py
+++ b/homeassistant/components/esphome/__init__.py
@@ -95,8 +95,11 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
"""Cleanup the socket client on HA stop."""
await _cleanup_instance(hass, entry)
+ # Use async_listen instead of async_listen_once so that we don't deregister
+ # the callback twice when shutting down Home Assistant.
+ # "Unable to remove unknown listener .onetime_listener>"
entry_data.cleanup_callbacks.append(
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop)
+ hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
)
@callback
@@ -365,6 +368,7 @@ async def platform_async_setup_entry(
"""
entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id]
entry_data.info[component_key] = {}
+ entry_data.old_info[component_key] = {}
entry_data.state[component_key] = {}
@callback
@@ -390,7 +394,13 @@ async def platform_async_setup_entry(
# Remove old entities
for info in old_infos.values():
entry_data.async_remove_entity(hass, component_key, info.key)
+
+ # First copy the now-old info into the backup object
+ entry_data.old_info[component_key] = entry_data.info[component_key]
+ # Then update the actual info
entry_data.info[component_key] = new_infos
+
+ # Add entities to Home Assistant
async_add_entities(add_entities)
signal = DISPATCHER_ON_LIST.format(entry_id=entry.entry_id)
@@ -479,7 +489,9 @@ class EsphomeEntity(Entity):
}
self._remove_callbacks.append(
async_dispatcher_connect(
- self.hass, DISPATCHER_UPDATE_ENTITY.format(**kwargs), self._on_update
+ self.hass,
+ DISPATCHER_UPDATE_ENTITY.format(**kwargs),
+ self._on_state_update,
)
)
@@ -493,14 +505,23 @@ class EsphomeEntity(Entity):
async_dispatcher_connect(
self.hass,
DISPATCHER_ON_DEVICE_UPDATE.format(**kwargs),
- self.async_schedule_update_ha_state,
+ self._on_device_update,
)
)
- async def _on_update(self) -> None:
+ async def _on_state_update(self) -> None:
"""Update the entity state when state or static info changed."""
self.async_schedule_update_ha_state()
+ async def _on_device_update(self) -> None:
+ """Update the entity state when device info has changed."""
+ if self._entry_data.available:
+ # Don't update the HA state yet when the device comes online.
+ # Only update the HA state when the full state arrives
+ # through the next entity state packet.
+ return
+ self.async_schedule_update_ha_state()
+
async def async_will_remove_from_hass(self) -> None:
"""Unregister callbacks."""
for remove_callback in self._remove_callbacks:
@@ -513,7 +534,13 @@ class EsphomeEntity(Entity):
@property
def _static_info(self) -> EntityInfo:
- return self._entry_data.info[self._component_key][self._key]
+ # Check if value is in info database. Use a single lookup.
+ info = self._entry_data.info[self._component_key].get(self._key)
+ if info is not None:
+ return info
+ # This entity is in the removal project and has been removed from .info
+ # already, look in old_info
+ return self._entry_data.old_info[self._component_key].get(self._key)
@property
def _device_info(self) -> DeviceInfo:
diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py
index cc2e0cede23..c3615c4726d 100644
--- a/homeassistant/components/esphome/camera.py
+++ b/homeassistant/components/esphome/camera.py
@@ -47,9 +47,9 @@ class EsphomeCamera(Camera, EsphomeEntity):
def _state(self) -> Optional[CameraState]:
return super()._state
- async def _on_update(self) -> None:
+ async def _on_state_update(self) -> None:
"""Notify listeners of new image when update arrives."""
- await super()._on_update()
+ await super()._on_state_update()
async with self._image_cond:
self._image_cond.notify_all()
diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py
index 7337aec4541..1dfe2184952 100644
--- a/homeassistant/components/esphome/climate.py
+++ b/homeassistant/components/esphome/climate.py
@@ -17,6 +17,7 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE_RANGE,
PRESET_AWAY,
HVAC_MODE_OFF,
+ PRESET_HOME,
)
from homeassistant.const import (
ATTR_TEMPERATURE,
@@ -96,7 +97,7 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
@property
def preset_modes(self):
"""Return preset modes."""
- return [PRESET_AWAY] if self._static_info.supports_away else []
+ return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else []
@property
def target_temperature_step(self) -> float:
@@ -126,6 +127,9 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
features |= SUPPORT_PRESET_MODE
return features
+ # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
+ # pylint: disable=invalid-overridden-method
+
@esphome_state_property
def hvac_mode(self) -> Optional[str]:
"""Return current operation ie. heat, cool, idle."""
diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py
index 9680ed46acd..47c00f43463 100644
--- a/homeassistant/components/esphome/config_flow.py
+++ b/homeassistant/components/esphome/config_flow.py
@@ -44,11 +44,12 @@ class EsphomeFlowHandler(config_entries.ConfigFlow):
@property
def _name(self):
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
return self.context.get("name")
@_name.setter
def _name(self, value):
- # pylint: disable=unsupported-assignment-operation
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["name"] = value
self.context["title_placeholders"] = {"name": self._name}
diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py
index 31b895b4eb2..980fc936940 100644
--- a/homeassistant/components/esphome/cover.py
+++ b/homeassistant/components/esphome/cover.py
@@ -70,6 +70,9 @@ class EsphomeCover(EsphomeEntity, CoverDevice):
def _state(self) -> Optional[CoverState]:
return super()._state
+ # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
+ # pylint: disable=invalid-overridden-method
+
@esphome_state_property
def is_closed(self) -> Optional[bool]:
"""Return if the cover is closed or not."""
diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py
index b7f9ad9b347..d916e1a90c8 100644
--- a/homeassistant/components/esphome/entry_data.py
+++ b/homeassistant/components/esphome/entry_data.py
@@ -56,6 +56,13 @@ class RuntimeEntryData:
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)
+
+ # A second list of EntityInfo objects
+ # This is necessary for when an entity is being removed. HA requires
+ # some static info to be accessible during removal (unique_id, maybe others)
+ # If an entity can't find anything in the info array, it will look for info here.
+ old_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)
diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py
index 44059673f15..cddb75b41bf 100644
--- a/homeassistant/components/esphome/fan.py
+++ b/homeassistant/components/esphome/fan.py
@@ -92,6 +92,9 @@ class EsphomeFan(EsphomeEntity, FanEntity):
key=self._static_info.key, oscillating=oscillating
)
+ # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
+ # pylint: disable=invalid-overridden-method
+
@esphome_state_property
def is_on(self) -> Optional[bool]:
"""Return true if the entity is on."""
diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py
index 1205521706e..9a2a0ccd0bc 100644
--- a/homeassistant/components/esphome/light.py
+++ b/homeassistant/components/esphome/light.py
@@ -61,6 +61,9 @@ class EsphomeLight(EsphomeEntity, Light):
def _state(self) -> Optional[LightState]:
return super()._state
+ # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
+ # pylint: disable=invalid-overridden-method
+
@esphome_state_property
def is_on(self) -> Optional[bool]:
"""Return true if the switch is on."""
@@ -91,7 +94,7 @@ class EsphomeLight(EsphomeEntity, Light):
"""Turn the entity off."""
data = {"key": self._static_info.key, "state": False}
if ATTR_FLASH in kwargs:
- data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]]
+ data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]]
if ATTR_TRANSITION in kwargs:
data["transition_length"] = kwargs[ATTR_TRANSITION]
await self._client.light_command(**data)
diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json
index bde64762121..40691c653f5 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/integrations/esphome",
"requirements": [
- "aioesphomeapi==2.2.0"
+ "aioesphomeapi==2.4.2"
],
"dependencies": [],
"zeroconf": ["_esphomelib._tcp.local."],
diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py
index 3168bae7ec8..b6adbf93c41 100644
--- a/homeassistant/components/esphome/sensor.py
+++ b/homeassistant/components/esphome/sensor.py
@@ -37,6 +37,10 @@ async def async_setup_entry(
)
+# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
+# pylint: disable=invalid-overridden-method
+
+
class EsphomeSensor(EsphomeEntity):
"""A sensor implementation for esphome."""
@@ -53,6 +57,11 @@ class EsphomeSensor(EsphomeEntity):
"""Return the icon."""
return self._static_info.icon
+ @property
+ def force_update(self) -> bool:
+ """Return if this sensor should force a state update."""
+ return self._static_info.force_update
+
@esphome_state_property
def state(self) -> Optional[str]:
"""Return the state of the entity."""
diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py
index f66bfaa39f3..b52d630e1b4 100644
--- a/homeassistant/components/esphome/switch.py
+++ b/homeassistant/components/esphome/switch.py
@@ -49,6 +49,8 @@ class EsphomeSwitch(EsphomeEntity, SwitchDevice):
"""Return true if we do optimistic updates."""
return self._static_info.assumed_state
+ # https://github.com/PyCQA/pylint/issues/3150 for @esphome_state_property
+ # pylint: disable=invalid-overridden-method
@esphome_state_property
def is_on(self) -> Optional[bool]:
"""Return true if the switch is on."""
diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py
index df6aed3582f..191d6ab5315 100644
--- a/homeassistant/components/eufy/__init__.py
+++ b/homeassistant/components/eufy/__init__.py
@@ -1,5 +1,6 @@
"""Support for Eufy devices."""
import logging
+import lakeside
import voluptuous as vol
@@ -56,7 +57,6 @@ EUFY_DISPATCH = {
def setup(hass, config):
"""Set up Eufy devices."""
- import lakeside
if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]:
data = lakeside.get_devices(
diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py
index f5359e6f2f6..21c26606bdd 100644
--- a/homeassistant/components/eufy/light.py
+++ b/homeassistant/components/eufy/light.py
@@ -1,5 +1,6 @@
"""Support for Eufy lights."""
import logging
+import lakeside
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -36,7 +37,6 @@ class EufyLight(Light):
def __init__(self, device):
"""Initialize the light."""
- import lakeside
self._temp = None
self._brightness = None
diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py
index 3d05ef5d351..2e13886dd2a 100644
--- a/homeassistant/components/eufy/switch.py
+++ b/homeassistant/components/eufy/switch.py
@@ -1,5 +1,6 @@
"""Support for Eufy switches."""
import logging
+import lakeside
from homeassistant.components.switch import SwitchDevice
@@ -18,7 +19,6 @@ class EufySwitch(SwitchDevice):
def __init__(self, device):
"""Initialize the light."""
- import lakeside
self._state = None
self._name = device["name"]
diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py
index 506617e4c60..f7fa9deffa0 100644
--- a/homeassistant/components/everlights/light.py
+++ b/homeassistant/components/everlights/light.py
@@ -1,25 +1,26 @@
"""Support for EverLights lights."""
-import logging
from datetime import timedelta
+import logging
from typing import Tuple
+import pyeverlights
import voluptuous as vol
-from homeassistant.const import CONF_HOSTS
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
- ATTR_HS_COLOR,
ATTR_EFFECT,
- SUPPORT_BRIGHTNESS,
- SUPPORT_EFFECT,
- SUPPORT_COLOR,
- Light,
+ ATTR_HS_COLOR,
PLATFORM_SCHEMA,
+ SUPPORT_BRIGHTNESS,
+ SUPPORT_COLOR,
+ SUPPORT_EFFECT,
+ Light,
)
+from homeassistant.const import CONF_HOSTS
+from homeassistant.exceptions import PlatformNotReady
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.exceptions import PlatformNotReady
_LOGGER = logging.getLogger(__name__)
@@ -46,8 +47,6 @@ def color_int_to_rgb(value: int) -> Tuple[int, int, int]:
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the EverLights lights from configuration.yaml."""
- import pyeverlights
-
lights = []
for ipaddr in config[CONF_HOSTS]:
@@ -159,8 +158,6 @@ class EverLightsLight(Light):
async def async_update(self):
"""Synchronize state with control box."""
- import pyeverlights
-
try:
self._status = await self._api.get_status()
except pyeverlights.ConnectionError:
diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py
index 14bf1223953..29f89dc08d6 100644
--- a/homeassistant/components/evohome/__init__.py
+++ b/homeassistant/components/evohome/__init__.py
@@ -8,11 +8,11 @@ import re
from typing import Any, Dict, Optional, Tuple
import aiohttp.client_exceptions
-import voluptuous as vol
+import evohomeasync
import evohomeasync2
+import voluptuous as vol
from homeassistant.const import (
- CONF_ACCESS_TOKEN,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
@@ -28,14 +28,17 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
import homeassistant.util.dt as dt_util
-from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS
+from .const import DOMAIN, EVO_FOLLOW, GWS, STORAGE_KEY, STORAGE_VERSION, TCS
_LOGGER = logging.getLogger(__name__)
-CONF_ACCESS_TOKEN_EXPIRES = "access_token_expires"
-CONF_REFRESH_TOKEN = "refresh_token"
+ACCESS_TOKEN = "access_token"
+ACCESS_TOKEN_EXPIRES = "access_token_expires"
+REFRESH_TOKEN = "refresh_token"
+USER_DATA = "user_data"
CONF_LOCATION_IDX = "location_idx"
+
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
SCAN_INTERVAL_MINIMUM = timedelta(seconds=60)
@@ -96,14 +99,15 @@ def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]:
def _handle_exception(err) -> bool:
+ """Return False if the exception can't be ignored."""
try:
raise err
except evohomeasync2.AuthenticationError:
_LOGGER.error(
- "Failed to (re)authenticate with the vendor's server. "
+ "Failed to authenticate with the vendor's server. "
"Check your network and the vendor's service status page. "
- "Check that your username and password are correct. "
+ "Also check that your username and password are correct. "
"Message is: %s",
err,
)
@@ -135,14 +139,77 @@ def _handle_exception(err) -> bool:
)
return False
- raise # we don't expect/handle any other ClientResponseError
+ raise # we don't expect/handle any other Exceptions
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Create a (EMEA/EU-based) Honeywell evohome system."""
- broker = EvoBroker(hass, config[DOMAIN])
- if not await broker.init_client():
+
+ async def load_auth_tokens(store) -> Tuple[Dict, Optional[Dict]]:
+ app_storage = await store.async_load()
+ tokens = dict(app_storage if app_storage else {})
+
+ if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]:
+ # any tokens wont be valid, and store might be be corrupt
+ await store.async_save({})
+ return ({}, None)
+
+ # evohomeasync2 requires naive/local datetimes as strings
+ if tokens.get(ACCESS_TOKEN_EXPIRES) is not None:
+ tokens[ACCESS_TOKEN_EXPIRES] = _dt_to_local_naive(
+ dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES])
+ )
+
+ user_data = tokens.pop(USER_DATA, None)
+ return (tokens, user_data)
+
+ store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
+ tokens, user_data = await load_auth_tokens(store)
+
+ client_v2 = evohomeasync2.EvohomeClient(
+ config[DOMAIN][CONF_USERNAME],
+ config[DOMAIN][CONF_PASSWORD],
+ **tokens,
+ session=async_get_clientsession(hass),
+ )
+
+ try:
+ await client_v2.login()
+ except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
+ _handle_exception(err)
return False
+ finally:
+ config[DOMAIN][CONF_PASSWORD] = "REDACTED"
+
+ loc_idx = config[DOMAIN][CONF_LOCATION_IDX]
+ try:
+ loc_config = client_v2.installation_info[loc_idx][GWS][0][TCS][0]
+ except IndexError:
+ _LOGGER.error(
+ "Config error: '%s' = %s, but the valid range is 0-%s. "
+ "Unable to continue. Fix any configuration errors and restart HA.",
+ CONF_LOCATION_IDX,
+ loc_idx,
+ len(client_v2.installation_info) - 1,
+ )
+ return False
+
+ _LOGGER.debug("Config = %s", loc_config)
+
+ client_v1 = evohomeasync.EvohomeClient(
+ client_v2.username,
+ client_v2.password,
+ user_data=user_data,
+ session=async_get_clientsession(hass),
+ )
+
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN]["broker"] = broker = EvoBroker(
+ hass, client_v2, client_v1, store, config[DOMAIN]
+ )
+
+ await broker.save_auth_tokens()
+ await broker.update() # get initial state
hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config))
if broker.tcs.hotwater:
@@ -160,116 +227,100 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
class EvoBroker:
"""Container for evohome client and data."""
- def __init__(self, hass, params) -> None:
+ def __init__(self, hass, client, client_v1, store, params) -> None:
"""Initialize the evohome client and its data structure."""
self.hass = hass
+ self.client = client
+ self.client_v1 = client_v1
+ self._store = store
self.params = params
- self.config = {}
-
- self.client = self.tcs = None
- self._app_storage = {}
-
- hass.data[DOMAIN] = {}
- hass.data[DOMAIN]["broker"] = self
-
- async def init_client(self) -> bool:
- """Initialse the evohome data broker.
-
- Return True if this is successful, otherwise return False.
- """
- refresh_token, access_token, access_token_expires = (
- await self._load_auth_tokens()
- )
-
- # evohomeasync2 uses naive/local datetimes
- if access_token_expires is not None:
- access_token_expires = _dt_to_local_naive(access_token_expires)
-
- client = self.client = evohomeasync2.EvohomeClient(
- self.params[CONF_USERNAME],
- self.params[CONF_PASSWORD],
- refresh_token=refresh_token,
- access_token=access_token,
- access_token_expires=access_token_expires,
- session=async_get_clientsession(self.hass),
- )
-
- try:
- await client.login()
- except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
- if not _handle_exception(err):
- return False
-
- finally:
- self.params[CONF_PASSWORD] = "REDACTED"
-
- self.hass.add_job(self._save_auth_tokens())
-
- loc_idx = self.params[CONF_LOCATION_IDX]
- try:
- self.config = client.installation_info[loc_idx][GWS][0][TCS][0]
-
- except IndexError:
- _LOGGER.error(
- "Config error: '%s' = %s, but its valid range is 0-%s. "
- "Unable to continue. "
- "Fix any configuration errors and restart HA.",
- CONF_LOCATION_IDX,
- loc_idx,
- len(client.installation_info) - 1,
- )
- return False
+ loc_idx = params[CONF_LOCATION_IDX]
+ self.config = client.installation_info[loc_idx][GWS][0][TCS][0]
self.tcs = (
client.locations[loc_idx] # pylint: disable=protected-access
._gateways[0]
._control_systems[0]
)
+ self.temps = None
- _LOGGER.debug("Config = %s", self.config)
- if _LOGGER.isEnabledFor(logging.DEBUG): # don't do an I/O unless required
- await self.update() # includes: _LOGGER.debug("Status = %s"...
-
- return True
-
- async def _load_auth_tokens(
- self
- ) -> Tuple[Optional[str], Optional[str], Optional[datetime]]:
- store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
- app_storage = self._app_storage = await store.async_load()
-
- if app_storage is None:
- app_storage = self._app_storage = {}
-
- if app_storage.get(CONF_USERNAME) == self.params[CONF_USERNAME]:
- refresh_token = app_storage.get(CONF_REFRESH_TOKEN)
- access_token = app_storage.get(CONF_ACCESS_TOKEN)
- at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES)
- if at_expires_str:
- at_expires_dt = dt_util.parse_datetime(at_expires_str)
- else:
- at_expires_dt = None
-
- return (refresh_token, access_token, at_expires_dt)
-
- return (None, None, None) # account switched: so tokens wont be valid
-
- async def _save_auth_tokens(self, *args) -> None:
+ async def save_auth_tokens(self) -> None:
+ """Save access tokens and session IDs to the store for later use."""
# evohomeasync2 uses naive/local datetimes
access_token_expires = _local_dt_to_aware(self.client.access_token_expires)
- self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME]
- self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token
- self._app_storage[CONF_ACCESS_TOKEN] = self.client.access_token
- self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat()
+ app_storage = {CONF_USERNAME: self.client.username}
+ app_storage[REFRESH_TOKEN] = self.client.refresh_token
+ app_storage[ACCESS_TOKEN] = self.client.access_token
+ app_storage[ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat()
- store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
- await store.async_save(self._app_storage)
+ if self.client_v1 and self.client_v1.user_data:
+ app_storage[USER_DATA] = {
+ "userInfo": {"userID": self.client_v1.user_data["userInfo"]["userID"]},
+ "sessionId": self.client_v1.user_data["sessionId"],
+ }
+ else:
+ app_storage[USER_DATA] = None
- self.hass.helpers.event.async_track_point_in_utc_time(
- self._save_auth_tokens,
- access_token_expires + self.params[CONF_SCAN_INTERVAL],
- )
+ await self._store.async_save(app_storage)
+
+ async def _update_v1(self, *args, **kwargs) -> None:
+ """Get the latest high-precision temperatures of the default Location."""
+
+ def get_session_id(client_v1) -> Optional[str]:
+ user_data = client_v1.user_data if client_v1 else None
+ return user_data.get("sessionId") if user_data else None
+
+ session_id = get_session_id(self.client_v1)
+
+ try:
+ temps = list(await self.client_v1.temperatures(force_refresh=True))
+
+ except aiohttp.ClientError as err:
+ _LOGGER.warning(
+ "Unable to obtain the latest high-precision temperatures. "
+ "Check your network and the vendor's service status page. "
+ "Proceeding with low-precision temperatures. "
+ "Message is: %s",
+ err,
+ )
+ self.temps = None # these are now stale, will fall back to v2 temps
+
+ else:
+ if (
+ str(self.client_v1.location_id)
+ != self.client.locations[self.params[CONF_LOCATION_IDX]].locationId
+ ):
+ _LOGGER.warning(
+ "The v2 API's configured location doesn't match "
+ "the v1 API's default location (there is more than one location), "
+ "so the high-precision feature will be disabled"
+ )
+ self.client_v1 = self.temps = None
+ else:
+ self.temps = {str(i["id"]): i["temp"] for i in temps}
+
+ _LOGGER.debug("Temperatures = %s", self.temps)
+
+ if session_id != get_session_id(self.client_v1):
+ await self.save_auth_tokens()
+
+ async def _update_v2(self, *args, **kwargs) -> None:
+ """Get the latest modes, temperatures, setpoints of a Location."""
+ access_token = self.client.access_token
+
+ loc_idx = self.params[CONF_LOCATION_IDX]
+ try:
+ status = await self.client.locations[loc_idx].status()
+ except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
+ _handle_exception(err)
+ else:
+ self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN)
+
+ _LOGGER.debug("Status = %s", status[GWS][0][TCS][0])
+
+ if access_token != self.client.access_token:
+ await self.save_auth_tokens()
async def update(self, *args, **kwargs) -> None:
"""Get the latest state data of an entire evohome Location.
@@ -278,17 +329,13 @@ class EvoBroker:
operating mode of the Controller and the current temp of its children (e.g.
Zones, DHW controller).
"""
- loc_idx = self.params[CONF_LOCATION_IDX]
+ await self._update_v2()
- try:
- status = await self.client.locations[loc_idx].status()
- except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
- _handle_exception(err)
- else:
- # inform the evohome devices that state data has been updated
- self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN)
+ if self.client_v1:
+ await self._update_v1()
- _LOGGER.debug("Status = %s", status[GWS][0][TCS][0])
+ # inform the evohome devices that state data has been updated
+ self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN)
class EvoDevice(Entity):
@@ -305,10 +352,8 @@ class EvoDevice(Entity):
self._evo_tcs = evo_broker.tcs
self._unique_id = self._name = self._icon = self._precision = None
-
- self._device_state_attrs = {}
- self._state_attributes = []
self._supported_features = None
+ self._device_state_attrs = {}
@callback
def _refresh(self) -> None:
@@ -394,9 +439,13 @@ class EvoChild(EvoDevice):
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature of a Zone."""
- if self._evo_device.temperatureStatus["isAvailable"]:
- return self._evo_device.temperatureStatus["temperature"]
- return None
+ if not self._evo_device.temperatureStatus["isAvailable"]:
+ return None
+
+ if self._evo_broker.temps:
+ return self._evo_broker.temps[self._evo_device.zoneId]
+
+ return self._evo_device.temperatureStatus["temperature"]
@property
def setpoints(self) -> Dict[str, Any]:
@@ -411,37 +460,44 @@ class EvoChild(EvoDevice):
day_of_week = int(day_time.strftime("%w")) # 0 is Sunday
time_of_day = day_time.strftime("%H:%M:%S")
- # Iterate today's switchpoints until past the current time of day...
- day = self._schedule["DailySchedules"][day_of_week]
- sp_idx = -1 # last switchpoint of the day before
- for i, tmp in enumerate(day["Switchpoints"]):
- if time_of_day > tmp["TimeOfDay"]:
- sp_idx = i # current setpoint
- else:
- break
+ try:
+ # Iterate today's switchpoints until past the current time of day...
+ day = self._schedule["DailySchedules"][day_of_week]
+ sp_idx = -1 # last switchpoint of the day before
+ for i, tmp in enumerate(day["Switchpoints"]):
+ if time_of_day > tmp["TimeOfDay"]:
+ sp_idx = i # current setpoint
+ else:
+ break
- # Did the current SP start yesterday? Does the next start SP tomorrow?
- this_sp_day = -1 if sp_idx == -1 else 0
- next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0
+ # Did the current SP start yesterday? Does the next start SP tomorrow?
+ this_sp_day = -1 if sp_idx == -1 else 0
+ next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0
- for key, offset, idx in [
- ("this", this_sp_day, sp_idx),
- ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)),
- ]:
- sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d")
- day = self._schedule["DailySchedules"][(day_of_week + offset) % 7]
- switchpoint = day["Switchpoints"][idx]
+ for key, offset, idx in [
+ ("this", this_sp_day, sp_idx),
+ ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)),
+ ]:
+ sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d")
+ day = self._schedule["DailySchedules"][(day_of_week + offset) % 7]
+ switchpoint = day["Switchpoints"][idx]
- dt_local_aware = _local_dt_to_aware(
- dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}")
+ dt_local_aware = _local_dt_to_aware(
+ dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}")
+ )
+
+ self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat()
+ try:
+ self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"]
+ except KeyError:
+ self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"]
+
+ except IndexError:
+ self._setpoints = {}
+ _LOGGER.warning(
+ "Failed to get setpoints - please report as an issue", exc_info=True
)
- self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat()
- try:
- self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"]
- except KeyError:
- self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"]
-
return self._setpoints
async def _update_schedule(self) -> None:
@@ -454,6 +510,8 @@ class EvoChild(EvoDevice):
self._evo_device.schedule(), refresh=False
)
+ _LOGGER.debug("Schedule['%s'] = %s", self.name, self._schedule)
+
async def async_update(self) -> None:
"""Get the latest state data."""
next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00")
diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py
index 7df2db1b17e..82a7001539d 100644
--- a/homeassistant/components/evohome/climate.py
+++ b/homeassistant/components/evohome/climate.py
@@ -1,6 +1,6 @@
"""Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems."""
import logging
-from typing import Optional, List
+from typing import List, Optional
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
@@ -14,26 +14,26 @@ from homeassistant.components.climate.const import (
PRESET_ECO,
PRESET_HOME,
PRESET_NONE,
- SUPPORT_TARGET_TEMPERATURE,
SUPPORT_PRESET_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import PRECISION_TENTHS
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util.dt import parse_datetime
-from . import CONF_LOCATION_IDX, EvoDevice, EvoChild
+from . import CONF_LOCATION_IDX, EvoChild, EvoDevice
from .const import (
DOMAIN,
- EVO_RESET,
EVO_AUTO,
EVO_AUTOECO,
EVO_AWAY,
EVO_CUSTOM,
EVO_DAYOFF,
- EVO_HEATOFF,
EVO_FOLLOW,
- EVO_TEMPOVER,
+ EVO_HEATOFF,
EVO_PERMOVER,
+ EVO_RESET,
+ EVO_TEMPOVER,
)
_LOGGER = logging.getLogger(__name__)
@@ -72,14 +72,13 @@ async def async_setup_platform(
return
broker = hass.data[DOMAIN]["broker"]
- loc_idx = broker.params[CONF_LOCATION_IDX]
_LOGGER.debug(
"Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)",
broker.tcs.modelType,
broker.tcs.systemId,
broker.tcs.location.name,
- loc_idx,
+ broker.params[CONF_LOCATION_IDX],
)
# special case of RoundModulation/RoundWireless (is a single zone system)
@@ -148,9 +147,12 @@ class EvoZone(EvoChild, EvoClimateDevice):
self._name = evo_device.name
self._icon = "mdi:radiator"
- self._precision = self._evo_device.setpointCapabilities["valueResolution"]
self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE
self._preset_modes = list(HA_PRESET_TO_EVO)
+ if evo_broker.client_v1:
+ self._precision = PRECISION_TENTHS
+ else:
+ self._precision = self._evo_device.setpointCapabilities["valueResolution"]
@property
def hvac_mode(self) -> str:
diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json
index 5633880be35..0b112df42bb 100644
--- a/homeassistant/components/evohome/manifest.json
+++ b/homeassistant/components/evohome/manifest.json
@@ -3,7 +3,7 @@
"name": "Evohome",
"documentation": "https://www.home-assistant.io/integrations/evohome",
"requirements": [
- "evohome-async==0.3.3b4"
+ "evohome-async==0.3.4b1"
],
"dependencies": [],
"codeowners": ["@zxdavb"]
diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py
index 37bdcd82afc..e29dbb49af2 100644
--- a/homeassistant/components/evohome/water_heater.py
+++ b/homeassistant/components/evohome/water_heater.py
@@ -7,7 +7,7 @@ from homeassistant.components.water_heater import (
SUPPORT_OPERATION_MODE,
WaterHeaterDevice,
)
-from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON
+from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util.dt import parse_datetime
@@ -55,7 +55,7 @@ class EvoDHW(EvoChild, WaterHeaterDevice):
self._name = "DHW controller"
self._icon = "mdi:thermometer-lines"
- self._precision = PRECISION_WHOLE
+ self._precision = PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE
self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE
@property
diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py
new file mode 100644
index 00000000000..1053861e2bf
--- /dev/null
+++ b/homeassistant/components/fan/reproduce_state.py
@@ -0,0 +1,100 @@
+"""Reproduce an Fan state."""
+import asyncio
+import logging
+from types import MappingProxyType
+from typing import Iterable, Optional
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ STATE_ON,
+ STATE_OFF,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+)
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ DOMAIN,
+ ATTR_DIRECTION,
+ ATTR_OSCILLATING,
+ ATTR_SPEED,
+ SERVICE_OSCILLATE,
+ SERVICE_SET_DIRECTION,
+ SERVICE_SET_SPEED,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+VALID_STATES = {STATE_ON, STATE_OFF}
+ATTRIBUTES = { # attribute: service
+ ATTR_DIRECTION: SERVICE_SET_DIRECTION,
+ ATTR_OSCILLATING: SERVICE_OSCILLATE,
+ ATTR_SPEED: SERVICE_SET_SPEED,
+}
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if state.state not in VALID_STATES:
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state and all(
+ check_attr_equal(cur_state.attributes, state.attributes, attr)
+ for attr in ATTRIBUTES
+ ):
+ return
+
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+ service_calls = {} # service: service_data
+
+ if state.state == STATE_ON:
+ # The fan should be on
+ if cur_state.state != STATE_ON:
+ # Turn on the fan at first
+ service_calls[SERVICE_TURN_ON] = service_data
+
+ for attr, service in ATTRIBUTES.items():
+ # Call services to adjust the attributes
+ if attr in state.attributes and not check_attr_equal(
+ state.attributes, cur_state.attributes, attr
+ ):
+ data = service_data.copy()
+ data[attr] = state.attributes[attr]
+ service_calls[service] = data
+
+ elif state.state == STATE_OFF:
+ service_calls[SERVICE_TURN_OFF] = service_data
+
+ for service, data in service_calls.items():
+ await hass.services.async_call(
+ DOMAIN, service, data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Fan states."""
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
+
+
+def check_attr_equal(
+ attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str
+) -> bool:
+ """Return true if the given attributes are equal."""
+ return attr1.get(attr_str) == attr2.get(attr_str)
diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml
index 16d3742d9ab..0e3978690e6 100644
--- a/homeassistant/components/fan/services.yaml
+++ b/homeassistant/components/fan/services.yaml
@@ -42,7 +42,7 @@ toggle:
fields:
entity_id:
description: Name(s) of the entities to toggle
- exampl: 'fan.living_room'
+ example: 'fan.living_room'
set_direction:
description: Set the fan rotation.
diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py
index 44ec95f8213..e4ec154620e 100644
--- a/homeassistant/components/feedreader/__init__.py
+++ b/homeassistant/components/feedreader/__init__.py
@@ -6,6 +6,7 @@ from threading import Lock
import pickle
import voluptuous as vol
+import feedparser
from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL
from homeassistant.helpers.event import track_time_interval
@@ -87,8 +88,6 @@ class FeedManager:
def _update(self):
"""Update the feed and publish new entries to the event bus."""
- import feedparser
-
_LOGGER.info("Fetching new data from feed %s", self._url)
self._feed = feedparser.parse(
self._url,
diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py
index 51e1cac3859..673a34230fc 100644
--- a/homeassistant/components/ffmpeg/__init__.py
+++ b/homeassistant/components/ffmpeg/__init__.py
@@ -3,6 +3,7 @@ import logging
import re
import voluptuous as vol
+from haffmpeg.tools import FFVersion
from homeassistant.core import callback
from homeassistant.const import (
@@ -105,7 +106,6 @@ class FFmpegManager:
async def async_get_version(self):
"""Return ffmpeg version."""
- from haffmpeg.tools import FFVersion
ffversion = FFVersion(self._bin, self.hass.loop)
self._version = await ffversion.get_version()
diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py
index 598ffe36bd4..0f500176933 100644
--- a/homeassistant/components/ffmpeg/camera.py
+++ b/homeassistant/components/ffmpeg/camera.py
@@ -3,6 +3,8 @@ import asyncio
import logging
import voluptuous as vol
+from haffmpeg.camera import CameraMjpeg
+from haffmpeg.tools import ImageFrame, IMAGE_JPEG
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM
from homeassistant.const import CONF_NAME
@@ -53,7 +55,6 @@ class FFmpegCamera(Camera):
async def async_camera_image(self):
"""Return a still image response from the camera."""
- from haffmpeg.tools import ImageFrame, IMAGE_JPEG
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
@@ -66,7 +67,6 @@ class FFmpegCamera(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
- from haffmpeg.camera import CameraMjpeg
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
await stream.open_camera(self._input, extra_cmd=self._extra_arguments)
diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py
index 534477d88cf..0d4b8d61e7a 100644
--- a/homeassistant/components/fitbit/sensor.py
+++ b/homeassistant/components/fitbit/sensor.py
@@ -4,6 +4,9 @@ import logging
import datetime
import time
+from fitbit import Fitbit
+from fitbit.api import FitbitOauth2Client
+from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError
import voluptuous as vol
from homeassistant.core import callback
@@ -234,13 +237,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if "fitbit" in _CONFIGURING:
hass.components.configurator.request_done(_CONFIGURING.pop("fitbit"))
- import fitbit
-
access_token = config_file.get(ATTR_ACCESS_TOKEN)
refresh_token = config_file.get(ATTR_REFRESH_TOKEN)
expires_at = config_file.get(ATTR_LAST_SAVED_AT)
if None not in (access_token, refresh_token):
- authd_client = fitbit.Fitbit(
+ authd_client = Fitbit(
config_file.get(ATTR_CLIENT_ID),
config_file.get(ATTR_CLIENT_SECRET),
access_token=access_token,
@@ -294,7 +295,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
else:
- oauth = fitbit.api.FitbitOauth2Client(
+ oauth = FitbitOauth2Client(
config_file.get(ATTR_CLIENT_ID), config_file.get(ATTR_CLIENT_SECRET)
)
@@ -337,9 +338,6 @@ class FitbitAuthCallbackView(HomeAssistantView):
@callback
def get(self, request):
"""Finish OAuth callback request."""
- from oauthlib.oauth2.rfc6749.errors import MismatchingStateError
- from oauthlib.oauth2.rfc6749.errors import MissingTokenError
-
hass = request.app["hass"]
data = request.query
diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py
index 4fa97334889..416d39e5332 100644
--- a/homeassistant/components/flic/binary_sensor.py
+++ b/homeassistant/components/flic/binary_sensor.py
@@ -2,6 +2,14 @@
import logging
import threading
+from pyflic import (
+ FlicClient,
+ ButtonConnectionChannel,
+ ClickType,
+ ConnectionStatus,
+ ScanWizard,
+ ScanWizardResult,
+)
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -49,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the flic platform."""
- import pyflic
# Initialize flic client responsible for
# connecting to buttons and retrieving events
@@ -58,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
discovery = config.get(CONF_DISCOVERY)
try:
- client = pyflic.FlicClient(host, port)
+ client = FlicClient(host, port)
except ConnectionRefusedError:
_LOGGER.error("Failed to connect to flic server")
return
@@ -88,15 +95,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def start_scanning(config, add_entities, client):
"""Start a new flic client for scanning and connecting to new buttons."""
- import pyflic
-
- scan_wizard = pyflic.ScanWizard()
+ scan_wizard = ScanWizard()
def scan_completed_callback(scan_wizard, result, address, name):
"""Restart scan wizard to constantly check for new buttons."""
- if result == pyflic.ScanWizardResult.WizardSuccess:
+ if result == ScanWizardResult.WizardSuccess:
_LOGGER.info("Found new button %s", address)
- elif result != pyflic.ScanWizardResult.WizardFailedTimeout:
+ elif result != ScanWizardResult.WizardFailedTimeout:
_LOGGER.warning(
"Failed to connect to button %s. Reason: %s", address, result
)
@@ -123,7 +128,6 @@ class FlicButton(BinarySensorDevice):
def __init__(self, hass, client, address, timeout, ignored_click_types):
"""Initialize the flic button."""
- import pyflic
self._hass = hass
self._address = address
@@ -131,10 +135,10 @@ class FlicButton(BinarySensorDevice):
self._is_down = False
self._ignored_click_types = ignored_click_types or []
self._hass_click_types = {
- pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE,
- pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE,
- pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE,
- pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD,
+ ClickType.ButtonClick: CLICK_TYPE_SINGLE,
+ ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE,
+ ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE,
+ ClickType.ButtonHold: CLICK_TYPE_HOLD,
}
self._channel = self._create_channel()
@@ -142,9 +146,7 @@ class FlicButton(BinarySensorDevice):
def _create_channel(self):
"""Create a new connection channel to the button."""
- import pyflic
-
- channel = pyflic.ButtonConnectionChannel(self._address)
+ channel = ButtonConnectionChannel(self._address)
channel.on_button_up_or_down = self._on_up_down
# If all types of clicks should be ignored, skip registering callbacks
@@ -212,12 +214,10 @@ class FlicButton(BinarySensorDevice):
def _on_up_down(self, channel, click_type, was_queued, time_diff):
"""Update device state, if event was not queued."""
- import pyflic
-
if was_queued and self._queued_event_check(click_type, time_diff):
return
- self._is_down = click_type == pyflic.ClickType.ButtonDown
+ self._is_down = click_type == ClickType.ButtonDown
self.schedule_update_ha_state()
def _on_click(self, channel, click_type, was_queued, time_diff):
@@ -243,9 +243,7 @@ class FlicButton(BinarySensorDevice):
def _connection_status_changed(self, channel, connection_status, disconnect_reason):
"""Remove device, if button disconnects."""
- import pyflic
-
- if connection_status == pyflic.ConnectionStatus.Disconnected:
+ if connection_status == ConnectionStatus.Disconnected:
_LOGGER.warning(
"Button (%s) disconnected. Reason: %s", self.address, disconnect_reason
)
diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py
index 800ccd1938f..7b58ffbe449 100644
--- a/homeassistant/components/flux/switch.py
+++ b/homeassistant/components/flux/switch.py
@@ -31,10 +31,12 @@ from homeassistant.const import (
CONF_LIGHTS,
CONF_MODE,
SERVICE_TURN_ON,
+ STATE_ON,
SUN_EVENT_SUNRISE,
SUN_EVENT_SUNSET,
)
from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.util import slugify
from homeassistant.util.color import (
@@ -169,7 +171,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
hass.services.async_register(DOMAIN, service_name, async_update)
-class FluxSwitch(SwitchDevice):
+class FluxSwitch(SwitchDevice, RestoreEntity):
"""Representation of a Flux switch."""
def __init__(
@@ -214,6 +216,12 @@ class FluxSwitch(SwitchDevice):
"""Return true if switch is on."""
return self.unsub_tracker is not None
+ async def async_added_to_hass(self):
+ """Call when entity about to be added to hass."""
+ last_state = await self.async_get_last_state()
+ if last_state and last_state.state == STATE_ON:
+ await self.async_turn_on()
+
async def async_turn_on(self, **kwargs):
"""Turn on flux."""
if self.is_on:
diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py
index 0a95de783fa..5bd84cd157f 100644
--- a/homeassistant/components/flux_led/light.py
+++ b/homeassistant/components/flux_led/light.py
@@ -3,6 +3,7 @@ import logging
import socket
import random
+from flux_led import BulbScanner, WifiLedBulb
import voluptuous as vol
from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE
@@ -135,8 +136,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Flux lights."""
- import flux_led
-
lights = []
light_ips = []
@@ -156,7 +155,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return
# Find the bulbs on the LAN
- scanner = flux_led.BulbScanner()
+ scanner = BulbScanner()
scanner.scan(timeout=10)
for device in scanner.getBulbInfo():
ipaddr = device["ipaddr"]
@@ -187,9 +186,8 @@ class FluxLight(Light):
def _connect(self):
"""Connect to Flux light."""
- import flux_led
- self._bulb = flux_led.WifiLedBulb(self._ipaddr, timeout=5)
+ self._bulb = WifiLedBulb(self._ipaddr, timeout=5)
if self._protocol:
self._bulb.setProtocol(self._protocol)
diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py
index 63e9956d0df..0e2ca4073bf 100644
--- a/homeassistant/components/foscam/camera.py
+++ b/homeassistant/components/foscam/camera.py
@@ -1,11 +1,26 @@
"""This component provides basic support for Foscam IP cameras."""
import logging
+import asyncio
+
+from libpyfoscam import FoscamCamera
import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM
-from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT
+from homeassistant.const import (
+ CONF_NAME,
+ CONF_USERNAME,
+ CONF_PASSWORD,
+ CONF_PORT,
+ ATTR_ENTITY_ID,
+)
from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.service import async_extract_entity_ids
+
+from .const import DOMAIN as FOSCAM_DOMAIN
+from .const import DATA as FOSCAM_DATA
+from .const import ENTITIES as FOSCAM_ENTITIES
+
_LOGGER = logging.getLogger(__name__)
@@ -15,7 +30,32 @@ CONF_RTSP_PORT = "rtsp_port"
DEFAULT_NAME = "Foscam Camera"
DEFAULT_PORT = 88
-FOSCAM_COMM_ERROR = -8
+SERVICE_PTZ = "ptz"
+ATTR_MOVEMENT = "movement"
+ATTR_TRAVELTIME = "travel_time"
+
+DEFAULT_TRAVELTIME = 0.125
+
+DIR_UP = "up"
+DIR_DOWN = "down"
+DIR_LEFT = "left"
+DIR_RIGHT = "right"
+
+DIR_TOPLEFT = "top_left"
+DIR_TOPRIGHT = "top_right"
+DIR_BOTTOMLEFT = "bottom_left"
+DIR_BOTTOMRIGHT = "bottom_right"
+
+MOVEMENT_ATTRS = {
+ DIR_UP: "ptz_move_up",
+ DIR_DOWN: "ptz_move_down",
+ DIR_LEFT: "ptz_move_left",
+ DIR_RIGHT: "ptz_move_right",
+ DIR_TOPLEFT: "ptz_move_top_left",
+ DIR_TOPRIGHT: "ptz_move_top_right",
+ DIR_BOTTOMLEFT: "ptz_move_bottom_left",
+ DIR_BOTTOMRIGHT: "ptz_move_bottom_right",
+}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -28,44 +68,114 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
}
)
+SERVICE_PTZ_SCHEMA = vol.Schema(
+ {
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Required(ATTR_MOVEMENT): vol.In(
+ [
+ DIR_UP,
+ DIR_DOWN,
+ DIR_LEFT,
+ DIR_RIGHT,
+ DIR_TOPLEFT,
+ DIR_TOPRIGHT,
+ DIR_BOTTOMLEFT,
+ DIR_BOTTOMRIGHT,
+ ]
+ ),
+ vol.Optional(ATTR_TRAVELTIME, default=DEFAULT_TRAVELTIME): cv.small_float,
+ }
+)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Foscam IP Camera."""
- add_entities([FoscamCam(config)])
+
+ async def async_handle_ptz(service):
+ """Handle PTZ service call."""
+ movement = service.data[ATTR_MOVEMENT]
+ travel_time = service.data[ATTR_TRAVELTIME]
+ entity_ids = await async_extract_entity_ids(hass, service)
+
+ if not entity_ids:
+ return
+
+ _LOGGER.debug("Moving '%s' camera(s): %s", movement, entity_ids)
+
+ all_cameras = hass.data[FOSCAM_DATA][FOSCAM_ENTITIES]
+ target_cameras = [
+ camera for camera in all_cameras if camera.entity_id in entity_ids
+ ]
+
+ for camera in target_cameras:
+ await camera.async_perform_ptz(movement, travel_time)
+
+ hass.services.async_register(
+ FOSCAM_DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA
+ )
+
+ camera = FoscamCamera(
+ config[CONF_IP],
+ config[CONF_PORT],
+ config[CONF_USERNAME],
+ config[CONF_PASSWORD],
+ verbose=False,
+ )
+
+ rtsp_port = config.get(CONF_RTSP_PORT)
+ if not rtsp_port:
+ ret, response = await hass.async_add_executor_job(camera.get_port_info)
+
+ if ret == 0:
+ rtsp_port = response.get("rtspPort") or response.get("mediaPort")
+
+ ret, response = await hass.async_add_executor_job(camera.get_motion_detect_config)
+
+ motion_status = False
+ if ret != 0 and response == 1:
+ motion_status = True
+
+ async_add_entities(
+ [
+ HassFoscamCamera(
+ camera,
+ config[CONF_NAME],
+ config[CONF_USERNAME],
+ config[CONF_PASSWORD],
+ rtsp_port,
+ motion_status,
+ )
+ ]
+ )
-class FoscamCam(Camera):
+class HassFoscamCamera(Camera):
"""An implementation of a Foscam IP camera."""
- def __init__(self, device_info):
+ def __init__(self, camera, name, username, password, rtsp_port, motion_status):
"""Initialize a Foscam camera."""
- from libpyfoscam import FoscamCamera
-
super().__init__()
- ip_address = device_info.get(CONF_IP)
- port = device_info.get(CONF_PORT)
- self._username = device_info.get(CONF_USERNAME)
- self._password = device_info.get(CONF_PASSWORD)
- self._name = device_info.get(CONF_NAME)
- self._motion_status = False
+ self._foscam_session = camera
+ self._name = name
+ self._username = username
+ self._password = password
+ self._rtsp_port = rtsp_port
+ self._motion_status = motion_status
- self._foscam_session = FoscamCamera(
- ip_address, port, self._username, self._password, verbose=False
+ async def async_added_to_hass(self):
+ """Handle entity addition to hass."""
+ entities = self.hass.data.setdefault(FOSCAM_DATA, {}).setdefault(
+ FOSCAM_ENTITIES, []
)
-
- self._rtsp_port = device_info.get(CONF_RTSP_PORT)
- if not self._rtsp_port:
- result, response = self._foscam_session.get_port_info()
- if result == 0:
- self._rtsp_port = response.get("rtspPort") or response.get("mediaPort")
+ entities.append(self)
def camera_image(self):
"""Return a still image response from the camera."""
# Send the request to snap a picture and return raw jpg data
# Handle exception if host is not reachable or url failed
result, response = self._foscam_session.snap_picture_2()
- if result == FOSCAM_COMM_ERROR:
+ if result != 0:
return None
return response
@@ -97,19 +207,47 @@ class FoscamCam(Camera):
"""Enable motion detection in camera."""
try:
ret = self._foscam_session.enable_motion_detection()
- self._motion_status = ret == FOSCAM_COMM_ERROR
+
+ if ret != 0:
+ return
+
+ self._motion_status = True
except TypeError:
_LOGGER.debug("Communication problem")
- self._motion_status = False
def disable_motion_detection(self):
"""Disable motion detection."""
try:
ret = self._foscam_session.disable_motion_detection()
- self._motion_status = ret == FOSCAM_COMM_ERROR
+
+ if ret != 0:
+ return
+
+ self._motion_status = False
except TypeError:
_LOGGER.debug("Communication problem")
- self._motion_status = False
+
+ async def async_perform_ptz(self, movement, travel_time):
+ """Perform a PTZ action on the camera."""
+ _LOGGER.debug("PTZ action '%s' on %s", movement, self._name)
+
+ movement_function = getattr(self._foscam_session, MOVEMENT_ATTRS[movement])
+
+ ret, _ = await self.hass.async_add_executor_job(movement_function)
+
+ if ret != 0:
+ _LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret)
+ return
+
+ await asyncio.sleep(travel_time)
+
+ ret, _ = await self.hass.async_add_executor_job(
+ self._foscam_session.ptz_stop_run
+ )
+
+ if ret != 0:
+ _LOGGER.error("Error stopping movement on '%s': %s", self._name, ret)
+ return
@property
def name(self):
diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py
new file mode 100644
index 00000000000..63b4b74a763
--- /dev/null
+++ b/homeassistant/components/foscam/const.py
@@ -0,0 +1,5 @@
+"""Constants for Foscam component."""
+
+DOMAIN = "foscam"
+DATA = "foscam"
+ENTITIES = "entities"
diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json
index b2c44c113ee..6a47012ef84 100644
--- a/homeassistant/components/foscam/manifest.json
+++ b/homeassistant/components/foscam/manifest.json
@@ -6,5 +6,5 @@
"libpyfoscam==1.0"
],
"dependencies": [],
- "codeowners": []
+ "codeowners": ["@skgsergio"]
}
diff --git a/homeassistant/components/foscam/services.yaml b/homeassistant/components/foscam/services.yaml
new file mode 100644
index 00000000000..64e68dd5bc4
--- /dev/null
+++ b/homeassistant/components/foscam/services.yaml
@@ -0,0 +1,12 @@
+ptz:
+ description: Pan/Tilt service for Foscam camera.
+ fields:
+ entity_id:
+ description: Name(s) of entities to move.
+ example: 'camera.living_room_camera'
+ movement:
+ description: "Direction of the movement. Allowed values: up, down, left, right, top_left, top_right, bottom_left, bottom_right."
+ example: 'up'
+ travel_time:
+ description: "(Optional) Travel time in seconds. Allowed values: float from 0 to 1. Default: 0.125"
+ example: 0.125
diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py
index afe0aa3ed02..ab4deec96f7 100644
--- a/homeassistant/components/fritz/device_tracker.py
+++ b/homeassistant/components/fritz/device_tracker.py
@@ -1,15 +1,16 @@
"""Support for FRITZ!Box routers."""
import logging
+from fritzconnection import FritzHosts # pylint: disable=import-error
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN,
PLATFORM_SCHEMA,
DeviceScanner,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -41,11 +42,9 @@ class FritzBoxScanner(DeviceScanner):
self.password = config[CONF_PASSWORD]
self.success_init = True
- import fritzconnection as fc # pylint: disable=import-error
-
# Establish a connection to the FRITZ!Box.
try:
- self.fritz_box = fc.FritzHosts(
+ self.fritz_box = FritzHosts(
address=self.host, user=self.username, password=self.password
)
except (ValueError, TypeError):
diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json
index e6c1fee2c95..15a3406891f 100644
--- a/homeassistant/components/fritz/manifest.json
+++ b/homeassistant/components/fritz/manifest.json
@@ -3,7 +3,7 @@
"name": "Fritz",
"documentation": "https://www.home-assistant.io/integrations/fritz",
"requirements": [
- "fritzconnection==0.6.5"
+ "fritzconnection==0.8.4"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py
index a053bc6c7ca..40aa3a881d1 100644
--- a/homeassistant/components/fritzbox/__init__.py
+++ b/homeassistant/components/fritzbox/__init__.py
@@ -1,9 +1,9 @@
"""Support for AVM Fritz!Box smarthome devices."""
import logging
+from pyfritzhome import Fritzhome, LoginError
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_DEVICES,
CONF_HOST,
@@ -12,6 +12,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers import discovery
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -52,7 +53,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the fritzbox component."""
- from pyfritzhome import Fritzhome, LoginError
fritz_list = []
diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json
index 35c27b7ca84..f85f16d6c0d 100644
--- a/homeassistant/components/fritzbox_callmonitor/manifest.json
+++ b/homeassistant/components/fritzbox_callmonitor/manifest.json
@@ -3,7 +3,7 @@
"name": "Fritzbox callmonitor",
"documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor",
"requirements": [
- "fritzconnection==0.6.5"
+ "fritzconnection==0.8.4"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py
index 4dada44f4e5..b1d601ce382 100644
--- a/homeassistant/components/fritzbox_callmonitor/sensor.py
+++ b/homeassistant/components/fritzbox_callmonitor/sensor.py
@@ -1,24 +1,25 @@
"""Sensor to monitor incoming/outgoing phone calls on a Fritz!Box router."""
+import datetime
import logging
+import re
import socket
import threading
-import datetime
import time
-import re
+import fritzconnection as fc # pylint: disable=import-error
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST,
- CONF_PORT,
CONF_NAME,
CONF_PASSWORD,
+ CONF_PORT,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
-from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -248,8 +249,6 @@ class FritzBoxPhonebook:
self.number_dict = None
self.prefixes = prefixes or []
- import fritzconnection as fc # pylint: disable=import-error
-
# Establish a connection to the FRITZ!Box.
self.fph = fc.FritzPhonebook(
address=self.host, user=self.username, password=self.password
diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json
index 88a7ab5a338..9afaa71e699 100644
--- a/homeassistant/components/fritzbox_netmonitor/manifest.json
+++ b/homeassistant/components/fritzbox_netmonitor/manifest.json
@@ -3,7 +3,7 @@
"name": "Fritzbox netmonitor",
"documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor",
"requirements": [
- "fritzconnection==0.6.5"
+ "fritzconnection==0.8.4"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py
index 9d07e7a8055..0a82c5e29c3 100644
--- a/homeassistant/components/fritzbox_netmonitor/sensor.py
+++ b/homeassistant/components/fritzbox_netmonitor/sensor.py
@@ -1,14 +1,18 @@
"""Support for monitoring an AVM Fritz!Box router."""
-import logging
from datetime import timedelta
-from requests.exceptions import RequestException
+import logging
+from fritzconnection import FritzStatus # pylint: disable=import-error
+from fritzconnection.fritzconnection import ( # pylint: disable=import-error
+ FritzConnectionException,
+)
+from requests.exceptions import RequestException
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, CONF_HOST, STATE_UNAVAILABLE
-from homeassistant.helpers.entity import Entity
+from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -45,15 +49,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the FRITZ!Box monitor sensors."""
- # pylint: disable=import-error
- import fritzconnection as fc
- from fritzconnection.fritzconnection import FritzConnectionException
-
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
try:
- fstatus = fc.FritzStatus(address=host)
+ fstatus = FritzStatus(address=host)
except (ValueError, TypeError, FritzConnectionException):
fstatus = None
diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py
index dcb700d6636..cc629c54dc3 100644
--- a/homeassistant/components/fritzdect/switch.py
+++ b/homeassistant/components/fritzdect/switch.py
@@ -1,20 +1,21 @@
"""Support for FRITZ!DECT Switches."""
import logging
-from requests.exceptions import RequestException, HTTPError
-
+from fritzhome.fritz import FritzBox
+from requests.exceptions import HTTPError, RequestException
import voluptuous as vol
-from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
+from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
from homeassistant.const import (
+ ATTR_TEMPERATURE,
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
- POWER_WATT,
ENERGY_KILO_WATT_HOUR,
+ POWER_WATT,
+ TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
_LOGGER = logging.getLogger(__name__)
@@ -42,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Add all switches connected to Fritz Box."""
- from fritzhome.fritz import FritzBox
host = config.get(CONF_HOST)
username = config.get(CONF_USERNAME)
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index e46423c8271..541d1bf473d 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -6,23 +6,23 @@ import os
import pathlib
from typing import Any, Dict, Optional, Set, Tuple
-from aiohttp import web, web_urldispatcher, hdrs
-import voluptuous as vol
+from aiohttp import hdrs, web, web_urldispatcher
+import hass_frontend
import jinja2
+import voluptuous as vol
from yarl import URL
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components import websocket_api
+from homeassistant.components.http.view import HomeAssistantView
from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
from .storage import async_setup_frontend_storage
-
# mypy: allow-untyped-defs, no-check-untyped-defs
# Fix mimetypes for borked Windows machines
@@ -242,8 +242,6 @@ def _frontend_root(dev_repo_path):
if dev_repo_path is not None:
return pathlib.Path(dev_repo_path) / "hass_frontend"
- import hass_frontend
-
return hass_frontend.where()
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index 67a66bc9612..aa7ad8b18f9 100644
--- a/homeassistant/components/frontend/manifest.json
+++ b/homeassistant/components/frontend/manifest.json
@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
- "home-assistant-frontend==20191002.2"
+ "home-assistant-frontend==20191025.1"
],
"dependencies": [
"api",
diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py
index 8ab379b050b..010420d0f98 100644
--- a/homeassistant/components/frontier_silicon/media_player.py
+++ b/homeassistant/components/frontier_silicon/media_player.py
@@ -1,9 +1,11 @@
"""Support for Frontier Silicon Devices (Medion, Hama, Auna,...)."""
import logging
+from afsapi import AFSAPI
+import requests
import voluptuous as vol
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
SUPPORT_NEXT_TRACK,
@@ -64,8 +66,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Frontier Silicon platform."""
- import requests
-
if discovery_info is not None:
async_add_entities(
[AFSAPIDevice(discovery_info["ssdp_description"], DEFAULT_PASSWORD)], True
@@ -118,8 +118,6 @@ class AFSAPIDevice(MediaPlayerDevice):
connected to the device in between the updates and invalidated the
existing session (i.e UNDOK).
"""
- from afsapi import AFSAPI
-
return AFSAPI(self._device_url, self._password)
@property
diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py
index eba768f82e3..7b9e79dbb3e 100644
--- a/homeassistant/components/futurenow/light.py
+++ b/homeassistant/components/futurenow/light.py
@@ -2,15 +2,16 @@
import logging
+import pyfnip
import voluptuous as vol
-from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT, CONF_DEVICES
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
+ PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
Light,
- PLATFORM_SCHEMA,
)
+from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PORT
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -68,8 +69,6 @@ class FutureNowLight(Light):
def __init__(self, device):
"""Initialize the light."""
- import pyfnip
-
self._name = device["name"]
self._dimmable = device["dimmable"]
self._channel = device["channel"]
diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py
index 19303fdc6d2..36779b28df2 100644
--- a/homeassistant/components/gc100/__init__.py
+++ b/homeassistant/components/gc100/__init__.py
@@ -1,9 +1,10 @@
"""Support for controlling Global Cache gc100."""
import logging
+import gc100
import voluptuous as vol
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -31,8 +32,6 @@ CONFIG_SCHEMA = vol.Schema(
# pylint: disable=no-member
def setup(hass, base_config):
"""Set up the gc100 component."""
- import gc100
-
config = base_config[DOMAIN]
host = config[CONF_HOST]
port = config[CONF_PORT]
diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py
index d9f6c877cbc..b34c46a9f26 100644
--- a/homeassistant/components/geniushub/__init__.py
+++ b/homeassistant/components/geniushub/__init__.py
@@ -4,9 +4,8 @@ import logging
from typing import Any, Dict, Optional
import aiohttp
-import voluptuous as vol
-
from geniushubclient import GeniusHub
+import voluptuous as vol
from homeassistant.const import (
ATTR_TEMPERATURE,
@@ -22,8 +21,8 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import (
- async_dispatcher_send,
async_dispatcher_connect,
+ async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
@@ -214,7 +213,7 @@ class GeniusZone(GeniusEntity):
super().__init__()
self._zone = zone
- self._unique_id = f"{broker.hub_uid}_device_{zone.id}"
+ self._unique_id = f"{broker.hub_uid}_zone_{zone.id}"
self._max_temp = self._min_temp = self._supported_features = None
diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py
index f27b1cc7f1a..9a19edd9f8b 100644
--- a/homeassistant/components/geniushub/climate.py
+++ b/homeassistant/components/geniushub/climate.py
@@ -1,14 +1,17 @@
"""Support for Genius Hub climate devices."""
-from typing import Optional, List
+from typing import List, Optional
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
- HVAC_MODE_OFF,
+ CURRENT_HVAC_HEAT,
+ CURRENT_HVAC_IDLE,
+ CURRENT_HVAC_OFF,
HVAC_MODE_HEAT,
- PRESET_BOOST,
+ HVAC_MODE_OFF,
PRESET_ACTIVITY,
- SUPPORT_TARGET_TEMPERATURE,
+ PRESET_BOOST,
SUPPORT_PRESET_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
@@ -68,6 +71,17 @@ class GeniusClimateZone(GeniusZone, ClimateDevice):
"""Return the list of available hvac operation modes."""
return list(HA_HVAC_TO_GH)
+ @property
+ def hvac_action(self) -> Optional[str]:
+ """Return the current running hvac operation if supported."""
+ if "_state" in self._zone.data: # only for v3 API
+ if not self._zone.data["_state"].get("bIsActive"):
+ return CURRENT_HVAC_OFF
+ if self._zone.data["_state"].get("bOutRequestHeat"):
+ return CURRENT_HVAC_HEAT
+ return CURRENT_HVAC_IDLE
+ return None
+
@property
def preset_mode(self) -> Optional[str]:
"""Return the current preset mode, e.g., home, away, temp."""
diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json
index 96497388a48..f9e8e6eb4f0 100644
--- a/homeassistant/components/geniushub/manifest.json
+++ b/homeassistant/components/geniushub/manifest.json
@@ -3,7 +3,7 @@
"name": "Genius Hub",
"documentation": "https://www.home-assistant.io/integrations/geniushub",
"requirements": [
- "geniushub-client==0.6.26"
+ "geniushub-client==0.6.28"
],
"dependencies": [],
"codeowners": ["@zxdavb"]
diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py
index 2f5d9bceb8b..bd73c700e65 100644
--- a/homeassistant/components/geniushub/sensor.py
+++ b/homeassistant/components/geniushub/sensor.py
@@ -94,6 +94,8 @@ class GeniusIssue(GeniusEntity):
super().__init__()
self._hub = broker.client
+ self._unique_id = f"{broker.hub_uid}_{GH_LEVEL_MAPPING[level]}"
+
self._name = f"GeniusHub {GH_LEVEL_MAPPING[level]}"
self._level = level
self._issues = []
diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py
index cd4f536e14f..4141e9f8c04 100644
--- a/homeassistant/components/geniushub/water_heater.py
+++ b/homeassistant/components/geniushub/water_heater.py
@@ -2,9 +2,9 @@
from typing import List
from homeassistant.components.water_heater import (
- WaterHeaterDevice,
- SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+ WaterHeaterDevice,
)
from homeassistant.const import STATE_OFF
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json
index 8fd19f6b034..c681807ad01 100644
--- a/homeassistant/components/geo_rss_events/manifest.json
+++ b/homeassistant/components/geo_rss_events/manifest.json
@@ -1,10 +1,12 @@
{
"domain": "geo_rss_events",
- "name": "Geo rss events",
+ "name": "Geo RSS events",
"documentation": "https://www.home-assistant.io/integrations/geo_rss_events",
"requirements": [
"georss_generic_client==0.2"
],
"dependencies": [],
- "codeowners": []
+ "codeowners": [
+ "@exxamalte"
+ ]
}
diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py
index 9f336668142..39e6c5c7e82 100644
--- a/homeassistant/components/geo_rss_events/sensor.py
+++ b/homeassistant/components/geo_rss_events/sensor.py
@@ -12,6 +12,8 @@ import logging
from datetime import timedelta
import voluptuous as vol
+from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA
+from georss_client.generic_feed import GenericFeed
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -108,7 +110,6 @@ class GeoRssServiceSensor(Entity):
self._state = None
self._state_attributes = None
self._unit_of_measurement = unit_of_measurement
- from georss_client.generic_feed import GenericFeed
self._feed = GenericFeed(
coordinates,
@@ -146,10 +147,9 @@ class GeoRssServiceSensor(Entity):
def update(self):
"""Update this sensor from the GeoRSS service."""
- import georss_client
status, feed_entries = self._feed.update()
- if status == georss_client.UPDATE_OK:
+ if status == UPDATE_OK:
_LOGGER.debug(
"Adding events to sensor %s: %s", self.entity_id, feed_entries
)
@@ -159,7 +159,7 @@ class GeoRssServiceSensor(Entity):
for entry in feed_entries:
matrix[entry.title] = f"{entry.distance_to_home:.0f}km"
self._state_attributes = matrix
- elif status == georss_client.UPDATE_OK_NO_DATA:
+ elif status == UPDATE_OK_NO_DATA:
_LOGGER.debug("Update successful, but no data received from %s", self._feed)
# Don't change the state or state attributes.
else:
diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json
index 0b5e3c0df9f..02593bf603d 100644
--- a/homeassistant/components/github/manifest.json
+++ b/homeassistant/components/github/manifest.json
@@ -3,7 +3,7 @@
"name": "Github",
"documentation": "https://www.home-assistant.io/integrations/github",
"requirements": [
- "PyGithub==1.43.5"
+ "PyGithub==1.43.8"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py
index a85364ebeca..5e8200b41ab 100644
--- a/homeassistant/components/github/sensor.py
+++ b/homeassistant/components/github/sensor.py
@@ -1,6 +1,7 @@
"""Support for GitHub."""
from datetime import timedelta
import logging
+import github
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -148,8 +149,6 @@ class GitHubData:
def __init__(self, repository, access_token=None, server_url=None):
"""Set up GitHub."""
- import github
-
self._github = github
self.setup_error = False
diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py
index d8055c88f30..9edbe9733a8 100644
--- a/homeassistant/components/gitlab_ci/sensor.py
+++ b/homeassistant/components/gitlab_ci/sensor.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -141,12 +142,10 @@ class GitLabData:
def __init__(self, gitlab_id, priv_token, interval, url):
"""Fetch data from GitLab API for most recent CI job."""
- import gitlab
self._gitlab_id = gitlab_id
- self._gitlab = gitlab.Gitlab(url, private_token=priv_token, per_page=1)
+ self._gitlab = Gitlab(url, private_token=priv_token, per_page=1)
self._gitlab.auth()
- self._gitlab_exceptions = gitlab.exceptions
self.update = Throttle(interval)(self._update)
self.available = False
@@ -174,9 +173,9 @@ class GitLabData:
self.build_id = _last_job.attributes.get("id")
self.branch = _last_job.attributes.get("ref")
self.available = True
- except self._gitlab_exceptions.GitlabAuthenticationError as erra:
+ except GitlabAuthenticationError as erra:
_LOGGER.error("Authentication Error: %s", erra)
self.available = False
- except self._gitlab_exceptions.GitlabGetError as errg:
+ except GitlabGetError as errg:
_LOGGER.error("Project Not Found: %s", errg)
self.available = False
diff --git a/homeassistant/components/glances/.translations/ca.json b/homeassistant/components/glances/.translations/ca.json
new file mode 100644
index 00000000000..edff236623e
--- /dev/null
+++ b/homeassistant/components/glances/.translations/ca.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat."
+ },
+ "error": {
+ "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3",
+ "wrong_version": "Versi\u00f3 no compatible (2 o 3 necess\u00e0ria)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "name": "Nom",
+ "password": "Contrasenya",
+ "port": "Port",
+ "ssl": "Utilitza SSL/TLS per connectar-te al sistema Glances",
+ "username": "Nom d'usuari",
+ "verify_ssl": "Verifica la certificaci\u00f3 del sistema",
+ "version": "Versi\u00f3 de l'API de Glances (2 o 3)"
+ },
+ "title": "Configuraci\u00f3 de Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3"
+ },
+ "description": "Opcions de configuraci\u00f3 per a Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/da.json b/homeassistant/components/glances/.translations/da.json
new file mode 100644
index 00000000000..7779c6e40a0
--- /dev/null
+++ b/homeassistant/components/glances/.translations/da.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "V\u00e6rten er allerede konfigureret."
+ },
+ "error": {
+ "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt",
+ "wrong_version": "Version underst\u00f8ttes ikke (kun 2 eller 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e6rt",
+ "name": "Navn",
+ "password": "Adgangskode",
+ "port": "Port",
+ "ssl": "Brug SSL/TLS til at oprette forbindelse til Glances-systemet",
+ "username": "Brugernavn",
+ "verify_ssl": "Bekr\u00e6ft certificering af systemet",
+ "version": "Glances API version (2 eller 3)"
+ },
+ "title": "Ops\u00e6tning af Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Opdateringsfrekvens"
+ },
+ "description": "Konfigurationsindstillinger for Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/de.json b/homeassistant/components/glances/.translations/de.json
new file mode 100644
index 00000000000..04fed0fdc49
--- /dev/null
+++ b/homeassistant/components/glances/.translations/de.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Host ist bereits konfiguriert."
+ },
+ "error": {
+ "cannot_connect": "Verbindung zum Host nicht m\u00f6glich",
+ "wrong_version": "Version nicht unterst\u00fctzt (nur 2 oder 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Name",
+ "password": "Passwort",
+ "port": "Port",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Aktualisierungsfrequenz"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/en.json b/homeassistant/components/glances/.translations/en.json
new file mode 100644
index 00000000000..ef1a8fb5e31
--- /dev/null
+++ b/homeassistant/components/glances/.translations/en.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Host is already configured."
+ },
+ "error": {
+ "cannot_connect": "Unable to connect to host",
+ "wrong_version": "Version not supported (2 or 3 only)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Name",
+ "password": "Password",
+ "port": "Port",
+ "ssl": "Use SSL/TLS to connect to the Glances system",
+ "username": "Username",
+ "verify_ssl": "Verify the certification of the system",
+ "version": "Glances API Version (2 or 3)"
+ },
+ "title": "Setup Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Update frequency"
+ },
+ "description": "Configure options for Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/es.json b/homeassistant/components/glances/.translations/es.json
new file mode 100644
index 00000000000..1b6b0335192
--- /dev/null
+++ b/homeassistant/components/glances/.translations/es.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El host ya est\u00e1 configurado."
+ },
+ "error": {
+ "cannot_connect": "No se puede conectar al host",
+ "wrong_version": "Versi\u00f3n no soportada (s\u00f3lo 2 o 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nombre",
+ "password": "Contrase\u00f1a",
+ "port": "Puerto",
+ "ssl": "Utilice SSL/TLS para conectarse al sistema Glances",
+ "username": "Nombre de usuario",
+ "verify_ssl": "Verificar la certificaci\u00f3n del sistema",
+ "version": "Versi\u00f3n API Glances (2 o 3)"
+ },
+ "title": "Configurar Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Frecuencia de actualizaci\u00f3n"
+ },
+ "description": "Configurar opciones para Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/fi.json b/homeassistant/components/glances/.translations/fi.json
new file mode 100644
index 00000000000..43ccf405d14
--- /dev/null
+++ b/homeassistant/components/glances/.translations/fi.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "Nimi",
+ "password": "Salasana",
+ "port": "portti"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/fr.json b/homeassistant/components/glances/.translations/fr.json
new file mode 100644
index 00000000000..0391012c4cd
--- /dev/null
+++ b/homeassistant/components/glances/.translations/fr.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9."
+ },
+ "error": {
+ "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te",
+ "wrong_version": "Version non prise en charge (2 ou 3 uniquement)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "name": "Nom",
+ "password": "Mot de passe",
+ "port": "Port",
+ "ssl": "V\u00e9rifier la certification du syst\u00e8me",
+ "username": "Nom d'utilisateur",
+ "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me",
+ "version": "Glances API Version (2 ou 3)"
+ },
+ "title": "Installation de Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Fr\u00e9quence de mise \u00e0 jour"
+ },
+ "description": "Configurer les options pour Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/ko.json b/homeassistant/components/glances/.translations/ko.json
new file mode 100644
index 00000000000..ad19b589d5d
--- /dev/null
+++ b/homeassistant/components/glances/.translations/ko.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "wrong_version": "\ud574\ub2f9 \ubc84\uc804\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (2 \ub610\ub294 3\ub9cc \uc9c0\uc6d0)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "name": "\uc774\ub984",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "port": "\ud3ec\ud2b8",
+ "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec Glances \uc2dc\uc2a4\ud15c\uc5d0 \uc5f0\uacb0",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
+ "verify_ssl": "\uc2dc\uc2a4\ud15c \uc778\uc99d \ud655\uc778",
+ "version": "Glances API \ubc84\uc804 (2 \ub610\ub294 3)"
+ },
+ "title": "Glances \uc124\uce58"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4"
+ },
+ "description": "Glances \uc635\uc158 \uad6c\uc131"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/lb.json b/homeassistant/components/glances/.translations/lb.json
new file mode 100644
index 00000000000..06723a4bd12
--- /dev/null
+++ b/homeassistant/components/glances/.translations/lb.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "cannot_connect": "Kann sech net mam Server verbannen.",
+ "wrong_version": "Versioun net \u00ebnnerst\u00ebtzt (n\u00ebmmen 2 oder 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Apparat",
+ "name": "Numm",
+ "password": "Passwuert",
+ "port": "Port",
+ "ssl": "Benotzt SSL/TLS fir sech mam Usiichte System ze verbannen",
+ "username": "Benotzernumm",
+ "verify_ssl": "Zertifikatioun vum System iwwerpr\u00e9iwen",
+ "version": "API Versioun vun den Usiichten (2 oder 3)"
+ },
+ "title": "Usiichten konfigur\u00e9ieren"
+ }
+ },
+ "title": "Usiichten"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Intervalle vun de Mise \u00e0 jour"
+ },
+ "description": "Optioune konfigur\u00e9ieren fir d'Usiichten"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/nl.json b/homeassistant/components/glances/.translations/nl.json
new file mode 100644
index 00000000000..7de81bfee98
--- /dev/null
+++ b/homeassistant/components/glances/.translations/nl.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Host is al geconfigureerd."
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken met host",
+ "wrong_version": "Versie niet ondersteund (alleen 2 of 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Naam",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "ssl": "Gebruik SSL / TLS om verbinding te maken met het Glances-systeem",
+ "username": "Gebruikersnaam",
+ "verify_ssl": "Controleer de certificering van het systeem",
+ "version": "Glances API-versie (2 of 3)"
+ },
+ "title": "Glances instellen"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Update frequentie"
+ },
+ "description": "Configureer opties voor Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/no.json b/homeassistant/components/glances/.translations/no.json
new file mode 100644
index 00000000000..7cf28cc34d0
--- /dev/null
+++ b/homeassistant/components/glances/.translations/no.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Verten er allerede konfigurert."
+ },
+ "error": {
+ "cannot_connect": "Kan ikke koble til vert",
+ "wrong_version": "Versjonen st\u00f8ttes ikke (bare 2 eller 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "name": "Navn",
+ "password": "Passord",
+ "port": "Port",
+ "ssl": "Bruk SSL / TLS for \u00e5 koble til Glances-systemet",
+ "username": "Brukernavn",
+ "verify_ssl": "Bekreft sertifiseringen av systemet",
+ "version": "Glances API-versjon (2 eller 3)"
+ },
+ "title": "Oppsett av Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Oppdater frekvens"
+ },
+ "description": "Konfigurasjonsalternativer for Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/pl.json b/homeassistant/components/glances/.translations/pl.json
new file mode 100644
index 00000000000..21052c7acdc
--- /dev/null
+++ b/homeassistant/components/glances/.translations/pl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Host jest ju\u017c skonfigurowany."
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem",
+ "wrong_version": "Wersja nieobs\u0142ugiwana (tylko 2 lub 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nazwa",
+ "password": "Has\u0142o",
+ "port": "Port",
+ "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z systemem Glances",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/ru.json b/homeassistant/components/glances/.translations/ru.json
new file mode 100644
index 00000000000..8effcc6ab16
--- /dev/null
+++ b/homeassistant/components/glances/.translations/ru.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.",
+ "wrong_version": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0435\u0440\u0441\u0438\u0438 2 \u0438 3."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
+ "username": "\u041b\u043e\u0433\u0438\u043d",
+ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u044b",
+ "version": "\u0412\u0435\u0440\u0441\u0438\u044f API Glances (2 \u0438\u043b\u0438 3)"
+ },
+ "title": "Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f"
+ },
+ "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/sl.json b/homeassistant/components/glances/.translations/sl.json
new file mode 100644
index 00000000000..b1d0fda94b5
--- /dev/null
+++ b/homeassistant/components/glances/.translations/sl.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Gostitelj je \u017ee konfiguriran."
+ },
+ "error": {
+ "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem",
+ "wrong_version": "Razli\u010dica ni podprta (samo 2 ali 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Ime",
+ "password": "Geslo",
+ "port": "Vrata",
+ "ssl": "Za povezavo s sistemom Glances uporabite SSL/TLS",
+ "username": "Uporabni\u0161ko ime",
+ "verify_ssl": "Preverite veljavnost potrdila sistema",
+ "version": "Glances API Version (2 ali 3)"
+ },
+ "title": "Nastavite Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Pogostost posodabljanja"
+ },
+ "description": "Konfiguracija mo\u017enosti za Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/th.json b/homeassistant/components/glances/.translations/th.json
new file mode 100644
index 00000000000..718c857c490
--- /dev/null
+++ b/homeassistant/components/glances/.translations/th.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/zh-Hant.json b/homeassistant/components/glances/.translations/zh-Hant.json
new file mode 100644
index 00000000000..12ba7670355
--- /dev/null
+++ b/homeassistant/components/glances/.translations/zh-Hant.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef",
+ "wrong_version": "\u7248\u672c\u4e0d\u652f\u63f4\uff08\u50c5 2 \u6216 3\uff09"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "name": "\u540d\u7a31",
+ "password": "\u5bc6\u78bc",
+ "port": "\u901a\u8a0a\u57e0",
+ "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 Glances \u7cfb\u7d71",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31",
+ "verify_ssl": "\u9a57\u8b49\u7cfb\u7d71\u8a8d\u8b49",
+ "version": "Glances API \u7248\u672c\uff082 \u6216 3\uff09"
+ },
+ "title": "\u8a2d\u5b9a Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u66f4\u65b0\u983b\u7387"
+ },
+ "description": "Glances \u8a2d\u5b9a\u9078\u9805"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py
index b458d8788fc..d09aa782534 100644
--- a/homeassistant/components/glances/__init__.py
+++ b/homeassistant/components/glances/__init__.py
@@ -1 +1,174 @@
-"""The glances component."""
+"""The Glances component."""
+from datetime import timedelta
+import logging
+
+from glances_api import Glances, exceptions
+import voluptuous as vol
+
+from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_NAME,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SCAN_INTERVAL,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.core import Config, HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.event import async_track_time_interval
+
+from .const import (
+ CONF_VERSION,
+ DATA_UPDATED,
+ DEFAULT_HOST,
+ DEFAULT_NAME,
+ DEFAULT_PORT,
+ DEFAULT_SCAN_INTERVAL,
+ DEFAULT_VERSION,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+GLANCES_SCHEMA = vol.All(
+ vol.Schema(
+ {
+ vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_SSL, default=False): cv.boolean,
+ vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
+ vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]),
+ }
+ )
+)
+
+CONFIG_SCHEMA = vol.Schema(
+ {DOMAIN: vol.All(cv.ensure_list, [GLANCES_SCHEMA])}, extra=vol.ALLOW_EXTRA
+)
+
+
+async def async_setup(hass: HomeAssistant, config: Config) -> bool:
+ """Configure Glances using config flow only."""
+ if DOMAIN in config:
+ for entry in config[DOMAIN]:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=entry
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up Glances from config entry."""
+ client = GlancesData(hass, config_entry)
+ hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client
+ if not await client.async_setup():
+ return False
+
+ return True
+
+
+async def async_unload_entry(hass, config_entry):
+ """Unload a config entry."""
+ await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+ return True
+
+
+class GlancesData:
+ """Get the latest data from Glances api."""
+
+ def __init__(self, hass, config_entry):
+ """Initialize the Glances data."""
+ self.hass = hass
+ self.config_entry = config_entry
+ self.api = None
+ self.unsub_timer = None
+ self.available = False
+
+ @property
+ def host(self):
+ """Return client host."""
+ return self.config_entry.data[CONF_HOST]
+
+ async def async_update(self):
+ """Get the latest data from the Glances REST API."""
+ try:
+ await self.api.get_data()
+ self.available = True
+ except exceptions.GlancesApiError:
+ _LOGGER.error("Unable to fetch data from Glances")
+ self.available = False
+ _LOGGER.debug("Glances data updated")
+ async_dispatcher_send(self.hass, DATA_UPDATED)
+
+ async def async_setup(self):
+ """Set up the Glances client."""
+ try:
+ self.api = get_api(self.hass, self.config_entry.data)
+ await self.api.get_data()
+ self.available = True
+ _LOGGER.debug("Successfully connected to Glances")
+
+ except exceptions.GlancesApiConnectionError:
+ _LOGGER.debug("Can not connect to Glances")
+ raise ConfigEntryNotReady
+
+ self.add_options()
+ self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL])
+ self.config_entry.add_update_listener(self.async_options_updated)
+
+ self.hass.async_create_task(
+ self.hass.config_entries.async_forward_entry_setup(
+ self.config_entry, "sensor"
+ )
+ )
+ return True
+
+ def add_options(self):
+ """Add options for Glances integration."""
+ if not self.config_entry.options:
+ options = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}
+ self.hass.config_entries.async_update_entry(
+ self.config_entry, options=options
+ )
+
+ def set_scan_interval(self, scan_interval):
+ """Update scan interval."""
+
+ async def refresh(event_time):
+ """Get the latest data from Glances api."""
+ await self.async_update()
+
+ if self.unsub_timer is not None:
+ self.unsub_timer()
+ self.unsub_timer = async_track_time_interval(
+ self.hass, refresh, timedelta(seconds=scan_interval)
+ )
+
+ @staticmethod
+ async def async_options_updated(hass, entry):
+ """Triggered by config entry options updates."""
+ hass.data[DOMAIN][entry.entry_id].set_scan_interval(
+ entry.options[CONF_SCAN_INTERVAL]
+ )
+
+
+def get_api(hass, entry):
+ """Return the api from glances_api."""
+ params = entry.copy()
+ params.pop(CONF_NAME)
+ verify_ssl = params.pop(CONF_VERIFY_SSL)
+ session = async_get_clientsession(hass, verify_ssl)
+ return Glances(hass.loop, session, **params)
diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py
new file mode 100644
index 00000000000..3c86fae0357
--- /dev/null
+++ b/homeassistant/components/glances/config_flow.py
@@ -0,0 +1,130 @@
+"""Config flow for Glances."""
+import glances_api
+import voluptuous as vol
+
+from homeassistant import config_entries, core, exceptions
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_NAME,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SCAN_INTERVAL,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.core import callback
+
+from . import get_api
+from .const import (
+ CONF_VERSION,
+ DEFAULT_HOST,
+ DEFAULT_NAME,
+ DEFAULT_PORT,
+ DEFAULT_SCAN_INTERVAL,
+ DEFAULT_VERSION,
+ DOMAIN,
+ SUPPORTED_VERSIONS,
+)
+
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
+ vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
+ vol.Optional(CONF_USERNAME): str,
+ vol.Optional(CONF_PASSWORD): str,
+ vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
+ vol.Required(CONF_VERSION, default=DEFAULT_VERSION): int,
+ vol.Optional(CONF_SSL, default=False): bool,
+ vol.Optional(CONF_VERIFY_SSL, default=False): bool,
+ }
+)
+
+
+async def validate_input(hass: core.HomeAssistant, data):
+ """Validate the user input allows us to connect."""
+ for entry in hass.config_entries.async_entries(DOMAIN):
+ if entry.data[CONF_HOST] == data[CONF_HOST]:
+ raise AlreadyConfigured
+
+ if data[CONF_VERSION] not in SUPPORTED_VERSIONS:
+ raise WrongVersion
+ try:
+ api = get_api(hass, data)
+ await api.get_data()
+ except glances_api.exceptions.GlancesApiConnectionError:
+ raise CannotConnect
+
+
+class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a Glances config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return GlancesOptionsFlowHandler(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ if user_input is not None:
+ try:
+ await validate_input(self.hass, user_input)
+ return self.async_create_entry(
+ title=user_input[CONF_NAME], data=user_input
+ )
+ except AlreadyConfigured:
+ return self.async_abort(reason="already_configured")
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except WrongVersion:
+ errors[CONF_VERSION] = "wrong_version"
+
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
+
+ async def async_step_import(self, import_config):
+ """Import from Glances sensor config."""
+
+ return await self.async_step_user(user_input=import_config)
+
+
+class GlancesOptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle Glances client options."""
+
+ def __init__(self, config_entry):
+ """Initialize Glances options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage the Glances options."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ options = {
+ vol.Optional(
+ CONF_SCAN_INTERVAL,
+ default=self.config_entry.options.get(
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
+ ),
+ ): int
+ }
+
+ return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+ """Error to indicate we cannot connect."""
+
+
+class AlreadyConfigured(exceptions.HomeAssistantError):
+ """Error to indicate host is already configured."""
+
+
+class WrongVersion(exceptions.HomeAssistantError):
+ """Error to indicate the selected version is wrong."""
diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py
new file mode 100644
index 00000000000..e47586ea245
--- /dev/null
+++ b/homeassistant/components/glances/const.py
@@ -0,0 +1,36 @@
+"""Constants for Glances component."""
+from homeassistant.const import TEMP_CELSIUS
+
+DOMAIN = "glances"
+CONF_VERSION = "version"
+
+DEFAULT_HOST = "localhost"
+DEFAULT_NAME = "Glances"
+DEFAULT_PORT = 61208
+DEFAULT_VERSION = 3
+DEFAULT_SCAN_INTERVAL = 60
+
+DATA_UPDATED = "glances_data_updated"
+SUPPORTED_VERSIONS = [2, 3]
+
+SENSOR_TYPES = {
+ "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"],
+ "disk_use": ["Disk used", "GiB", "mdi:harddisk"],
+ "disk_free": ["Disk free", "GiB", "mdi:harddisk"],
+ "memory_use_percent": ["RAM used percent", "%", "mdi:memory"],
+ "memory_use": ["RAM used", "MiB", "mdi:memory"],
+ "memory_free": ["RAM free", "MiB", "mdi:memory"],
+ "swap_use_percent": ["Swap used percent", "%", "mdi:memory"],
+ "swap_use": ["Swap used", "GiB", "mdi:memory"],
+ "swap_free": ["Swap free", "GiB", "mdi:memory"],
+ "processor_load": ["CPU load", "15 min", "mdi:memory"],
+ "process_running": ["Running", "Count", "mdi:memory"],
+ "process_total": ["Total", "Count", "mdi:memory"],
+ "process_thread": ["Thread", "Count", "mdi:memory"],
+ "process_sleeping": ["Sleeping", "Count", "mdi:memory"],
+ "cpu_use_percent": ["CPU used", "%", "mdi:memory"],
+ "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"],
+ "docker_active": ["Containers active", "", "mdi:docker"],
+ "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"],
+ "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"],
+}
diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json
index 775d208c1c4..6067b1a9868 100644
--- a/homeassistant/components/glances/manifest.json
+++ b/homeassistant/components/glances/manifest.json
@@ -1,12 +1,14 @@
{
"domain": "glances",
"name": "Glances",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/glances",
"requirements": [
"glances_api==0.2.0"
],
"dependencies": [],
"codeowners": [
- "@fabaff"
+ "@fabaff",
+ "@engrbm87"
]
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py
index 90b4b386f37..760958f0dee 100644
--- a/homeassistant/components/glances/sensor.py
+++ b/homeassistant/components/glances/sensor.py
@@ -1,114 +1,31 @@
"""Support gathering system information of hosts which are running glances."""
-from datetime import timedelta
import logging
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- CONF_HOST,
- CONF_NAME,
- CONF_PORT,
- CONF_USERNAME,
- CONF_PASSWORD,
- CONF_SSL,
- CONF_VERIFY_SSL,
- CONF_RESOURCES,
- STATE_UNAVAILABLE,
- TEMP_CELSIUS,
-)
-from homeassistant.exceptions import PlatformNotReady
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
+
+from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
-CONF_VERSION = "version"
-
-DEFAULT_HOST = "localhost"
-DEFAULT_NAME = "Glances"
-DEFAULT_PORT = "61208"
-DEFAULT_VERSION = 2
-
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
-
-SENSOR_TYPES = {
- "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"],
- "disk_use": ["Disk used", "GiB", "mdi:harddisk"],
- "disk_free": ["Disk free", "GiB", "mdi:harddisk"],
- "memory_use_percent": ["RAM used percent", "%", "mdi:memory"],
- "memory_use": ["RAM used", "MiB", "mdi:memory"],
- "memory_free": ["RAM free", "MiB", "mdi:memory"],
- "swap_use_percent": ["Swap used percent", "%", "mdi:memory"],
- "swap_use": ["Swap used", "GiB", "mdi:memory"],
- "swap_free": ["Swap free", "GiB", "mdi:memory"],
- "processor_load": ["CPU load", "15 min", "mdi:memory"],
- "process_running": ["Running", "Count", "mdi:memory"],
- "process_total": ["Total", "Count", "mdi:memory"],
- "process_thread": ["Thread", "Count", "mdi:memory"],
- "process_sleeping": ["Sleeping", "Count", "mdi:memory"],
- "cpu_use_percent": ["CPU used", "%", "mdi:memory"],
- "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"],
- "docker_active": ["Containers active", "", "mdi:docker"],
- "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"],
- "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"],
-}
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_USERNAME): cv.string,
- vol.Optional(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_SSL, default=False): cv.boolean,
- vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
- vol.Optional(CONF_RESOURCES, default=["disk_use"]): vol.All(
- cv.ensure_list, [vol.In(SENSOR_TYPES)]
- ),
- vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]),
- }
-)
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the Glances sensors is done through async_setup_entry."""
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Glances sensors."""
- from glances_api import Glances
-
- name = config[CONF_NAME]
- host = config[CONF_HOST]
- port = config[CONF_PORT]
- version = config[CONF_VERSION]
- var_conf = config[CONF_RESOURCES]
- username = config.get(CONF_USERNAME)
- password = config.get(CONF_PASSWORD)
- ssl = config[CONF_SSL]
- verify_ssl = config[CONF_VERIFY_SSL]
-
- session = async_get_clientsession(hass, verify_ssl)
- glances = GlancesData(
- Glances(
- hass.loop,
- session,
- host=host,
- port=port,
- version=version,
- username=username,
- password=password,
- ssl=ssl,
- )
- )
-
- await glances.async_update()
-
- if glances.api.data is None:
- raise PlatformNotReady
+ glances_data = hass.data[DOMAIN][config_entry.entry_id]
+ name = config_entry.data[CONF_NAME]
dev = []
- for resource in var_conf:
- dev.append(GlancesSensor(glances, name, resource))
+ for sensor_type in SENSOR_TYPES:
+ dev.append(
+ GlancesSensor(glances_data, name, SENSOR_TYPES[sensor_type][0], sensor_type)
+ )
async_add_entities(dev, True)
@@ -116,9 +33,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class GlancesSensor(Entity):
"""Implementation of a Glances sensor."""
- def __init__(self, glances, name, sensor_type):
+ def __init__(self, glances_data, name, sensor_name, sensor_type):
"""Initialize the sensor."""
- self.glances = glances
+ self.glances_data = glances_data
+ self._sensor_name = sensor_name
self._name = name
self.type = sensor_type
self._state = None
@@ -127,7 +45,12 @@ class GlancesSensor(Entity):
@property
def name(self):
"""Return the name of the sensor."""
- return "{} {}".format(self._name, SENSOR_TYPES[self.type][0])
+ return f"{self._name} {self._sensor_name}"
+
+ @property
+ def unique_id(self):
+ """Set unique_id for sensor."""
+ return f"{self.glances_data.host}-{self.name}"
@property
def icon(self):
@@ -142,17 +65,31 @@ class GlancesSensor(Entity):
@property
def available(self):
"""Could the device be accessed during the last update call."""
- return self.glances.available
+ return self.glances_data.available
@property
def state(self):
"""Return the state of the resources."""
return self._state
+ @property
+ def should_poll(self):
+ """Return the polling requirement for this sensor."""
+ return False
+
+ async def async_added_to_hass(self):
+ """Handle entity which will be added."""
+ async_dispatcher_connect(
+ self.hass, DATA_UPDATED, self._schedule_immediate_update
+ )
+
+ @callback
+ def _schedule_immediate_update(self):
+ self.async_schedule_update_ha_state(True)
+
async def async_update(self):
"""Get the latest data from REST API."""
- await self.glances.async_update()
- value = self.glances.api.data
+ value = self.glances_data.api.data
if value is not None:
if self.type == "disk_use_percent":
@@ -249,24 +186,3 @@ class GlancesSensor(Entity):
self._state = round(mem_use / 1024 ** 2, 1)
except KeyError:
self._state = STATE_UNAVAILABLE
-
-
-class GlancesData:
- """The class for handling the data retrieval."""
-
- def __init__(self, api):
- """Initialize the data object."""
- self.api = api
- self.available = True
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- async def async_update(self):
- """Get the latest data from the Glances REST API."""
- from glances_api.exceptions import GlancesApiError
-
- try:
- await self.api.get_data()
- self.available = True
- except GlancesApiError:
- _LOGGER.error("Unable to fetch data from Glances")
- self.available = False
diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json
new file mode 100644
index 00000000000..1bd7275daef
--- /dev/null
+++ b/homeassistant/components/glances/strings.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "title": "Glances",
+ "step": {
+ "user": {
+ "title": "Setup Glances",
+ "data": {
+ "name": "Name",
+ "host": "Host",
+ "username": "Username",
+ "password": "Password",
+ "port": "Port",
+ "version": "Glances API Version (2 or 3)",
+ "ssl": "Use SSL/TLS to connect to the Glances system",
+ "verify_ssl": "Verify the certification of the system"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "Unable to connect to host",
+ "wrong_version": "Version not supported (2 or 3 only)"
+ },
+ "abort": {
+ "already_configured": "Host is already configured."
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "description": "Configure options for Glances",
+ "data": {
+ "scan_interval": "Update frequency"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gntp/notify.py b/homeassistant/components/gntp/notify.py
index 48c02cf0ba8..5c05b097a1f 100644
--- a/homeassistant/components/gntp/notify.py
+++ b/homeassistant/components/gntp/notify.py
@@ -2,17 +2,18 @@
import logging
import os
+import gntp.errors
+import gntp.notifier
import voluptuous as vol
-from homeassistant.const import CONF_PASSWORD, CONF_PORT
-import homeassistant.helpers.config_validation as cv
-
from homeassistant.components.notify import (
ATTR_TITLE,
ATTR_TITLE_DEFAULT,
PLATFORM_SCHEMA,
BaseNotificationService,
)
+from homeassistant.const import CONF_PASSWORD, CONF_PORT
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -69,9 +70,6 @@ class GNTPNotificationService(BaseNotificationService):
def __init__(self, app_name, app_icon, hostname, password, port):
"""Initialize the service."""
- import gntp.notifier
- import gntp.errors
-
self.gntp = gntp.notifier.GrowlNotifier(
applicationName=app_name,
notifications=["Notification"],
diff --git a/homeassistant/components/goalfeed/__init__.py b/homeassistant/components/goalfeed/__init__.py
index 3a14eb2831d..cdca99e0309 100644
--- a/homeassistant/components/goalfeed/__init__.py
+++ b/homeassistant/components/goalfeed/__init__.py
@@ -1,11 +1,12 @@
"""Component for the Goalfeed service."""
import json
+import pysher
import requests
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+import homeassistant.helpers.config_validation as cv
# Version downgraded due to regression in library
# For details: https://github.com/nlsdfnbch/Pysher/issues/38
@@ -30,8 +31,6 @@ GOALFEED_APP_ID = "bfd4ed98c1ff22c04074"
def setup(hass, config):
"""Set up the Goalfeed component."""
- import pysher
-
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py
index 62aa2212bb1..9cb9be0fa4f 100644
--- a/homeassistant/components/google/__init__.py
+++ b/homeassistant/components/google/__init__.py
@@ -4,6 +4,15 @@ import logging
import os
import yaml
+import httplib2
+from oauth2client.client import (
+ OAuth2WebServerFlow,
+ OAuth2DeviceCodeError,
+ FlowExchangeError,
+)
+from oauth2client.file import Storage
+from googleapiclient import discovery as google_discovery
+
import voluptuous as vol
from voluptuous.error import Error as VoluptuousError
@@ -126,13 +135,6 @@ def do_authentication(hass, hass_config, config):
Notify user of user_code and verification_url then poll
until we have an access token.
"""
- from oauth2client.client import (
- OAuth2WebServerFlow,
- OAuth2DeviceCodeError,
- FlowExchangeError,
- )
- from oauth2client.file import Storage
-
oauth = OAuth2WebServerFlow(
client_id=config[CONF_CLIENT_ID],
client_secret=config[CONF_CLIENT_SECRET],
@@ -341,10 +343,6 @@ class GoogleCalendarService:
def get(self):
"""Get the calendar service from the storage file token."""
- import httplib2
- from oauth2client.file import Storage
- from googleapiclient import discovery as google_discovery
-
credentials = Storage(self.token_file).get()
http = credentials.authorize(httplib2.Http())
service = google_discovery.build(
diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py
index 31e9f186a4e..8a6eb644621 100644
--- a/homeassistant/components/google/calendar.py
+++ b/homeassistant/components/google/calendar.py
@@ -3,6 +3,8 @@ import copy
from datetime import timedelta
import logging
+from httplib2 import ServerNotFoundError # pylint: disable=import-error
+
from homeassistant.components.calendar import (
ENTITY_ID_FORMAT,
CalendarEventDevice,
@@ -126,9 +128,6 @@ class GoogleCalendarData:
self.event = None
def _prepare_query(self):
- # pylint: disable=import-error
- from httplib2 import ServerNotFoundError
-
try:
service = self.calendar_service.get()
except ServerNotFoundError:
diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py
index a1252d67fff..ebf906b6f2a 100644
--- a/homeassistant/components/google_assistant/__init__.py
+++ b/homeassistant/components/google_assistant/__init__.py
@@ -28,9 +28,13 @@ from .const import (
CONF_ENTITY_CONFIG,
CONF_EXPOSE,
CONF_ALIASES,
+ CONF_REPORT_STATE,
CONF_ROOM_HINT,
CONF_ALLOW_UNLOCK,
CONF_SECURE_DEVICES_PIN,
+ CONF_SERVICE_ACCOUNT,
+ CONF_CLIENT_EMAIL,
+ CONF_PRIVATE_KEY,
)
from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401
from .const import EVENT_QUERY_RECEIVED # noqa: F401
@@ -47,6 +51,24 @@ ENTITY_SCHEMA = vol.Schema(
}
)
+GOOGLE_SERVICE_ACCOUNT = vol.Schema(
+ {
+ vol.Required(CONF_PRIVATE_KEY): cv.string,
+ vol.Required(CONF_CLIENT_EMAIL): cv.string,
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+def _check_report_state(data):
+ if data[CONF_REPORT_STATE]:
+ if CONF_SERVICE_ACCOUNT not in data:
+ raise vol.Invalid(
+ "If report state is enabled, a service account must exist"
+ )
+ return data
+
+
GOOGLE_ASSISTANT_SCHEMA = vol.All(
cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"),
vol.Schema(
@@ -63,9 +85,12 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All(
vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean,
# str on purpose, makes sure it is configured correctly.
vol.Optional(CONF_SECURE_DEVICES_PIN): str,
+ vol.Optional(CONF_REPORT_STATE, default=False): cv.boolean,
+ vol.Optional(CONF_SERVICE_ACCOUNT): GOOGLE_SERVICE_ACCOUNT,
},
extra=vol.PREVENT_EXTRA,
),
+ _check_report_state,
)
CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA)
diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py
index 54abd54caaf..03253e244fe 100644
--- a/homeassistant/components/google_assistant/const.py
+++ b/homeassistant/components/google_assistant/const.py
@@ -32,6 +32,10 @@ CONF_API_KEY = "api_key"
CONF_ROOM_HINT = "room"
CONF_ALLOW_UNLOCK = "allow_unlock"
CONF_SECURE_DEVICES_PIN = "secure_devices_pin"
+CONF_REPORT_STATE = "report_state"
+CONF_SERVICE_ACCOUNT = "service_account"
+CONF_CLIENT_EMAIL = "client_email"
+CONF_PRIVATE_KEY = "private_key"
DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [
@@ -72,7 +76,10 @@ TYPE_ALARM = PREFIX_TYPES + "SECURITYSYSTEM"
SERVICE_REQUEST_SYNC = "request_sync"
HOMEGRAPH_URL = "https://homegraph.googleapis.com/"
+HOMEGRAPH_SCOPE = "https://www.googleapis.com/auth/homegraph"
+HOMEGRAPH_TOKEN_URL = "https://accounts.google.com/o/oauth2/token"
REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + "v1/devices:requestSync"
+REPORT_STATE_BASE_URL = HOMEGRAPH_URL + "v1/devices:reportStateAndNotification"
# Error codes used for SmartHomeError class
# https://developers.google.com/actions/reference/smarthome/errors-exceptions
diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py
index 933f0c07999..96b9b93d70a 100644
--- a/homeassistant/components/google_assistant/helpers.py
+++ b/homeassistant/components/google_assistant/helpers.py
@@ -1,10 +1,15 @@
"""Helper classes for Google Assistant integration."""
from asyncio import gather
from collections.abc import Mapping
-from typing import List
+import logging
+import pprint
+from typing import List, Optional
+
+from aiohttp.web import json_response
from homeassistant.core import Context, callback, HomeAssistant, State
from homeassistant.helpers.event import async_call_later
+from homeassistant.components import webhook
from homeassistant.const import (
CONF_NAME,
STATE_UNAVAILABLE,
@@ -15,6 +20,7 @@ from homeassistant.const import (
from . import trait
from .const import (
+ DOMAIN,
DOMAIN_TO_GOOGLE_TYPES,
CONF_ALIASES,
ERR_FUNCTION_NOT_SUPPORTED,
@@ -24,6 +30,7 @@ from .const import (
from .error import SmartHomeError
SYNC_DELAY = 15
+_LOGGER = logging.getLogger(__name__)
class AbstractConfig:
@@ -35,6 +42,7 @@ class AbstractConfig:
"""Initialize abstract config."""
self.hass = hass
self._google_sync_unsub = None
+ self._local_sdk_active = False
@property
def enabled(self):
@@ -61,12 +69,30 @@ class AbstractConfig:
"""Return if we're actively reporting states."""
return self._unsub_report_state is not None
+ @property
+ def is_local_sdk_active(self):
+ """Return if we're actively accepting local messages."""
+ return self._local_sdk_active
+
@property
def should_report_state(self):
"""Return if states should be proactively reported."""
# pylint: disable=no-self-use
return False
+ @property
+ def local_sdk_webhook_id(self):
+ """Return the local SDK webhook ID.
+
+ Return None to disable the local SDK.
+ """
+ return None
+
+ @property
+ def local_sdk_user_id(self):
+ """Return the user ID to be used for actions received via the local SDK."""
+ raise NotImplementedError
+
def should_expose(self, state) -> bool:
"""Return if entity should be exposed."""
raise NotImplementedError
@@ -131,15 +157,66 @@ class AbstractConfig:
Called when the user disconnects their account from Google.
"""
+ @callback
+ def async_enable_local_sdk(self):
+ """Enable the local SDK."""
+ webhook_id = self.local_sdk_webhook_id
+
+ if webhook_id is None:
+ return
+
+ webhook.async_register(
+ self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook
+ )
+
+ self._local_sdk_active = True
+
+ @callback
+ def async_disable_local_sdk(self):
+ """Disable the local SDK."""
+ if not self._local_sdk_active:
+ return
+
+ webhook.async_unregister(self.hass, self.local_sdk_webhook_id)
+ self._local_sdk_active = False
+
+ async def _handle_local_webhook(self, hass, webhook_id, request):
+ """Handle an incoming local SDK message."""
+ from . import smart_home
+
+ payload = await request.json()
+
+ if _LOGGER.isEnabledFor(logging.DEBUG):
+ _LOGGER.debug("Received local message:\n%s\n", pprint.pformat(payload))
+
+ if not self.enabled:
+ return json_response(smart_home.turned_off_response(payload))
+
+ result = await smart_home.async_handle_message(
+ self.hass, self, self.local_sdk_user_id, payload
+ )
+
+ if _LOGGER.isEnabledFor(logging.DEBUG):
+ _LOGGER.debug("Responding to local message:\n%s\n", pprint.pformat(result))
+
+ return json_response(result)
+
class RequestData:
"""Hold data associated with a particular request."""
- def __init__(self, config: AbstractConfig, user_id: str, request_id: str):
+ def __init__(
+ self,
+ config: AbstractConfig,
+ user_id: str,
+ request_id: str,
+ devices: Optional[List[dict]],
+ ):
"""Initialize the request data."""
self.config = config
self.request_id = request_id
self.context = Context(user_id=user_id)
+ self.devices = devices
def get_google_type(domain, device_class):
@@ -234,6 +311,15 @@ class GoogleEntity:
if aliases:
device["name"]["nicknames"] = aliases
+ if self.config.is_local_sdk_active:
+ device["otherDeviceIds"] = [{"deviceId": self.entity_id}]
+ device["customData"] = {
+ "webhookId": self.config.local_sdk_webhook_id,
+ "httpPort": self.hass.config.api.port,
+ "httpSSL": self.hass.config.api.use_ssl,
+ "proxyDeviceId": self.config.agent_user_id,
+ }
+
for trt in traits:
device["attributes"].update(trt.sync_attributes())
@@ -280,6 +366,11 @@ class GoogleEntity:
return attrs
+ @callback
+ def reachable_device_serialize(self):
+ """Serialize entity for a REACHABLE_DEVICE response."""
+ return {"verificationId": self.entity_id}
+
async def execute(self, data, command_payload):
"""Execute a command.
diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py
index aea226348b8..90fa1ced157 100644
--- a/homeassistant/components/google_assistant/http.py
+++ b/homeassistant/components/google_assistant/http.py
@@ -1,20 +1,38 @@
"""Support for Google Actions Smart Home Control."""
+import asyncio
+from datetime import timedelta
import logging
+from uuid import uuid4
+import jwt
+from aiohttp import ClientResponseError, ClientError
from aiohttp.web import Request, Response
# Typing imports
from homeassistant.components.http import HomeAssistantView
-from homeassistant.core import callback
+from homeassistant.core import callback, ServiceCall
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.util import dt as dt_util
from .const import (
GOOGLE_ASSISTANT_API_ENDPOINT,
+ CONF_API_KEY,
CONF_EXPOSE_BY_DEFAULT,
CONF_EXPOSED_DOMAINS,
CONF_ENTITY_CONFIG,
CONF_EXPOSE,
+ CONF_REPORT_STATE,
CONF_SECURE_DEVICES_PIN,
+ CONF_SERVICE_ACCOUNT,
+ CONF_CLIENT_EMAIL,
+ CONF_PRIVATE_KEY,
+ DOMAIN,
+ HOMEGRAPH_TOKEN_URL,
+ HOMEGRAPH_SCOPE,
+ REPORT_STATE_BASE_URL,
+ REQUEST_SYNC_BASE_URL,
+ SERVICE_REQUEST_SYNC,
)
from .smart_home import async_handle_message
from .helpers import AbstractConfig
@@ -22,6 +40,35 @@ from .helpers import AbstractConfig
_LOGGER = logging.getLogger(__name__)
+def _get_homegraph_jwt(time, iss, key):
+ now = int(time.timestamp())
+
+ jwt_raw = {
+ "iss": iss,
+ "scope": HOMEGRAPH_SCOPE,
+ "aud": HOMEGRAPH_TOKEN_URL,
+ "iat": now,
+ "exp": now + 3600,
+ }
+ return jwt.encode(jwt_raw, key, algorithm="RS256").decode("utf-8")
+
+
+async def _get_homegraph_token(hass, jwt_signed):
+ headers = {
+ "Authorization": "Bearer {}".format(jwt_signed),
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+ data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
+ "assertion": jwt_signed,
+ }
+
+ session = async_get_clientsession(hass)
+ async with session.post(HOMEGRAPH_TOKEN_URL, headers=headers, data=data) as res:
+ res.raise_for_status()
+ return await res.json()
+
+
class GoogleConfig(AbstractConfig):
"""Config for manual setup of Google."""
@@ -29,6 +76,8 @@ class GoogleConfig(AbstractConfig):
"""Initialize the config."""
super().__init__(hass)
self._config = config
+ self._access_token = None
+ self._access_token_renew = None
@property
def enabled(self):
@@ -50,6 +99,12 @@ class GoogleConfig(AbstractConfig):
"""Return entity config."""
return self._config.get(CONF_SECURE_DEVICES_PIN)
+ @property
+ def should_report_state(self):
+ """Return if states should be proactively reported."""
+ # pylint: disable=no-self-use
+ return self._config.get(CONF_REPORT_STATE)
+
def should_expose(self, state) -> bool:
"""Return if entity should be exposed."""
expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT)
@@ -79,11 +134,93 @@ class GoogleConfig(AbstractConfig):
"""If an entity should have 2FA checked."""
return True
+ async def _async_update_token(self, force=False):
+ if CONF_SERVICE_ACCOUNT not in self._config:
+ _LOGGER.error("Trying to get homegraph api token without service account")
+ return
+
+ now = dt_util.utcnow()
+ if not self._access_token or now > self._access_token_renew or force:
+ token = await _get_homegraph_token(
+ self.hass,
+ _get_homegraph_jwt(
+ now,
+ self._config[CONF_SERVICE_ACCOUNT][CONF_CLIENT_EMAIL],
+ self._config[CONF_SERVICE_ACCOUNT][CONF_PRIVATE_KEY],
+ ),
+ )
+ self._access_token = token["access_token"]
+ self._access_token_renew = now + timedelta(seconds=token["expires_in"])
+
+ async def async_call_homegraph_api(self, url, data):
+ """Call a homegraph api with authenticaiton."""
+ session = async_get_clientsession(self.hass)
+
+ async def _call():
+ headers = {
+ "Authorization": "Bearer {}".format(self._access_token),
+ "X-GFE-SSL": "yes",
+ }
+ async with session.post(url, headers=headers, json=data) as res:
+ _LOGGER.debug(
+ "Response on %s with data %s was %s", url, data, await res.text()
+ )
+ res.raise_for_status()
+
+ try:
+ await self._async_update_token()
+ try:
+ await _call()
+ except ClientResponseError as error:
+ if error.status == 401:
+ _LOGGER.warning(
+ "Request for %s unauthorized, renewing token and retrying", url
+ )
+ await self._async_update_token(True)
+ await _call()
+ else:
+ raise
+ except ClientResponseError as error:
+ _LOGGER.error("Request for %s failed: %d", url, error.status)
+ except (asyncio.TimeoutError, ClientError):
+ _LOGGER.error("Could not contact %s", url)
+
+ async def async_report_state(self, message):
+ """Send a state report to Google."""
+ data = {
+ "requestId": uuid4().hex,
+ "agentUserId": (await self.hass.auth.async_get_owner()).id,
+ "payload": message,
+ }
+ await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data)
+
@callback
def async_register_http(hass, cfg):
"""Register HTTP views for Google Assistant."""
- hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg)))
+ config = GoogleConfig(hass, cfg)
+ hass.http.register_view(GoogleAssistantView(config))
+ if config.should_report_state:
+ config.async_enable_report_state()
+
+ async def request_sync_service_handler(call: ServiceCall):
+ """Handle request sync service calls."""
+ agent_user_id = call.data.get("agent_user_id") or call.context.user_id
+
+ if agent_user_id is None:
+ _LOGGER.warning(
+ "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id."
+ )
+ return
+ await config.async_call_homegraph_api(
+ REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id}
+ )
+
+ # Register service only if api key is provided
+ if CONF_API_KEY not in cfg and CONF_SERVICE_ACCOUNT in cfg:
+ hass.services.async_register(
+ DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler
+ )
class GoogleAssistantView(HomeAssistantView):
diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py
index b842a552714..aacb90e9d2b 100644
--- a/homeassistant/components/google_assistant/report_state.py
+++ b/homeassistant/components/google_assistant/report_state.py
@@ -49,23 +49,23 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig
{"devices": {"states": {changed_entity: entity_data}}}
)
- async_call_later(
- hass, INITIAL_REPORT_DELAY, _async_report_all_states(hass, google_config)
- )
+ async def inital_report(_now):
+ """Report initially all states."""
+ entities = {}
+
+ for entity in async_get_entities(hass, google_config):
+ if not entity.should_expose():
+ continue
+
+ try:
+ entities[entity.entity_id] = entity.query_serialize()
+ except SmartHomeError:
+ continue
+
+ await google_config.async_report_state({"devices": {"states": entities}})
+
+ async_call_later(hass, INITIAL_REPORT_DELAY, inital_report)
return hass.helpers.event.async_track_state_change(
MATCH_ALL, async_entity_state_listener
)
-
-
-async def _async_report_all_states(hass: HomeAssistant, google_config: AbstractConfig):
- """Report all states."""
- entities = {}
-
- for entity in async_get_entities(hass, google_config):
- if not entity.should_expose():
- continue
-
- entities[entity.entity_id] = entity.query_serialize()
-
- await google_config.async_report_state({"devices": {"states": entities}})
diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py
index f9b311a3880..0944c9532ef 100644
--- a/homeassistant/components/google_assistant/smart_home.py
+++ b/homeassistant/components/google_assistant/smart_home.py
@@ -5,7 +5,7 @@ import logging
from homeassistant.util.decorator import Registry
-from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.const import ATTR_ENTITY_ID, __version__
from .const import (
ERR_PROTOCOL_ERROR,
@@ -24,9 +24,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_handle_message(hass, config, user_id, message):
"""Handle incoming API messages."""
- request_id: str = message.get("requestId")
-
- data = RequestData(config, user_id, request_id)
+ data = RequestData(config, user_id, message["requestId"], message.get("devices"))
response = await _process(hass, data, message)
@@ -67,6 +65,7 @@ async def _process(hass, data, message):
if result is None:
return None
+
return {"requestId": data.request_id, "payload": result}
@@ -74,7 +73,7 @@ async def _process(hass, data, message):
async def async_devices_sync(hass, data, payload):
"""Handle action.devices.SYNC request.
- https://developers.google.com/actions/smarthome/create-app#actiondevicessync
+ https://developers.google.com/assistant/smarthome/develop/process-intents#SYNC
"""
hass.bus.async_fire(
EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context
@@ -84,7 +83,7 @@ async def async_devices_sync(hass, data, payload):
*(
entity.sync_serialize()
for entity in async_get_entities(hass, data.config)
- if data.config.should_expose(entity.state)
+ if entity.should_expose()
)
)
@@ -100,7 +99,7 @@ async def async_devices_sync(hass, data, payload):
async def async_devices_query(hass, data, payload):
"""Handle action.devices.QUERY request.
- https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
+ https://developers.google.com/assistant/smarthome/develop/process-intents#QUERY
"""
devices = {}
for device in payload.get("devices", []):
@@ -128,7 +127,7 @@ async def async_devices_query(hass, data, payload):
async def handle_devices_execute(hass, data, payload):
"""Handle action.devices.EXECUTE request.
- https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
+ https://developers.google.com/assistant/smarthome/develop/process-intents#EXECUTE
"""
entities = {}
results = {}
@@ -196,12 +195,50 @@ async def handle_devices_execute(hass, data, payload):
async def async_devices_disconnect(hass, data: RequestData, payload):
"""Handle action.devices.DISCONNECT request.
- https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
+ https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT
"""
await data.config.async_deactivate_report_state()
return None
+@HANDLERS.register("action.devices.IDENTIFY")
+async def async_devices_identify(hass, data: RequestData, payload):
+ """Handle action.devices.IDENTIFY request.
+
+ https://developers.google.com/assistant/smarthome/develop/local#implement_the_identify_handler
+ """
+ return {
+ "device": {
+ "id": data.config.agent_user_id,
+ "isLocalOnly": True,
+ "isProxy": True,
+ "deviceInfo": {
+ "hwVersion": "UNKNOWN_HW_VERSION",
+ "manufacturer": "Home Assistant",
+ "model": "Home Assistant",
+ "swVersion": __version__,
+ },
+ }
+ }
+
+
+@HANDLERS.register("action.devices.REACHABLE_DEVICES")
+async def async_devices_reachable(hass, data: RequestData, payload):
+ """Handle action.devices.REACHABLE_DEVICES request.
+
+ https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
+ """
+ google_ids = set(dev["id"] for dev in (data.devices or []))
+
+ return {
+ "devices": [
+ entity.reachable_device_serialize()
+ for entity in async_get_entities(hass, data.config)
+ if entity.entity_id in google_ids and entity.should_expose()
+ ]
+ }
+
+
def turned_off_response(message):
"""Return a device turned off response."""
return {
diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py
index 32947867958..3ee72928fc1 100644
--- a/homeassistant/components/google_travel_time/sensor.py
+++ b/homeassistant/components/google_travel_time/sensor.py
@@ -1,25 +1,25 @@
"""Support for Google travel time sensors."""
+from datetime import datetime, timedelta
import logging
-from datetime import datetime
-from datetime import timedelta
+import googlemaps
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-import homeassistant.util.dt as dt_util
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_API_KEY,
- CONF_NAME,
- EVENT_HOMEASSISTANT_START,
+ ATTR_ATTRIBUTION,
ATTR_LATITUDE,
ATTR_LONGITUDE,
- ATTR_ATTRIBUTION,
+ CONF_API_KEY,
CONF_MODE,
+ CONF_NAME,
+ EVENT_HOMEASSISTANT_START,
)
from homeassistant.helpers import location
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
+import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -203,8 +203,6 @@ class GoogleTravelTimeSensor(Entity):
else:
self._destination = destination
- import googlemaps
-
self._client = googlemaps.Client(api_key, timeout=10)
try:
self.update()
diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py
index 197e424ce86..8696dde72cb 100644
--- a/homeassistant/components/gpsd/sensor.py
+++ b/homeassistant/components/gpsd/sensor.py
@@ -1,6 +1,8 @@
"""Support for GPSD."""
import logging
+import socket
+from gps3.agps3threaded import AGPS3mechanism
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -9,11 +11,11 @@ from homeassistant.const import (
ATTR_LONGITUDE,
ATTR_MODE,
CONF_HOST,
- CONF_PORT,
CONF_NAME,
+ CONF_PORT,
)
-from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -50,7 +52,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
# except GPSError:
# _LOGGER.warning('Not able to connect to GPSD')
# return False
- import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
@@ -69,8 +70,6 @@ class GpsdSensor(Entity):
def __init__(self, hass, name, host, port):
"""Initialize the GPSD sensor."""
- from gps3.agps3threaded import AGPS3mechanism
-
self.hass = hass
self._name = name
self._host = host
diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py
index 5a6ce2c51c2..8b85de598b0 100644
--- a/homeassistant/components/greenwave/light.py
+++ b/homeassistant/components/greenwave/light.py
@@ -1,7 +1,9 @@
"""Support for Greenwave Reality (TCP Connected) lights."""
-import logging
from datetime import timedelta
+import logging
+import os
+import greenwavereality as greenwave
import voluptuous as vol
from homeassistant.components.light import (
@@ -29,9 +31,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Greenwave Reality Platform."""
- import greenwavereality as greenwave
- import os
-
host = config.get(CONF_HOST)
tokenfile = hass.config.path(".greenwave")
if config.get(CONF_VERSION) == 3:
@@ -60,8 +59,6 @@ class GreenwaveLight(Light):
def __init__(self, light, host, token, gatewaydata):
"""Initialize a Greenwave Reality Light."""
- import greenwavereality as greenwave
-
self._did = int(light["did"])
self._name = light["name"]
self._state = int(light["state"])
@@ -98,22 +95,16 @@ class GreenwaveLight(Light):
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
- import greenwavereality as greenwave
-
temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100)
greenwave.set_brightness(self._host, self._did, temp_brightness, self._token)
greenwave.turn_on(self._host, self._did, self._token)
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
- import greenwavereality as greenwave
-
greenwave.turn_off(self._host, self._did, self._token)
def update(self):
"""Fetch new state data for this light."""
- import greenwavereality as greenwave
-
self._gatewaydata.update()
bulbs = self._gatewaydata.greenwave
@@ -128,8 +119,6 @@ class GatewayData:
def __init__(self, host, token):
"""Initialize the data object."""
- import greenwavereality as greenwave
-
self._host = host
self._token = token
self._greenwave = greenwave.grab_bulbs(host, token)
@@ -142,7 +131,5 @@ class GatewayData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from the gateway."""
- import greenwavereality as greenwave
-
self._greenwave = greenwave.grab_bulbs(self._host, self._token)
return self._greenwave
diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py
index 39574a2b03b..29126c82d44 100644
--- a/homeassistant/components/group/__init__.py
+++ b/homeassistant/components/group/__init__.py
@@ -36,7 +36,7 @@ from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA
from homeassistant.helpers.typing import HomeAssistantType
-# mypy: allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
DOMAIN = "group"
diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py
index c5200082f2f..f7a9643e5c8 100644
--- a/homeassistant/components/group/cover.py
+++ b/homeassistant/components/group/cover.py
@@ -44,6 +44,7 @@ from homeassistant.components.cover import (
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
+# mypy: no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
@@ -74,7 +75,7 @@ class CoverGroup(CoverDevice):
"""Initialize a CoverGroup entity."""
self._name = name
self._is_closed = False
- self._cover_position = 100
+ self._cover_position: Optional[int] = 100
self._tilt_position = None
self._supported_features = 0
self._assumed_state = True
@@ -178,7 +179,7 @@ class CoverGroup(CoverDevice):
return self._is_closed
@property
- def current_cover_position(self):
+ def current_cover_position(self) -> Optional[int]:
"""Return current position for all covers."""
return self._cover_position
diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py
index e77c858fc02..85804552494 100644
--- a/homeassistant/components/group/light.py
+++ b/homeassistant/components/group/light.py
@@ -45,6 +45,7 @@ from homeassistant.components.light import (
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
+# mypy: no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py
index 2ffb7fea049..e17990690fa 100644
--- a/homeassistant/components/group/notify.py
+++ b/homeassistant/components/group/notify.py
@@ -18,7 +18,7 @@ from homeassistant.components.notify import (
)
-# mypy: allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py
index 086545f0c76..07b450dd33e 100644
--- a/homeassistant/components/gtfs/sensor.py
+++ b/homeassistant/components/gtfs/sensor.py
@@ -5,6 +5,8 @@ import os
import threading
from typing import Any, Callable, Optional
+import pygtfs
+from sqlalchemy.sql import text
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -129,8 +131,6 @@ def get_next_departure(
tomorrow = now + datetime.timedelta(days=1)
tomorrow_date = tomorrow.strftime(dt_util.DATE_STR_FORMAT)
- from sqlalchemy.sql import text
-
# Fetch all departures for yesterday, today and optionally tomorrow,
# up to an overkill maximum in case of a departure every minute for those
# days.
@@ -353,8 +353,6 @@ def setup_platform(
_LOGGER.error("The given GTFS data file/folder was not found")
return
- import pygtfs
-
(gtfs_root, _) = os.path.splitext(data)
sqlite_file = f"{gtfs_root}.sqlite?check_same_thread=False"
@@ -375,7 +373,7 @@ class GTFSDepartureSensor(Entity):
def __init__(
self,
- pygtfs: Any,
+ gtfs: Any,
name: Optional[Any],
origin: Any,
destination: Any,
@@ -383,7 +381,7 @@ class GTFSDepartureSensor(Entity):
include_tomorrow: bool,
) -> None:
"""Initialize the sensor."""
- self._pygtfs = pygtfs
+ self._pygtfs = gtfs
self.origin = origin
self.destination = destination
self._include_tomorrow = include_tomorrow
diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json
index 0b6dbfcbe44..13142fee513 100644
--- a/homeassistant/components/hangouts/.translations/fr.json
+++ b/homeassistant/components/hangouts/.translations/fr.json
@@ -14,6 +14,7 @@
"data": {
"2fa": "Code PIN d'authentification \u00e0 2 facteurs"
},
+ "description": "Vide",
"title": "Authentification \u00e0 2 facteurs"
},
"user": {
@@ -22,6 +23,7 @@
"email": "Adresse e-mail",
"password": "Mot de passe"
},
+ "description": "Vide",
"title": "Connexion \u00e0 Google Hangouts"
}
},
diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json
index 3b1c755b358..385fc128b3b 100644
--- a/homeassistant/components/hangouts/.translations/ko.json
+++ b/homeassistant/components/hangouts/.translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\uad6c\uae00 \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
+ "already_configured": "Google \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
"unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
@@ -24,9 +24,9 @@
"password": "\ube44\ubc00\ubc88\ud638"
},
"description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.",
- "title": "\uad6c\uae00 \ud589\uc544\uc6c3 \ub85c\uadf8\uc778"
+ "title": "Google \ud589\uc544\uc6c3 \ub85c\uadf8\uc778"
}
},
- "title": "\uad6c\uae00 \ud589\uc544\uc6c3"
+ "title": "Google \ud589\uc544\uc6c3"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json
index 6942f683fa6..15d90a672de 100644
--- a/homeassistant/components/hangouts/.translations/ru.json
+++ b/homeassistant/components/hangouts/.translations/ru.json
@@ -2,7 +2,7 @@
"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.",
- "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430"
+ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
"invalid_2fa": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py
index 885ac5d1670..953994d6ac0 100644
--- a/homeassistant/components/hangouts/__init__.py
+++ b/homeassistant/components/hangouts/__init__.py
@@ -7,6 +7,7 @@ from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers import dispatcher, intent
import homeassistant.helpers.config_validation as cv
+from homeassistant.components.conversation.util import create_matcher
# We need an import from .config_flow, without it .config_flow is never loaded.
from .intents import HelpIntent
@@ -54,8 +55,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Set up the Hangouts bot component."""
- from homeassistant.components.conversation import create_matcher
-
config = config.get(DOMAIN)
if config is None:
hass.data[DOMAIN] = {
diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py
index 01948943adf..fd7cddcaed9 100644
--- a/homeassistant/components/harman_kardon_avr/media_player.py
+++ b/homeassistant/components/harman_kardon_avr/media_player.py
@@ -1,18 +1,19 @@
"""Support for interface with an Harman/Kardon or JBL AVR."""
import logging
+import hkavr
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
+ SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF,
+ SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_STEP,
- SUPPORT_TURN_ON,
- SUPPORT_SELECT_SOURCE,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -38,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discover_info=None):
"""Set up the AVR platform."""
- import hkavr
-
name = config[CONF_NAME]
host = config[CONF_HOST]
port = config[CONF_PORT]
diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py
index b78f276bf28..118af7fe34a 100644
--- a/homeassistant/components/harmony/remote.py
+++ b/homeassistant/components/harmony/remote.py
@@ -3,6 +3,12 @@ import asyncio
import json
import logging
+import aioharmony.exceptions as aioexc
+from aioharmony.harmonyapi import (
+ ClientCallbackType,
+ HarmonyAPI as HarmonyClient,
+ SendCommandDevice,
+)
import voluptuous as vol
from homeassistant.components import remote
@@ -23,8 +29,8 @@ from homeassistant.const import (
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
)
-import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady
+import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
@@ -165,8 +171,6 @@ class HarmonyRemote(remote.RemoteDevice):
def __init__(self, name, host, port, activity, out_path, delay_secs):
"""Initialize HarmonyRemote class."""
- from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient
-
self._name = name
self.host = host
self.port = port
@@ -180,8 +184,6 @@ class HarmonyRemote(remote.RemoteDevice):
async def async_added_to_hass(self):
"""Complete the initialization."""
- from aioharmony.harmonyapi import ClientCallbackType
-
_LOGGER.debug("%s: Harmony Hub added", self._name)
# Register the callbacks
self._client.callbacks = ClientCallbackType(
@@ -195,8 +197,6 @@ class HarmonyRemote(remote.RemoteDevice):
# activity
await self.new_config()
- import aioharmony.exceptions as aioexc
-
async def shutdown(_):
"""Close connection on shutdown."""
_LOGGER.debug("%s: Closing Harmony Hub", self._name)
@@ -234,8 +234,6 @@ class HarmonyRemote(remote.RemoteDevice):
async def connect(self):
"""Connect to the Harmony HUB."""
- import aioharmony.exceptions as aioexc
-
_LOGGER.debug("%s: Connecting", self._name)
try:
if not await self._client.connect():
@@ -284,8 +282,6 @@ class HarmonyRemote(remote.RemoteDevice):
async def async_turn_on(self, **kwargs):
"""Start an activity from the Harmony device."""
- import aioharmony.exceptions as aioexc
-
_LOGGER.debug("%s: Turn On", self.name)
activity = kwargs.get(ATTR_ACTIVITY, self._default_activity)
@@ -314,8 +310,6 @@ class HarmonyRemote(remote.RemoteDevice):
async def async_turn_off(self, **kwargs):
"""Start the PowerOff activity."""
- import aioharmony.exceptions as aioexc
-
_LOGGER.debug("%s: Turn Off", self.name)
try:
await self._client.power_off()
@@ -325,9 +319,6 @@ class HarmonyRemote(remote.RemoteDevice):
# pylint: disable=arguments-differ
async def async_send_command(self, command, **kwargs):
"""Send a list of commands to one device."""
- from aioharmony.harmonyapi import SendCommandDevice
- import aioharmony.exceptions as aioexc
-
_LOGGER.debug("%s: Send Command", self.name)
device = kwargs.get(ATTR_DEVICE)
if device is None:
@@ -390,8 +381,6 @@ class HarmonyRemote(remote.RemoteDevice):
async def change_channel(self, channel):
"""Change the channel using Harmony remote."""
- import aioharmony.exceptions as aioexc
-
_LOGGER.debug("%s: Changing channel to %s", self.name, channel)
try:
await self._client.change_channel(channel)
@@ -400,8 +389,6 @@ class HarmonyRemote(remote.RemoteDevice):
async def sync(self):
"""Sync the Harmony device with the web service."""
- import aioharmony.exceptions as aioexc
-
_LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name)
try:
await self._client.sync()
diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
index 6603728e037..e0c0a57375a 100644
--- a/homeassistant/components/hassio/__init__.py
+++ b/homeassistant/components/hassio/__init__.py
@@ -269,7 +269,7 @@ async def async_setup(hass, config):
if errors:
_LOGGER.error(errors)
hass.components.persistent_notification.async_create(
- "Config error. See dev-info panel for details.",
+ "Config error. See [the logs](/developer-tools/logs) for details.",
"Config validating",
f"{HASS_DOMAIN}.check_config",
)
diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py
index 4ecb9a8419f..53235f80dca 100644
--- a/homeassistant/components/hassio/ingress.py
+++ b/homeassistant/components/hassio/ingress.py
@@ -167,7 +167,14 @@ def _init_header(
# filter flags
for name, value in request.headers.items():
- if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING):
+ if name in (
+ hdrs.CONTENT_LENGTH,
+ hdrs.CONTENT_ENCODING,
+ hdrs.SEC_WEBSOCKET_EXTENSIONS,
+ hdrs.SEC_WEBSOCKET_PROTOCOL,
+ hdrs.SEC_WEBSOCKET_VERSION,
+ hdrs.SEC_WEBSOCKET_KEY,
+ ):
continue
headers[name] = value
diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml
index 33574c5dd71..30314c646b0 100644
--- a/homeassistant/components/hassio/services.yaml
+++ b/homeassistant/components/hassio/services.yaml
@@ -1,37 +1,84 @@
addon_install:
- description: Install a HassIO docker addon.
+ description: Install a Hass.io docker add-on.
fields:
- addon: {description: Name of addon., example: smb_config}
- version: {description: Optional or it will be use the latest version., example: '0.2'}
+ addon:
+ description: The add-on slug.
+ example: core_ssh
+ version:
+ description: Optional or it will be use the latest version.
+ example: "0.2"
+
addon_start:
- description: Start a HassIO docker addon.
+ description: Start a Hass.io docker add-on.
fields:
- addon: {description: Name of addon., example: smb_config}
+ addon:
+ description: The add-on slug.
+ example: core_ssh
+
+addon_restart:
+ description: Restart a Hass.io docker add-on.
+ fields:
+ addon:
+ description: The add-on slug.
+ example: core_ssh
+
+addon_stdin:
+ description: Write data to a Hass.io docker add-on stdin .
+ fields:
+ addon:
+ description: The add-on slug.
+ example: core_ssh
+
addon_stop:
- description: Stop a HassIO docker addon.
+ description: Stop a Hass.io docker add-on.
fields:
- addon: {description: Name of addon., example: smb_config}
+ addon:
+ description: The add-on slug.
+ example: core_ssh
+
addon_uninstall:
- description: Uninstall a HassIO docker addon.
+ description: Uninstall a Hass.io docker add-on.
fields:
- addon: {description: Name of addon., example: smb_config}
+ addon:
+ description: The add-on slug.
+ example: core_ssh
+
addon_update:
- description: Update a HassIO docker addon.
+ description: Update a Hass.io docker add-on.
fields:
- addon: {description: Name of addon., example: smb_config}
- version: {description: Optional or it will be use the latest version., example: '0.2'}
+ addon:
+ description: The add-on slug.
+ example: core_ssh
+ version:
+ description: Optional or it will be use the latest version.
+ example: "0.2"
+
homeassistant_update:
- description: Update HomeAssistant docker image.
+ description: Update the Home Assistant docker image.
fields:
- version: {description: Optional or it will be use the latest version., example: 0.40.1}
-host_reboot: {description: Reboot host computer.}
-host_shutdown: {description: Poweroff host computer.}
+ version:
+ description: Optional or it will be use the latest version.
+ example: 0.40.1
+
+host_reboot:
+ description: Reboot the host system.
+
+host_shutdown:
+ description: Poweroff the host system.
+
host_update:
- description: Update host computer.
+ description: Update the host system.
fields:
- version: {description: Optional or it will be use the latest version., example: '0.3'}
-supervisor_reload: {description: Reload HassIO supervisor addons/updates/configs.}
+ version:
+ description: Optional or it will be use the latest version.
+ example: "0.3"
+
+supervisor_reload:
+ description: Reload the Hass.io supervisor.
+
supervisor_update:
- description: Update HassIO supervisor.
+ description: Update the Hass.io supervisor.
fields:
- version: {description: Optional or it will be use the latest version., example: '0.3'}
+ version:
+ description: Optional or it will be use the latest version.
+ example: "0.3"
diff --git a/homeassistant/components/heos/.translations/ca.json b/homeassistant/components/heos/.translations/ca.json
index 60bd780547c..0987e11430b 100644
--- a/homeassistant/components/heos/.translations/ca.json
+++ b/homeassistant/components/heos/.translations/ca.json
@@ -12,7 +12,7 @@
"access_token": "Amfitri\u00f3",
"host": "Amfitri\u00f3"
},
- "description": "Introdueix el nom d'amfitri\u00f3 o l'adre\u00e7a IP d'un dispositiu Heos (preferiblement un connectat a la xarxa per cable).",
+ "description": "Introdueix el nom de l'amfitri\u00f3 o l'adre\u00e7a IP d'un dispositiu Heos (preferiblement un connectat a la xarxa per cable).",
"title": "Connexi\u00f3 amb Heos"
}
},
diff --git a/homeassistant/components/heos/.translations/pt.json b/homeassistant/components/heos/.translations/pt.json
index 33c83fdc738..099d1978436 100644
--- a/homeassistant/components/heos/.translations/pt.json
+++ b/homeassistant/components/heos/.translations/pt.json
@@ -3,7 +3,8 @@
"step": {
"user": {
"data": {
- "access_token": "Servidor"
+ "access_token": "Servidor",
+ "host": "Servidor"
}
}
},
diff --git a/homeassistant/components/heos/.translations/ru.json b/homeassistant/components/heos/.translations/ru.json
index f19b5e52064..8aacc8e165d 100644
--- a/homeassistant/components/heos/.translations/ru.json
+++ b/homeassistant/components/heos/.translations/ru.json
@@ -4,7 +4,7 @@
"already_setup": "\u041d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 HEOS \u0432 \u0441\u0435\u0442\u0438."
},
"error": {
- "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0445\u043e\u0441\u0442\u0443"
+ "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0445\u043e\u0441\u0442\u0443."
},
"step": {
"user": {
diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json
index 78917a5351b..11775ed3ae0 100644
--- a/homeassistant/components/hikvision/manifest.json
+++ b/homeassistant/components/hikvision/manifest.json
@@ -3,7 +3,7 @@
"name": "Hikvision",
"documentation": "https://www.home-assistant.io/integrations/hikvision",
"requirements": [
- "pyhik==0.2.3"
+ "pyhik==0.2.4"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py
index 05bce5f4eac..020b894c0f7 100644
--- a/homeassistant/components/hikvisioncam/switch.py
+++ b/homeassistant/components/hikvisioncam/switch.py
@@ -1,20 +1,22 @@
"""Support turning on/off motion detection on Hikvision cameras."""
import logging
+import hikvision.api
+from hikvision.error import HikvisionError, MissingParamError
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_NAME,
CONF_HOST,
+ CONF_NAME,
CONF_PASSWORD,
- CONF_USERNAME,
CONF_PORT,
+ CONF_USERNAME,
STATE_OFF,
STATE_ON,
)
-from homeassistant.helpers.entity import ToggleEntity
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import ToggleEntity
# This is the last working version, please test before updating
@@ -38,9 +40,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Hikvision camera."""
- import hikvision.api
- from hikvision.error import HikvisionError, MissingParamError
-
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/hipchat/__init__.py b/homeassistant/components/hipchat/__init__.py
deleted file mode 100644
index 8b79982fa43..00000000000
--- a/homeassistant/components/hipchat/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The hipchat component."""
diff --git a/homeassistant/components/hipchat/manifest.json b/homeassistant/components/hipchat/manifest.json
deleted file mode 100644
index 9d563719a2e..00000000000
--- a/homeassistant/components/hipchat/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "domain": "hipchat",
- "name": "Hipchat",
- "documentation": "https://www.home-assistant.io/integrations/hipchat",
- "requirements": [
- "hipnotify==1.0.8"
- ],
- "dependencies": [],
- "codeowners": []
-}
diff --git a/homeassistant/components/hipchat/notify.py b/homeassistant/components/hipchat/notify.py
deleted file mode 100644
index 03556db386a..00000000000
--- a/homeassistant/components/hipchat/notify.py
+++ /dev/null
@@ -1,108 +0,0 @@
-"""HipChat platform for notify component."""
-import logging
-
-import voluptuous as vol
-
-from homeassistant.const import CONF_HOST, CONF_ROOM, CONF_TOKEN
-import homeassistant.helpers.config_validation as cv
-
-from homeassistant.components.notify import (
- ATTR_DATA,
- ATTR_TARGET,
- PLATFORM_SCHEMA,
- BaseNotificationService,
-)
-
-_LOGGER = logging.getLogger(__name__)
-
-CONF_COLOR = "color"
-CONF_NOTIFY = "notify"
-CONF_FORMAT = "format"
-
-DEFAULT_COLOR = "yellow"
-DEFAULT_FORMAT = "text"
-DEFAULT_HOST = "https://api.hipchat.com/"
-DEFAULT_NOTIFY = False
-
-VALID_COLORS = {"yellow", "green", "red", "purple", "gray", "random"}
-VALID_FORMATS = {"text", "html"}
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_ROOM): vol.Coerce(int),
- vol.Required(CONF_TOKEN): cv.string,
- vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): vol.In(VALID_COLORS),
- vol.Optional(CONF_FORMAT, default=DEFAULT_FORMAT): vol.In(VALID_FORMATS),
- vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
- vol.Optional(CONF_NOTIFY, default=DEFAULT_NOTIFY): cv.boolean,
- }
-)
-
-
-def get_service(hass, config, discovery_info=None):
- """Get the HipChat notification service."""
- return HipchatNotificationService(
- config[CONF_TOKEN],
- config[CONF_ROOM],
- config[CONF_COLOR],
- config[CONF_NOTIFY],
- config[CONF_FORMAT],
- config[CONF_HOST],
- )
-
-
-class HipchatNotificationService(BaseNotificationService):
- """Implement the notification service for HipChat."""
-
- def __init__(
- self, token, default_room, default_color, default_notify, default_format, host
- ):
- """Initialize the service."""
- self._token = token
- self._default_room = default_room
- self._default_color = default_color
- self._default_notify = default_notify
- self._default_format = default_format
- self._host = host
-
- self._rooms = {}
- self._get_room(self._default_room)
-
- def _get_room(self, room):
- """Get Room object, creating it if necessary."""
- from hipnotify import Room
-
- if room not in self._rooms:
- self._rooms[room] = Room(
- token=self._token, room_id=room, endpoint_url=self._host
- )
- return self._rooms[room]
-
- def send_message(self, message="", **kwargs):
- """Send a message."""
- color = self._default_color
- notify = self._default_notify
- message_format = self._default_format
-
- if kwargs.get(ATTR_DATA) is not None:
- data = kwargs.get(ATTR_DATA)
- if (data.get(CONF_COLOR) is not None) and (
- data.get(CONF_COLOR) in VALID_COLORS
- ):
- color = data.get(CONF_COLOR)
- if (data.get(CONF_NOTIFY) is not None) and isinstance(
- data.get(CONF_NOTIFY), bool
- ):
- notify = data.get(CONF_NOTIFY)
- if (data.get(CONF_FORMAT) is not None) and (
- data.get(CONF_FORMAT) in VALID_FORMATS
- ):
- message_format = data.get(CONF_FORMAT)
-
- targets = kwargs.get(ATTR_TARGET, [self._default_room])
-
- for target in targets:
- room = self._get_room(target)
- room.notify(
- msg=message, color=color, notify=notify, message_format=message_format
- )
diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py
index 3301097bab7..976821513b6 100644
--- a/homeassistant/components/hive/__init__.py
+++ b/homeassistant/components/hive/__init__.py
@@ -82,6 +82,7 @@ class HiveSession:
switch = None
weather = None
attributes = None
+ trv = None
def setup(hass, config):
diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py
index 1fb77ce6cb9..ed13e3019ce 100644
--- a/homeassistant/components/hive/climate.py
+++ b/homeassistant/components/hive/climate.py
@@ -8,6 +8,9 @@ from homeassistant.components.climate.const import (
PRESET_NONE,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
+ CURRENT_HVAC_IDLE,
+ CURRENT_HVAC_OFF,
+ CURRENT_HVAC_HEAT,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
@@ -26,6 +29,12 @@ HASS_TO_HIVE_STATE = {
HVAC_MODE_OFF: "OFF",
}
+HIVE_TO_HASS_HVAC_ACTION = {
+ "UNKNOWN": CURRENT_HVAC_OFF,
+ False: CURRENT_HVAC_IDLE,
+ True: CURRENT_HVAC_HEAT,
+}
+
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF]
SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST]
@@ -71,7 +80,11 @@ class HiveClimateEntity(HiveEntity, ClimateDevice):
"""Return the name of the Climate device."""
friendly_name = "Heating"
if self.node_name is not None:
- friendly_name = f"{self.node_name} {friendly_name}"
+ if self.device_type == "TRV":
+ friendly_name = self.node_name
+ else:
+ friendly_name = f"{self.node_name} {friendly_name}"
+
return friendly_name
@property
@@ -95,6 +108,13 @@ class HiveClimateEntity(HiveEntity, ClimateDevice):
"""
return HIVE_TO_HASS_STATE[self.session.heating.get_mode(self.node_id)]
+ @property
+ def hvac_action(self):
+ """Return current HVAC action."""
+ return HIVE_TO_HASS_HVAC_ACTION[
+ self.session.heating.operational_status(self.node_id, self.device_type)
+ ]
+
@property
def temperature_unit(self):
"""Return the unit of measurement."""
@@ -123,7 +143,10 @@ class HiveClimateEntity(HiveEntity, ClimateDevice):
@property
def preset_mode(self):
"""Return the current preset mode, e.g., home, away, temp."""
- if self.session.heating.get_boost(self.node_id) == "ON":
+ if (
+ self.device_type == "Heating"
+ and self.session.heating.get_boost(self.node_id) == "ON"
+ ):
return PRESET_BOOST
return None
diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json
index 4164283f9f8..e87e3387a62 100644
--- a/homeassistant/components/hive/manifest.json
+++ b/homeassistant/components/hive/manifest.json
@@ -3,7 +3,7 @@
"name": "Hive",
"documentation": "https://www.home-assistant.io/integrations/hive",
"requirements": [
- "pyhiveapi==0.2.19.2"
+ "pyhiveapi==0.2.19.3"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py
index 02e53d1de10..d2d6abdadb5 100644
--- a/homeassistant/components/homeassistant/__init__.py
+++ b/homeassistant/components/homeassistant/__init__.py
@@ -108,7 +108,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
if errors:
_LOGGER.error(errors)
hass.components.persistent_notification.async_create(
- "Config error. See dev-info panel for details.",
+ "Config error. See [the logs](/developer-tools/logs) for details.",
"Config validating",
f"{ha.DOMAIN}.check_config",
)
diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py
index 66b04109640..39b04f6d3ea 100644
--- a/homeassistant/components/homeassistant/scene.py
+++ b/homeassistant/components/homeassistant/scene.py
@@ -26,6 +26,36 @@ from homeassistant.helpers import (
from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene
+
+def _convert_states(states):
+ """Convert state definitions to State objects."""
+ result = {}
+
+ for entity_id in states:
+ entity_id = cv.entity_id(entity_id)
+
+ if isinstance(states[entity_id], dict):
+ entity_attrs = states[entity_id].copy()
+ state = entity_attrs.pop(ATTR_STATE, None)
+ attributes = entity_attrs
+ else:
+ state = states[entity_id]
+ attributes = {}
+
+ # YAML translates 'on' to a boolean
+ # http://yaml.org/type/bool.html
+ if isinstance(state, bool):
+ state = STATE_ON if state else STATE_OFF
+ elif not isinstance(state, str):
+ raise vol.Invalid(f"State for {entity_id} should be a string")
+
+ result[entity_id] = State(entity_id, state, attributes)
+
+ return result
+
+
+STATES_SCHEMA = vol.All(dict, _convert_states)
+
PLATFORM_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): HASS_DOMAIN,
@@ -34,9 +64,7 @@ PLATFORM_SCHEMA = vol.Schema(
[
{
vol.Required(CONF_NAME): cv.string,
- vol.Required(CONF_ENTITIES): {
- cv.entity_id: vol.Any(str, bool, dict)
- },
+ vol.Required(CONF_ENTITIES): STATES_SCHEMA,
}
],
),
@@ -44,6 +72,7 @@ PLATFORM_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
+SERVICE_APPLY = "apply"
SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES])
_LOGGER = logging.getLogger(__name__)
@@ -87,6 +116,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
SCENE_DOMAIN, SERVICE_RELOAD, reload_config
)
+ async def apply_service(call):
+ """Apply a scene."""
+ await async_reproduce_state(
+ hass, call.data[CONF_ENTITIES].values(), blocking=True, context=call.context
+ )
+
+ hass.services.async_register(
+ SCENE_DOMAIN,
+ SERVICE_APPLY,
+ apply_service,
+ vol.Schema({vol.Required(CONF_ENTITIES): STATES_SCHEMA}),
+ )
+
def _process_scenes_config(hass, async_add_entities, config):
"""Process multiple scenes and add them."""
@@ -97,41 +139,11 @@ def _process_scenes_config(hass, async_add_entities, config):
return
async_add_entities(
- HomeAssistantScene(hass, _process_scene_config(scene)) for scene in scene_config
+ HomeAssistantScene(hass, SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES]))
+ for scene in scene_config
)
-def _process_scene_config(scene_config):
- """Process passed in config into a format to work with.
-
- Async friendly.
- """
- name = scene_config.get(CONF_NAME)
-
- states = {}
- c_entities = dict(scene_config.get(CONF_ENTITIES, {}))
-
- for entity_id in c_entities:
- if isinstance(c_entities[entity_id], dict):
- entity_attrs = c_entities[entity_id].copy()
- state = entity_attrs.pop(ATTR_STATE, None)
- attributes = entity_attrs
- else:
- state = c_entities[entity_id]
- attributes = {}
-
- # YAML translates 'on' to a boolean
- # http://yaml.org/type/bool.html
- if isinstance(state, bool):
- state = STATE_ON if state else STATE_OFF
- else:
- state = str(state)
-
- states[entity_id.lower()] = State(entity_id, state, attributes)
-
- return SCENECONFIG(name, states)
-
-
class HomeAssistantScene(Scene):
"""A scene is a group of entities and the states we want them to be."""
@@ -148,8 +160,13 @@ class HomeAssistantScene(Scene):
@property
def device_state_attributes(self):
"""Return the scene state attributes."""
- return {ATTR_ENTITY_ID: list(self.scene_config.states.keys())}
+ return {ATTR_ENTITY_ID: list(self.scene_config.states)}
async def async_activate(self):
"""Activate scene. Try to get entities into requested state."""
- await async_reproduce_state(self.hass, self.scene_config.states.values(), True)
+ await async_reproduce_state(
+ self.hass,
+ self.scene_config.states.values(),
+ blocking=True,
+ context=self._context,
+ )
diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml
index 2219564abb8..cb3efb0d524 100644
--- a/homeassistant/components/homeassistant/services.yaml
+++ b/homeassistant/components/homeassistant/services.yaml
@@ -7,6 +7,16 @@ reload_core_config:
restart:
description: Restart the Home Assistant service.
+set_location:
+ description: Update the Home Assistant location.
+ fields:
+ latitude:
+ description: Latitude of your location
+ example: 32.87336
+ longitude:
+ description: Longitude of your location
+ example: 117.22743
+
stop:
description: Stop the Home Assistant service.
diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py
index d8aafb8e238..4c300e0a934 100644
--- a/homeassistant/components/homekit/__init__.py
+++ b/homeassistant/components/homekit/__init__.py
@@ -31,6 +31,7 @@ from homeassistant.util.decorator import Registry
from .const import (
BRIDGE_NAME,
+ CONF_ADVERTISE_IP,
CONF_AUTO_START,
CONF_ENTITY_CONFIG,
CONF_FEATURE_LIST,
@@ -89,6 +90,9 @@ CONFIG_SCHEMA = vol.Schema(
),
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string),
+ vol.Optional(CONF_ADVERTISE_IP): vol.All(
+ ipaddress.ip_address, cv.string
+ ),
vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean,
vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
@@ -112,13 +116,21 @@ async def async_setup(hass, config):
name = conf[CONF_NAME]
port = conf[CONF_PORT]
ip_address = conf.get(CONF_IP_ADDRESS)
+ advertise_ip = conf.get(CONF_ADVERTISE_IP)
auto_start = conf[CONF_AUTO_START]
safe_mode = conf[CONF_SAFE_MODE]
entity_filter = conf[CONF_FILTER]
entity_config = conf[CONF_ENTITY_CONFIG]
homekit = HomeKit(
- hass, name, port, ip_address, entity_filter, entity_config, safe_mode
+ hass,
+ name,
+ port,
+ ip_address,
+ entity_filter,
+ entity_config,
+ safe_mode,
+ advertise_ip,
)
await hass.async_add_executor_job(homekit.setup)
@@ -265,7 +277,15 @@ class HomeKit:
"""Class to handle all actions between HomeKit and Home Assistant."""
def __init__(
- self, hass, name, port, ip_address, entity_filter, entity_config, safe_mode
+ self,
+ hass,
+ name,
+ port,
+ ip_address,
+ entity_filter,
+ entity_config,
+ safe_mode,
+ advertise_ip=None,
):
"""Initialize a HomeKit object."""
self.hass = hass
@@ -275,6 +295,7 @@ class HomeKit:
self._filter = entity_filter
self._config = entity_config
self._safe_mode = safe_mode
+ self._advertise_ip = advertise_ip
self.status = STATUS_READY
self.bridge = None
@@ -289,7 +310,11 @@ class HomeKit:
ip_addr = self._ip_address or get_local_ip()
path = self.hass.config.path(HOMEKIT_FILE)
self.driver = HomeDriver(
- self.hass, address=ip_addr, port=self._port, persist_file=path
+ self.hass,
+ address=ip_addr,
+ port=self._port,
+ persist_file=path,
+ advertised_address=self._advertise_ip,
)
self.bridge = HomeBridge(self.hass, self.driver, self._name)
if self._safe_mode:
diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py
index d225225237f..82ec296da4b 100644
--- a/homeassistant/components/homekit/const.py
+++ b/homeassistant/components/homekit/const.py
@@ -10,6 +10,7 @@ ATTR_DISPLAY_NAME = "display_name"
ATTR_VALUE = "value"
# #### Config ####
+CONF_ADVERTISE_IP = "advertise_ip"
CONF_AUTO_START = "auto_start"
CONF_ENTITY_CONFIG = "entity_config"
CONF_FEATURE = "feature"
diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py
index 87c8d5247a5..a1450518e0c 100644
--- a/homeassistant/components/homekit/type_sensors.py
+++ b/homeassistant/components/homekit/type_sensors.py
@@ -96,7 +96,7 @@ class TemperatureSensor(HomeAccessory):
temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature)
_LOGGER.debug(
- "%s: Current temperature set to %d°C", self.entity_id, temperature
+ "%s: Current temperature set to %.1f°C", self.entity_id, temperature
)
diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py
index d60c94d420d..608c9a974e5 100644
--- a/homeassistant/components/homekit/util.py
+++ b/homeassistant/components/homekit/util.py
@@ -235,7 +235,7 @@ def convert_to_float(state):
def temperature_to_homekit(temperature, unit):
"""Convert temperature to Celsius for HomeKit."""
- return round(temp_util.convert(temperature, unit, TEMP_CELSIUS) * 2) / 2
+ return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1)
def temperature_to_states(temperature, unit):
diff --git a/homeassistant/components/homekit_controller/.translations/ru.json b/homeassistant/components/homekit_controller/.translations/ru.json
index c7770c6a064..44a57a1eb25 100644
--- a/homeassistant/components/homekit_controller/.translations/ru.json
+++ b/homeassistant/components/homekit_controller/.translations/ru.json
@@ -24,14 +24,14 @@
"data": {
"pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f"
},
- "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440",
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440.",
"title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit"
},
"user": {
"data": {
"device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e"
},
- "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435",
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.",
"title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit"
}
},
diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py
index 008e0f8566d..40bf87d6f0a 100644
--- a/homeassistant/components/homekit_controller/config_flow.py
+++ b/homeassistant/components/homekit_controller/config_flow.py
@@ -122,7 +122,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
_LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid)
- # pylint: disable=unsupported-assignment-operation
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["hkid"] = hkid
self.context["title_placeholders"] = {"name": name}
diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py
index 971a8a9cac0..32fa0bb358e 100644
--- a/homeassistant/components/homematic/light.py
+++ b/homeassistant/components/homematic/light.py
@@ -54,9 +54,12 @@ class HMLight(HMDevice, Light):
@property
def supported_features(self):
"""Flag supported features."""
+ features = SUPPORT_BRIGHTNESS
if "COLOR" in self._hmdevice.WRITENODE:
- return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT
- return SUPPORT_BRIGHTNESS
+ features |= SUPPORT_COLOR
+ if "PROGRAM" in self._hmdevice.WRITENODE:
+ features |= SUPPORT_EFFECT
+ return features
@property
def hs_color(self):
@@ -110,4 +113,6 @@ class HMLight(HMDevice, Light):
self._data[self._state] = None
if self.supported_features & SUPPORT_COLOR:
- self._data.update({"COLOR": None, "PROGRAM": None})
+ self._data.update({"COLOR": None})
+ if self.supported_features & SUPPORT_EFFECT:
+ self._data.update({"PROGRAM": None})
diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json
index 260e54e65c4..5db547e3f0a 100644
--- a/homeassistant/components/homematic/manifest.json
+++ b/homeassistant/components/homematic/manifest.json
@@ -3,7 +3,7 @@
"name": "Homematic",
"documentation": "https://www.home-assistant.io/integrations/homematic",
"requirements": [
- "pyhomematic==0.1.60"
+ "pyhomematic==0.1.61"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json
index 5155a42c4c3..57ab265d1c2 100644
--- a/homeassistant/components/homematicip_cloud/.translations/ru.json
+++ b/homeassistant/components/homematicip_cloud/.translations/ru.json
@@ -1,15 +1,15 @@
{
"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.",
- "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP",
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.",
"unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
"invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
"press_the_button": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443.",
- "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430",
- "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430"
+ "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
+ "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430."
},
"step": {
"init": {
diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py
index c8fb31998ef..9a0eb65aa3f 100644
--- a/homeassistant/components/homematicip_cloud/__init__.py
+++ b/homeassistant/components/homematicip_cloud/__init__.py
@@ -1,15 +1,16 @@
"""Support for HomematicIP Cloud devices."""
import logging
+from homematicip.aio.group import AsyncHeatingGroup
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_NAME
-from homeassistant.core import HomeAssistant
+from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME
from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.typing import ConfigType
+from homeassistant.helpers.config_validation import comp_entity_ids
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .config_flow import configured_haps
from .const import (
@@ -25,6 +26,7 @@ from .hap import HomematicipAuth, HomematicipHAP # noqa: F401
_LOGGER = logging.getLogger(__name__)
+ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index"
ATTR_DURATION = "duration"
ATTR_ENDTIME = "endtime"
ATTR_TEMPERATURE = "temperature"
@@ -35,6 +37,7 @@ SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period"
SERVICE_ACTIVATE_VACATION = "activate_vacation"
SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode"
SERVICE_DEACTIVATE_VACATION = "deactivate_vacation"
+SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile"
CONFIG_SCHEMA = vol.Schema(
{
@@ -86,8 +89,15 @@ SCHEMA_DEACTIVATE_VACATION = vol.Schema(
{vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))}
)
+SCHEMA_SET_ACTIVE_CLIMATE_PROFILE = vol.Schema(
+ {
+ vol.Required(ATTR_ENTITY_ID): comp_entity_ids,
+ vol.Required(ATTR_CLIMATE_PROFILE_INDEX): cv.positive_int,
+ }
+)
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Set up the HomematicIP Cloud component."""
hass.data[DOMAIN] = {}
@@ -117,9 +127,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home:
await home.activate_absence_with_duration(duration)
else:
- for hapid in hass.data[DOMAIN]:
- home = hass.data[DOMAIN][hapid].home
- await home.activate_absence_with_duration(duration)
+ for hap in hass.data[DOMAIN].values():
+ await hap.home.activate_absence_with_duration(duration)
hass.services.async_register(
DOMAIN,
@@ -138,9 +147,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home:
await home.activate_absence_with_period(endtime)
else:
- for hapid in hass.data[DOMAIN]:
- home = hass.data[DOMAIN][hapid].home
- await home.activate_absence_with_period(endtime)
+ for hap in hass.data[DOMAIN].values():
+ await hap.home.activate_absence_with_period(endtime)
hass.services.async_register(
DOMAIN,
@@ -160,9 +168,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home:
await home.activate_vacation(endtime, temperature)
else:
- for hapid in hass.data[DOMAIN]:
- home = hass.data[DOMAIN][hapid].home
- await home.activate_vacation(endtime, temperature)
+ for hap in hass.data[DOMAIN].values():
+ await hap.home.activate_vacation(endtime, temperature)
hass.services.async_register(
DOMAIN,
@@ -180,9 +187,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home:
await home.deactivate_absence()
else:
- for hapid in hass.data[DOMAIN]:
- home = hass.data[DOMAIN][hapid].home
- await home.deactivate_absence()
+ for hap in hass.data[DOMAIN].values():
+ await hap.home.deactivate_absence()
hass.services.async_register(
DOMAIN,
@@ -200,9 +206,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home:
await home.deactivate_vacation()
else:
- for hapid in hass.data[DOMAIN]:
- home = hass.data[DOMAIN][hapid].home
- await home.deactivate_vacation()
+ for hap in hass.data[DOMAIN].values():
+ await hap.home.deactivate_vacation()
hass.services.async_register(
DOMAIN,
@@ -211,17 +216,42 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
schema=SCHEMA_DEACTIVATE_VACATION,
)
+ async def _set_active_climate_profile(service):
+ """Service to set the active climate profile."""
+ entity_id_list = service.data[ATTR_ENTITY_ID]
+ climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1
+
+ for hap in hass.data[DOMAIN].values():
+ if entity_id_list != "all":
+ for entity_id in entity_id_list:
+ group = hap.hmip_device_by_entity_id.get(entity_id)
+ if group:
+ await group.set_active_profile(climate_profile_index)
+ else:
+ for group in hap.home.groups:
+ if isinstance(group, AsyncHeatingGroup):
+ await group.set_active_profile(climate_profile_index)
+
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_SET_ACTIVE_CLIMATE_PROFILE,
+ _set_active_climate_profile,
+ schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE,
+ )
+
def _get_home(hapid: str):
"""Return a HmIP home."""
- hap = hass.data[DOMAIN][hapid]
+ hap = hass.data[DOMAIN].get(hapid)
if hap:
return hap.home
+
+ _LOGGER.info("No matching access point found for access point id %s", hapid)
return None
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up an access point from a config entry."""
hap = HomematicipHAP(hass, entry)
hapid = entry.data[HMIPC_HAPID].replace("-", "").upper()
diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
index 592d234225c..f61bf6f6b56 100644
--- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py
+++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
@@ -2,7 +2,6 @@
import logging
from homematicip.aio.group import AsyncSecurityZoneGroup
-from homematicip.aio.home import AsyncHome
from homematicip.base.enums import WindowState
from homeassistant.components.alarm_control_panel import AlarmControlPanel
@@ -13,9 +12,10 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
-from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID
+from .hap import HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -28,18 +28,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the HomematicIP alrm control panel from a config entry."""
- home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
+ hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]]
devices = []
security_zones = []
- for group in home.groups:
+ for group in hap.home.groups:
if isinstance(group, AsyncSecurityZoneGroup):
security_zones.append(group)
if security_zones:
- devices.append(HomematicipAlarmControlPanel(home, security_zones))
+ devices.append(HomematicipAlarmControlPanel(hap, security_zones))
if devices:
async_add_entities(devices)
@@ -48,9 +48,9 @@ async def async_setup_entry(
class HomematicipAlarmControlPanel(AlarmControlPanel):
"""Representation of an alarm control panel."""
- def __init__(self, home: AsyncHome, security_zones) -> None:
+ def __init__(self, hap: HomematicipHAP, security_zones) -> None:
"""Initialize the alarm control panel."""
- self._home = home
+ self._home = hap.home
self.alarm_state = STATE_ALARM_DISARMED
for security_zone in security_zones:
@@ -59,6 +59,17 @@ class HomematicipAlarmControlPanel(AlarmControlPanel):
else:
self._external_alarm_zone = security_zone
+ @property
+ def device_info(self):
+ """Return device specific attributes."""
+ return {
+ "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")},
+ "name": self.name,
+ "manufacturer": "eQ-3",
+ "model": CONST_ALARM_CONTROL_PANEL_NAME,
+ "via_device": (HMIPC_DOMAIN, self._home.id),
+ }
+
@property
def state(self) -> str:
"""Return the state of the device."""
diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py
index 4ac4614379b..e308f96c208 100644
--- a/homeassistant/components/homematicip_cloud/binary_sensor.py
+++ b/homeassistant/components/homematicip_cloud/binary_sensor.py
@@ -2,6 +2,7 @@
import logging
from homematicip.aio.device import (
+ AsyncAccelerationSensor,
AsyncContactInterface,
AsyncDevice,
AsyncFullFlushContactInterface,
@@ -19,7 +20,6 @@ from homematicip.aio.device import (
AsyncWeatherSensorPro,
)
from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup
-from homematicip.aio.home import AsyncHome
from homematicip.base.enums import SmokeDetectorAlarmType, WindowState
from homeassistant.components.binary_sensor import (
@@ -28,6 +28,7 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASS_LIGHT,
DEVICE_CLASS_MOISTURE,
DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_MOVING,
DEVICE_CLASS_OPENING,
DEVICE_CLASS_PRESENCE,
DEVICE_CLASS_SAFETY,
@@ -35,13 +36,18 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
-from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP, ATTR_MODEL_TYPE
+from .device import ATTR_GROUP_MEMBER_UNREACHABLE
+from .hap import HomematicipHAP
_LOGGER = logging.getLogger(__name__)
+ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode"
+ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position"
+ATTR_ACCELERATION_SENSOR_SENSITIVITY = "acceleration_sensor_sensitivity"
+ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE = "acceleration_sensor_trigger_angle"
ATTR_LOW_BATTERY = "low_battery"
ATTR_MOISTURE_DETECTED = "moisture_detected"
ATTR_MOTION_DETECTED = "motion_detected"
@@ -54,7 +60,6 @@ ATTR_WINDOW_STATE = "window_state"
GROUP_ATTRIBUTES = {
"lowBat": ATTR_LOW_BATTERY,
- "modelType": ATTR_MODEL_TYPE,
"moistureDetected": ATTR_MOISTURE_DETECTED,
"motionDetected": ATTR_MOTION_DETECTED,
"powerMainsFailure": ATTR_POWER_MAINS_FAILURE,
@@ -63,6 +68,13 @@ GROUP_ATTRIBUTES = {
"waterlevelDetected": ATTR_WATER_LEVEL_DETECTED,
}
+SAM_DEVICE_ATTRIBUTES = {
+ "accelerationSensorNeutralPosition": ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION,
+ "accelerationSensorMode": ATTR_ACCELERATION_SENSOR_MODE,
+ "accelerationSensorSensitivity": ATTR_ACCELERATION_SENSOR_SENSITIVITY,
+ "accelerationSensorTriggerAngle": ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE,
+}
+
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the HomematicIP Cloud binary sensor devices."""
@@ -70,19 +82,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
- home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
+ hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]]
devices = []
- for device in home.devices:
+ for device in hap.home.devices:
+ if isinstance(device, AsyncAccelerationSensor):
+ devices.append(HomematicipAccelerationSensor(hap, device))
if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)):
- devices.append(HomematicipContactInterface(home, device))
+ devices.append(HomematicipContactInterface(hap, device))
if isinstance(
device,
(AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor),
):
- devices.append(HomematicipShutterContact(home, device))
+ devices.append(HomematicipShutterContact(hap, device))
if isinstance(
device,
(
@@ -91,33 +105,59 @@ async def async_setup_entry(
AsyncMotionDetectorPushButton,
),
):
- devices.append(HomematicipMotionDetector(home, device))
+ devices.append(HomematicipMotionDetector(hap, device))
if isinstance(device, AsyncPresenceDetectorIndoor):
- devices.append(HomematicipPresenceDetector(home, device))
+ devices.append(HomematicipPresenceDetector(hap, device))
if isinstance(device, AsyncSmokeDetector):
- devices.append(HomematicipSmokeDetector(home, device))
+ devices.append(HomematicipSmokeDetector(hap, device))
if isinstance(device, AsyncWaterSensor):
- devices.append(HomematicipWaterDetector(home, device))
+ devices.append(HomematicipWaterDetector(hap, device))
if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)):
- devices.append(HomematicipRainSensor(home, device))
+ devices.append(HomematicipRainSensor(hap, device))
if isinstance(
device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro)
):
- devices.append(HomematicipStormSensor(home, device))
- devices.append(HomematicipSunshineSensor(home, device))
+ devices.append(HomematicipStormSensor(hap, device))
+ devices.append(HomematicipSunshineSensor(hap, device))
if isinstance(device, AsyncDevice) and device.lowBat is not None:
- devices.append(HomematicipBatterySensor(home, device))
+ devices.append(HomematicipBatterySensor(hap, device))
- for group in home.groups:
+ for group in hap.home.groups:
if isinstance(group, AsyncSecurityGroup):
- devices.append(HomematicipSecuritySensorGroup(home, group))
+ devices.append(HomematicipSecuritySensorGroup(hap, group))
elif isinstance(group, AsyncSecurityZoneGroup):
- devices.append(HomematicipSecurityZoneSensorGroup(home, group))
+ devices.append(HomematicipSecurityZoneSensorGroup(hap, group))
if devices:
async_add_entities(devices)
+class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice):
+ """Representation of a HomematicIP Cloud acceleration sensor."""
+
+ @property
+ def device_class(self) -> str:
+ """Return the class of this sensor."""
+ return DEVICE_CLASS_MOVING
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if acceleration is detected."""
+ return self._device.accelerationSensorTriggered
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the acceleration sensor."""
+ state_attr = super().device_state_attributes
+
+ for attr, attr_key in SAM_DEVICE_ATTRIBUTES.items():
+ attr_value = getattr(self._device, attr, None)
+ if attr_value:
+ state_attr[attr_key] = attr_value
+
+ return state_attr
+
+
class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorDevice):
"""Representation of a HomematicIP Cloud contact interface."""
@@ -209,9 +249,9 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice):
"""Representation of a HomematicIP Cloud storm sensor."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize storm sensor."""
- super().__init__(home, device, "Storm")
+ super().__init__(hap, device, "Storm")
@property
def icon(self) -> str:
@@ -227,9 +267,9 @@ class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice):
"""Representation of a HomematicIP Cloud rain sensor."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize rain sensor."""
- super().__init__(home, device, "Raining")
+ super().__init__(hap, device, "Raining")
@property
def device_class(self) -> str:
@@ -245,9 +285,9 @@ class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice):
"""Representation of a HomematicIP Cloud sunshine sensor."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize sunshine sensor."""
- super().__init__(home, device, "Sunshine")
+ super().__init__(hap, device, "Sunshine")
@property
def device_class(self) -> str:
@@ -274,9 +314,9 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice):
"""Representation of a HomematicIP Cloud low battery sensor."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize battery sensor."""
- super().__init__(home, device, "Battery")
+ super().__init__(hap, device, "Battery")
@property
def device_class(self) -> str:
@@ -292,10 +332,10 @@ class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorDevice):
"""Representation of a HomematicIP Cloud security zone group."""
- def __init__(self, home: AsyncHome, device, post: str = "SecurityZone") -> None:
+ def __init__(self, hap: HomematicipHAP, device, post: str = "SecurityZone") -> None:
"""Initialize security zone group."""
device.modelType = f"HmIP-{post}"
- super().__init__(home, device, post)
+ super().__init__(hap, device, post)
@property
def device_class(self) -> str:
@@ -312,7 +352,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD
@property
def device_state_attributes(self):
"""Return the state attributes of the security zone group."""
- state_attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: True}
+ state_attr = super().device_state_attributes
for attr, attr_key in GROUP_ATTRIBUTES.items():
attr_value = getattr(self._device, attr, None)
@@ -349,9 +389,9 @@ class HomematicipSecuritySensorGroup(
):
"""Representation of a HomematicIP security group."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize security group."""
- super().__init__(home, device, "Sensors")
+ super().__init__(hap, device, "Sensors")
@property
def device_state_attributes(self):
diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py
index 794a8b44cbc..74d647c8c33 100644
--- a/homeassistant/components/homematicip_cloud/climate.py
+++ b/homeassistant/components/homematicip_cloud/climate.py
@@ -4,8 +4,7 @@ from typing import Awaitable
from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact
from homematicip.aio.group import AsyncHeatingGroup
-from homematicip.aio.home import AsyncHome
-from homematicip.base.enums import AbsenceType
+from homematicip.base.enums import AbsenceType, GroupType
from homematicip.functionalHomes import IndoorClimateHome
from homeassistant.components.climate import ClimateDevice
@@ -21,9 +20,13 @@ from homeassistant.components.climate.const import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
-from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
+from .hap import HomematicipHAP
+
+HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2}
+COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5}
_LOGGER = logging.getLogger(__name__)
@@ -38,14 +41,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the HomematicIP climate from a config entry."""
- home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
+ hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]]
devices = []
- for device in home.groups:
+ for device in hap.home.groups:
if isinstance(device, AsyncHeatingGroup):
- devices.append(HomematicipHeatingGroup(home, device))
+ devices.append(HomematicipHeatingGroup(hap, device))
if devices:
async_add_entities(devices)
@@ -54,13 +57,24 @@ async def async_setup_entry(
class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
"""Representation of a HomematicIP heating group."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None:
"""Initialize heating group."""
- device.modelType = "Group-Heating"
+ device.modelType = "HmIP-Heating-Group"
self._simple_heating = None
if device.actualTemperature is None:
self._simple_heating = _get_first_heating_thermostat(device)
- super().__init__(home, device)
+ super().__init__(hap, device)
+
+ @property
+ def device_info(self):
+ """Return device specific attributes."""
+ return {
+ "identifiers": {(HMIPC_DOMAIN, self._device.id)},
+ "name": self._device.label,
+ "manufacturer": "eQ-3",
+ "model": self._device.modelType,
+ "via_device": (HMIPC_DOMAIN, self._device.homeId),
+ }
@property
def temperature_unit(self) -> str:
@@ -96,7 +110,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
Need to be one of HVAC_MODE_*.
"""
if self._device.boostMode:
- return HVAC_MODE_AUTO
+ return HVAC_MODE_HEAT
if self._device.controlMode == HMIP_MANUAL_CM:
return HVAC_MODE_HEAT
@@ -118,6 +132,8 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
"""
if self._device.boostMode:
return PRESET_BOOST
+ if self.hvac_mode == HVAC_MODE_HEAT:
+ return PRESET_NONE
if self._device.controlMode == HMIP_ECO_CM:
absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType
if absence_type == AbsenceType.VACATION:
@@ -129,15 +145,15 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
]:
return PRESET_ECO
- return PRESET_NONE
+ if self._device.activeProfile:
+ return self._device.activeProfile.name
@property
def preset_modes(self):
- """Return a list of available preset modes.
-
- Requires SUPPORT_PRESET_MODE.
- """
- return [PRESET_NONE, PRESET_BOOST]
+ """Return a list of available preset modes incl profiles."""
+ presets = [PRESET_NONE, PRESET_BOOST]
+ presets.extend(self._device_profile_names)
+ return presets
@property
def min_temp(self) -> float:
@@ -169,6 +185,46 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
await self._device.set_boost(False)
if preset_mode == PRESET_BOOST:
await self._device.set_boost()
+ if preset_mode in self._device_profile_names:
+ profile_idx = self._get_profile_idx_by_name(preset_mode)
+ await self.async_set_hvac_mode(HVAC_MODE_AUTO)
+ await self._device.set_active_profile(profile_idx)
+
+ @property
+ def _device_profiles(self):
+ """Return the relevant profiles of the device."""
+ return [
+ profile
+ for profile in self._device.profiles
+ if profile.visible
+ and profile.name != ""
+ and profile.index in self._relevant_profile_group
+ ]
+
+ @property
+ def _device_profile_names(self):
+ """Return a collection of profile names."""
+ return [profile.name for profile in self._device_profiles]
+
+ def _get_profile_idx_by_name(self, profile_name):
+ """Return a profile index by name."""
+ relevant_index = self._relevant_profile_group
+ index_name = [
+ profile.index
+ for profile in self._device_profiles
+ if profile.name == profile_name
+ ]
+
+ return relevant_index[index_name[0]]
+
+ @property
+ def _relevant_profile_group(self):
+ """Return the relevant profile groups."""
+ return (
+ HEATING_PROFILES
+ if self._device.groupType == GroupType.HEATING
+ else COOLING_PROFILES
+ )
def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup):
@@ -176,4 +232,3 @@ def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup):
for device in heating_group.devices:
if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)):
return device
- return None
diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py
index a94ea7b53f1..1488f02f13b 100644
--- a/homeassistant/components/homematicip_cloud/config_flow.py
+++ b/homeassistant/components/homematicip_cloud/config_flow.py
@@ -4,7 +4,8 @@ from typing import Set
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
from .const import (
_LOGGER,
@@ -18,7 +19,7 @@ from .hap import HomematicipAuth
@callback
-def configured_haps(hass: HomeAssistant) -> Set[str]:
+def configured_haps(hass: HomeAssistantType) -> Set[str]:
"""Return a set of the configured access points."""
return set(
entry.data[HMIPC_HAPID]
diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py
index 9252c4322d9..63ac6f7310c 100644
--- a/homeassistant/components/homematicip_cloud/cover.py
+++ b/homeassistant/components/homematicip_cloud/cover.py
@@ -10,7 +10,7 @@ from homeassistant.components.cover import (
CoverDevice,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
@@ -28,16 +28,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the HomematicIP cover from a config entry."""
- home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
+ hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]]
devices = []
- for device in home.devices:
+ for device in hap.home.devices:
if isinstance(device, AsyncFullFlushBlind):
- devices.append(HomematicipCoverSlats(home, device))
+ devices.append(HomematicipCoverSlats(hap, device))
elif isinstance(device, AsyncFullFlushShutter):
- devices.append(HomematicipCoverShutter(home, device))
+ devices.append(HomematicipCoverShutter(hap, device))
if devices:
async_add_entities(devices)
diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py
index 1273278189d..b05c0e06928 100644
--- a/homeassistant/components/homematicip_cloud/device.py
+++ b/homeassistant/components/homematicip_cloud/device.py
@@ -3,13 +3,15 @@ import logging
from typing import Optional
from homematicip.aio.device import AsyncDevice
-from homematicip.aio.home import AsyncHome
+from homematicip.aio.group import AsyncGroup
-from homeassistant.components import homematicip_cloud
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity import Entity
+from .const import DOMAIN as HMIPC_DOMAIN
+from .hap import HomematicipHAP
+
_LOGGER = logging.getLogger(__name__)
ATTR_MODEL_TYPE = "model_type"
@@ -35,22 +37,25 @@ DEVICE_ATTRIBUTE_ICONS = {
DEVICE_ATTRIBUTES = {
"modelType": ATTR_MODEL_TYPE,
- "id": ATTR_ID,
"sabotage": ATTR_SABOTAGE,
"rssiDeviceValue": ATTR_RSSI_DEVICE,
"rssiPeerValue": ATTR_RSSI_PEER,
"deviceOverheated": ATTR_DEVICE_OVERHEATED,
"deviceOverloaded": ATTR_DEVICE_OVERLOADED,
"deviceUndervoltage": ATTR_DEVICE_UNTERVOLTAGE,
+ "id": ATTR_ID,
}
+GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE}
+
class HomematicipGenericDevice(Entity):
"""Representation of an HomematicIP generic device."""
- def __init__(self, home: AsyncHome, device, post: Optional[str] = None) -> None:
+ def __init__(self, hap: HomematicipHAP, device, post: Optional[str] = None) -> None:
"""Initialize the generic device."""
- self._home = home
+ self._hap = hap
+ self._home = hap.home
self._device = device
self.post = post
# Marker showing that the HmIP device hase been removed.
@@ -65,18 +70,19 @@ class HomematicipGenericDevice(Entity):
return {
"identifiers": {
# Serial numbers of Homematic IP device
- (homematicip_cloud.DOMAIN, self._device.id)
+ (HMIPC_DOMAIN, self._device.id)
},
"name": self._device.label,
"manufacturer": self._device.oem,
"model": self._device.modelType,
"sw_version": self._device.firmwareVersion,
- "via_device": (homematicip_cloud.DOMAIN, self._device.homeId),
+ "via_device": (HMIPC_DOMAIN, self._device.homeId),
}
return None
async def async_added_to_hass(self):
"""Register callbacks."""
+ self._hap.hmip_device_by_entity_id[self.entity_id] = self._device
self._device.on_update(self._async_device_changed)
self._device.on_remove(self._async_device_removed)
@@ -100,6 +106,7 @@ class HomematicipGenericDevice(Entity):
# Only go further if the device/entity should be removed from registries
# due to a removal of the HmIP device.
if self.hmip_device_removed:
+ del self._hap.hmip_device_by_entity_id[self.entity_id]
await self.async_remove_from_registries()
async def async_remove_from_registries(self) -> None:
@@ -173,6 +180,7 @@ class HomematicipGenericDevice(Entity):
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
state_attr = {}
+
if isinstance(self._device, AsyncDevice):
for attr, attr_key in DEVICE_ATTRIBUTES.items():
attr_value = getattr(self._device, attr, None)
@@ -181,4 +189,12 @@ class HomematicipGenericDevice(Entity):
state_attr[ATTR_IS_GROUP] = False
+ if isinstance(self._device, AsyncGroup):
+ for attr, attr_key in GROUP_ATTRIBUTES.items():
+ attr_value = getattr(self._device, attr, None)
+ if attr_value:
+ state_attr[attr_key] = attr_value
+
+ state_attr[ATTR_IS_GROUP] = True
+
return state_attr
diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py
index abba183d339..bef04180c6f 100644
--- a/homeassistant/components/homematicip_cloud/hap.py
+++ b/homeassistant/components/homematicip_cloud/hap.py
@@ -8,9 +8,10 @@ from homematicip.base.base_connection import HmipConnectionError
from homematicip.base.enums import EventType
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.typing import HomeAssistantType
from .const import COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN
from .errors import HmipcConnectionError
@@ -53,7 +54,7 @@ class HomematicipAuth:
except HmipConnectionError:
return False
- async def get_auth(self, hass, hapid, pin):
+ async def get_auth(self, hass: HomeAssistantType, hapid, pin):
"""Create a HomematicIP access point object."""
auth = AsyncAuth(hass.loop, async_get_clientsession(hass))
try:
@@ -69,7 +70,7 @@ class HomematicipAuth:
class HomematicipHAP:
"""Manages HomematicIP HTTP and WebSocket connection."""
- def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
+ def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry) -> None:
"""Initialize HomematicIP Cloud connection."""
self.hass = hass
self.config_entry = config_entry
@@ -79,6 +80,7 @@ class HomematicipHAP:
self._retry_task = None
self._tries = 0
self._accesspoint_connected = True
+ self.hmip_device_by_entity_id = {}
async def async_setup(self, tries: int = 0):
"""Initialize connection."""
@@ -219,10 +221,11 @@ class HomematicipHAP:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, component
)
+ self.hmip_device_by_entity_id = {}
return True
async def get_hap(
- self, hass: HomeAssistant, hapid: str, authtoken: str, name: str
+ self, hass: HomeAssistantType, hapid: str, authtoken: str, name: str
) -> AsyncHome:
"""Create a HomematicIP access point object."""
home = AsyncHome(hass.loop, async_get_clientsession(hass))
diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py
index 42ff6d30478..46a8d95729f 100644
--- a/homeassistant/components/homematicip_cloud/light.py
+++ b/homeassistant/components/homematicip_cloud/light.py
@@ -9,7 +9,6 @@ from homematicip.aio.device import (
AsyncFullFlushDimmer,
AsyncPluggableDimmer,
)
-from homematicip.aio.home import AsyncHome
from homematicip.base.enums import RGBColorState
from homematicip.base.functionalChannels import NotificationLightChannel
@@ -22,9 +21,10 @@ from homeassistant.components.light import (
Light,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
+from .hap import HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -38,29 +38,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the HomematicIP Cloud lights from a config entry."""
- home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
+ hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]]
devices = []
- for device in home.devices:
+ for device in hap.home.devices:
if isinstance(device, AsyncBrandSwitchMeasuring):
- devices.append(HomematicipLightMeasuring(home, device))
+ devices.append(HomematicipLightMeasuring(hap, device))
elif isinstance(device, AsyncBrandSwitchNotificationLight):
- devices.append(HomematicipLight(home, device))
+ devices.append(HomematicipLight(hap, device))
devices.append(
- HomematicipNotificationLight(home, device, device.topLightChannelIndex)
+ HomematicipNotificationLight(hap, device, device.topLightChannelIndex)
)
devices.append(
HomematicipNotificationLight(
- home, device, device.bottomLightChannelIndex
+ hap, device, device.bottomLightChannelIndex
)
)
elif isinstance(
device,
(AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer),
):
- devices.append(HomematicipDimmer(home, device))
+ devices.append(HomematicipDimmer(hap, device))
if devices:
async_add_entities(devices)
@@ -69,9 +69,9 @@ async def async_setup_entry(
class HomematicipLight(HomematicipGenericDevice, Light):
"""Representation of a HomematicIP Cloud light device."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the light device."""
- super().__init__(home, device)
+ super().__init__(hap, device)
@property
def is_on(self) -> bool:
@@ -107,9 +107,9 @@ class HomematicipLightMeasuring(HomematicipLight):
class HomematicipDimmer(HomematicipGenericDevice, Light):
"""Representation of HomematicIP Cloud dimmer light device."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the dimmer light device."""
- super().__init__(home, device)
+ super().__init__(hap, device)
@property
def is_on(self) -> bool:
@@ -119,9 +119,7 @@ class HomematicipDimmer(HomematicipGenericDevice, Light):
@property
def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
- if self._device.dimLevel:
- return int(self._device.dimLevel * 255)
- return 0
+ return int((self._device.dimLevel or 0.0) * 255)
@property
def supported_features(self) -> int:
@@ -143,13 +141,13 @@ class HomematicipDimmer(HomematicipGenericDevice, Light):
class HomematicipNotificationLight(HomematicipGenericDevice, Light):
"""Representation of HomematicIP Cloud dimmer light device."""
- def __init__(self, home: AsyncHome, device, channel: int) -> None:
+ def __init__(self, hap: HomematicipHAP, device, channel: int) -> None:
"""Initialize the dimmer light device."""
self.channel = channel
if self.channel == 2:
- super().__init__(home, device, "Top")
+ super().__init__(hap, device, "Top")
else:
- super().__init__(home, device, "Bottom")
+ super().__init__(hap, device, "Bottom")
self._color_switcher = {
RGBColorState.WHITE: [0.0, 0.0],
@@ -176,9 +174,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light):
@property
def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
- if self._func_channel.dimLevel:
- return int(self._func_channel.dimLevel * 255)
- return 0
+ return int((self._func_channel.dimLevel or 0.0) * 255)
@property
def hs_color(self) -> tuple:
diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json
index 40c8c7c3598..4feef19c8da 100644
--- a/homeassistant/components/homematicip_cloud/manifest.json
+++ b/homeassistant/components/homematicip_cloud/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"requirements": [
- "homematicip==0.10.12"
+ "homematicip==0.10.13"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py
index 770921288b9..9caa72ba15f 100644
--- a/homeassistant/components/homematicip_cloud/sensor.py
+++ b/homeassistant/components/homematicip_cloud/sensor.py
@@ -20,7 +20,6 @@ from homematicip.aio.device import (
AsyncWeatherSensorPlus,
AsyncWeatherSensorPro,
)
-from homematicip.aio.home import AsyncHome
from homematicip.base.enums import ValveState
from homeassistant.config_entries import ConfigEntry
@@ -32,10 +31,11 @@ from homeassistant.const import (
POWER_WATT,
TEMP_CELSIUS,
)
-from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE
+from .hap import HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -52,15 +52,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the HomematicIP Cloud sensors from a config entry."""
- home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
- devices = [HomematicipAccesspointStatus(home)]
- for device in home.devices:
+ hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]]
+ devices = [HomematicipAccesspointStatus(hap)]
+ for device in hap.home.devices:
if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)):
- devices.append(HomematicipHeatingThermostat(home, device))
- devices.append(HomematicipTemperatureSensor(home, device))
+ devices.append(HomematicipHeatingThermostat(hap, device))
+ devices.append(HomematicipTemperatureSensor(hap, device))
if isinstance(
device,
(
@@ -72,8 +72,8 @@ async def async_setup_entry(
AsyncWeatherSensorPro,
),
):
- devices.append(HomematicipTemperatureSensor(home, device))
- devices.append(HomematicipHumiditySensor(home, device))
+ devices.append(HomematicipTemperatureSensor(hap, device))
+ devices.append(HomematicipHumiditySensor(hap, device))
if isinstance(
device,
(
@@ -87,7 +87,7 @@ async def async_setup_entry(
AsyncWeatherSensorPro,
),
):
- devices.append(HomematicipIlluminanceSensor(home, device))
+ devices.append(HomematicipIlluminanceSensor(hap, device))
if isinstance(
device,
(
@@ -96,15 +96,15 @@ async def async_setup_entry(
AsyncFullFlushSwitchMeasuring,
),
):
- devices.append(HomematicipPowerSensor(home, device))
+ devices.append(HomematicipPowerSensor(hap, device))
if isinstance(
device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro)
):
- devices.append(HomematicipWindspeedSensor(home, device))
+ devices.append(HomematicipWindspeedSensor(hap, device))
if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)):
- devices.append(HomematicipTodayRainSensor(home, device))
+ devices.append(HomematicipTodayRainSensor(hap, device))
if isinstance(device, AsyncPassageDetector):
- devices.append(HomematicipPassageDetectorDeltaCounter(home, device))
+ devices.append(HomematicipPassageDetectorDeltaCounter(hap, device))
if devices:
async_add_entities(devices)
@@ -113,9 +113,9 @@ async def async_setup_entry(
class HomematicipAccesspointStatus(HomematicipGenericDevice):
"""Representation of an HomeMaticIP Cloud access point."""
- def __init__(self, home: AsyncHome) -> None:
+ def __init__(self, hap: HomematicipHAP) -> None:
"""Initialize access point device."""
- super().__init__(home, home)
+ super().__init__(hap, hap.home)
@property
def device_info(self):
@@ -151,15 +151,20 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice):
@property
def device_state_attributes(self):
"""Return the state attributes of the access point."""
- return {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: False}
+ state_attr = super().device_state_attributes
+
+ state_attr[ATTR_MODEL_TYPE] = "HmIP-HAP"
+ state_attr[ATTR_IS_GROUP] = False
+
+ return state_attr
class HomematicipHeatingThermostat(HomematicipGenericDevice):
"""Representation of a HomematicIP heating thermostat device."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize heating thermostat device."""
- super().__init__(home, device, "Heating")
+ super().__init__(hap, device, "Heating")
@property
def icon(self) -> str:
@@ -186,9 +191,9 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice):
class HomematicipHumiditySensor(HomematicipGenericDevice):
"""Representation of a HomematicIP Cloud humidity device."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the thermometer device."""
- super().__init__(home, device, "Humidity")
+ super().__init__(hap, device, "Humidity")
@property
def device_class(self) -> str:
@@ -209,9 +214,9 @@ class HomematicipHumiditySensor(HomematicipGenericDevice):
class HomematicipTemperatureSensor(HomematicipGenericDevice):
"""Representation of a HomematicIP Cloud thermometer device."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the thermometer device."""
- super().__init__(home, device, "Temperature")
+ super().__init__(hap, device, "Temperature")
@property
def device_class(self) -> str:
@@ -246,9 +251,9 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice):
class HomematicipIlluminanceSensor(HomematicipGenericDevice):
"""Representation of a HomematicIP Illuminance device."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
- super().__init__(home, device, "Illuminance")
+ super().__init__(hap, device, "Illuminance")
@property
def device_class(self) -> str:
@@ -272,9 +277,9 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice):
class HomematicipPowerSensor(HomematicipGenericDevice):
"""Representation of a HomematicIP power measuring device."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
- super().__init__(home, device, "Power")
+ super().__init__(hap, device, "Power")
@property
def device_class(self) -> str:
@@ -295,9 +300,9 @@ class HomematicipPowerSensor(HomematicipGenericDevice):
class HomematicipWindspeedSensor(HomematicipGenericDevice):
"""Representation of a HomematicIP wind speed sensor."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
- super().__init__(home, device, "Windspeed")
+ super().__init__(hap, device, "Windspeed")
@property
def state(self) -> float:
@@ -315,7 +320,7 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice):
state_attr = super().device_state_attributes
wind_direction = getattr(self._device, "windDirection", None)
- if wind_direction:
+ if wind_direction is not None:
state_attr[ATTR_WIND_DIRECTION] = _get_wind_direction(wind_direction)
wind_direction_variation = getattr(self._device, "windDirectionVariation", None)
@@ -328,9 +333,9 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice):
class HomematicipTodayRainSensor(HomematicipGenericDevice):
"""Representation of a HomematicIP rain counter of a day sensor."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
- super().__init__(home, device, "Today Rain")
+ super().__init__(hap, device, "Today Rain")
@property
def state(self) -> float:
@@ -346,10 +351,6 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice):
class HomematicipPassageDetectorDeltaCounter(HomematicipGenericDevice):
"""Representation of a HomematicIP passage detector delta counter."""
- def __init__(self, home: AsyncHome, device) -> None:
- """Initialize the device."""
- super().__init__(home, device)
-
@property
def state(self) -> int:
"""Representation of the HomematicIP passage detector delta counter value."""
diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml
index cf93b3065ee..f426c9b5d22 100644
--- a/homeassistant/components/homematicip_cloud/services.yaml
+++ b/homeassistant/components/homematicip_cloud/services.yaml
@@ -7,7 +7,7 @@ activate_eco_mode_with_duration:
description: The duration of eco mode in minutes.
example: 60
accesspoint_id:
- description: The ID of the Homematic IP Access Point
+ description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx
activate_eco_mode_with_period:
@@ -17,7 +17,7 @@ activate_eco_mode_with_period:
description: The time when the eco mode should automatically be disabled.
example: 2019-02-17 14:00
accesspoint_id:
- description: The ID of the Homematic IP Access Point
+ description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx
activate_vacation:
@@ -30,20 +30,31 @@ activate_vacation:
description: the set temperature during the vacation mode.
example: 18.5
accesspoint_id:
- description: The ID of the Homematic IP Access Point
+ description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx
deactivate_eco_mode:
description: Deactivates the eco mode immediately.
fields:
accesspoint_id:
- description: The ID of the Homematic IP Access Point
+ description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx
deactivate_vacation:
description: Deactivates the vacation mode immediately.
fields:
accesspoint_id:
- description: The ID of the Homematic IP Access Point
+ description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx
+set_active_climate_profile:
+ description: Set the active climate profile index.
+ fields:
+ entity_id:
+ description: The ID of the climte entity. Use 'all' keyword to switch the profile for all entities.
+ example: climate.livingroom
+ climate_profile_index:
+ description: The index of the climate profile (1 based)
+ example: 1
+
+
diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py
index ababf793f0c..dae6019b378 100644
--- a/homeassistant/components/homematicip_cloud/switch.py
+++ b/homeassistant/components/homematicip_cloud/switch.py
@@ -12,14 +12,14 @@ from homematicip.aio.device import (
AsyncPrintedCircuitBoardSwitchBattery,
)
from homematicip.aio.group import AsyncSwitchingGroup
-from homematicip.aio.home import AsyncHome
from homeassistant.components.switch import SwitchDevice
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
-from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP
+from .device import ATTR_GROUP_MEMBER_UNREACHABLE
+from .hap import HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -30,12 +30,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the HomematicIP switch from a config entry."""
- home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
+ hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]]
devices = []
- for device in home.devices:
+ for device in hap.home.devices:
if isinstance(device, AsyncBrandSwitchMeasuring):
# BrandSwitchMeasuring inherits PlugableSwitchMeasuring
# This device is implemented in the light platform and will
@@ -44,24 +44,24 @@ async def async_setup_entry(
elif isinstance(
device, (AsyncPlugableSwitchMeasuring, AsyncFullFlushSwitchMeasuring)
):
- devices.append(HomematicipSwitchMeasuring(home, device))
+ devices.append(HomematicipSwitchMeasuring(hap, device))
elif isinstance(
device, (AsyncPlugableSwitch, AsyncPrintedCircuitBoardSwitchBattery)
):
- devices.append(HomematicipSwitch(home, device))
+ devices.append(HomematicipSwitch(hap, device))
elif isinstance(device, AsyncOpenCollector8Module):
for channel in range(1, 9):
- devices.append(HomematicipMultiSwitch(home, device, channel))
+ devices.append(HomematicipMultiSwitch(hap, device, channel))
elif isinstance(device, AsyncMultiIOBox):
for channel in range(1, 3):
- devices.append(HomematicipMultiSwitch(home, device, channel))
+ devices.append(HomematicipMultiSwitch(hap, device, channel))
elif isinstance(device, AsyncPrintedCircuitBoardSwitch2):
for channel in range(1, 3):
- devices.append(HomematicipMultiSwitch(home, device, channel))
+ devices.append(HomematicipMultiSwitch(hap, device, channel))
- for group in home.groups:
+ for group in hap.home.groups:
if isinstance(group, AsyncSwitchingGroup):
- devices.append(HomematicipGroupSwitch(home, group))
+ devices.append(HomematicipGroupSwitch(hap, group))
if devices:
async_add_entities(devices)
@@ -70,9 +70,9 @@ async def async_setup_entry(
class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice):
"""representation of a HomematicIP Cloud switch device."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the switch device."""
- super().__init__(home, device)
+ super().__init__(hap, device)
@property
def is_on(self) -> bool:
@@ -91,10 +91,10 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice):
class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice):
"""representation of a HomematicIP switching group."""
- def __init__(self, home: AsyncHome, device, post: str = "Group") -> None:
+ def __init__(self, hap: HomematicipHAP, device, post: str = "Group") -> None:
"""Initialize switching group."""
device.modelType = f"HmIP-{post}"
- super().__init__(home, device, post)
+ super().__init__(hap, device, post)
@property
def is_on(self) -> bool:
@@ -113,9 +113,11 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice):
@property
def device_state_attributes(self):
"""Return the state attributes of the switch-group."""
- state_attr = {ATTR_IS_GROUP: True}
+ state_attr = super().device_state_attributes
+
if self._device.unreach:
state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True
+
return state_attr
async def async_turn_on(self, **kwargs):
@@ -146,10 +148,10 @@ class HomematicipSwitchMeasuring(HomematicipSwitch):
class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice):
"""Representation of a HomematicIP Cloud multi switch device."""
- def __init__(self, home: AsyncHome, device, channel: int):
+ def __init__(self, hap: HomematicipHAP, device, channel: int):
"""Initialize the multi switch device."""
self.channel = channel
- super().__init__(home, device, f"Channel{channel}")
+ super().__init__(hap, device, f"Channel{channel}")
@property
def unique_id(self) -> str:
diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py
index ed9098559a3..5aa3f28c45d 100644
--- a/homeassistant/components/homematicip_cloud/weather.py
+++ b/homeassistant/components/homematicip_cloud/weather.py
@@ -6,15 +6,15 @@ from homematicip.aio.device import (
AsyncWeatherSensorPlus,
AsyncWeatherSensorPro,
)
-from homematicip.aio.home import AsyncHome
from homematicip.base.enums import WeatherCondition
from homeassistant.components.weather import WeatherEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS
-from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
+from .hap import HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -43,18 +43,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
- hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the HomematicIP weather sensor from a config entry."""
- home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
+ hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]]
devices = []
- for device in home.devices:
+ for device in hap.home.devices:
if isinstance(device, AsyncWeatherSensorPro):
- devices.append(HomematicipWeatherSensorPro(home, device))
+ devices.append(HomematicipWeatherSensorPro(hap, device))
elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)):
- devices.append(HomematicipWeatherSensor(home, device))
+ devices.append(HomematicipWeatherSensor(hap, device))
- devices.append(HomematicipHomeWeather(home))
+ devices.append(HomematicipHomeWeather(hap))
if devices:
async_add_entities(devices)
@@ -63,9 +63,9 @@ async def async_setup_entry(
class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity):
"""representation of a HomematicIP Cloud weather sensor plus & basic."""
- def __init__(self, home: AsyncHome, device) -> None:
+ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the weather sensor."""
- super().__init__(home, device)
+ super().__init__(hap, device)
@property
def name(self) -> str:
@@ -121,10 +121,10 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor):
class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity):
"""representation of a HomematicIP Cloud home weather."""
- def __init__(self, home: AsyncHome) -> None:
+ def __init__(self, hap: HomematicipHAP) -> None:
"""Initialize the home weather."""
- home.weather.modelType = "HmIP-Home-Weather"
- super().__init__(home, home)
+ hap.home.modelType = "HmIP-Home-Weather"
+ super().__init__(hap, hap.home)
@property
def available(self) -> bool:
diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py
index cf95c21a8d1..04c715dc010 100644
--- a/homeassistant/components/hp_ilo/sensor.py
+++ b/homeassistant/components/hp_ilo/sensor.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+import hpilo
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -180,8 +181,6 @@ class HpIloData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from HP iLO."""
- import hpilo
-
try:
self.data = hpilo.Ilo(
hostname=self._host,
diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py
index ac76911b9f6..18b7ff27ab4 100644
--- a/homeassistant/components/html5/notify.py
+++ b/homeassistant/components/html5/notify.py
@@ -9,6 +9,9 @@ import time
import uuid
from aiohttp.hdrs import AUTHORIZATION
+import jwt
+from pywebpush import WebPusher
+from py_vapid import Vapid
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -56,7 +59,7 @@ def gcm_api_deprecated(value):
"Configuring html5_push_notifications via the GCM api"
" has been deprecated and will stop working after April 11,"
" 2019. Use the VAPID configuration instead. For instructions,"
- " see https://www.home-assistant.io/components/notify.html5/"
+ " see https://www.home-assistant.io/integrations/html5/"
)
return value
@@ -311,7 +314,6 @@ class HTML5PushCallbackView(HomeAssistantView):
def decode_jwt(self, token):
"""Find the registration that signed this JWT and return it."""
- import jwt
# 1. Check claims w/o verifying to see if a target is in there.
# 2. If target in claims, attempt to verify against the given name.
@@ -335,7 +337,6 @@ class HTML5PushCallbackView(HomeAssistantView):
# https://auth0.com/docs/quickstart/backend/python
def check_authorization_header(self, request):
"""Check the authorization header."""
- import jwt
auth = request.headers.get(AUTHORIZATION, None)
if not auth:
@@ -491,7 +492,6 @@ class HTML5NotificationService(BaseNotificationService):
def _push_message(self, payload, **kwargs):
"""Send the message."""
- from pywebpush import WebPusher
timestamp = int(time.time())
ttl = int(kwargs.get(ATTR_TTL, DEFAULT_TTL))
@@ -550,7 +550,6 @@ class HTML5NotificationService(BaseNotificationService):
def add_jwt(timestamp, target, tag, jwt_secret):
"""Create JWT json to put into payload."""
- import jwt
jwt_exp = datetime.fromtimestamp(timestamp) + timedelta(days=JWT_VALID_DAYS)
jwt_claims = {
@@ -565,7 +564,6 @@ def add_jwt(timestamp, target, tag, jwt_secret):
def create_vapid_headers(vapid_email, subscription_info, vapid_private_key):
"""Create encrypted headers to send to WebPusher."""
- from py_vapid import Vapid
if vapid_email and vapid_private_key and ATTR_ENDPOINT in subscription_info:
url = urlparse(subscription_info.get(ATTR_ENDPOINT))
diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py
index a8aaa3390a7..4df606a3c1b 100644
--- a/homeassistant/components/http/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -17,7 +17,6 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util as hass_util
from homeassistant.util import ssl as ssl_util
-from homeassistant.util.logging import HideSensitiveDataFilter
from .auth import setup_auth
from .ban import setup_bans
@@ -32,7 +31,6 @@ from .view import HomeAssistantView # noqa
DOMAIN = "http"
-CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host"
CONF_SERVER_PORT = "server_port"
CONF_BASE_URL = "base_url"
@@ -42,7 +40,6 @@ CONF_SSL_KEY = "ssl_key"
CONF_CORS_ORIGINS = "cors_allowed_origins"
CONF_USE_X_FORWARDED_FOR = "use_x_forwarded_for"
CONF_TRUSTED_PROXIES = "trusted_proxies"
-CONF_TRUSTED_NETWORKS = "trusted_networks"
CONF_LOGIN_ATTEMPTS_THRESHOLD = "login_attempts_threshold"
CONF_IP_BAN_ENABLED = "ip_ban_enabled"
CONF_SSL_PROFILE = "ssl_profile"
@@ -59,37 +56,8 @@ DEFAULT_CORS = "https://cast.home-assistant.io"
NO_LOGIN_ATTEMPT_THRESHOLD = -1
-def trusted_networks_deprecated(value):
- """Warn user trusted_networks config is deprecated."""
- if not value:
- return value
-
- _LOGGER.warning(
- "Configuring trusted_networks via the http integration has been"
- " deprecated. Use the trusted networks auth provider instead."
- " For instructions, see https://www.home-assistant.io/docs/"
- "authentication/providers/#trusted-networks"
- )
- return value
-
-
-def api_password_deprecated(value):
- """Warn user api_password config is deprecated."""
- if not value:
- return value
-
- _LOGGER.warning(
- "Configuring api_password via the http integration has been"
- " deprecated. Use the legacy api password auth provider instead."
- " For instructions, see https://www.home-assistant.io/docs/"
- "authentication/providers/#legacy-api-password"
- )
- return value
-
-
HTTP_SCHEMA = vol.Schema(
{
- vol.Optional(CONF_API_PASSWORD): vol.All(cv.string, api_password_deprecated),
vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string,
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
vol.Optional(CONF_BASE_URL): cv.string,
@@ -103,9 +71,6 @@ HTTP_SCHEMA = vol.Schema(
vol.Inclusive(CONF_TRUSTED_PROXIES, "proxy"): vol.All(
cv.ensure_list, [ip_network]
),
- vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All(
- cv.ensure_list, [ip_network], trusted_networks_deprecated
- ),
vol.Optional(
CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD
): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD),
@@ -128,6 +93,7 @@ class ApiConfig:
"""Initialize a new API config object."""
self.host = host
self.port = port
+ self.use_ssl = use_ssl
host = host.rstrip("/")
if host.startswith(("http://", "https://")):
@@ -148,7 +114,6 @@ async def async_setup(hass, config):
if conf is None:
conf = HTTP_SCHEMA({})
- api_password = conf.get(CONF_API_PASSWORD)
server_host = conf[CONF_SERVER_HOST]
server_port = conf[CONF_SERVER_PORT]
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
@@ -161,11 +126,6 @@ async def async_setup(hass, config):
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
ssl_profile = conf[CONF_SSL_PROFILE]
- if api_password is not None:
- logging.getLogger("aiohttp.access").addFilter(
- HideSensitiveDataFilter(api_password)
- )
-
server = HomeAssistantHTTP(
hass,
server_host=server_host,
diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py
index 4ff581aef02..97bd9b7d4bc 100644
--- a/homeassistant/components/http/auth.py
+++ b/homeassistant/components/http/auth.py
@@ -1,14 +1,11 @@
"""Authentication for HTTP component."""
-import base64
import logging
from aiohttp import hdrs
from aiohttp.web import middleware
import jwt
-from homeassistant.auth.providers import legacy_api_password
from homeassistant.auth.util import generate_secret
-from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.core import callback
from homeassistant.util import dt as dt_util
@@ -52,16 +49,6 @@ def async_sign_path(hass, refresh_token_id, path, expiration):
@callback
def setup_auth(hass, app):
"""Create auth middleware for the app."""
- old_auth_warning = set()
-
- support_legacy = hass.auth.support_legacy
- if support_legacy:
- _LOGGER.warning("legacy_api_password support has been enabled.")
-
- trusted_networks = []
- for prv in hass.auth.auth_providers:
- if prv.type == "trusted_networks":
- trusted_networks += prv.trusted_networks
async def async_validate_auth_header(request):
"""
@@ -75,40 +62,16 @@ def setup_auth(hass, app):
# If no space in authorization header
return False
- if auth_type == "Bearer":
- refresh_token = await hass.auth.async_validate_access_token(auth_val)
- if refresh_token is None:
- return False
+ if auth_type != "Bearer":
+ return False
- request[KEY_HASS_USER] = refresh_token.user
- return True
+ refresh_token = await hass.auth.async_validate_access_token(auth_val)
- if auth_type == "Basic" and support_legacy:
- decoded = base64.b64decode(auth_val).decode("utf-8")
- try:
- username, password = decoded.split(":", 1)
- except ValueError:
- # If no ':' in decoded
- return False
+ if refresh_token is None:
+ return False
- if username != "homeassistant":
- return False
-
- user = await legacy_api_password.async_validate_password(hass, password)
- if user is None:
- return False
-
- request[KEY_HASS_USER] = user
- _LOGGER.info(
- "Basic auth with api_password is going to deprecate,"
- " please use a bearer token to access %s from %s",
- request.path,
- request[KEY_REAL_IP],
- )
- old_auth_warning.add(request.path)
- return True
-
- return False
+ request[KEY_HASS_USER] = refresh_token.user
+ return True
async def async_validate_signed_request(request):
"""Validate a signed request."""
@@ -140,50 +103,16 @@ def setup_auth(hass, app):
request[KEY_HASS_USER] = refresh_token.user
return True
- async def async_validate_trusted_networks(request):
- """Test if request is from a trusted ip."""
- ip_addr = request[KEY_REAL_IP]
-
- if not any(ip_addr in trusted_network for trusted_network in trusted_networks):
- return False
-
- user = await hass.auth.async_get_owner()
- if user is None:
- return False
-
- request[KEY_HASS_USER] = user
- return True
-
- async def async_validate_legacy_api_password(request, password):
- """Validate api_password."""
- user = await legacy_api_password.async_validate_password(hass, password)
- if user is None:
- return False
-
- request[KEY_HASS_USER] = user
- return True
-
@middleware
async def auth_middleware(request, handler):
"""Authenticate as middleware."""
authenticated = False
- if HTTP_HEADER_HA_AUTH in request.headers or DATA_API_PASSWORD in request.query:
- if request.path not in old_auth_warning:
- _LOGGER.log(
- logging.INFO if support_legacy else logging.WARNING,
- "api_password is going to deprecate. You need to use a"
- " bearer token to access %s from %s",
- request.path,
- request[KEY_REAL_IP],
- )
- old_auth_warning.add(request.path)
-
if hdrs.AUTHORIZATION in request.headers and await async_validate_auth_header(
request
):
- # it included both use_auth and api_password Basic auth
authenticated = True
+ auth_type = "bearer token"
# We first start with a string check to avoid parsing query params
# for every request.
@@ -193,39 +122,15 @@ def setup_auth(hass, app):
and await async_validate_signed_request(request)
):
authenticated = True
+ auth_type = "signed request"
- elif trusted_networks and await async_validate_trusted_networks(request):
- if request.path not in old_auth_warning:
- # When removing this, don't forget to remove the print logic
- # in http/view.py
- request["deprecate_warning_message"] = (
- "Access from trusted networks without auth token is "
- "going to be removed in Home Assistant 0.96. Configure "
- "the trusted networks auth provider or use long-lived "
- "access tokens to access {} from {}".format(
- request.path, request[KEY_REAL_IP]
- )
- )
- old_auth_warning.add(request.path)
- authenticated = True
-
- elif (
- support_legacy
- and HTTP_HEADER_HA_AUTH in request.headers
- and await async_validate_legacy_api_password(
- request, request.headers[HTTP_HEADER_HA_AUTH]
+ if authenticated:
+ _LOGGER.debug(
+ "Authenticated %s for %s using %s",
+ request[KEY_REAL_IP],
+ request.path,
+ auth_type,
)
- ):
- authenticated = True
-
- elif (
- support_legacy
- and DATA_API_PASSWORD in request.query
- and await async_validate_legacy_api_password(
- request, request.query[DATA_API_PASSWORD]
- )
- ):
- authenticated = True
request[KEY_AUTHENTICATED] = authenticated
return await handler(request)
diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py
index 39ff45fd4e4..de4547f4782 100644
--- a/homeassistant/components/http/cors.py
+++ b/homeassistant/components/http/cors.py
@@ -2,7 +2,7 @@
from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource
from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION
-from homeassistant.const import HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH
+from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH
from homeassistant.core import callback
@@ -13,7 +13,6 @@ ALLOWED_CORS_HEADERS = [
ACCEPT,
HTTP_HEADER_X_REQUESTED_WITH,
CONTENT_TYPE,
- HTTP_HEADER_HA_AUTH,
AUTHORIZATION,
]
VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource)
@@ -22,6 +21,8 @@ VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource)
@callback
def setup_cors(app, origins):
"""Set up CORS."""
+ # This import should remain here. That way the HTTP integration can always
+ # be imported by other integrations without it's requirements being installed.
import aiohttp_cors
cors = aiohttp_cors.setup(
diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py
index 66864eba55e..804c90d4f96 100644
--- a/homeassistant/components/http/view.py
+++ b/homeassistant/components/http/view.py
@@ -17,7 +17,6 @@ from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import Context, is_callback
from homeassistant.helpers.json import JSONEncoder
-from .ban import process_success_login
from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP
_LOGGER = logging.getLogger(__name__)
@@ -106,13 +105,8 @@ def request_handler_factory(view, handler):
authenticated = request.get(KEY_AUTHENTICATED, False)
- if view.requires_auth:
- if authenticated:
- if "deprecate_warning_message" in request:
- _LOGGER.warning(request["deprecate_warning_message"])
- await process_success_login(request)
- else:
- raise HTTPUnauthorized()
+ if view.requires_auth and not authenticated:
+ raise HTTPUnauthorized()
_LOGGER.debug(
"Serving %s to %s (auth: %s)",
diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py
index f94b11d5ada..954ba60abbf 100644
--- a/homeassistant/components/htu21d/sensor.py
+++ b/homeassistant/components/htu21d/sensor.py
@@ -3,11 +3,13 @@ from datetime import timedelta
from functools import partial
import logging
+from i2csense.htu21d import HTU21D # pylint: disable=import-error
+import smbus # pylint: disable=import-error
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME, TEMP_FAHRENHEIT
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.util.temperature import celsius_to_fahrenheit
@@ -34,9 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the HTU21D sensor."""
- import smbus # pylint: disable=import-error
- from i2csense.htu21d import HTU21D # pylint: disable=import-error
-
name = config.get(CONF_NAME)
bus_number = config.get(CONF_I2C_BUS)
temp_unit = hass.config.units.temperature_unit
diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json
index 9062e427a27..33b1ffbfe86 100644
--- a/homeassistant/components/hue/.translations/pl.json
+++ b/homeassistant/components/hue/.translations/pl.json
@@ -12,7 +12,7 @@
},
"error": {
"linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.",
- "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."
+ "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie."
},
"step": {
"init": {
diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json
index 79a46e1861b..c749a498e44 100644
--- a/homeassistant/components/hue/.translations/ru.json
+++ b/homeassistant/components/hue/.translations/ru.json
@@ -2,17 +2,17 @@
"config": {
"abort": {
"all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.",
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.",
- "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"
+ "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": {
- "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f",
- "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430"
+ "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.",
+ "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430."
},
"step": {
"init": {
@@ -23,7 +23,7 @@
},
"link": {
"description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435 \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 Philips Hue \u0432 Home Assistant.\n\n",
- "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c"
+ "title": "Philips Hue"
}
},
"title": "Philips Hue"
diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py
index 064e18e7a81..027ec205195 100644
--- a/homeassistant/components/hue/__init__.py
+++ b/homeassistant/components/hue/__init__.py
@@ -8,11 +8,11 @@ from homeassistant import config_entries
from homeassistant.const import CONF_FILENAME, CONF_HOST
from homeassistant.helpers import config_validation as cv, device_registry as dr
-from .const import DOMAIN
from .bridge import HueBridge
-
-# Loading the config flow file will register the flow
-from .config_flow import configured_hosts
+from .config_flow import (
+ configured_hosts,
+) # Loading the config flow file will register the flow
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py
index 9c84cb5d61c..e4b7dd85e37 100644
--- a/homeassistant/components/hue/binary_sensor.py
+++ b/homeassistant/components/hue/binary_sensor.py
@@ -1,19 +1,31 @@
"""Hue binary sensor entities."""
+
+from aiohue.sensors import TYPE_ZLL_PRESENCE
+
from homeassistant.components.binary_sensor import (
- BinarySensorDevice,
DEVICE_CLASS_MOTION,
+ BinarySensorDevice,
)
from homeassistant.components.hue.sensor_base import (
GenericZLLSensor,
+ SensorManager,
async_setup_entry as shared_async_setup_entry,
)
-
PRESENCE_NAME_FORMAT = "{} motion"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Defer binary sensor setup to the shared sensor module."""
+ SensorManager.sensor_config_map.update(
+ {
+ TYPE_ZLL_PRESENCE: {
+ "binary": True,
+ "name_format": PRESENCE_NAME_FORMAT,
+ "class": HuePresence,
+ }
+ }
+ )
await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=True)
diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py
index 9c0e94bc3bd..ebd71ba7c1c 100644
--- a/homeassistant/components/hue/config_flow.py
+++ b/homeassistant/components/hue/config_flow.py
@@ -50,6 +50,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+
def __init__(self):
"""Initialize the Hue flow."""
self.host = None
diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py
new file mode 100644
index 00000000000..971509ab647
--- /dev/null
+++ b/homeassistant/components/hue/helpers.py
@@ -0,0 +1,33 @@
+"""Helper functions for Philips Hue."""
+from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
+from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg
+
+from .const import DOMAIN
+
+
+async def remove_devices(hass, config_entry, api_ids, current):
+ """Get items that are removed from api."""
+ removed_items = []
+
+ for item_id in current:
+ if item_id in api_ids:
+ continue
+
+ # Device is removed from Hue, so we remove it from Home Assistant
+ entity = current[item_id]
+ removed_items.append(item_id)
+ await entity.async_remove()
+ ent_registry = await get_ent_reg(hass)
+ if entity.entity_id in ent_registry.entities:
+ ent_registry.async_remove(entity.entity_id)
+ dev_registry = await get_dev_reg(hass)
+ device = dev_registry.async_get_device(
+ identifiers={(DOMAIN, entity.device_id)}, connections=set()
+ )
+ if device is not None:
+ dev_registry.async_update_device(
+ device.id, remove_config_entry_id=config_entry.entry_id
+ )
+
+ for item_id in removed_items:
+ del current[item_id]
diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py
index 5a3379f71ce..041eb76c1d3 100644
--- a/homeassistant/components/hue/light.py
+++ b/homeassistant/components/hue/light.py
@@ -2,37 +2,36 @@
import asyncio
from datetime import timedelta
import logging
-from time import monotonic
import random
+from time import monotonic
import aiohue
import async_timeout
-from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg
-from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
-
from homeassistant.components import hue
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
ATTR_FLASH,
- ATTR_TRANSITION,
ATTR_HS_COLOR,
+ ATTR_TRANSITION,
EFFECT_COLORLOOP,
EFFECT_RANDOM,
FLASH_LONG,
FLASH_SHORT,
SUPPORT_BRIGHTNESS,
+ SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
SUPPORT_FLASH,
- SUPPORT_COLOR,
SUPPORT_TRANSITION,
Light,
)
from homeassistant.util import color
+from .helpers import remove_devices
+
SCAN_INTERVAL = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
@@ -226,7 +225,6 @@ async def async_update_items(
bridge.available = True
new_items = []
- removed_items = []
for item_id in api:
if item_id not in current:
@@ -238,31 +236,11 @@ async def async_update_items(
elif item_id not in progress_waiting:
current[item_id].async_schedule_update_ha_state()
- for item_id in current:
- if item_id in api:
- continue
-
- # Device is removed from Hue, so we remove it from Home Assistant
- entity = current[item_id]
- removed_items.append(item_id)
- await entity.async_remove()
- ent_registry = await get_ent_reg(hass)
- if entity.entity_id in ent_registry.entities:
- ent_registry.async_remove(entity.entity_id)
- dev_registry = await get_dev_reg(hass)
- device = dev_registry.async_get_device(
- identifiers={(hue.DOMAIN, entity.unique_id)}, connections=set()
- )
- dev_registry.async_update_device(
- device.id, remove_config_entry_id=config_entry.entry_id
- )
+ await remove_devices(hass, config_entry, api, current)
if new_items:
async_add_entities(new_items)
- for item_id in removed_items:
- del current[item_id]
-
class HueLight(Light):
"""Representation of a Hue light."""
@@ -300,9 +278,14 @@ class HueLight(Light):
@property
def unique_id(self):
- """Return the ID of this Hue light."""
+ """Return the unique ID of this Hue light."""
return self.light.uniqueid
+ @property
+ def device_id(self):
+ """Return the ID of this Hue light."""
+ return self.unique_id
+
@property
def name(self):
"""Return the name of the Hue light."""
@@ -384,7 +367,7 @@ class HueLight(Light):
return None
return {
- "identifiers": {(hue.DOMAIN, self.unique_id)},
+ "identifiers": {(hue.DOMAIN, self.device_id)},
"name": self.name,
"manufacturer": self.light.manufacturername,
# productname added in Hue Bridge API 1.24
diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py
index 457ed761202..f2e02d49ecf 100644
--- a/homeassistant/components/hue/sensor.py
+++ b/homeassistant/components/hue/sensor.py
@@ -1,15 +1,17 @@
"""Hue sensor entities."""
+from aiohue.sensors import TYPE_ZLL_LIGHTLEVEL, TYPE_ZLL_TEMPERATURE
+
+from homeassistant.components.hue.sensor_base import (
+ GenericZLLSensor,
+ SensorManager,
+ async_setup_entry as shared_async_setup_entry,
+)
from homeassistant.const import (
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
TEMP_CELSIUS,
)
from homeassistant.helpers.entity import Entity
-from homeassistant.components.hue.sensor_base import (
- GenericZLLSensor,
- async_setup_entry as shared_async_setup_entry,
-)
-
LIGHT_LEVEL_NAME_FORMAT = "{} light level"
TEMPERATURE_NAME_FORMAT = "{} temperature"
@@ -17,6 +19,20 @@ TEMPERATURE_NAME_FORMAT = "{} temperature"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Defer sensor setup to the shared sensor module."""
+ SensorManager.sensor_config_map.update(
+ {
+ TYPE_ZLL_LIGHTLEVEL: {
+ "binary": False,
+ "name_format": LIGHT_LEVEL_NAME_FORMAT,
+ "class": HueLightLevel,
+ },
+ TYPE_ZLL_TEMPERATURE: {
+ "binary": False,
+ "name_format": TEMPERATURE_NAME_FORMAT,
+ "class": HueTemperature,
+ },
+ }
+ )
await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=False)
diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py
index 96b9b8bf5d6..7236dfbd886 100644
--- a/homeassistant/components/hue/sensor_base.py
+++ b/homeassistant/components/hue/sensor_base.py
@@ -4,6 +4,8 @@ from datetime import timedelta
import logging
from time import monotonic
+from aiohue import AiohueException
+from aiohue.sensors import TYPE_ZLL_PRESENCE
import async_timeout
from homeassistant.components import hue
@@ -11,6 +13,7 @@ from homeassistant.exceptions import NoEntitySpecifiedError
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
+from .helpers import remove_devices
CURRENT_SENSORS = "current_sensors"
SENSOR_MANAGER_FORMAT = "{}_sensor_manager"
@@ -34,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities, binary=False
sm_key = SENSOR_MANAGER_FORMAT.format(config_entry.data["host"])
manager = hass.data[hue.DOMAIN].get(sm_key)
if manager is None:
- manager = SensorManager(hass, bridge)
+ manager = SensorManager(hass, bridge, config_entry)
hass.data[hue.DOMAIN][sm_key] = manager
manager.register_component(binary, async_add_entities)
@@ -50,42 +53,14 @@ class SensorManager:
SCAN_INTERVAL = timedelta(seconds=5)
sensor_config_map = {}
- def __init__(self, hass, bridge):
+ def __init__(self, hass, bridge, config_entry):
"""Initialize the sensor manager."""
- import aiohue
- from .binary_sensor import HuePresence, PRESENCE_NAME_FORMAT
- from .sensor import (
- HueLightLevel,
- HueTemperature,
- LIGHT_LEVEL_NAME_FORMAT,
- TEMPERATURE_NAME_FORMAT,
- )
-
self.hass = hass
self.bridge = bridge
+ self.config_entry = config_entry
self._component_add_entities = {}
self._started = False
- self.sensor_config_map.update(
- {
- aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: {
- "binary": False,
- "name_format": LIGHT_LEVEL_NAME_FORMAT,
- "class": HueLightLevel,
- },
- aiohue.sensors.TYPE_ZLL_TEMPERATURE: {
- "binary": False,
- "name_format": TEMPERATURE_NAME_FORMAT,
- "class": HueTemperature,
- },
- aiohue.sensors.TYPE_ZLL_PRESENCE: {
- "binary": True,
- "name_format": PRESENCE_NAME_FORMAT,
- "class": HuePresence,
- },
- }
- )
-
def register_component(self, binary, async_add_entities):
"""Register async_add_entities methods for components."""
self._component_add_entities[binary] = async_add_entities
@@ -115,15 +90,13 @@ class SensorManager:
async def async_update_items(self):
"""Update sensors from the bridge."""
- import aiohue
-
api = self.bridge.api.sensors
try:
start = monotonic()
with async_timeout.timeout(4):
await api.update()
- except (asyncio.TimeoutError, aiohue.AiohueException) as err:
+ except (asyncio.TimeoutError, AiohueException) as err:
_LOGGER.debug("Failed to fetch sensor: %s", err)
if not self.bridge.available:
@@ -162,7 +135,7 @@ class SensorManager:
# finding the remaining ones that may or may not be related to the
# presence sensors.
for item_id in api:
- if api[item_id].type != aiohue.sensors.TYPE_ZLL_PRESENCE:
+ if api[item_id].type != TYPE_ZLL_PRESENCE:
continue
primary_sensor_devices[_device_id(api[item_id])] = api[item_id]
@@ -194,6 +167,13 @@ class SensorManager:
else:
new_sensors.append(current[api[item_id].uniqueid])
+ await remove_devices(
+ self.hass,
+ self.config_entry,
+ [value.uniqueid for value in api.values()],
+ current,
+ )
+
async_add_sensor_entities = self._component_add_entities.get(False)
async_add_binary_entities = self._component_add_entities.get(True)
if new_sensors and async_add_sensor_entities:
diff --git a/homeassistant/components/hydroquebec/__init__.py b/homeassistant/components/hydroquebec/__init__.py
deleted file mode 100644
index 08a12f7955e..00000000000
--- a/homeassistant/components/hydroquebec/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The hydroquebec component."""
diff --git a/homeassistant/components/hydroquebec/manifest.json b/homeassistant/components/hydroquebec/manifest.json
deleted file mode 100644
index dbe8af0b41b..00000000000
--- a/homeassistant/components/hydroquebec/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "domain": "hydroquebec",
- "name": "Hydroquebec",
- "documentation": "https://www.home-assistant.io/integrations/hydroquebec",
- "requirements": [
- "pyhydroquebec==2.2.2"
- ],
- "dependencies": [],
- "codeowners": []
-}
diff --git a/homeassistant/components/hydroquebec/sensor.py b/homeassistant/components/hydroquebec/sensor.py
deleted file mode 100644
index c3ad79c1c98..00000000000
--- a/homeassistant/components/hydroquebec/sensor.py
+++ /dev/null
@@ -1,227 +0,0 @@
-"""
-Support for HydroQuebec.
-
-Get data from 'My Consumption Profile' page:
-https://www.hydroquebec.com/portail/en/group/clientele/portrait-de-consommation
-
-For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/sensor.hydroquebec/
-"""
-import logging
-from datetime import timedelta
-
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- CONF_USERNAME,
- CONF_PASSWORD,
- ENERGY_KILO_WATT_HOUR,
- CONF_NAME,
- CONF_MONITORED_VARIABLES,
- TEMP_CELSIUS,
-)
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
-import homeassistant.helpers.config_validation as cv
-
-_LOGGER = logging.getLogger(__name__)
-
-KILOWATT_HOUR = ENERGY_KILO_WATT_HOUR
-PRICE = "CAD"
-DAYS = "days"
-CONF_CONTRACT = "contract"
-
-DEFAULT_NAME = "HydroQuebec"
-
-REQUESTS_TIMEOUT = 15
-MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
-SCAN_INTERVAL = timedelta(hours=1)
-
-SENSOR_TYPES = {
- "balance": ["Balance", PRICE, "mdi:square-inc-cash"],
- "period_total_bill": ["Period total bill", PRICE, "mdi:square-inc-cash"],
- "period_length": ["Period length", DAYS, "mdi:calendar-today"],
- "period_total_days": ["Period total days", DAYS, "mdi:calendar-today"],
- "period_mean_daily_bill": ["Period mean daily bill", PRICE, "mdi:square-inc-cash"],
- "period_mean_daily_consumption": [
- "Period mean daily consumption",
- KILOWATT_HOUR,
- "mdi:flash",
- ],
- "period_total_consumption": [
- "Period total consumption",
- KILOWATT_HOUR,
- "mdi:flash",
- ],
- "period_lower_price_consumption": [
- "Period lower price consumption",
- KILOWATT_HOUR,
- "mdi:flash",
- ],
- "period_higher_price_consumption": [
- "Period higher price consumption",
- KILOWATT_HOUR,
- "mdi:flash",
- ],
- "yesterday_total_consumption": [
- "Yesterday total consumption",
- KILOWATT_HOUR,
- "mdi:flash",
- ],
- "yesterday_lower_price_consumption": [
- "Yesterday lower price consumption",
- KILOWATT_HOUR,
- "mdi:flash",
- ],
- "yesterday_higher_price_consumption": [
- "Yesterday higher price consumption",
- KILOWATT_HOUR,
- "mdi:flash",
- ],
- "yesterday_average_temperature": [
- "Yesterday average temperature",
- TEMP_CELSIUS,
- "mdi:thermometer",
- ],
- "period_average_temperature": [
- "Period average temperature",
- TEMP_CELSIUS,
- "mdi:thermometer",
- ],
-}
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_MONITORED_VARIABLES): vol.All(
- cv.ensure_list, [vol.In(SENSOR_TYPES)]
- ),
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_CONTRACT): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
-)
-
-HOST = "https://www.hydroquebec.com"
-HOME_URL = f"{HOST}/portail/web/clientele/authentification"
-PROFILE_URL = "{}/portail/fr/group/clientele/" "portrait-de-consommation".format(HOST)
-MONTHLY_MAP = (
- ("period_total_bill", "montantFacturePeriode"),
- ("period_length", "nbJourLecturePeriode"),
- ("period_total_days", "nbJourPrevuPeriode"),
- ("period_mean_daily_bill", "moyenneDollarsJourPeriode"),
- ("period_mean_daily_consumption", "moyenneKwhJourPeriode"),
- ("period_total_consumption", "consoTotalPeriode"),
- ("period_lower_price_consumption", "consoRegPeriode"),
- ("period_higher_price_consumption", "consoHautPeriode"),
-)
-DAILY_MAP = (
- ("yesterday_total_consumption", "consoTotalQuot"),
- ("yesterday_lower_price_consumption", "consoRegQuot"),
- ("yesterday_higher_price_consumption", "consoHautQuot"),
-)
-
-
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the HydroQuebec sensor."""
- # Create a data fetcher to support all of the configured sensors. Then make
- # the first call to init the data.
-
- username = config.get(CONF_USERNAME)
- password = config.get(CONF_PASSWORD)
- contract = config.get(CONF_CONTRACT)
-
- httpsession = hass.helpers.aiohttp_client.async_get_clientsession()
- hydroquebec_data = HydroquebecData(username, password, httpsession, contract)
- contracts = await hydroquebec_data.get_contract_list()
- if not contracts:
- return
- _LOGGER.info("Contract list: %s", ", ".join(contracts))
-
- name = config.get(CONF_NAME)
-
- sensors = []
- for variable in config[CONF_MONITORED_VARIABLES]:
- sensors.append(HydroQuebecSensor(hydroquebec_data, variable, name))
-
- async_add_entities(sensors, True)
-
-
-class HydroQuebecSensor(Entity):
- """Implementation of a HydroQuebec sensor."""
-
- def __init__(self, hydroquebec_data, sensor_type, name):
- """Initialize the sensor."""
- self.client_name = name
- self.type = sensor_type
- self._name = SENSOR_TYPES[sensor_type][0]
- self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
- self._icon = SENSOR_TYPES[sensor_type][2]
- self.hydroquebec_data = hydroquebec_data
- self._state = None
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return f"{self.client_name} {self._name}"
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return self._unit_of_measurement
-
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- return self._icon
-
- async def async_update(self):
- """Get the latest data from Hydroquebec and update the state."""
- await self.hydroquebec_data.async_update()
- if self.hydroquebec_data.data.get(self.type) is not None:
- self._state = round(self.hydroquebec_data.data[self.type], 2)
-
-
-class HydroquebecData:
- """Get data from HydroQuebec."""
-
- def __init__(self, username, password, httpsession, contract=None):
- """Initialize the data object."""
- from pyhydroquebec import HydroQuebecClient
-
- self.client = HydroQuebecClient(
- username, password, REQUESTS_TIMEOUT, httpsession
- )
- self._contract = contract
- self.data = {}
-
- async def get_contract_list(self):
- """Return the contract list."""
- # Fetch data
- ret = await self._fetch_data()
- if ret:
- return self.client.get_contracts()
- return []
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- async def _fetch_data(self):
- """Fetch latest data from HydroQuebec."""
- from pyhydroquebec.client import PyHydroQuebecError
-
- try:
- await self.client.fetch_data()
- except PyHydroQuebecError as exp:
- _LOGGER.error("Error on receive last Hydroquebec data: %s", exp)
- return False
- return True
-
- async def async_update(self):
- """Return the latest collected data from HydroQuebec."""
- await self._fetch_data()
- self.data = self.client.get_data(self._contract)[self._contract]
diff --git a/homeassistant/components/iaqualink/.translations/de.json b/homeassistant/components/iaqualink/.translations/de.json
new file mode 100644
index 00000000000..d929022c905
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/de.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Es kann nur eine einzige iAqualink-Verbindung konfiguriert werden."
+ },
+ "error": {
+ "connection_failure": "Die Verbindung zu iAqualink ist nicht m\u00f6glich. Bitte \u00fcberpr\u00fcfe den Benutzernamen und das Passwort."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername/E-Mail-Adresse"
+ },
+ "description": "Bitte geben Sie den Benutzernamen und das Passwort f\u00fcr Ihr iAqualink-Konto ein.",
+ "title": "Mit iAqualink verbinden"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json
index 35444dd422b..9a93c19ef20 100644
--- a/homeassistant/components/iaqualink/.translations/ru.json
+++ b/homeassistant/components/iaqualink/.translations/ru.json
@@ -10,7 +10,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
+ "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.",
"title": "Jandy iAqualink"
diff --git a/homeassistant/components/ifttt/.translations/ca.json b/homeassistant/components/ifttt/.translations/ca.json
index 597328a2ee4..979ed3cd71f 100644
--- a/homeassistant/components/ifttt/.translations/ca.json
+++ b/homeassistant/components/ifttt/.translations/ca.json
@@ -5,7 +5,7 @@
"one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
},
"create_entry": {
- "default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants."
+ "default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants."
},
"step": {
"user": {
diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py
index bed0cb45b1d..05d773e9fd6 100644
--- a/homeassistant/components/ifttt/__init__.py
+++ b/homeassistant/components/ifttt/__init__.py
@@ -2,6 +2,7 @@
import json
import logging
+import pyfttt
import requests
import voluptuous as vol
@@ -69,7 +70,6 @@ async def async_setup(hass, config):
target_keys[target] = api_keys[target]
try:
- import pyfttt
for target, key in target_keys.items():
res = pyfttt.send_event(key, event, value1, value2, value3)
diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py
index 057d832b4fa..8ad045c9f7a 100644
--- a/homeassistant/components/ign_sismologia/geo_location.py
+++ b/homeassistant/components/ign_sismologia/geo_location.py
@@ -19,8 +19,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.event import track_time_interval
-REQUIREMENTS = ["georss_ign_sismologia_client==0.2"]
-
_LOGGER = logging.getLogger(__name__)
ATTR_EXTERNAL_ID = "external_id"
diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json
index 4a96e9828cb..6a88a358f1d 100644
--- a/homeassistant/components/image_processing/manifest.json
+++ b/homeassistant/components/image_processing/manifest.json
@@ -3,7 +3,7 @@
"name": "Image processing",
"documentation": "https://www.home-assistant.io/integrations/image_processing",
"requirements": [
- "pillow==6.1.0"
+ "pillow==6.2.0"
],
"dependencies": [
"camera"
diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py
index 0ae79d34cf0..a10fefa1b16 100644
--- a/homeassistant/components/imap/sensor.py
+++ b/homeassistant/components/imap/sensor.py
@@ -2,6 +2,7 @@
import asyncio
import logging
+from aioimaplib import IMAP4_SSL, AioImapException
import async_timeout
import voluptuous as vol
@@ -107,24 +108,20 @@ class ImapSensor(Entity):
async def connection(self):
"""Return a connection to the server, establishing it if necessary."""
- import aioimaplib
-
if self._connection is None:
try:
- self._connection = aioimaplib.IMAP4_SSL(self._server, self._port)
+ self._connection = IMAP4_SSL(self._server, self._port)
await self._connection.wait_hello_from_server()
await self._connection.login(self._user, self._password)
await self._connection.select(self._folder)
self._does_push = self._connection.has_capability("IDLE")
- except (aioimaplib.AioImapException, asyncio.TimeoutError):
+ except (AioImapException, asyncio.TimeoutError):
self._connection = None
return self._connection
async def idle_loop(self):
"""Wait for data pushed from server."""
- import aioimaplib
-
while True:
try:
if await self.connection():
@@ -138,17 +135,15 @@ class ImapSensor(Entity):
await idle
else:
await self.async_update_ha_state()
- except (aioimaplib.AioImapException, asyncio.TimeoutError):
+ except (AioImapException, asyncio.TimeoutError):
self.disconnected()
async def async_update(self):
"""Periodic polling of state."""
- import aioimaplib
-
try:
if await self.connection():
await self.refresh_email_count()
- except (aioimaplib.AioImapException, asyncio.TimeoutError):
+ except (AioImapException, asyncio.TimeoutError):
self.disconnected()
async def refresh_email_count(self):
diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py
index c5171cde646..62dceae0dad 100644
--- a/homeassistant/components/imap_email_content/sensor.py
+++ b/homeassistant/components/imap_email_content/sensor.py
@@ -4,6 +4,7 @@ import datetime
import email
from collections import deque
+import imaplib
import voluptuous as vol
from homeassistant.helpers.entity import Entity
@@ -88,8 +89,6 @@ class EmailReader:
def connect(self):
"""Login and setup the connection."""
- import imaplib
-
try:
self.connection = imaplib.IMAP4_SSL(self._server, self._port)
self.connection.login(self._user, self._password)
@@ -110,8 +109,6 @@ class EmailReader:
def read_next(self):
"""Read the next email from the email server."""
- import imaplib
-
try:
self.connection.select(self._folder, readonly=True)
diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py
index d6f72209f06..adf57e35093 100644
--- a/homeassistant/components/incomfort/__init__.py
+++ b/homeassistant/components/incomfort/__init__.py
@@ -1,14 +1,18 @@
"""Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway."""
import logging
+from typing import Optional
from aiohttp import ClientResponseError
-import voluptuous as vol
from incomfortclient import Gateway as InComfortGateway
+import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -53,3 +57,38 @@ async def async_setup(hass, hass_config):
)
return True
+
+
+class IncomfortEntity(Entity):
+ """Base class for all InComfort entities."""
+
+ def __init__(self) -> None:
+ """Initialize the class."""
+ self._unique_id = self._name = None
+
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Return a unique ID."""
+ return self._unique_id
+
+ @property
+ def name(self) -> Optional[str]:
+ """Return the name of the sensor."""
+ return self._name
+
+
+class IncomfortChild(IncomfortEntity):
+ """Base class for all InComfort entities (excluding the boiler)."""
+
+ async def async_added_to_hass(self) -> None:
+ """Set up a listener when this entity is added to HA."""
+ async_dispatcher_connect(self.hass, DOMAIN, self._refresh)
+
+ @callback
+ def _refresh(self) -> None:
+ self.async_schedule_update_ha_state(force_refresh=True)
+
+ @property
+ def should_poll(self) -> bool:
+ """Return False as this device should never be polled."""
+ return False
diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py
index 39a45429cb1..b5dbd8e223d 100644
--- a/homeassistant/components/incomfort/binary_sensor.py
+++ b/homeassistant/components/incomfort/binary_sensor.py
@@ -1,11 +1,9 @@
"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway."""
from typing import Any, Dict, Optional
-from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.core import callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice
-from . import DOMAIN
+from . import DOMAIN, IncomfortChild
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -18,34 +16,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class IncomfortFailed(BinarySensorDevice):
+class IncomfortFailed(IncomfortChild, BinarySensorDevice):
"""Representation of an InComfort Failed sensor."""
def __init__(self, client, heater) -> None:
"""Initialize the binary sensor."""
+ super().__init__()
+
self._unique_id = f"{heater.serial_no}_failed"
+ self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_failed")
+ self._name = "Boiler Fault"
self._client = client
self._heater = heater
- async def async_added_to_hass(self) -> None:
- """Set up a listener when this entity is added to HA."""
- async_dispatcher_connect(self.hass, DOMAIN, self._refresh)
-
- @callback
- def _refresh(self) -> None:
- self.async_schedule_update_ha_state(force_refresh=True)
-
- @property
- def unique_id(self) -> Optional[str]:
- """Return a unique ID."""
- return self._unique_id
-
- @property
- def name(self) -> Optional[str]:
- """Return the name of the sensor."""
- return "Fault state"
-
@property
def is_on(self) -> bool:
"""Return the status of the sensor."""
@@ -55,8 +39,3 @@ class IncomfortFailed(BinarySensorDevice):
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return the device state attributes."""
return {"fault_code": self._heater.status["fault_code"]}
-
- @property
- def should_poll(self) -> bool:
- """Return False as this device should never be polled."""
- return False
diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py
index 3918244d4e8..95ccf186372 100644
--- a/homeassistant/components/incomfort/climate.py
+++ b/homeassistant/components/incomfort/climate.py
@@ -1,16 +1,14 @@
"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway."""
from typing import Any, Dict, List, Optional
-from homeassistant.components.climate import ClimateDevice
+from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice
from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
-from homeassistant.core import callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from . import DOMAIN
+from . import DOMAIN, IncomfortChild
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -24,39 +22,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([InComfortClimate(client, heater, r) for r in heater.rooms])
-class InComfortClimate(ClimateDevice):
+class InComfortClimate(IncomfortChild, ClimateDevice):
"""Representation of an InComfort/InTouch climate device."""
def __init__(self, client, heater, room) -> None:
"""Initialize the climate device."""
+ super().__init__()
+
self._unique_id = f"{heater.serial_no}_{room.room_no}"
+ self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_{room.room_no}")
+ self._name = f"Thermostat {room.room_no}"
self._client = client
self._room = room
- self._name = f"Room {room.room_no}"
-
- async def async_added_to_hass(self) -> None:
- """Set up a listener when this entity is added to HA."""
- async_dispatcher_connect(self.hass, DOMAIN, self._refresh)
-
- @callback
- def _refresh(self) -> None:
- self.async_schedule_update_ha_state(force_refresh=True)
-
- @property
- def should_poll(self) -> bool:
- """Return False as this device should never be polled."""
- return False
-
- @property
- def unique_id(self) -> Optional[str]:
- """Return a unique ID."""
- return self._unique_id
-
- @property
- def name(self) -> str:
- """Return the name of the climate device."""
- return self._name
@property
def device_state_attributes(self) -> Dict[str, Any]:
diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py
index 772b5dab183..f3170b7b9bb 100644
--- a/homeassistant/components/incomfort/sensor.py
+++ b/homeassistant/components/incomfort/sensor.py
@@ -1,18 +1,16 @@
"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway."""
from typing import Any, Dict, Optional
+from homeassistant.components.sensor import ENTITY_ID_FORMAT
from homeassistant.const import (
- PRESSURE_BAR,
- TEMP_CELSIUS,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
+ PRESSURE_BAR,
+ TEMP_CELSIUS,
)
-from homeassistant.core import callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
-from . import DOMAIN
+from . import DOMAIN, IncomfortChild
INCOMFORT_HEATER_TEMP = "CV Temp"
INCOMFORT_PRESSURE = "CV Pressure"
@@ -42,42 +40,28 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class IncomfortSensor(Entity):
+class IncomfortSensor(IncomfortChild):
"""Representation of an InComfort/InTouch sensor device."""
def __init__(self, client, heater, name) -> None:
"""Initialize the sensor."""
+ super().__init__()
+
self._client = client
self._heater = heater
self._unique_id = f"{heater.serial_no}_{slugify(name)}"
+ self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_{slugify(name)}")
+ self._name = f"Boiler {name}"
- self._name = name
self._device_class = None
+ self._state_attr = INCOMFORT_MAP_ATTRS[name][0]
self._unit_of_measurement = None
- async def async_added_to_hass(self) -> None:
- """Set up a listener when this entity is added to HA."""
- async_dispatcher_connect(self.hass, DOMAIN, self._refresh)
-
- @callback
- def _refresh(self) -> None:
- self.async_schedule_update_ha_state(force_refresh=True)
-
- @property
- def unique_id(self) -> Optional[str]:
- """Return a unique ID."""
- return self._unique_id
-
- @property
- def name(self) -> Optional[str]:
- """Return the name of the sensor."""
- return self._name
-
@property
def state(self) -> Optional[str]:
"""Return the state of the sensor."""
- return self._heater.status[INCOMFORT_MAP_ATTRS[self._name][0]]
+ return self._heater.status[self._state_attr]
@property
def device_class(self) -> Optional[str]:
@@ -89,11 +73,6 @@ class IncomfortSensor(Entity):
"""Return the unit of measurement of the sensor."""
return self._unit_of_measurement
- @property
- def should_poll(self) -> bool:
- """Return False as this device should never be polled."""
- return False
-
class IncomfortPressure(IncomfortSensor):
"""Representation of an InTouch CV Pressure sensor."""
@@ -113,11 +92,11 @@ class IncomfortTemperature(IncomfortSensor):
"""Initialize the signal strength sensor."""
super().__init__(client, heater, name)
+ self._attr = INCOMFORT_MAP_ATTRS[name][1]
self._device_class = DEVICE_CLASS_TEMPERATURE
self._unit_of_measurement = TEMP_CELSIUS
@property
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return the device state attributes."""
- key = INCOMFORT_MAP_ATTRS[self._name][1]
- return {key: self._heater.status[key]}
+ return {self._attr: self._heater.status[self._attr]}
diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py
index 70423611705..0015107b40f 100644
--- a/homeassistant/components/incomfort/water_heater.py
+++ b/homeassistant/components/incomfort/water_heater.py
@@ -1,14 +1,15 @@
"""Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway."""
import asyncio
import logging
-from typing import Any, Dict, Optional
+from typing import Any, Dict
from aiohttp import ClientResponseError
-from homeassistant.components.water_heater import WaterHeaterDevice
+
+from homeassistant.components.water_heater import ENTITY_ID_FORMAT, WaterHeaterDevice
from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.dispatcher import async_dispatcher_send
-from . import DOMAIN
+from . import DOMAIN, IncomfortEntity
_LOGGER = logging.getLogger(__name__)
@@ -26,26 +27,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([IncomfortWaterHeater(client, heater)])
-class IncomfortWaterHeater(WaterHeaterDevice):
+class IncomfortWaterHeater(IncomfortEntity, WaterHeaterDevice):
"""Representation of an InComfort/Intouch water_heater device."""
def __init__(self, client, heater) -> None:
"""Initialize the water_heater device."""
+ super().__init__()
+
self._unique_id = f"{heater.serial_no}"
+ self.entity_id = ENTITY_ID_FORMAT.format(DOMAIN)
+ self._name = "Boiler"
self._client = client
self._heater = heater
- @property
- def unique_id(self) -> Optional[str]:
- """Return a unique ID."""
- return self._unique_id
-
- @property
- def name(self) -> str:
- """Return the name of the water_heater device."""
- return "Boiler"
-
@property
def icon(self) -> str:
"""Return the icon of the water_heater device."""
diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py
index 2bb5207aa85..86d489621ea 100644
--- a/homeassistant/components/influxdb/__init__.py
+++ b/homeassistant/components/influxdb/__init__.py
@@ -353,7 +353,11 @@ class InfluxThread(threading.Thread):
_LOGGER.debug("Wrote %d events", len(json))
break
- except (exceptions.InfluxDBClientError, IOError) as err:
+ except (
+ exceptions.InfluxDBClientError,
+ exceptions.InfluxDBServerError,
+ IOError,
+ ) as err:
if retry < self.max_tries:
time.sleep(RETRY_DELAY)
else:
diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py
new file mode 100644
index 00000000000..09a30e65210
--- /dev/null
+++ b/homeassistant/components/input_datetime/reproduce_state.py
@@ -0,0 +1,111 @@
+"""Reproduce an Input datetime state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.util import dt as dt_util
+
+from . import (
+ ATTR_DATE,
+ ATTR_DATETIME,
+ ATTR_TIME,
+ CONF_HAS_DATE,
+ CONF_HAS_TIME,
+ DOMAIN,
+ SERVICE_SET_DATETIME,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def is_valid_datetime(string: str) -> bool:
+ """Test if string dt is a valid datetime."""
+ try:
+ return dt_util.parse_datetime(string) is not None
+ except ValueError:
+ return False
+
+
+def is_valid_date(string: str) -> bool:
+ """Test if string dt is a valid date."""
+ try:
+ return dt_util.parse_date(string) is not None
+ except ValueError:
+ return False
+
+
+def is_valid_time(string: str) -> bool:
+ """Test if string dt is a valid time."""
+ try:
+ return dt_util.parse_time(string) is not None
+ except ValueError:
+ return False
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if not (
+ (
+ is_valid_datetime(state.state)
+ and cur_state.attributes.get(CONF_HAS_DATE)
+ and cur_state.attributes.get(CONF_HAS_TIME)
+ )
+ or (
+ is_valid_date(state.state)
+ and cur_state.attributes.get(CONF_HAS_DATE)
+ and not cur_state.attributes.get(CONF_HAS_TIME)
+ )
+ or (
+ is_valid_time(state.state)
+ and cur_state.attributes.get(CONF_HAS_TIME)
+ and not cur_state.attributes.get(CONF_HAS_DATE)
+ )
+ ):
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state:
+ return
+
+ service = SERVICE_SET_DATETIME
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+
+ has_time = cur_state.attributes.get(CONF_HAS_TIME)
+ has_date = cur_state.attributes.get(CONF_HAS_DATE)
+
+ if has_time and has_date:
+ service_data[ATTR_DATETIME] = state.state
+ elif has_time:
+ service_data[ATTR_TIME] = state.state
+ elif has_date:
+ service_data[ATTR_DATE] = state.state
+ else:
+ _LOGGER.warning("input_datetime needs either has_date or has_time or both")
+ return
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Input datetime states."""
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py
new file mode 100644
index 00000000000..97a4837d371
--- /dev/null
+++ b/homeassistant/components/input_number/reproduce_state.py
@@ -0,0 +1,52 @@
+"""Reproduce an Input number state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ try:
+ float(state.state)
+ except ValueError:
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state:
+ return
+
+ service = SERVICE_SET_VALUE
+ service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state}
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Input number states."""
+ # Reproduce states in parallel.
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py
new file mode 100644
index 00000000000..657f518cd3d
--- /dev/null
+++ b/homeassistant/components/input_select/reproduce_state.py
@@ -0,0 +1,80 @@
+"""Reproduce an Input select state."""
+import asyncio
+import logging
+from types import MappingProxyType
+from typing import Iterable, Optional
+
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ DOMAIN,
+ SERVICE_SELECT_OPTION,
+ SERVICE_SET_OPTIONS,
+ ATTR_OPTION,
+ ATTR_OPTIONS,
+)
+
+ATTR_GROUP = [ATTR_OPTION, ATTR_OPTIONS]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ # Return if we can't find entity
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state and all(
+ check_attr_equal(cur_state.attributes, state.attributes, attr)
+ for attr in ATTR_GROUP
+ ):
+ return
+
+ # Set service data
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+
+ # If options are specified, call SERVICE_SET_OPTIONS
+ if ATTR_OPTIONS in state.attributes:
+ service = SERVICE_SET_OPTIONS
+ service_data[ATTR_OPTIONS] = state.attributes[ATTR_OPTIONS]
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+ # Remove ATTR_OPTIONS from service_data so we can reuse service_data in next call
+ del service_data[ATTR_OPTIONS]
+
+ # Call SERVICE_SELECT_OPTION
+ service = SERVICE_SELECT_OPTION
+ service_data[ATTR_OPTION] = state.state
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Input select states."""
+ # Reproduce states in parallel.
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
+
+
+def check_attr_equal(
+ attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str
+) -> bool:
+ """Return true if the given attributes are equal."""
+ return attr1.get(attr_str) == attr2.get(attr_str)
diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py
new file mode 100644
index 00000000000..f64c5c019f6
--- /dev/null
+++ b/homeassistant/components/input_text/reproduce_state.py
@@ -0,0 +1,46 @@
+"""Reproduce an Input text state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ # Return if we can't find the entity
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state:
+ return
+
+ # Call service
+ service = SERVICE_SET_VALUE
+ service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state}
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Input text states."""
+ # Reproduce states in parallel.
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py
index 4015d472ce8..11f224dbfcc 100644
--- a/homeassistant/components/insteon/__init__.py
+++ b/homeassistant/components/insteon/__init__.py
@@ -3,6 +3,38 @@ import collections
import logging
from typing import Dict
+import insteonplm
+from insteonplm.devices import ALDBStatus
+from insteonplm.states.cover import Cover
+from insteonplm.states.dimmable import (
+ DimmableKeypadA,
+ DimmableRemote,
+ DimmableSwitch,
+ DimmableSwitch_Fan,
+)
+from insteonplm.states.onOff import (
+ OnOffKeypad,
+ OnOffKeypadA,
+ OnOffSwitch,
+ OnOffSwitch_OutletBottom,
+ OnOffSwitch_OutletTop,
+ OpenClosedRelay,
+)
+from insteonplm.states.sensor import (
+ IoLincSensor,
+ LeakSensorDryWet,
+ OnOffSensor,
+ SmokeCO2Sensor,
+ VariableSensor,
+)
+from insteonplm.states.x10 import (
+ X10AllLightsOffSensor,
+ X10AllLightsOnSensor,
+ X10AllUnitsOffSensor,
+ X10DimmableSwitch,
+ X10OnOffSensor,
+ X10OnOffSwitch,
+)
import voluptuous as vol
from homeassistant.const import (
@@ -16,8 +48,8 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.helpers import discovery
-from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -240,8 +272,6 @@ STATE_NAME_LABEL_MAP = {
async def async_setup(hass, config):
"""Set up the connection to the modem."""
- import insteonplm
-
ipdb = IPDB()
insteon_modem = None
@@ -496,41 +526,6 @@ class IPDB:
def __init__(self):
"""Create the INSTEON Product Database (IPDB)."""
- from insteonplm.states.cover import Cover
-
- from insteonplm.states.onOff import (
- OnOffSwitch,
- OnOffSwitch_OutletTop,
- OnOffSwitch_OutletBottom,
- OpenClosedRelay,
- OnOffKeypadA,
- OnOffKeypad,
- )
-
- from insteonplm.states.dimmable import (
- DimmableSwitch,
- DimmableSwitch_Fan,
- DimmableRemote,
- DimmableKeypadA,
- )
-
- from insteonplm.states.sensor import (
- VariableSensor,
- OnOffSensor,
- SmokeCO2Sensor,
- IoLincSensor,
- LeakSensorDryWet,
- )
-
- from insteonplm.states.x10 import (
- X10DimmableSwitch,
- X10OnOffSwitch,
- X10OnOffSensor,
- X10AllUnitsOffSensor,
- X10AllLightsOnSensor,
- X10AllLightsOffSensor,
- )
-
self.states = [
State(Cover, "cover"),
State(OnOffSwitch_OutletTop, "switch"),
@@ -685,8 +680,6 @@ class InsteonEntity(Entity):
def print_aldb_to_log(aldb):
"""Print the All-Link Database to the log file."""
- from insteonplm.devices import ALDBStatus
-
_LOGGER.info("ALDB load status is %s", aldb.status.name)
if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]:
_LOGGER.warning("Device All-Link database not loaded")
diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py
index eda601b09de..753ea60efa4 100644
--- a/homeassistant/components/iperf3/__init__.py
+++ b/homeassistant/components/iperf3/__init__.py
@@ -1,19 +1,20 @@
"""Support for Iperf3 network measurement tool."""
-import logging
from datetime import timedelta
+import logging
+import iperf3
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import (
+ CONF_HOST,
+ CONF_HOSTS,
CONF_MONITORED_CONDITIONS,
CONF_PORT,
- CONF_HOST,
CONF_PROTOCOL,
- CONF_HOSTS,
CONF_SCAN_INTERVAL,
)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
@@ -80,8 +81,6 @@ SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST, default=None): cv.string})
async def async_setup(hass, config):
"""Set up the iperf3 component."""
- import iperf3
-
hass.data[DOMAIN] = {}
conf = config[DOMAIN]
diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json
index a302572ed12..0db504c629c 100644
--- a/homeassistant/components/ipma/.translations/ru.json
+++ b/homeassistant/components/ipma/.translations/ru.json
@@ -10,7 +10,7 @@
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
},
- "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b",
+ "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b.",
"title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435"
}
},
diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json
index 0c3afc88c94..336877fda13 100644
--- a/homeassistant/components/iqvia/.translations/ru.json
+++ b/homeassistant/components/iqvia/.translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"error": {
"identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.",
- "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441"
+ "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441."
},
"step": {
"user": {
diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json
index caf422938b2..04723a6a1f6 100644
--- a/homeassistant/components/iqvia/manifest.json
+++ b/homeassistant/components/iqvia/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iqvia",
"requirements": [
- "numpy==1.17.1",
+ "numpy==1.17.3",
"pyiqvia==0.2.1"
],
"dependencies": [],
diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py
index 5f38c3d166e..002b2e958f7 100644
--- a/homeassistant/components/iss/binary_sensor.py
+++ b/homeassistant/components/iss/binary_sensor.py
@@ -1,18 +1,19 @@
"""Support for International Space Station data sensor."""
-import logging
from datetime import timedelta
+import logging
+import pyiss
import requests
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
+from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
from homeassistant.const import (
- CONF_NAME,
- ATTR_LONGITUDE,
ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+ CONF_NAME,
CONF_SHOW_ON_MAP,
)
+import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -113,8 +114,6 @@ class IssData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from the ISS API."""
- import pyiss
-
try:
iss = pyiss.ISS()
self.is_above = iss.is_ISS_above(self.latitude, self.longitude)
diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py
index 324dcb019b3..96796e37a6a 100644
--- a/homeassistant/components/isy994/__init__.py
+++ b/homeassistant/components/isy994/__init__.py
@@ -3,6 +3,8 @@ from collections import namedtuple
import logging
from urllib.parse import urlparse
+import PyISY
+from PyISY.Nodes import Group
import voluptuous as vol
from homeassistant.const import (
@@ -312,8 +314,6 @@ def _categorize_nodes(
# Don't import this node as a device at all
continue
- from PyISY.Nodes import Group
-
if isinstance(node, Group):
hass.data[ISY994_NODES][SCENE_DOMAIN].append(node)
continue
@@ -419,8 +419,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
_LOGGER.error("isy994 host value in configuration is invalid")
return False
- import PyISY
-
# Connect to ISY controller.
isy = PyISY.ISY(
host.hostname,
diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py
index 9895b54a50d..5390111890c 100644
--- a/homeassistant/components/itach/remote.py
+++ b/homeassistant/components/itach/remote.py
@@ -1,19 +1,20 @@
"""Support for iTach IR devices."""
import logging
+import pyitachip2ir
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components import remote
-from homeassistant.const import (
- DEVICE_DEFAULT_NAME,
- CONF_NAME,
- CONF_MAC,
- CONF_HOST,
- CONF_PORT,
- CONF_DEVICES,
-)
from homeassistant.components.remote import PLATFORM_SCHEMA
+from homeassistant.const import (
+ CONF_DEVICES,
+ CONF_HOST,
+ CONF_MAC,
+ CONF_NAME,
+ CONF_PORT,
+ DEVICE_DEFAULT_NAME,
+)
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -55,8 +56,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ITach connection and devices."""
- import pyitachip2ir
-
itachip2ir = pyitachip2ir.ITachIP2IR(
config.get(CONF_MAC), config.get(CONF_HOST), int(config.get(CONF_PORT))
)
diff --git a/homeassistant/components/izone/.translations/de.json b/homeassistant/components/izone/.translations/de.json
new file mode 100644
index 00000000000..3c7ebfa937f
--- /dev/null
+++ b/homeassistant/components/izone/.translations/de.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Es wurden keine iZone-Ger\u00e4te im Netzwerk gefunden.",
+ "single_instance_allowed": "Es ist nur eine einzige Konfiguration von iZone erforderlich."
+ },
+ "step": {
+ "confirm": {
+ "description": "M\u00f6chten Sie iZone einrichten?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/nl.json b/homeassistant/components/izone/.translations/nl.json
new file mode 100644
index 00000000000..979441f7288
--- /dev/null
+++ b/homeassistant/components/izone/.translations/nl.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Geen iZone-apparaten gevonden op het netwerk.",
+ "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van iZone nodig."
+ },
+ "step": {
+ "confirm": {
+ "description": "Wilt u iZone instellen?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py
index c7bbbdb2d90..bbe0c1d24fd 100644
--- a/homeassistant/components/jewish_calendar/__init__.py
+++ b/homeassistant/components/jewish_calendar/__init__.py
@@ -20,8 +20,7 @@ SENSOR_TYPES = {
"data": {
"date": ["Date", "mdi:judaism"],
"weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"],
- "holiday_name": ["Holiday name", "mdi:calendar-star"],
- "holiday_type": ["Holiday type", "mdi:counter"],
+ "holiday": ["Holiday", "mdi:calendar-star"],
"omer_count": ["Day of the Omer", "mdi:counter"],
},
"time": {
diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json
index 7b6653ba832..08182daedd0 100644
--- a/homeassistant/components/jewish_calendar/manifest.json
+++ b/homeassistant/components/jewish_calendar/manifest.json
@@ -3,7 +3,7 @@
"name": "Jewish calendar",
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
"requirements": [
- "hdate==0.9.0"
+ "hdate==0.9.1"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py
index 405838b1fb1..54a3d1497aa 100644
--- a/homeassistant/components/jewish_calendar/sensor.py
+++ b/homeassistant/components/jewish_calendar/sensor.py
@@ -23,7 +23,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
for sensor, sensor_info in SENSOR_TYPES["data"].items()
]
sensors.extend(
- JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info)
+ JewishCalendarTimeSensor(hass.data[DOMAIN], sensor, sensor_info)
for sensor, sensor_info in SENSOR_TYPES["time"].items()
)
@@ -44,6 +44,7 @@ class JewishCalendarSensor(Entity):
self._havdalah_offset = data["havdalah_offset"]
self._diaspora = data["diaspora"]
self._state = None
+ self._holiday_attrs = {}
@property
def name(self):
@@ -63,7 +64,7 @@ class JewishCalendarSensor(Entity):
async def async_update(self):
"""Update the state of the sensor."""
now = dt_util.now()
- _LOGGER.debug("Now: %s Timezone = %s", now, now.tzinfo)
+ _LOGGER.debug("Now: %s Location: %r", now, self._location)
today = now.date()
sunset = dt_util.as_local(
@@ -72,16 +73,6 @@ class JewishCalendarSensor(Entity):
_LOGGER.debug("Now: %s Sunset: %s", now, sunset)
- def make_zmanim(date):
- """Create a Zmanim object."""
- return hdate.Zmanim(
- date=date,
- location=self._location,
- candle_lighting_offset=self._candle_lighting_offset,
- havdalah_offset=self._havdalah_offset,
- hebrew=self._hebrew,
- )
-
date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew)
# The Jewish day starts after darkness (called "tzais") and finishes at
@@ -92,7 +83,7 @@ class JewishCalendarSensor(Entity):
# tomorrow based on sunset ("shkia"), for others based on "tzais".
# Hence the following variables.
after_tzais_date = after_shkia_date = date
- today_times = make_zmanim(today)
+ today_times = self.make_zmanim(today)
if now > sunset:
after_shkia_date = date.next_day
@@ -100,37 +91,91 @@ class JewishCalendarSensor(Entity):
if today_times.havdalah and now > today_times.havdalah:
after_tzais_date = date.next_day
+ self._state = self.get_state(after_shkia_date, after_tzais_date)
+ _LOGGER.debug("New value for %s: %s", self._type, self._state)
+
+ def make_zmanim(self, date):
+ """Create a Zmanim object."""
+ return hdate.Zmanim(
+ date=date,
+ location=self._location,
+ candle_lighting_offset=self._candle_lighting_offset,
+ havdalah_offset=self._havdalah_offset,
+ hebrew=self._hebrew,
+ )
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if self._type == "holiday":
+ return self._holiday_attrs
+
+ return {}
+
+ def get_state(self, after_shkia_date, after_tzais_date):
+ """For a given type of sensor, return the state."""
# Terminology note: by convention in py-libhdate library, "upcoming"
# refers to "current" or "upcoming" dates.
if self._type == "date":
- self._state = after_shkia_date.hebrew_date
- elif self._type == "weekly_portion":
+ return after_shkia_date.hebrew_date
+ if self._type == "weekly_portion":
# Compute the weekly portion based on the upcoming shabbat.
- self._state = after_tzais_date.upcoming_shabbat.parasha
- elif self._type == "holiday_name":
- self._state = after_shkia_date.holiday_description
- elif self._type == "holiday_type":
- self._state = after_shkia_date.holiday_type
- elif self._type == "upcoming_shabbat_candle_lighting":
- times = make_zmanim(after_tzais_date.upcoming_shabbat.previous_day.gdate)
- self._state = times.candle_lighting
- elif self._type == "upcoming_candle_lighting":
- times = make_zmanim(
+ return after_tzais_date.upcoming_shabbat.parasha
+ if self._type == "holiday":
+ self._holiday_attrs["type"] = after_shkia_date.holiday_type.name
+ self._holiday_attrs["id"] = after_shkia_date.holiday_name
+ return after_shkia_date.holiday_description
+ if self._type == "omer_count":
+ return after_shkia_date.omer_day
+
+ return None
+
+
+class JewishCalendarTimeSensor(JewishCalendarSensor):
+ """Implement attrbutes for sensors returning times."""
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return dt_util.as_utc(self._state) if self._state is not None else None
+
+ @property
+ def device_class(self):
+ """Return the class of this sensor."""
+ return "timestamp"
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ attrs = {}
+
+ if self._state is None:
+ return attrs
+
+ attrs["timestamp"] = self._state.timestamp()
+
+ return attrs
+
+ def get_state(self, after_shkia_date, after_tzais_date):
+ """For a given type of sensor, return the state."""
+ if self._type == "upcoming_shabbat_candle_lighting":
+ times = self.make_zmanim(
+ after_tzais_date.upcoming_shabbat.previous_day.gdate
+ )
+ return times.candle_lighting
+ if self._type == "upcoming_candle_lighting":
+ times = self.make_zmanim(
after_tzais_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate
)
- self._state = times.candle_lighting
- elif self._type == "upcoming_shabbat_havdalah":
- times = make_zmanim(after_tzais_date.upcoming_shabbat.gdate)
- self._state = times.havdalah
- elif self._type == "upcoming_havdalah":
- times = make_zmanim(
+ return times.candle_lighting
+ if self._type == "upcoming_shabbat_havdalah":
+ times = self.make_zmanim(after_tzais_date.upcoming_shabbat.gdate)
+ return times.havdalah
+ if self._type == "upcoming_havdalah":
+ times = self.make_zmanim(
after_tzais_date.upcoming_shabbat_or_yom_tov.last_day.gdate
)
- self._state = times.havdalah
- elif self._type == "omer_count":
- self._state = after_shkia_date.omer_day
- else:
- times = make_zmanim(today).zmanim
- self._state = times[self._type].time()
+ return times.havdalah
- _LOGGER.debug("New value: %s", self._state)
+ times = self.make_zmanim(dt_util.now()).zmanim
+ return times[self._type]
diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py
index a176236b224..207dac7836a 100644
--- a/homeassistant/components/juicenet/__init__.py
+++ b/homeassistant/components/juicenet/__init__.py
@@ -1,12 +1,13 @@
"""Support for Juicenet cloud."""
import logging
+import pyjuicenet
import voluptuous as vol
-from homeassistant.helpers import discovery
from homeassistant.const import CONF_ACCESS_TOKEN
-from homeassistant.helpers.entity import Entity
+from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -20,8 +21,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Juicenet component."""
- import pyjuicenet
-
hass.data[DOMAIN] = {}
access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py
index 8c61ad54184..d043dc15eaf 100644
--- a/homeassistant/components/kaiterra/__init__.py
+++ b/homeassistant/components/kaiterra/__init__.py
@@ -1,35 +1,33 @@
"""Support for Kaiterra devices."""
import voluptuous as vol
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.event import async_track_time_interval
-from homeassistant.helpers.discovery import async_load_platform
-from homeassistant.helpers import config_validation as cv
-
from homeassistant.const import (
CONF_API_KEY,
- CONF_DEVICES,
CONF_DEVICE_ID,
+ CONF_DEVICES,
+ CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_TYPE,
- CONF_NAME,
)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers.event import async_track_time_interval
+from .api_data import KaiterraApiData
from .const import (
AVAILABLE_AQI_STANDARDS,
- AVAILABLE_UNITS,
AVAILABLE_DEVICE_TYPES,
+ AVAILABLE_UNITS,
CONF_AQI_STANDARD,
CONF_PREFERRED_UNITS,
- DOMAIN,
DEFAULT_AQI_STANDARD,
DEFAULT_PREFERRED_UNIT,
DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
KAITERRA_COMPONENTS,
)
-from .api_data import KaiterraApiData
-
KAITERRA_DEVICE_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): cv.string,
diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py
index 70699de394c..1de1a4bd6c5 100644
--- a/homeassistant/components/kaiterra/air_quality.py
+++ b/homeassistant/components/kaiterra/air_quality.py
@@ -1,16 +1,14 @@
"""Support for Kaiterra Air Quality Sensors."""
from homeassistant.components.air_quality import AirQualityEntity
-
+from homeassistant.const import CONF_DEVICE_ID, CONF_NAME
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.const import CONF_DEVICE_ID, CONF_NAME
-
from .const import (
- DOMAIN,
- ATTR_VOC,
ATTR_AQI_LEVEL,
ATTR_AQI_POLLUTANT,
+ ATTR_VOC,
DISPATCHER_KAITERRA,
+ DOMAIN,
)
diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py
index 81e28438d56..e0f4d817e03 100644
--- a/homeassistant/components/kaiterra/api_data.py
+++ b/homeassistant/components/kaiterra/api_data.py
@@ -1,21 +1,17 @@
"""Data for all Kaiterra devices."""
+import asyncio
from logging import getLogger
-import asyncio
-
-import async_timeout
-
from aiohttp.client_exceptions import ClientResponseError
+import async_timeout
+from kaiterra_async_client import AQIStandard, KaiterraAPIClient, Units
-from kaiterra_async_client import KaiterraAPIClient, AQIStandard, Units
-
+from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID, CONF_DEVICES, CONF_TYPE
from homeassistant.helpers.dispatcher import async_dispatcher_send
-from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_DEVICE_ID, CONF_TYPE
-
from .const import (
- AQI_SCALE,
AQI_LEVEL,
+ AQI_SCALE,
CONF_AQI_STANDARD,
CONF_PREFERRED_UNITS,
DISPATCHER_KAITERRA,
@@ -60,9 +56,10 @@ class KaiterraApiData:
with async_timeout.timeout(10):
data = await self._api.get_latest_sensor_readings(self._devices)
except (ClientResponseError, asyncio.TimeoutError):
- _LOGGER.debug("Couldn't fetch data")
+ _LOGGER.debug("Couldn't fetch data from Kaiterra API")
self.data = {}
async_dispatcher_send(self._hass, DISPATCHER_KAITERRA)
+ return
_LOGGER.debug("New data retrieved: %s", data)
@@ -102,8 +99,7 @@ class KaiterraApiData:
device["aqi_pollutant"] = {"value": main_pollutant}
self.data[self._devices_ids[i]] = device
-
- async_dispatcher_send(self._hass, DISPATCHER_KAITERRA)
except IndexError as err:
_LOGGER.error("Parsing error %s", err)
- async_dispatcher_send(self._hass, DISPATCHER_KAITERRA)
+
+ async_dispatcher_send(self._hass, DISPATCHER_KAITERRA)
diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py
index 4ff6435b64d..e86d6f7d836 100644
--- a/homeassistant/components/kaiterra/sensor.py
+++ b/homeassistant/components/kaiterra/sensor.py
@@ -1,11 +1,9 @@
"""Support for Kaiterra Temperature ahn Humidity Sensors."""
+from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
-
-from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
-
-from .const import DOMAIN, DISPATCHER_KAITERRA
+from .const import DISPATCHER_KAITERRA, DOMAIN
SENSORS = [
{"name": "Temperature", "prop": "rtemp", "device_class": "temperature"},
diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json
index 41e45a9e578..4613d2d9608 100644
--- a/homeassistant/components/keenetic_ndms2/manifest.json
+++ b/homeassistant/components/keenetic_ndms2/manifest.json
@@ -3,8 +3,10 @@
"name": "Keenetic ndms2",
"documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2",
"requirements": [
- "ndms2_client==0.0.9"
+ "ndms2_client==0.0.10"
],
"dependencies": [],
- "codeowners": []
+ "codeowners": [
+ "@foxel"
+ ]
}
diff --git a/homeassistant/components/keyboard/__init__.py b/homeassistant/components/keyboard/__init__.py
index 39725eec86b..0c5acf5b593 100644
--- a/homeassistant/components/keyboard/__init__.py
+++ b/homeassistant/components/keyboard/__init__.py
@@ -1,4 +1,5 @@
"""Support to emulate keyboard presses on host machine."""
+from pykeyboard import PyKeyboard # pylint: disable=import-error
import voluptuous as vol
from homeassistant.const import (
@@ -17,9 +18,8 @@ TAP_KEY_SCHEMA = vol.Schema({})
def setup(hass, config):
"""Listen for keyboard events."""
- import pykeyboard # pylint: disable=import-error
- keyboard = pykeyboard.PyKeyboard()
+ keyboard = PyKeyboard()
keyboard.special_key_assignment()
hass.services.register(
diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py
index 77f91a50dfa..8948fbd0b8f 100644
--- a/homeassistant/components/kira/__init__.py
+++ b/homeassistant/components/kira/__init__.py
@@ -2,11 +2,13 @@
import logging
import os
+import pykira
import voluptuous as vol
from voluptuous.error import Error as VoluptuousError
import yaml
from homeassistant.const import (
+ CONF_CODE,
CONF_DEVICE,
CONF_HOST,
CONF_NAME,
@@ -15,7 +17,6 @@ from homeassistant.const import (
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
STATE_UNKNOWN,
- CONF_CODE,
)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
@@ -93,8 +94,6 @@ def load_codes(path):
def setup(hass, config):
"""Set up the KIRA component."""
- import pykira
-
sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, [])
remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, [])
# If no sensors or remotes were specified, add a sensor
diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py
index 5d0c4be3d07..00d5d18f013 100644
--- a/homeassistant/components/knx/__init__.py
+++ b/homeassistant/components/knx/__init__.py
@@ -2,6 +2,11 @@
import logging
import voluptuous as vol
+from xknx import XKNX
+from xknx.devices import ActionCallback, DateTime, DateTimeBroadcastType, ExposeSensor
+from xknx.exceptions import XKNXException
+from xknx.io import DEFAULT_MCAST_PORT, ConnectionConfig, ConnectionType
+from xknx.knx import AddressFilter, DPTArray, DPTBinary, GroupAddress, Telegram
from homeassistant.const import (
CONF_ENTITY_ID,
@@ -90,13 +95,10 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Set up the KNX component."""
- from xknx.exceptions import XKNXException
-
try:
hass.data[DATA_KNX] = KNXModule(hass, config)
hass.data[DATA_KNX].async_create_exposures()
await hass.data[DATA_KNX].start()
-
except XKNXException as ex:
_LOGGER.warning("Can't connect to KNX interface: %s", ex)
hass.components.persistent_notification.async_create(
@@ -157,8 +159,6 @@ class KNXModule:
def init_xknx(self):
"""Initialize of KNX object."""
- from xknx import XKNX
-
self.xknx = XKNX(
config=self.config_file(),
loop=self.hass.loop,
@@ -198,8 +198,6 @@ class KNXModule:
def connection_config_routing(self):
"""Return the connection_config if routing is configured."""
- from xknx.io import ConnectionConfig, ConnectionType
-
local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP)
return ConnectionConfig(
connection_type=ConnectionType.ROUTING, local_ip=local_ip
@@ -207,8 +205,6 @@ class KNXModule:
def connection_config_tunneling(self):
"""Return the connection_config if tunneling is configured."""
- from xknx.io import ConnectionConfig, ConnectionType, DEFAULT_MCAST_PORT
-
gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_HOST)
gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT)
local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP)
@@ -224,8 +220,6 @@ class KNXModule:
def connection_config_auto(self):
"""Return the connection_config if auto is configured."""
# pylint: disable=no-self-use
- from xknx.io import ConnectionConfig
-
return ConnectionConfig()
def register_callbacks(self):
@@ -234,8 +228,6 @@ class KNXModule:
CONF_KNX_FIRE_EVENT in self.config[DOMAIN]
and self.config[DOMAIN][CONF_KNX_FIRE_EVENT]
):
- from xknx.knx import AddressFilter
-
address_filters = list(
map(AddressFilter, self.config[DOMAIN][CONF_KNX_FIRE_EVENT_FILTER])
)
@@ -274,8 +266,6 @@ class KNXModule:
async def service_send_to_knx_bus(self, call):
"""Service for sending an arbitrary KNX message to the KNX bus."""
- from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray
-
attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD)
attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS)
@@ -304,9 +294,7 @@ class KNXAutomation:
script_name = "{} turn ON script".format(device.get_name())
self.script = Script(hass, action, script_name)
- import xknx
-
- self.action = xknx.devices.ActionCallback(
+ self.action = ActionCallback(
hass.data[DATA_KNX].xknx, self.script.async_run, hook=hook, counter=counter
)
device.actions.append(self.action)
@@ -325,8 +313,6 @@ class KNXExposeTime:
@callback
def async_register(self):
"""Register listener."""
- from xknx.devices import DateTime, DateTimeBroadcastType
-
broadcast_type_string = self.type.upper()
broadcast_type = DateTimeBroadcastType[broadcast_type_string]
self.device = DateTime(
@@ -350,8 +336,6 @@ class KNXExposeSensor:
@callback
def async_register(self):
"""Register listener."""
- from xknx.devices import ExposeSensor
-
self.device = ExposeSensor(
self.xknx,
name=self.entity_id,
diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py
index fbe9c4e421e..94a171d9c2a 100644
--- a/homeassistant/components/knx/binary_sensor.py
+++ b/homeassistant/components/knx/binary_sensor.py
@@ -1,5 +1,6 @@
"""Support for KNX/IP binary sensors."""
import voluptuous as vol
+from xknx.devices import BinarySensor
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
@@ -70,9 +71,8 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
def async_add_entities_config(hass, config, async_add_entities):
"""Set up binary senor for KNX platform configured within platform."""
name = config[CONF_NAME]
- import xknx
- binary_sensor = xknx.devices.BinarySensor(
+ binary_sensor = BinarySensor(
hass.data[DATA_KNX].xknx,
name=name,
group_address_state=config[CONF_STATE_ADDRESS],
diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py
index 07aac11b972..819fb1794c3 100644
--- a/homeassistant/components/knx/climate.py
+++ b/homeassistant/components/knx/climate.py
@@ -1,20 +1,22 @@
"""Support for KNX/IP climate devices."""
-from typing import Optional, List
+from typing import List, Optional
import voluptuous as vol
+from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode
+from xknx.knx import HVACOperationMode
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
from homeassistant.components.climate.const import (
+ HVAC_MODE_AUTO,
+ HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
- HVAC_MODE_COOL,
- HVAC_MODE_AUTO,
- PRESET_ECO,
- PRESET_SLEEP,
PRESET_AWAY,
PRESET_COMFORT,
+ PRESET_ECO,
+ PRESET_SLEEP,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
@@ -44,6 +46,7 @@ CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address"
CONF_OPERATION_MODES = "operation_modes"
CONF_ON_OFF_ADDRESS = "on_off_address"
CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address"
+CONF_ON_OFF_INVERT = "on_off_invert"
CONF_MIN_TEMP = "min_temp"
CONF_MAX_TEMP = "max_temp"
@@ -51,6 +54,7 @@ DEFAULT_NAME = "KNX Climate"
DEFAULT_SETPOINT_SHIFT_STEP = 0.5
DEFAULT_SETPOINT_SHIFT_MAX = 6
DEFAULT_SETPOINT_SHIFT_MIN = -6
+DEFAULT_ON_OFF_INVERT = False
# Map KNX operation modes to HA modes. This list might not be full.
OPERATION_MODES = {
# Map DPT 201.105 HVAC control modes
@@ -102,6 +106,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string,
vol.Optional(CONF_ON_OFF_ADDRESS): cv.string,
vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT): cv.boolean,
vol.Optional(CONF_OPERATION_MODES): vol.All(
cv.ensure_list, [vol.In(OPERATION_MODES)]
),
@@ -132,9 +137,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
@callback
def async_add_entities_config(hass, config, async_add_entities):
"""Set up climate for KNX platform configured within platform."""
- import xknx
-
- climate_mode = xknx.devices.ClimateMode(
+ climate_mode = XknxClimateMode(
hass.data[DATA_KNX].xknx,
name=config[CONF_NAME] + " Mode",
group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS),
@@ -162,7 +165,7 @@ def async_add_entities_config(hass, config, async_add_entities):
)
hass.data[DATA_KNX].xknx.devices.add(climate_mode)
- climate = xknx.devices.Climate(
+ climate = XknxClimate(
hass.data[DATA_KNX].xknx,
name=config[CONF_NAME],
group_address_temperature=config[CONF_TEMPERATURE_ADDRESS],
@@ -182,6 +185,7 @@ def async_add_entities_config(hass, config, async_add_entities):
min_temp=config.get(CONF_MIN_TEMP),
max_temp=config.get(CONF_MAX_TEMP),
mode=climate_mode,
+ on_off_invert=config[CONF_ON_OFF_INVERT],
)
hass.data[DATA_KNX].xknx.devices.add(climate)
@@ -298,8 +302,6 @@ class KNXClimate(ClimateDevice):
elif self.device.supports_on_off and hvac_mode == HVAC_MODE_HEAT:
await self.device.turn_on()
elif self.device.mode.supports_operation_mode:
- from xknx.knx import HVACOperationMode
-
knx_operation_mode = HVACOperationMode(OPERATION_MODES_INV.get(hvac_mode))
await self.device.mode.set_operation_mode(knx_operation_mode)
await self.async_update_ha_state()
@@ -333,8 +335,6 @@ class KNXClimate(ClimateDevice):
This method must be run in the event loop and returns a coroutine.
"""
if self.device.mode.supports_operation_mode:
- from xknx.knx import HVACOperationMode
-
knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode))
await self.device.mode.set_operation_mode(knx_operation_mode)
await self.async_update_ha_state()
diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py
index 9af7c11678a..976d1286c9f 100644
--- a/homeassistant/components/knx/cover.py
+++ b/homeassistant/components/knx/cover.py
@@ -1,5 +1,6 @@
"""Support for KNX/IP covers."""
import voluptuous as vol
+from xknx.devices import Cover as XknxCover
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -74,9 +75,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
@callback
def async_add_entities_config(hass, config, async_add_entities):
"""Set up cover for KNX platform configured within platform."""
- import xknx
-
- cover = xknx.devices.Cover(
+ cover = XknxCover(
hass.data[DATA_KNX].xknx,
name=config[CONF_NAME],
group_address_long=config.get(CONF_MOVE_LONG_ADDRESS),
diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py
index 71a82c6df2a..81bf4ad3c83 100644
--- a/homeassistant/components/knx/light.py
+++ b/homeassistant/components/knx/light.py
@@ -2,6 +2,7 @@
from enum import Enum
import voluptuous as vol
+from xknx.devices import Light as XknxLight
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -98,8 +99,6 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
@callback
def async_add_entities_config(hass, config, async_add_entities):
"""Set up light for KNX platform configured within platform."""
- import xknx
-
group_address_tunable_white = None
group_address_tunable_white_state = None
group_address_color_temp = None
@@ -111,7 +110,7 @@ def async_add_entities_config(hass, config, async_add_entities):
group_address_tunable_white = config.get(CONF_COLOR_TEMP_ADDRESS)
group_address_tunable_white_state = config.get(CONF_COLOR_TEMP_STATE_ADDRESS)
- light = xknx.devices.Light(
+ light = XknxLight(
hass.data[DATA_KNX].xknx,
name=config[CONF_NAME],
group_address_switch=config[CONF_ADDRESS],
diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py
index b83edb89eb1..64d513b8624 100644
--- a/homeassistant/components/knx/notify.py
+++ b/homeassistant/components/knx/notify.py
@@ -1,5 +1,6 @@
"""Support for KNX/IP notification services."""
import voluptuous as vol
+from xknx.devices import Notification as XknxNotification
from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService
from homeassistant.const import CONF_ADDRESS, CONF_NAME
@@ -42,9 +43,7 @@ def async_get_service_discovery(hass, discovery_info):
@callback
def async_get_service_config(hass, config):
"""Set up notification for KNX platform configured within platform."""
- import xknx
-
- notification = xknx.devices.Notification(
+ notification = XknxNotification(
hass.data[DATA_KNX].xknx,
name=config[CONF_NAME],
group_address=config[CONF_ADDRESS],
diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py
index d635384092f..c8c6ac2bcfb 100644
--- a/homeassistant/components/knx/scene.py
+++ b/homeassistant/components/knx/scene.py
@@ -1,5 +1,6 @@
"""Support for KNX scenes."""
import voluptuous as vol
+from xknx.devices import Scene as XknxScene
from homeassistant.components.scene import CONF_PLATFORM, Scene
from homeassistant.const import CONF_ADDRESS, CONF_NAME
@@ -42,9 +43,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
@callback
def async_add_entities_config(hass, config, async_add_entities):
"""Set up scene for KNX platform configured within platform."""
- import xknx
-
- scene = xknx.devices.Scene(
+ scene = XknxScene(
hass.data[DATA_KNX].xknx,
name=config[CONF_NAME],
group_address=config[CONF_ADDRESS],
diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py
index 9a19ba91b7a..a0a0f6ea18d 100644
--- a/homeassistant/components/knx/sensor.py
+++ b/homeassistant/components/knx/sensor.py
@@ -1,5 +1,6 @@
"""Support for KNX/IP sensors."""
import voluptuous as vol
+from xknx.devices import Sensor as XknxSensor
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_TYPE
@@ -44,9 +45,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
@callback
def async_add_entities_config(hass, config, async_add_entities):
"""Set up sensor for KNX platform configured within platform."""
- import xknx
-
- sensor = xknx.devices.Sensor(
+ sensor = XknxSensor(
hass.data[DATA_KNX].xknx,
name=config[CONF_NAME],
group_address_state=config[CONF_STATE_ADDRESS],
diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py
index 72a5b5dcdd7..e9a0df5c983 100644
--- a/homeassistant/components/knx/switch.py
+++ b/homeassistant/components/knx/switch.py
@@ -1,5 +1,6 @@
"""Support for KNX/IP switches."""
import voluptuous as vol
+from xknx.devices import Switch as XknxSwitch
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
from homeassistant.const import CONF_ADDRESS, CONF_NAME
@@ -41,9 +42,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
@callback
def async_add_entities_config(hass, config, async_add_entities):
"""Set up switch for KNX platform configured within platform."""
- import xknx
-
- switch = xknx.devices.Switch(
+ switch = XknxSwitch(
hass.data[DATA_KNX].xknx,
name=config[CONF_NAME],
group_address=config[CONF_ADDRESS],
diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py
index 9f0aab6c00c..9b2ba01e90a 100644
--- a/homeassistant/components/kodi/media_player.py
+++ b/homeassistant/components/kodi/media_player.py
@@ -7,6 +7,10 @@ import socket
import urllib
import aiohttp
+import jsonrpc_base
+import jsonrpc_async
+import jsonrpc_websocket
+
import voluptuous as vol
from homeassistant.components.kodi import SERVICE_CALL_METHOD
@@ -231,8 +235,6 @@ def cmd(func):
@wraps(func)
async def wrapper(obj, *args, **kwargs):
"""Wrap all command methods."""
- import jsonrpc_base
-
try:
await func(obj, *args, **kwargs)
except jsonrpc_base.jsonrpc.TransportError as exc:
@@ -268,9 +270,6 @@ class KodiDevice(MediaPlayerDevice):
unique_id=None,
):
"""Initialize the Kodi device."""
- import jsonrpc_async
- import jsonrpc_websocket
-
self.hass = hass
self._name = name
self._unique_id = unique_id
@@ -389,8 +388,6 @@ class KodiDevice(MediaPlayerDevice):
async def _get_players(self):
"""Return the active player objects or None."""
- import jsonrpc_base
-
try:
return await self.server.Player.GetActivePlayers()
except jsonrpc_base.jsonrpc.TransportError:
@@ -420,8 +417,6 @@ class KodiDevice(MediaPlayerDevice):
async def async_ws_connect(self):
"""Connect to Kodi via websocket protocol."""
- import jsonrpc_base
-
try:
ws_loop_future = await self._ws_server.ws_connect()
except jsonrpc_base.jsonrpc.TransportError:
@@ -801,8 +796,6 @@ class KodiDevice(MediaPlayerDevice):
async def async_call_method(self, method, **kwargs):
"""Run Kodi JSONRPC API method with params."""
- import jsonrpc_base
-
_LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs)
result_ok = False
try:
@@ -850,8 +843,6 @@ class KodiDevice(MediaPlayerDevice):
All the albums of an artist can be added with
media_name="ALL"
"""
- import jsonrpc_base
-
params = {"playlistid": 0}
if media_type == "SONG":
if media_id is None:
diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py
index 41dfc42b5de..1072cf1b732 100644
--- a/homeassistant/components/kodi/notify.py
+++ b/homeassistant/components/kodi/notify.py
@@ -2,6 +2,8 @@
import logging
import aiohttp
+import jsonrpc_async
+
import voluptuous as vol
from homeassistant.const import (
@@ -77,8 +79,6 @@ class KodiNotificationService(BaseNotificationService):
def __init__(self, hass, url, auth=None):
"""Initialize the service."""
- import jsonrpc_async
-
self._url = url
kwargs = {"timeout": DEFAULT_TIMEOUT, "session": async_get_clientsession(hass)}
@@ -90,8 +90,6 @@ class KodiNotificationService(BaseNotificationService):
async def async_send_message(self, message="", **kwargs):
"""Send a message to Kodi."""
- import jsonrpc_async
-
try:
data = kwargs.get(ATTR_DATA) or {}
diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py
index 4cc872fb78b..624d359e154 100644
--- a/homeassistant/components/konnected/__init__.py
+++ b/homeassistant/components/konnected/__init__.py
@@ -4,59 +4,58 @@ import hmac
import json
import logging
-import voluptuous as vol
-
from aiohttp.hdrs import AUTHORIZATION
from aiohttp.web import Request, Response
+import konnected
+import voluptuous as vol
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
from homeassistant.components.discovery import SERVICE_KONNECTED
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_STATE,
+ CONF_ACCESS_TOKEN,
+ CONF_BINARY_SENSORS,
+ CONF_DEVICES,
+ CONF_HOST,
+ CONF_ID,
+ CONF_NAME,
+ CONF_PIN,
+ CONF_PORT,
+ CONF_SENSORS,
+ CONF_SWITCHES,
+ CONF_TYPE,
+ CONF_ZONE,
EVENT_HOMEASSISTANT_START,
HTTP_BAD_REQUEST,
HTTP_NOT_FOUND,
HTTP_UNAUTHORIZED,
- CONF_DEVICES,
- CONF_BINARY_SENSORS,
- CONF_SENSORS,
- CONF_SWITCHES,
- CONF_HOST,
- CONF_PORT,
- CONF_ID,
- CONF_NAME,
- CONF_TYPE,
- CONF_PIN,
- CONF_ZONE,
- CONF_ACCESS_TOKEN,
- ATTR_ENTITY_ID,
- ATTR_STATE,
STATE_ON,
)
+from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.dispatcher import dispatcher_send
-from homeassistant.helpers import discovery
-from homeassistant.helpers import config_validation as cv
from .const import (
CONF_ACTIVATION,
CONF_API_HOST,
+ CONF_BLINK,
+ CONF_DHT_SENSORS,
+ CONF_DISCOVERY,
+ CONF_DS18B20_SENSORS,
+ CONF_INVERSE,
CONF_MOMENTARY,
CONF_PAUSE,
CONF_POLL_INTERVAL,
CONF_REPEAT,
- CONF_INVERSE,
- CONF_BLINK,
- CONF_DISCOVERY,
- CONF_DHT_SENSORS,
- CONF_DS18B20_SENSORS,
DOMAIN,
- STATE_LOW,
- STATE_HIGH,
- PIN_TO_ZONE,
- ZONE_TO_PIN,
ENDPOINT_ROOT,
- UPDATE_ENDPOINT,
+ PIN_TO_ZONE,
SIGNAL_SENSOR_UPDATE,
+ STATE_HIGH,
+ STATE_LOW,
+ UPDATE_ENDPOINT,
+ ZONE_TO_PIN,
)
from .handlers import HANDLERS
@@ -141,8 +140,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Set up the Konnected platform."""
- import konnected
-
cfg = config.get(DOMAIN)
if cfg is None:
cfg = {}
@@ -336,8 +333,6 @@ class DiscoveredDevice:
self.host = host
self.port = port
- import konnected
-
self.client = konnected.Client(host, str(port))
self.status = self.client.get_status()
diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py
index a355cabba56..a8914853e84 100644
--- a/homeassistant/components/konnected/handlers.py
+++ b/homeassistant/components/konnected/handlers.py
@@ -1,16 +1,16 @@
"""Handle Konnected messages."""
import logging
-from homeassistant.helpers.dispatcher import async_dispatcher_send
-from homeassistant.util import decorator
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_STATE,
- DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_TEMPERATURE,
)
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.util import decorator
-from .const import CONF_INVERSE, SIGNAL_SENSOR_UPDATE, SIGNAL_DS18B20_NEW
+from .const import CONF_INVERSE, SIGNAL_DS18B20_NEW, SIGNAL_SENSOR_UPDATE
_LOGGER = logging.getLogger(__name__)
HANDLERS = decorator.Registry()
diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py
index 736792aefd8..68d727626cf 100644
--- a/homeassistant/components/lastfm/sensor.py
+++ b/homeassistant/components/lastfm/sensor.py
@@ -2,10 +2,12 @@
import logging
import re
+import pylast as lastfm
+from pylast import WSError
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_API_KEY, ATTR_ATTRIBUTION
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -30,9 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Last.fm sensor platform."""
- import pylast as lastfm
- from pylast import WSError
-
api_key = config[CONF_API_KEY]
users = config.get(CONF_USERS)
@@ -53,11 +52,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class LastfmSensor(Entity):
"""A class for the Last.fm account."""
- def __init__(self, user, lastfm):
+ def __init__(self, user, lastfm_api):
"""Initialize the sensor."""
- self._user = lastfm.get_user(user)
+ self._user = lastfm_api.get_user(user)
self._name = user
- self._lastfm = lastfm
+ self._lastfm = lastfm_api
self._state = "Not Scrobbling"
self._playcount = None
self._lastplayed = None
diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py
index 5c98f86a2bc..30cfbf17074 100644
--- a/homeassistant/components/lg_soundbar/media_player.py
+++ b/homeassistant/components/lg_soundbar/media_player.py
@@ -1,14 +1,15 @@
"""Support for LG soundbars."""
import logging
+import temescal
+
from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import (
+ SUPPORT_SELECT_SOUND_MODE,
SUPPORT_SELECT_SOURCE,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
- SUPPORT_SELECT_SOUND_MODE,
)
-
from homeassistant.const import STATE_ON
_LOGGER = logging.getLogger(__name__)
@@ -32,8 +33,6 @@ class LGDevice(MediaPlayerDevice):
def __init__(self, discovery_info):
"""Initialize the LG speakers."""
- import temescal
-
host = discovery_info.get("host")
port = discovery_info.get("port")
@@ -140,7 +139,6 @@ class LGDevice(MediaPlayerDevice):
@property
def sound_mode(self):
"""Return the current sound mode."""
- import temescal
if self._equaliser == -1:
return ""
@@ -149,8 +147,6 @@ class LGDevice(MediaPlayerDevice):
@property
def sound_mode_list(self):
"""Return the available sound modes."""
- import temescal
-
modes = []
for equaliser in self._equalisers:
modes.append(temescal.equalisers[equaliser])
@@ -159,8 +155,6 @@ class LGDevice(MediaPlayerDevice):
@property
def source(self):
"""Return the current input source."""
- import temescal
-
if self._function == -1:
return ""
return temescal.functions[self._function]
@@ -168,8 +162,6 @@ class LGDevice(MediaPlayerDevice):
@property
def source_list(self):
"""List of available input sources."""
- import temescal
-
sources = []
for function in self._functions:
sources.append(temescal.functions[function])
@@ -191,12 +183,8 @@ class LGDevice(MediaPlayerDevice):
def select_source(self, source):
"""Select input source."""
- import temescal
-
self._device.set_func(temescal.functions.index(source))
def select_sound_mode(self, sound_mode):
"""Set Sound Mode for Receiver.."""
- import temescal
-
self._device.set_eq(temescal.equalisers.index(sound_mode))
diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json
index d033da4bae7..eba3a47ead8 100644
--- a/homeassistant/components/life360/.translations/ru.json
+++ b/homeassistant/components/life360/.translations/ru.json
@@ -1,16 +1,16 @@
{
"config": {
"abort": {
- "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435",
+ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430."
},
"create_entry": {
"default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438."
},
"error": {
- "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435",
- "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d",
- "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360",
+ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
+ "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.",
+ "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360.",
"user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430."
},
"step": {
diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py
index f4ae2c4030a..6e921a59afe 100644
--- a/homeassistant/components/lifx/__init__.py
+++ b/homeassistant/components/lifx/__init__.py
@@ -1,12 +1,12 @@
"""Support for LIFX."""
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries
-from homeassistant.const import CONF_PORT
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
-from .const import DOMAIN
+from homeassistant.const import CONF_PORT
+import homeassistant.helpers.config_validation as cv
+from .const import DOMAIN
CONF_SERVER = "server"
CONF_BROADCAST = "broadcast"
diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py
index b324dc0cad8..71fe7247c12 100644
--- a/homeassistant/components/lifx/config_flow.py
+++ b/homeassistant/components/lifx/config_flow.py
@@ -1,13 +1,14 @@
"""Config flow flow LIFX."""
-from homeassistant.helpers import config_entry_flow
+import aiolifx
+
from homeassistant import config_entries
+from homeassistant.helpers import config_entry_flow
+
from .const import DOMAIN
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
- import aiolifx
-
lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan()
return len(lifx_ip_addresses) > 0
diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py
index d183dcb0fa2..50e36e8db0a 100644
--- a/homeassistant/components/lifx/light.py
+++ b/homeassistant/components/lifx/light.py
@@ -6,6 +6,8 @@ import logging
import math
import sys
+import aiolifx as aiolifx_module
+import aiolifx_effects as aiolifx_effects_module
import voluptuous as vol
from homeassistant import util
@@ -151,15 +153,11 @@ LIFX_EFFECT_STOP_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_id
def aiolifx():
"""Return the aiolifx module."""
- import aiolifx as aiolifx_module
-
return aiolifx_module
def aiolifx_effects():
"""Return the aiolifx_effects module."""
- import aiolifx_effects as aiolifx_effects_module
-
return aiolifx_effects_module
diff --git a/homeassistant/components/lifx_legacy/light.py b/homeassistant/components/lifx_legacy/light.py
index 78a333018f9..8f767a2f559 100644
--- a/homeassistant/components/lifx_legacy/light.py
+++ b/homeassistant/components/lifx_legacy/light.py
@@ -9,6 +9,7 @@ https://home-assistant.io/components/light.lifx/
"""
import logging
+import liffylights
import voluptuous as vol
from homeassistant.components.light import (
@@ -16,19 +17,19 @@ from homeassistant.components.light import (
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_TRANSITION,
+ PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
- SUPPORT_COLOR_TEMP,
SUPPORT_COLOR,
+ SUPPORT_COLOR_TEMP,
SUPPORT_TRANSITION,
Light,
- PLATFORM_SCHEMA,
-)
-from homeassistant.helpers.event import track_time_change
-from homeassistant.util.color import (
- color_temperature_mired_to_kelvin,
- color_temperature_kelvin_to_mired,
)
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.event import track_time_change
+from homeassistant.util.color import (
+ color_temperature_kelvin_to_mired,
+ color_temperature_mired_to_kelvin,
+)
_LOGGER = logging.getLogger(__name__)
@@ -71,8 +72,6 @@ class LIFX:
def __init__(self, add_entities_callback, server_addr=None, broadcast_addr=None):
"""Initialize the light."""
- import liffylights
-
self._devices = []
self._add_entities_callback = add_entities_callback
diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json
index fd30e931718..4a1dc82bbd6 100644
--- a/homeassistant/components/light/.translations/fr.json
+++ b/homeassistant/components/light/.translations/fr.json
@@ -10,7 +10,7 @@
"is_on": "{entity_name} est allum\u00e9"
},
"trigger_type": {
- "turned_off": "{entity_name} d\u00e9sactiv\u00e9",
+ "turned_off": "{entity_name} est d\u00e9sactiv\u00e9",
"turned_on": "{entity_name} activ\u00e9"
}
}
diff --git a/homeassistant/components/light/.translations/lv.json b/homeassistant/components/light/.translations/lv.json
new file mode 100644
index 00000000000..7668dfa5ac8
--- /dev/null
+++ b/homeassistant/components/light/.translations/lv.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "turned_off": "{entity_name} tika izsl\u0113gta",
+ "turned_on": "{entity_name} tika iesl\u0113gta"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json
index 33a38fc930e..05589210dba 100644
--- a/homeassistant/components/light/.translations/pl.json
+++ b/homeassistant/components/light/.translations/pl.json
@@ -10,8 +10,8 @@
"is_on": "\u015bwiat\u0142o {entity_name} jest w\u0142\u0105czone"
},
"trigger_type": {
- "turned_off": "wy\u0142\u0105czenie {entity_name}",
- "turned_on": "w\u0142\u0105czenie {entity_name}"
+ "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}",
+ "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json
index a6a7994b7c3..8ca964606ae 100644
--- a/homeassistant/components/light/.translations/ru.json
+++ b/homeassistant/components/light/.translations/ru.json
@@ -6,12 +6,12 @@
"turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}"
},
"condition_type": {
- "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
- "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e"
+ "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438"
},
"trigger_type": {
- "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
- "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435"
+ "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index ed61d961d88..fbd908a9e45 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -292,7 +292,8 @@ async def async_setup(hass, config):
preprocess_turn_on_alternatives(params)
turn_lights_off, off_params = preprocess_turn_off(params)
- update_tasks = []
+ poll_lights = []
+ change_tasks = []
for light in target_lights:
light.async_set_context(service.context)
@@ -305,17 +306,22 @@ async def async_setup(hass, config):
preprocess_turn_on_alternatives(pars)
turn_light_off, off_pars = preprocess_turn_off(pars)
if turn_light_off:
- await light.async_turn_off(**off_pars)
+ task = light.async_request_call(light.async_turn_off(**off_pars))
else:
- await light.async_turn_on(**pars)
+ task = light.async_request_call(light.async_turn_on(**pars))
- if not light.should_poll:
- continue
+ change_tasks.append(task)
- update_tasks.append(light.async_update_ha_state(True))
+ if light.should_poll:
+ poll_lights.append(light)
- if update_tasks:
- await asyncio.wait(update_tasks)
+ if change_tasks:
+ await asyncio.wait(change_tasks)
+
+ if poll_lights:
+ await asyncio.wait(
+ [light.async_update_ha_state(True) for light in poll_lights]
+ )
# Listen for light on and light off service calls.
hass.services.async_register(
diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py
index 4abf34e6661..e87ae3bf945 100644
--- a/homeassistant/components/light/device_condition.py
+++ b/homeassistant/components/light/device_condition.py
@@ -1,5 +1,5 @@
"""Provides device conditions for lights."""
-from typing import List
+from typing import Dict, List
import voluptuous as vol
from homeassistant.core import HomeAssistant
@@ -21,9 +21,16 @@ def async_condition_from_config(
"""Evaluate state based on configuration."""
if config_validation:
config = CONDITION_SCHEMA(config)
- return toggle_entity.async_condition_from_config(config, config_validation)
+ return toggle_entity.async_condition_from_config(config)
-async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_conditions(
+ hass: HomeAssistant, device_id: str
+) -> List[Dict[str, str]]:
"""List device conditions."""
return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)
+
+
+async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict:
+ """List condition capabilities."""
+ return await toggle_entity.async_get_condition_capabilities(hass, config)
diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py
index 5bd5d83e1c0..432d24d3c14 100644
--- a/homeassistant/components/light/device_trigger.py
+++ b/homeassistant/components/light/device_trigger.py
@@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
-async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict:
+async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List trigger capabilities."""
- return await toggle_entity.async_get_trigger_capabilities(hass, trigger)
+ return await toggle_entity.async_get_trigger_capabilities(hass, config)
diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py
new file mode 100644
index 00000000000..ae618f7a8ef
--- /dev/null
+++ b/homeassistant/components/light/reproduce_state.py
@@ -0,0 +1,94 @@
+"""Reproduce an Light state."""
+import asyncio
+import logging
+from types import MappingProxyType
+from typing import Iterable, Optional
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ STATE_ON,
+ STATE_OFF,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+)
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ DOMAIN,
+ ATTR_BRIGHTNESS,
+ ATTR_COLOR_TEMP,
+ ATTR_EFFECT,
+ ATTR_HS_COLOR,
+ ATTR_RGB_COLOR,
+ ATTR_WHITE_VALUE,
+ ATTR_XY_COLOR,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+VALID_STATES = {STATE_ON, STATE_OFF}
+ATTR_GROUP = [ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_WHITE_VALUE]
+COLOR_GROUP = [ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_XY_COLOR]
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if state.state not in VALID_STATES:
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state and all(
+ check_attr_equal(cur_state.attributes, state.attributes, attr)
+ for attr in ATTR_GROUP + COLOR_GROUP
+ ):
+ return
+
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+
+ if state.state == STATE_ON:
+ service = SERVICE_TURN_ON
+ for attr in ATTR_GROUP:
+ # All attributes that are not colors
+ if attr in state.attributes:
+ service_data[attr] = state.attributes[attr]
+
+ for color_attr in COLOR_GROUP:
+ # Choose the first color that is specified
+ if color_attr in state.attributes:
+ service_data[color_attr] = state.attributes[color_attr]
+ break
+
+ elif state.state == STATE_OFF:
+ service = SERVICE_TURN_OFF
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Light states."""
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
+
+
+def check_attr_equal(
+ attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str
+) -> bool:
+ """Return true if the given attributes are equal."""
+ return attr1.get(attr_str) == attr2.get(attr_str)
diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml
index ef944d75efc..9173f79f964 100644
--- a/homeassistant/components/light/services.yaml
+++ b/homeassistant/components/light/services.yaml
@@ -5,22 +5,22 @@ turn_on:
fields:
entity_id:
description: Name(s) of entities to turn on
- example: 'light.kitchen'
+ example: "light.kitchen"
transition:
description: Duration in seconds it takes to get to next state
example: 60
rgb_color:
description: Color for the light in RGB-format.
- example: '[255, 100, 100]'
+ example: "[255, 100, 100]"
color_name:
description: A human readable color name.
- example: 'red'
+ example: "red"
hs_color:
description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
- example: '[300, 70]'
+ example: "[300, 70]"
xy_color:
description: Color for the light in XY-format.
- example: '[0.52, 0.43]'
+ example: "[0.52, 0.43]"
color_temp:
description: Color temperature for the light in mireds.
example: 250
@@ -29,7 +29,7 @@ turn_on:
example: 4000
white_value:
description: Number between 0..255 indicating level of white.
- example: '250'
+ example: "250"
brightness:
description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light.
example: 120
@@ -40,12 +40,14 @@ turn_on:
description: Name of a light profile to use.
example: relax
flash:
- description: If the light should flash.
+ description: If the light should flash. Valid values are short and long.
+ example: short
values:
- short
- long
effect:
description: Light effect.
+ example: random
values:
- colorloop
- random
@@ -55,12 +57,13 @@ turn_off:
fields:
entity_id:
description: Name(s) of entities to turn off.
- example: 'light.kitchen'
+ example: "light.kitchen"
transition:
description: Duration in seconds it takes to get to next state.
example: 60
flash:
- description: If the light should flash.
+ description: If the light should flash. Valid values are short and long.
+ example: short
values:
- short
- long
@@ -68,23 +71,69 @@ turn_off:
toggle:
description: Toggles a light.
fields:
- '...':
- description: All turn_on parameters can be used.
+ entity_id:
+ description: Name(s) of entities to turn on
+ example: "light.kitchen"
+ transition:
+ description: Duration in seconds it takes to get to next state
+ example: 60
+ rgb_color:
+ description: Color for the light in RGB-format.
+ example: "[255, 100, 100]"
+ color_name:
+ description: A human readable color name.
+ example: "red"
+ hs_color:
+ description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
+ example: "[300, 70]"
+ xy_color:
+ description: Color for the light in XY-format.
+ example: "[0.52, 0.43]"
+ color_temp:
+ description: Color temperature for the light in mireds.
+ example: 250
+ kelvin:
+ description: Color temperature for the light in Kelvin.
+ example: 4000
+ white_value:
+ description: Number between 0..255 indicating level of white.
+ example: "250"
+ brightness:
+ description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light.
+ example: 120
+ brightness_pct:
+ description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light.
+ example: 47
+ profile:
+ description: Name of a light profile to use.
+ example: relax
+ flash:
+ description: If the light should flash. Valid values are short and long.
+ example: short
+ values:
+ - short
+ - long
+ effect:
+ description: Light effect.
+ example: random
+ values:
+ - colorloop
+ - random
lifx_set_state:
description: Set a color/brightness and possibliy turn the light on/off.
fields:
entity_id:
description: Name(s) of entities to set a state on.
- example: 'light.garage'
- '...':
+ example: "light.garage"
+ "...":
description: All turn_on parameters can be used to specify a color.
infrared:
description: Automatic infrared level (0..255) when light brightness is low.
example: 255
zones:
description: List of zone numbers to affect (8 per LIFX Z, starts at 0).
- example: '[0,5]'
+ example: "[0,5]"
transition:
description: Duration in seconds it takes to get to the final state.
example: 10
@@ -97,19 +146,19 @@ lifx_effect_pulse:
fields:
entity_id:
description: Name(s) of entities to run the effect on.
- example: 'light.kitchen'
+ example: "light.kitchen"
mode:
- description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid.'
+ description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid."
example: strobe
brightness:
description: Number between 0..255 indicating brightness of the temporary color.
example: 120
color_name:
description: A human readable color name.
- example: 'red'
+ example: "red"
rgb_color:
description: The temporary color in RGB-format.
- example: '[255, 100, 100]'
+ example: "[255, 100, 100]"
period:
description: Duration of the effect in seconds (default 1.0).
example: 3
@@ -125,7 +174,7 @@ lifx_effect_colorloop:
fields:
entity_id:
description: Name(s) of entities to run the effect on.
- example: 'light.disco1, light.disco2, light.disco3'
+ example: "light.disco1, light.disco2, light.disco3"
brightness:
description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light.
example: 120
@@ -147,14 +196,14 @@ lifx_effect_stop:
fields:
entity_id:
description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere.
- example: 'light.bedroom'
+ example: "light.bedroom"
xiaomi_miio_set_scene:
description: Set a fixed scene.
fields:
entity_id:
description: Name of the light entity.
- example: 'light.xiaomi_miio'
+ example: "light.xiaomi_miio"
scene:
description: Number of the fixed scene, between 1 and 4.
example: 1
@@ -164,7 +213,7 @@ xiaomi_miio_set_delayed_turn_off:
fields:
entity_id:
description: Name of the light entity.
- example: 'light.xiaomi_miio'
+ example: "light.xiaomi_miio"
time_period:
description: Time period for the delayed turn off.
example: "5, '0:05', {'minutes': 5}"
diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json
index a4f68fa8687..d4fa7ee4d11 100644
--- a/homeassistant/components/linky/.translations/pl.json
+++ b/homeassistant/components/linky/.translations/pl.json
@@ -14,7 +14,7 @@
"user": {
"data": {
"password": "Has\u0142o",
- "username": "E-mail"
+ "username": "Adres e-mail"
},
"description": "Wprowad\u017a dane uwierzytelniaj\u0105ce",
"title": "Linky"
diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json
index b569cce9239..463343490a7 100644
--- a/homeassistant/components/linky/.translations/ru.json
+++ b/homeassistant/components/linky/.translations/ru.json
@@ -4,11 +4,11 @@
"username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430."
},
"error": {
- "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443",
- "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)",
- "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)",
+ "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.",
+ "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).",
+ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).",
"username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.",
- "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c"
+ "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c."
},
"step": {
"user": {
@@ -16,7 +16,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
},
- "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435",
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"title": "Linky"
}
},
diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py
index a7f3d7bb03e..1d382b43525 100644
--- a/homeassistant/components/linky/__init__.py
+++ b/homeassistant/components/linky/__init__.py
@@ -47,9 +47,12 @@ async def async_setup(hass, config):
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Set up Linky sensors."""
-
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
-
return True
+
+
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
+ """Unload Linky sensors."""
+ return await hass.config_entries.async_forward_entry_unload(entry, "sensor")
diff --git a/homeassistant/components/linode/__init__.py b/homeassistant/components/linode/__init__.py
index 6f590c33e08..a18b63d7226 100644
--- a/homeassistant/components/linode/__init__.py
+++ b/homeassistant/components/linode/__init__.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+import linode
import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN
@@ -35,8 +36,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Linode component."""
- import linode
-
conf = config[DOMAIN]
access_token = conf.get(CONF_ACCESS_TOKEN)
@@ -58,16 +57,12 @@ class Linode:
def __init__(self, access_token):
"""Initialize the Linode connection."""
- import linode
-
self._access_token = access_token
self.data = None
self.manager = linode.LinodeClient(token=self._access_token)
def get_node_id(self, node_name):
"""Get the status of a Linode Instance."""
- import linode
-
node_id = None
try:
@@ -83,8 +78,6 @@ class Linode:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Use the data from Linode API."""
- import linode
-
try:
self.data = self.manager.linode.get_instances()
except linode.errors.ApiError as _ex:
diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py
index 9256c3ad18d..bc02affdaed 100644
--- a/homeassistant/components/linux_battery/sensor.py
+++ b/homeassistant/components/linux_battery/sensor.py
@@ -2,6 +2,7 @@
import logging
import os
+from batinfo import Batteries
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -72,15 +73,12 @@ class LinuxBatterySensor(Entity):
def __init__(self, name, battery_id, system):
"""Initialize the battery sensor."""
- import batinfo
-
- self._battery = batinfo.Batteries()
+ self._battery = Batteries()
self._name = name
self._battery_stat = None
self._battery_id = battery_id - 1
self._system = system
- self._unit_of_measurement = "%"
@property
def name(self):
@@ -100,7 +98,7 @@ class LinuxBatterySensor(Entity):
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
- return self._unit_of_measurement
+ return "%"
@property
def device_state_attributes(self):
diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py
index 47814d00e9a..bfc8e455624 100644
--- a/homeassistant/components/lirc/__init__.py
+++ b/homeassistant/components/lirc/__init__.py
@@ -1,12 +1,13 @@
"""Support for LIRC devices."""
# pylint: disable=no-member, import-error
+import logging
import threading
import time
-import logging
+import lirc
import voluptuous as vol
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START
+from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
_LOGGER = logging.getLogger(__name__)
@@ -23,8 +24,6 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the LIRC capability."""
- import lirc
-
# blocking=True gives unexpected behavior (multiple responses for 1 press)
# also by not blocking, we allow hass to shut down the thread gracefully
# on exit.
@@ -61,8 +60,6 @@ class LircInterface(threading.Thread):
def run(self):
"""Run the loop of the LIRC interface thread."""
- import lirc
-
_LOGGER.debug("LIRC interface thread started")
while not self.stopped.isSet():
try:
diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py
index c466d71c4c5..996b4f33b50 100644
--- a/homeassistant/components/liveboxplaytv/media_player.py
+++ b/homeassistant/components/liveboxplaytv/media_player.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+from liveboxplaytv import LiveboxPlayTv
+import pyteleloisirs
import requests
import voluptuous as vol
@@ -85,7 +87,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
def __init__(self, host, port, name):
"""Initialize the Livebox Play TV device."""
- from liveboxplaytv import LiveboxPlayTv
self._client = LiveboxPlayTv(host, port)
# Assume that the appliance is not muted
@@ -103,7 +104,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
async def async_update(self):
"""Retrieve the latest data."""
- import pyteleloisirs
try:
self._state = self.refresh_state()
diff --git a/homeassistant/components/locative/.translations/hu.json b/homeassistant/components/locative/.translations/hu.json
index e90910c29a2..3528f1c1e45 100644
--- a/homeassistant/components/locative/.translations/hu.json
+++ b/homeassistant/components/locative/.translations/hu.json
@@ -6,7 +6,7 @@
},
"step": {
"user": {
- "description": "Biztosan be szeretn\u00e9d be\u00e1ll\u00edtani a Locative Webhookot?",
+ "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Locative Webhook-ot?",
"title": "Locative Webhook be\u00e1ll\u00edt\u00e1sa"
}
},
diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py
index 61e0b1f7474..ed8bcb6e7e5 100644
--- a/homeassistant/components/locative/__init__.py
+++ b/homeassistant/components/locative/__init__.py
@@ -127,8 +127,7 @@ async def async_unload_entry(hass, entry):
"""Unload a config entry."""
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
- await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
- return True
+ return await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
# pylint: disable=invalid-name
diff --git a/homeassistant/components/lock/.translations/ca.json b/homeassistant/components/lock/.translations/ca.json
new file mode 100644
index 00000000000..53198a21573
--- /dev/null
+++ b/homeassistant/components/lock/.translations/ca.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "Bloqueja {entity_name}",
+ "open": "Obre {entity_name}",
+ "unlock": "Desbloqueja {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} est\u00e0 bloquejat/ada",
+ "is_unlocked": "{entity_name} est\u00e0 desbloquejat/ada"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/da.json b/homeassistant/components/lock/.translations/da.json
new file mode 100644
index 00000000000..de4f603ac43
--- /dev/null
+++ b/homeassistant/components/lock/.translations/da.json
@@ -0,0 +1,12 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "L\u00e5s {entity_name}",
+ "open": "\u00c5ben {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} er l\u00e5st",
+ "is_unlocked": "{entity_name} er l\u00e5st op"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/de.json b/homeassistant/components/lock/.translations/de.json
new file mode 100644
index 00000000000..02c387ff487
--- /dev/null
+++ b/homeassistant/components/lock/.translations/de.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_locked": "{entity_name} ist gesperrt",
+ "is_unlocked": "{entity_name} ist entsperrt"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/en.json b/homeassistant/components/lock/.translations/en.json
new file mode 100644
index 00000000000..a9800eecadd
--- /dev/null
+++ b/homeassistant/components/lock/.translations/en.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "Lock {entity_name}",
+ "open": "Open {entity_name}",
+ "unlock": "Unlock {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} is locked",
+ "is_unlocked": "{entity_name} is unlocked"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/es.json b/homeassistant/components/lock/.translations/es.json
new file mode 100644
index 00000000000..c6ef789e9cb
--- /dev/null
+++ b/homeassistant/components/lock/.translations/es.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "Bloquear {entity_name}",
+ "open": "Abrir {entity_name}",
+ "unlock": "Desbloquear {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} est\u00e1 bloqueado",
+ "is_unlocked": "{entity_name} est\u00e1 desbloqueado"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/fr.json b/homeassistant/components/lock/.translations/fr.json
new file mode 100644
index 00000000000..748a1e9290c
--- /dev/null
+++ b/homeassistant/components/lock/.translations/fr.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "V\u00e9rouiller {entity_name}",
+ "open": "Ouvre {entity_name}",
+ "unlock": "D\u00e9verrouiller {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} est verrouill\u00e9",
+ "is_unlocked": "{entity_name} est d\u00e9verrouill\u00e9"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/it.json b/homeassistant/components/lock/.translations/it.json
new file mode 100644
index 00000000000..f56ef71060b
--- /dev/null
+++ b/homeassistant/components/lock/.translations/it.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_locked": "{entity_name} \u00e8 bloccato",
+ "is_unlocked": "{entity_name} \u00e8 sbloccato"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/ko.json b/homeassistant/components/lock/.translations/ko.json
new file mode 100644
index 00000000000..6abd9cd60e6
--- /dev/null
+++ b/homeassistant/components/lock/.translations/ko.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "{entity_name} \uc7a0\uae08",
+ "open": "{entity_name} \uc5f4\uae30",
+ "unlock": "{entity_name} \uc7a0\uae08 \ud574\uc81c"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4",
+ "is_unlocked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/lb.json b/homeassistant/components/lock/.translations/lb.json
new file mode 100644
index 00000000000..90dd7e6087a
--- /dev/null
+++ b/homeassistant/components/lock/.translations/lb.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "{entity_name} sp\u00e4ren",
+ "open": "{entity_name} opmaachen",
+ "unlock": "{entity_name} entsp\u00e4ren"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} ass gespaart",
+ "is_unlocked": "{entity_name} ass entspaart"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/nl.json b/homeassistant/components/lock/.translations/nl.json
new file mode 100644
index 00000000000..099b7308334
--- /dev/null
+++ b/homeassistant/components/lock/.translations/nl.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "Vergrendel {entity_name}",
+ "open": "Open {entity_name}",
+ "unlock": "Ontgrendel {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} is vergrendeld",
+ "is_unlocked": "{entity_name} is ontgrendeld"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/no.json b/homeassistant/components/lock/.translations/no.json
new file mode 100644
index 00000000000..28c809a82d1
--- /dev/null
+++ b/homeassistant/components/lock/.translations/no.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "L\u00e5s {entity_name}",
+ "open": "\u00c5pne {entity_name}",
+ "unlock": "L\u00e5s opp {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} er l\u00e5st",
+ "is_unlocked": "{entity_name} er l\u00e5st opp"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/pl.json b/homeassistant/components/lock/.translations/pl.json
new file mode 100644
index 00000000000..a3fe7358398
--- /dev/null
+++ b/homeassistant/components/lock/.translations/pl.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "zablokuj {entity_name}",
+ "open": "otw\u00f3rz {entity_name}",
+ "unlock": "odblokuj {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "zamek {entity_name} jest zamkni\u0119ty",
+ "is_unlocked": "zamek {entity_name} jest otwarty"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/ru.json b/homeassistant/components/lock/.translations/ru.json
new file mode 100644
index 00000000000..1610668721f
--- /dev/null
+++ b/homeassistant/components/lock/.translations/ru.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c {entity_name}",
+ "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c {entity_name}",
+ "unlock": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_unlocked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/sl.json b/homeassistant/components/lock/.translations/sl.json
new file mode 100644
index 00000000000..d2e32499d2e
--- /dev/null
+++ b/homeassistant/components/lock/.translations/sl.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "Zakleni {entity_name}",
+ "open": "Odpri {entity_name}",
+ "unlock": "Odkleni {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} je/so zaklenjen/a",
+ "is_unlocked": "{entity_name} je/so odklenjen/a"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/.translations/zh-Hant.json b/homeassistant/components/lock/.translations/zh-Hant.json
new file mode 100644
index 00000000000..7c8abb76e16
--- /dev/null
+++ b/homeassistant/components/lock/.translations/zh-Hant.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "\u4e0a\u9396 {entity_name}",
+ "open": "\u958b\u555f {entity_name}",
+ "unlock": "\u89e3\u9396 {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} \u5df2\u4e0a\u9396",
+ "is_unlocked": "{entity_name} \u5df2\u89e3\u9396"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py
new file mode 100644
index 00000000000..c678bfc17cf
--- /dev/null
+++ b/homeassistant/components/lock/device_action.py
@@ -0,0 +1,92 @@
+"""Provides device automations for Lock."""
+from typing import Optional, List
+import voluptuous as vol
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ CONF_DOMAIN,
+ CONF_TYPE,
+ CONF_DEVICE_ID,
+ CONF_ENTITY_ID,
+ SERVICE_LOCK,
+ SERVICE_OPEN,
+ SERVICE_UNLOCK,
+)
+from homeassistant.core import HomeAssistant, Context
+from homeassistant.helpers import entity_registry
+import homeassistant.helpers.config_validation as cv
+from . import DOMAIN, SUPPORT_OPEN
+
+ACTION_TYPES = {"lock", "unlock", "open"}
+
+ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_TYPE): vol.In(ACTION_TYPES),
+ vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
+ }
+)
+
+
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+ """List device actions for Lock devices."""
+ registry = await entity_registry.async_get_registry(hass)
+ actions = []
+
+ # Get all the integrations entities for this device
+ for entry in entity_registry.async_entries_for_device(registry, device_id):
+ if entry.domain != DOMAIN:
+ continue
+
+ # Add actions for each entity that belongs to this integration
+ actions.append(
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "lock",
+ }
+ )
+ actions.append(
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "unlock",
+ }
+ )
+
+ state = hass.states.get(entry.entity_id)
+ if state:
+ features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ if features & (SUPPORT_OPEN):
+ actions.append(
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "open",
+ }
+ )
+
+ return actions
+
+
+async def async_call_action_from_config(
+ hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+) -> None:
+ """Execute a device action."""
+ config = ACTION_SCHEMA(config)
+
+ service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
+
+ if config[CONF_TYPE] == "lock":
+ service = SERVICE_LOCK
+ elif config[CONF_TYPE] == "unlock":
+ service = SERVICE_UNLOCK
+ elif config[CONF_TYPE] == "open":
+ service = SERVICE_OPEN
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, blocking=True, context=context
+ )
diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py
new file mode 100644
index 00000000000..328da6ad450
--- /dev/null
+++ b/homeassistant/components/lock/device_condition.py
@@ -0,0 +1,79 @@
+"""Provides device automations for Lock."""
+from typing import List
+import voluptuous as vol
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_CONDITION,
+ CONF_DOMAIN,
+ CONF_TYPE,
+ CONF_DEVICE_ID,
+ CONF_ENTITY_ID,
+ STATE_LOCKED,
+ STATE_UNLOCKED,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import condition, config_validation as cv, entity_registry
+from homeassistant.helpers.typing import ConfigType, TemplateVarsType
+from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
+from . import DOMAIN
+
+CONDITION_TYPES = {"is_locked", "is_unlocked"}
+
+CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES),
+ }
+)
+
+
+async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]:
+ """List device conditions for Lock devices."""
+ registry = await entity_registry.async_get_registry(hass)
+ conditions = []
+
+ # Get all the integrations entities for this device
+ for entry in entity_registry.async_entries_for_device(registry, device_id):
+ if entry.domain != DOMAIN:
+ continue
+
+ # Add conditions for each entity that belongs to this integration
+ conditions.append(
+ {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "is_locked",
+ }
+ )
+ conditions.append(
+ {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "is_unlocked",
+ }
+ )
+
+ return conditions
+
+
+def async_condition_from_config(
+ config: ConfigType, config_validation: bool
+) -> condition.ConditionCheckerType:
+ """Create a function to test a device condition."""
+ if config_validation:
+ config = CONDITION_SCHEMA(config)
+ if config[CONF_TYPE] == "is_locked":
+ state = STATE_LOCKED
+ else:
+ state = STATE_UNLOCKED
+
+ def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
+ """Test if an entity is a certain state."""
+ return condition.state(hass, config[ATTR_ENTITY_ID], state)
+
+ return test_is_state
diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py
new file mode 100644
index 00000000000..dc1bee85839
--- /dev/null
+++ b/homeassistant/components/lock/reproduce_state.py
@@ -0,0 +1,61 @@
+"""Reproduce an Lock state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ STATE_LOCKED,
+ STATE_UNLOCKED,
+ SERVICE_LOCK,
+ SERVICE_UNLOCK,
+)
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+VALID_STATES = {STATE_LOCKED, STATE_UNLOCKED}
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if state.state not in VALID_STATES:
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state:
+ return
+
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+
+ if state.state == STATE_LOCKED:
+ service = SERVICE_LOCK
+ elif state.state == STATE_UNLOCKED:
+ service = SERVICE_UNLOCK
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Lock states."""
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml
index 0b4688c02a2..d17e00addd1 100644
--- a/homeassistant/components/lock/services.yaml
+++ b/homeassistant/components/lock/services.yaml
@@ -47,6 +47,16 @@ lock:
description: An optional code to lock the lock with.
example: 1234
+open:
+ description: Open all or specified locks.
+ fields:
+ entity_id:
+ description: Name of lock to open.
+ example: 'lock.front_door'
+ code:
+ description: An optional code to open the lock with.
+ example: 1234
+
set_usercode:
description: Set a usercode to lock.
fields:
diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json
new file mode 100644
index 00000000000..9c858916476
--- /dev/null
+++ b/homeassistant/components/lock/strings.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "Lock {entity_name}",
+ "open": "Open {entity_name}",
+ "unlock": "Unlock {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} is locked",
+ "is_unlocked": "{entity_name} is unlocked"
+ }
+ }
+}
diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py
index 3c5e828765c..8675f778a26 100644
--- a/homeassistant/components/logbook/__init__.py
+++ b/homeassistant/components/logbook/__init__.py
@@ -2,12 +2,26 @@
from datetime import timedelta
from itertools import groupby
import logging
+import time
+from sqlalchemy.exc import SQLAlchemyError
import voluptuous as vol
-from homeassistant.loader import bind_hass
from homeassistant.components import sun
+from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME
+from homeassistant.components.homekit.const import (
+ ATTR_DISPLAY_NAME,
+ ATTR_VALUE,
+ DOMAIN as DOMAIN_HOMEKIT,
+ EVENT_HOMEKIT_CHANGED,
+)
from homeassistant.components.http import HomeAssistantView
+from homeassistant.components.recorder.models import Events, States
+from homeassistant.components.recorder.util import (
+ QUERY_RETRY_WAIT,
+ RETRIES,
+ session_scope,
+)
from homeassistant.const import (
ATTR_DOMAIN,
ATTR_ENTITY_ID,
@@ -16,26 +30,21 @@ from homeassistant.const import (
ATTR_SERVICE,
CONF_EXCLUDE,
CONF_INCLUDE,
+ EVENT_AUTOMATION_TRIGGERED,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
EVENT_LOGBOOK_ENTRY,
- EVENT_STATE_CHANGED,
- EVENT_AUTOMATION_TRIGGERED,
EVENT_SCRIPT_STARTED,
+ EVENT_STATE_CHANGED,
HTTP_BAD_REQUEST,
STATE_NOT_HOME,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import DOMAIN as HA_DOMAIN, State, callback, split_entity_id
-from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME
-from homeassistant.components.homekit.const import (
- ATTR_DISPLAY_NAME,
- ATTR_VALUE,
- DOMAIN as DOMAIN_HOMEKIT,
- EVENT_HOMEKIT_CHANGED,
-)
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entityfilter import generate_filter
+from homeassistant.loader import bind_hass
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -371,11 +380,6 @@ def humanify(hass, events):
def _get_related_entity_ids(session, entity_filter):
- from homeassistant.components.recorder.models import States
- from homeassistant.components.recorder.util import RETRIES, QUERY_RETRY_WAIT
- from sqlalchemy.exc import SQLAlchemyError
- import time
-
timer_start = time.perf_counter()
query = session.query(States).with_entities(States.entity_id).distinct()
@@ -402,8 +406,6 @@ def _get_related_entity_ids(session, entity_filter):
def _generate_filter_from_config(config):
- from homeassistant.helpers.entityfilter import generate_filter
-
excluded_entities = []
excluded_domains = []
included_entities = []
@@ -425,9 +427,6 @@ def _generate_filter_from_config(config):
def _get_events(hass, config, start_day, end_day, entity_id=None):
"""Get events for a period of time."""
- from homeassistant.components.recorder.models import Events, States
- from homeassistant.components.recorder.util import session_scope
-
entities_filter = _generate_filter_from_config(config)
def yield_events(query):
diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml
index e69de29bb2d..08c463feed2 100644
--- a/homeassistant/components/logbook/services.yaml
+++ b/homeassistant/components/logbook/services.yaml
@@ -0,0 +1,15 @@
+log:
+ description: Create a custom entry in your logbook.
+ fields:
+ name:
+ description: Custom name for an entity, can be referenced with entity_id
+ example: "Kitchen"
+ message:
+ description: Message of the custom logbook entry
+ example: "is being used"
+ entity_id:
+ description: Entity to reference in custom logbook entry [Optional]
+ example: "light.kitchen"
+ domain:
+ description: Icon of domain to display in custom logbook entry [Optional]
+ example: "light"
\ No newline at end of file
diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml
index 4d1ba649d36..514aac4c71c 100644
--- a/homeassistant/components/logger/services.yaml
+++ b/homeassistant/components/logger/services.yaml
@@ -1,6 +1,22 @@
set_default_level:
description: Set the default log level for components.
fields:
- level: {description: 'Default severity level. Possible values are notset, debug,
- info, warn, warning, error, fatal, critical', example: debug}
-set_level: {description: Set log level for components.}
+ level:
+ description: "Default severity level. Possible values are debug, info, warn, warning, error, fatal, critical"
+ example: debug
+
+set_level:
+ description: Set log level for components.
+ fields:
+ homeassistant.core:
+ description: "Example on how to change the logging level for a Home Assistant core components. Possible values are debug, info, warn, warning, error, fatal, critical"
+ example: debug
+ homeassistant.components.mqtt:
+ description: "Example on how to change the logging level for an Integration. Possible values are debug, info, warn, warning, error, fatal, critical"
+ example: warning
+ custom_components.my_integration:
+ description: "Example on how to change the logging level for a Custom Integration. Possible values are debug, info, warn, warning, error, fatal, critical"
+ example: debug
+ aiohttp:
+ description: "Example on how to change the logging level for a Python module. Possible values are debug, info, warn, warning, error, fatal, critical"
+ example: error
diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py
index 27b81d8331e..ec8f1595168 100644
--- a/homeassistant/components/logi_circle/camera.py
+++ b/homeassistant/components/logi_circle/camera.py
@@ -148,11 +148,11 @@ class LogiCam(Camera):
async def async_turn_off(self):
"""Disable streaming mode for this camera."""
- await self._camera.set_streaming_mode(False)
+ await self._camera.set_config("streaming", False)
async def async_turn_on(self):
"""Enable streaming mode for this camera."""
- await self._camera.set_streaming_mode(True)
+ await self._camera.set_config("streaming", True)
@property
def should_poll(self):
diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py
index 994c3e2fd89..537907d9d0a 100644
--- a/homeassistant/components/loopenergy/sensor.py
+++ b/homeassistant/components/loopenergy/sensor.py
@@ -1,6 +1,7 @@
"""Support for Loop Energy sensors."""
import logging
+import pyloopenergy
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -54,8 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Loop Energy sensors."""
- import pyloopenergy
-
elec_config = config.get(CONF_ELEC)
gas_config = config.get(CONF_GAS, {})
diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py
index 87a32767cc2..59c3251a437 100644
--- a/homeassistant/components/luci/device_tracker.py
+++ b/homeassistant/components/luci/device_tracker.py
@@ -8,12 +8,19 @@ from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA,
DeviceScanner,
)
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_USERNAME
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_SSL = False
+DEFAULT_VERIFY_SSL = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -21,6 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
+ vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
}
)
@@ -44,6 +52,7 @@ class LuciDeviceScanner(DeviceScanner):
config[CONF_USERNAME],
config[CONF_PASSWORD],
config[CONF_SSL],
+ config[CONF_VERIFY_SSL],
)
self.last_results = {}
diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json
index 646fc1a3cbf..d7cf72ebaf5 100644
--- a/homeassistant/components/luci/manifest.json
+++ b/homeassistant/components/luci/manifest.json
@@ -3,8 +3,11 @@
"name": "Luci",
"documentation": "https://www.home-assistant.io/integrations/luci",
"requirements": [
- "openwrt-luci-rpc==1.1.1"
+ "openwrt-luci-rpc==1.1.2"
],
"dependencies": [],
- "codeowners": ["@fbradyirl"]
+ "codeowners": [
+ "@fbradyirl",
+ "@mzdrale"
+ ]
}
diff --git a/homeassistant/components/luftdaten/.translations/nn.json b/homeassistant/components/luftdaten/.translations/nn.json
new file mode 100644
index 00000000000..52b1ec33166
--- /dev/null
+++ b/homeassistant/components/luftdaten/.translations/nn.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Luftdaten"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json
index 7ae83b550e3..1a05137f82d 100644
--- a/homeassistant/components/luftdaten/.translations/ru.json
+++ b/homeassistant/components/luftdaten/.translations/ru.json
@@ -1,8 +1,8 @@
{
"config": {
"error": {
- "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten",
- "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d",
+ "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten.",
+ "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.",
"sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d."
},
"step": {
diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py
index 86129eafc02..3dca82404c0 100644
--- a/homeassistant/components/luftdaten/__init__.py
+++ b/homeassistant/components/luftdaten/__init__.py
@@ -1,6 +1,8 @@
"""Support for Luftdaten stations."""
import logging
+from luftdaten import Luftdaten
+from luftdaten.exceptions import LuftdatenError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
@@ -114,8 +116,6 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, config_entry):
"""Set up Luftdaten as config entry."""
- from luftdaten import Luftdaten
- from luftdaten.exceptions import LuftdatenError
if not isinstance(config_entry.data[CONF_SENSOR_ID], int):
_async_fixup_sensor_id(hass, config_entry, config_entry.data[CONF_SENSOR_ID])
@@ -172,12 +172,9 @@ async def async_unload_entry(hass, config_entry):
)
remove_listener()
- for component in ("sensor",):
- await hass.config_entries.async_forward_entry_unload(config_entry, component)
-
hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT].pop(config_entry.entry_id)
- return True
+ return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
class LuftDatenData:
@@ -191,8 +188,6 @@ class LuftDatenData:
async def async_update(self):
"""Update sensor/binary sensor data."""
- from luftdaten.exceptions import LuftdatenError
-
try:
await self.client.get_data()
diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py
index 7a8ef0df8ba..1f382b86c0f 100644
--- a/homeassistant/components/luftdaten/config_flow.py
+++ b/homeassistant/components/luftdaten/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow to configure the Luftdaten component."""
from collections import OrderedDict
+from luftdaten import Luftdaten
+from luftdaten.exceptions import LuftdatenConnectionError
import voluptuous as vol
from homeassistant import config_entries
@@ -60,7 +62,6 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow):
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
- from luftdaten import Luftdaten, exceptions
if not user_input:
return self._show_form()
@@ -75,7 +76,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow):
try:
await luftdaten.get_data()
valid = await luftdaten.validate_sensor()
- except exceptions.LuftdatenConnectionError:
+ except LuftdatenConnectionError:
return self._show_form({CONF_SENSOR_ID: "communication_error"})
if not valid:
diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py
index c64789ec4dd..60f3a192b07 100644
--- a/homeassistant/components/lupusec/__init__.py
+++ b/homeassistant/components/lupusec/__init__.py
@@ -1,11 +1,12 @@
"""Support for Lupusec Home Security system."""
import logging
+import lupupy
+from lupupy.exceptions import LupusecException
import voluptuous as vol
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers import discovery
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_IP_ADDRESS
+from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -34,8 +35,6 @@ LUPUSEC_PLATFORMS = ["alarm_control_panel", "binary_sensor", "switch"]
def setup(hass, config):
"""Set up Lupusec component."""
- from lupupy.exceptions import LupusecException
-
conf = config[DOMAIN]
username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD]
@@ -67,8 +66,6 @@ class LupusecSystem:
def __init__(self, username, password, ip_address, name):
"""Initialize the system."""
- import lupupy
-
self.lupusec = lupupy.Lupusec(username, password, ip_address)
self.name = name
diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py
index ccd45e9f874..b2a332a03e7 100644
--- a/homeassistant/components/lupusec/binary_sensor.py
+++ b/homeassistant/components/lupusec/binary_sensor.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+import lupupy.constants as CONST
+
from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorDevice
from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice
@@ -16,8 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if discovery_info is None:
return
- import lupupy.constants as CONST
-
data = hass.data[LUPUSEC_DOMAIN]
device_types = [CONST.TYPE_OPENING]
diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py
index b6391959397..a6864f39ef7 100644
--- a/homeassistant/components/lupusec/switch.py
+++ b/homeassistant/components/lupusec/switch.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+import lupupy.constants as CONST
+
from homeassistant.components.switch import SwitchDevice
from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice
@@ -16,8 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if discovery_info is None:
return
- import lupupy.constants as CONST
-
data = hass.data[LUPUSEC_DOMAIN]
devices = []
diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py
index de3ca40fd1d..09ab0fc747b 100644
--- a/homeassistant/components/lutron/__init__.py
+++ b/homeassistant/components/lutron/__init__.py
@@ -68,18 +68,19 @@ def setup(hass, base_config):
hass.data[LUTRON_DEVICES]["switch"].append((area.name, output))
for keypad in area.keypads:
for button in keypad.buttons:
- # This is the best way to determine if a button does anything
- # useful until pylutron is updated to provide information on
- # which buttons actually control scenes.
- for led in keypad.leds:
- if (
- led.number == button.number
- and button.name != "Unknown Button"
- and button.button_type in ("SingleAction", "Toggle")
- ):
- hass.data[LUTRON_DEVICES]["scene"].append(
- (area.name, keypad.name, button, led)
- )
+ # If the button has a function assigned to it, add it as a scene
+ if button.name != "Unknown Button" and button.button_type in (
+ "SingleAction",
+ "Toggle",
+ ):
+ # Associate an LED with a button if there is one
+ led = next(
+ (led for led in keypad.leds if led.number == button.number),
+ None,
+ )
+ hass.data[LUTRON_DEVICES]["scene"].append(
+ (area.name, keypad.name, button, led)
+ )
hass.data[LUTRON_BUTTONS].append(LutronButton(hass, keypad, button))
if area.occupancy_group is not None:
diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py
index 3b9ccae1681..abf75a1e318 100644
--- a/homeassistant/components/lw12wifi/light.py
+++ b/homeassistant/components/lw12wifi/light.py
@@ -2,6 +2,7 @@
import logging
+import lw12
import voluptuous as vol
from homeassistant.components.light import (
@@ -9,18 +10,17 @@ from homeassistant.components.light import (
ATTR_EFFECT,
ATTR_HS_COLOR,
ATTR_TRANSITION,
- Light,
PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
- SUPPORT_EFFECT,
SUPPORT_COLOR,
+ SUPPORT_EFFECT,
SUPPORT_TRANSITION,
+ Light,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
-
_LOGGER = logging.getLogger(__name__)
@@ -38,8 +38,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up LW-12 WiFi LED Controller platform."""
- import lw12
-
# Assign configuration variables.
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
@@ -107,8 +105,6 @@ class LW12WiFi(Light):
Use the Enum element name for display.
"""
- import lw12
-
return [effect.name.replace("_", " ").title() for effect in lw12.LW12_EFFECT]
@property
@@ -123,8 +119,6 @@ class LW12WiFi(Light):
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
- import lw12
-
self._light.light_on()
if ATTR_HS_COLOR in kwargs:
self._rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py
index 66ab87a6569..174ecf1882e 100644
--- a/homeassistant/components/magicseaweed/sensor.py
+++ b/homeassistant/components/magicseaweed/sensor.py
@@ -1,19 +1,21 @@
"""Support for magicseaweed data from magicseaweed.com."""
from datetime import timedelta
import logging
+
+import magicseaweed
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_API_KEY,
- CONF_NAME,
- CONF_MONITORED_CONDITIONS,
ATTR_ATTRIBUTION,
+ CONF_API_KEY,
+ CONF_MONITORED_CONDITIONS,
+ CONF_NAME,
)
import homeassistant.helpers.config_validation as cv
-import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
+import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -175,8 +177,6 @@ class MagicSeaweedData:
def __init__(self, api_key, spot_id, units):
"""Initialize the data object."""
- import magicseaweed
-
self._msw = magicseaweed.MSW_Forecast(api_key, spot_id, None, units)
self.currently = None
self.hourly = {}
diff --git a/homeassistant/components/mailgun/.translations/ca.json b/homeassistant/components/mailgun/.translations/ca.json
index f43467de7d9..6bcb737588a 100644
--- a/homeassistant/components/mailgun/.translations/ca.json
+++ b/homeassistant/components/mailgun/.translations/ca.json
@@ -5,7 +5,7 @@
"one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
},
"create_entry": {
- "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants."
+ "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants."
},
"step": {
"user": {
diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json
index 39503154b6c..094940e6f90 100644
--- a/homeassistant/components/mailgun/.translations/ru.json
+++ b/homeassistant/components/mailgun/.translations/ru.json
@@ -10,7 +10,7 @@
"step": {
"user": {
"description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Mailgun?",
- "title": "Mailgun Webhook"
+ "title": "Mailgun"
}
},
"title": "Mailgun"
diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json
index e041ba2f669..cacdf9dd502 100644
--- a/homeassistant/components/mastodon/manifest.json
+++ b/homeassistant/components/mastodon/manifest.json
@@ -3,7 +3,7 @@
"name": "Mastodon",
"documentation": "https://www.home-assistant.io/integrations/mastodon",
"requirements": [
- "Mastodon.py==1.4.6"
+ "Mastodon.py==1.5.0"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py
index e7f7de5917f..88716de5773 100644
--- a/homeassistant/components/mastodon/notify.py
+++ b/homeassistant/components/mastodon/notify.py
@@ -1,13 +1,14 @@
"""Mastodon platform for notify component."""
import logging
+from mastodon import Mastodon
+from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError
import voluptuous as vol
+from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService
from homeassistant.const import CONF_ACCESS_TOKEN
import homeassistant.helpers.config_validation as cv
-from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService
-
_LOGGER = logging.getLogger(__name__)
CONF_BASE_URL = "base_url"
@@ -28,9 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the Mastodon notification service."""
- from mastodon import Mastodon
- from mastodon.Mastodon import MastodonUnauthorizedError
-
client_id = config.get(CONF_CLIENT_ID)
client_secret = config.get(CONF_CLIENT_SECRET)
access_token = config.get(CONF_ACCESS_TOKEN)
@@ -60,8 +58,6 @@ class MastodonNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
- from mastodon.Mastodon import MastodonAPIError
-
try:
self._api.toot(message)
except MastodonAPIError:
diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py
index c059ad6a1b6..088052c469e 100644
--- a/homeassistant/components/mcp23017/binary_sensor.py
+++ b/homeassistant/components/mcp23017/binary_sensor.py
@@ -2,6 +2,10 @@
import logging
import voluptuous as vol
+import board # pylint: disable=import-error
+import busio # pylint: disable=import-error
+import adafruit_mcp230xx # pylint: disable=import-error
+import digitalio # pylint: disable=import-error
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
from homeassistant.const import DEVICE_DEFAULT_NAME
@@ -37,10 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MCP23017 binary sensors."""
- import board
- import busio
- import adafruit_mcp230xx
-
pull_mode = config[CONF_PULL_MODE]
invert_logic = config[CONF_INVERT_LOGIC]
i2c_address = config[CONF_I2C_ADDRESS]
@@ -65,8 +65,6 @@ class MCP23017BinarySensor(BinarySensorDevice):
def __init__(self, name, pin, pull_mode, invert_logic):
"""Initialize the MCP23017 binary sensor."""
- import digitalio
-
self._name = name or DEVICE_DEFAULT_NAME
self._pin = pin
self._pull_mode = pull_mode
diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json
index 2dbffd829f8..13c36424dd6 100644
--- a/homeassistant/components/mcp23017/manifest.json
+++ b/homeassistant/components/mcp23017/manifest.json
@@ -3,7 +3,7 @@
"name": "MCP23017 I/O Expander",
"documentation": "https://www.home-assistant.io/integrations/mcp23017",
"requirements": [
- "RPi.GPIO==0.6.5",
+ "RPi.GPIO==0.7.0",
"adafruit-blinka==1.2.1",
"adafruit-circuitpython-mcp230xx==1.1.2"
],
diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py
index 46978319198..399ed17c44b 100644
--- a/homeassistant/components/mcp23017/switch.py
+++ b/homeassistant/components/mcp23017/switch.py
@@ -2,6 +2,10 @@
import logging
import voluptuous as vol
+import board # pylint: disable=import-error
+import busio # pylint: disable=import-error
+import adafruit_mcp230xx # pylint: disable=import-error
+import digitalio # pylint: disable=import-error
from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import DEVICE_DEFAULT_NAME
@@ -31,10 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the MCP23017 devices."""
- import board
- import busio
- import adafruit_mcp230xx
-
invert_logic = config.get(CONF_INVERT_LOGIC)
i2c_address = config.get(CONF_I2C_ADDRESS)
@@ -54,8 +54,6 @@ class MCP23017Switch(ToggleEntity):
def __init__(self, name, pin, invert_logic):
"""Initialize the pin."""
- import digitalio
-
self._name = name or DEVICE_DEFAULT_NAME
self._pin = pin
self._invert_logic = invert_logic
diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json
index 886535555d5..4fd5470ebdf 100644
--- a/homeassistant/components/media_extractor/manifest.json
+++ b/homeassistant/components/media_extractor/manifest.json
@@ -3,7 +3,7 @@
"name": "Media extractor",
"documentation": "https://www.home-assistant.io/integrations/media_extractor",
"requirements": [
- "youtube_dl==2019.09.28"
+ "youtube_dl==2019.10.22"
],
"dependencies": [
"media_player"
diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml
index e69de29bb2d..c5588c34134 100644
--- a/homeassistant/components/media_extractor/services.yaml
+++ b/homeassistant/components/media_extractor/services.yaml
@@ -0,0 +1,13 @@
+play_media:
+ description: Downloads file from given url.
+ fields:
+ entity_id:
+ description: Name(s) of entities to play media on.
+ example: 'media_player.living_room_chromecast'
+ media_content_id:
+ description: The ID of the content to play. Platform dependent.
+ example: 'https://soundcloud.com/bruttoband/brutto-11'
+ media_content_type:
+ description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC.
+ example: 'music'
+
diff --git a/homeassistant/components/melissa/__init__.py b/homeassistant/components/melissa/__init__.py
index 830036b072a..c03939e3e9c 100644
--- a/homeassistant/components/melissa/__init__.py
+++ b/homeassistant/components/melissa/__init__.py
@@ -1,9 +1,10 @@
"""Support for Melissa climate."""
import logging
+import melissa
import voluptuous as vol
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
@@ -28,8 +29,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Set up the Melissa Climate component."""
- import melissa
-
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py
index 10ea6200c6f..38f4977c96a 100644
--- a/homeassistant/components/melissa/climate.py
+++ b/homeassistant/components/melissa/climate.py
@@ -156,7 +156,9 @@ class MelissaClimate(ClimateDevice):
return
mode = self.hass_mode_to_melissa(hvac_mode)
- await self.async_send({self._api.MODE: mode})
+ await self.async_send(
+ {self._api.MODE: mode, self._api.STATE: self._api.STATE_ON}
+ )
async def async_send(self, value):
"""Send action to service."""
diff --git a/homeassistant/components/message_bird/notify.py b/homeassistant/components/message_bird/notify.py
index 5df02ef69c4..ce1d275a832 100644
--- a/homeassistant/components/message_bird/notify.py
+++ b/homeassistant/components/message_bird/notify.py
@@ -1,16 +1,17 @@
"""MessageBird platform for notify component."""
import logging
+import messagebird
+from messagebird.client import ErrorException
import voluptuous as vol
-from homeassistant.const import CONF_API_KEY, CONF_SENDER
-import homeassistant.helpers.config_validation as cv
-
from homeassistant.components.notify import (
ATTR_TARGET,
PLATFORM_SCHEMA,
BaseNotificationService,
)
+from homeassistant.const import CONF_API_KEY, CONF_SENDER
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -26,8 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the MessageBird notification service."""
- import messagebird
-
client = messagebird.Client(config[CONF_API_KEY])
try:
# validates the api key
@@ -49,8 +48,6 @@ class MessageBirdNotificationService(BaseNotificationService):
def send_message(self, message=None, **kwargs):
"""Send a message to a specified target."""
- from messagebird.client import ErrorException
-
targets = kwargs.get(ATTR_TARGET)
if not targets:
_LOGGER.error("No target specified")
diff --git a/homeassistant/components/met/.translations/nl.json b/homeassistant/components/met/.translations/nl.json
index 87f13084f7e..c8b120b855a 100644
--- a/homeassistant/components/met/.translations/nl.json
+++ b/homeassistant/components/met/.translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "Naam bestaat al"
+ "name_exists": "Locatie bestaat al."
},
"step": {
"user": {
diff --git a/homeassistant/components/met/.translations/nn.json b/homeassistant/components/met/.translations/nn.json
index 0e024a0e1eb..6daa5b2657a 100644
--- a/homeassistant/components/met/.translations/nn.json
+++ b/homeassistant/components/met/.translations/nn.json
@@ -6,6 +6,7 @@
"name": "Namn"
}
}
- }
+ },
+ "title": "Met.no"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/met/.translations/pt.json b/homeassistant/components/met/.translations/pt.json
new file mode 100644
index 00000000000..c7081cd694a
--- /dev/null
+++ b/homeassistant/components/met/.translations/pt.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json
index 559382cf209..768152084aa 100644
--- a/homeassistant/components/met/.translations/ru.json
+++ b/homeassistant/components/met/.translations/ru.json
@@ -11,10 +11,10 @@
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
},
- "description": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442",
+ "description": "\u041d\u043e\u0440\u0432\u0435\u0436\u0441\u043a\u0438\u0439 \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442.",
"title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435"
}
},
- "title": "Met.no"
+ "title": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u041d\u043e\u0440\u0432\u0435\u0433\u0438\u0438 (Met.no)"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py
index 3ca55533ce3..98d94ebe6ca 100644
--- a/homeassistant/components/metoffice/sensor.py
+++ b/homeassistant/components/metoffice/sensor.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+import datapoint as dp
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -92,8 +93,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Met Office sensor platform."""
- import datapoint as dp
-
api_key = config.get(CONF_API_KEY)
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
@@ -193,8 +192,6 @@ class MetOfficeCurrentData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from Datapoint."""
- import datapoint as dp
-
try:
forecast = self._datapoint.get_forecast_for_site(self._site.id, "3hourly")
self.data = forecast.now()
diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py
index bb7a64005ce..09350588d46 100644
--- a/homeassistant/components/metoffice/weather.py
+++ b/homeassistant/components/metoffice/weather.py
@@ -1,6 +1,7 @@
"""Support for UK Met Office weather service."""
import logging
+import datapoint as dp
import voluptuous as vol
from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity
@@ -35,8 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Met Office weather platform."""
- import datapoint as dp
-
name = config.get(CONF_NAME)
datapoint = dp.connection(api_key=config.get(CONF_API_KEY))
diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json
index 16ae94c212e..5834897ee90 100644
--- a/homeassistant/components/microsoft/manifest.json
+++ b/homeassistant/components/microsoft/manifest.json
@@ -3,7 +3,7 @@
"name": "Microsoft",
"documentation": "https://www.home-assistant.io/integrations/microsoft",
"requirements": [
- "pycsspeechtts==1.0.2"
+ "pycsspeechtts==1.0.3"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py
index 3536c788bb9..d214f6648dd 100644
--- a/homeassistant/components/microsoft/tts.py
+++ b/homeassistant/components/microsoft/tts.py
@@ -14,6 +14,7 @@ CONF_RATE = "rate"
CONF_VOLUME = "volume"
CONF_PITCH = "pitch"
CONF_CONTOUR = "contour"
+CONF_REGION = "region"
_LOGGER = logging.getLogger(__name__)
@@ -72,6 +73,7 @@ DEFAULT_RATE = 0
DEFAULT_VOLUME = 0
DEFAULT_PITCH = "default"
DEFAULT_CONTOUR = ""
+DEFAULT_REGION = "eastus"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -87,6 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
),
vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): cv.string,
vol.Optional(CONF_CONTOUR, default=DEFAULT_CONTOUR): cv.string,
+ vol.Optional(CONF_REGION, default=DEFAULT_REGION): cv.string,
}
)
@@ -102,13 +105,16 @@ def get_engine(hass, config):
config[CONF_VOLUME],
config[CONF_PITCH],
config[CONF_CONTOUR],
+ config[CONF_REGION],
)
class MicrosoftProvider(Provider):
"""The Microsoft speech API provider."""
- def __init__(self, apikey, lang, gender, ttype, rate, volume, pitch, contour):
+ def __init__(
+ self, apikey, lang, gender, ttype, rate, volume, pitch, contour, region
+ ):
"""Init Microsoft TTS service."""
self._apikey = apikey
self._lang = lang
@@ -119,6 +125,7 @@ class MicrosoftProvider(Provider):
self._volume = f"{volume}%"
self._pitch = pitch
self._contour = contour
+ self._region = region
self.name = "Microsoft"
@property
@@ -138,7 +145,7 @@ class MicrosoftProvider(Provider):
from pycsspeechtts import pycsspeechtts
try:
- trans = pycsspeechtts.TTSTranslator(self._apikey)
+ trans = pycsspeechtts.TTSTranslator(self._apikey, self._region)
data = trans.speak(
language=language,
gender=self._gender,
diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py
index 28020a80175..a08c4ce5eac 100644
--- a/homeassistant/components/miflora/sensor.py
+++ b/homeassistant/components/miflora/sensor.py
@@ -1,20 +1,31 @@
"""Support for Xiaomi Mi Flora BLE plant sensor."""
from datetime import timedelta
import logging
+
+import btlewrap
+from btlewrap import BluetoothBackendException
+from miflora import miflora_poller
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.helpers.entity import Entity
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_FORCE_UPDATE,
+ CONF_MAC,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
- CONF_MAC,
CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+
+try:
+ import bluepy.btle # noqa: F401 pylint: disable=unused-import
+
+ BACKEND = btlewrap.BluepyBackend
+except ImportError:
+ BACKEND = btlewrap.GatttoolBackend
_LOGGER = logging.getLogger(__name__)
@@ -53,17 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the MiFlora sensor."""
- from miflora import miflora_poller
-
- try:
- import bluepy.btle # noqa: F401 pylint: disable=unused-import
- from btlewrap import BluepyBackend
-
- backend = BluepyBackend
- except ImportError:
- from btlewrap import GatttoolBackend
-
- backend = GatttoolBackend
+ backend = BACKEND
_LOGGER.debug("Miflora is using %s backend.", backend.__name__)
cache = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL).total_seconds()
@@ -152,8 +153,6 @@ class MiFloraSensor(Entity):
This uses a rolling median over 3 values to filter out outliers.
"""
- from btlewrap import BluetoothBackendException
-
try:
_LOGGER.debug("Polling data for %s", self.name)
data = self.poller.parameter_value(self.parameter)
diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py
index adeba48dbc8..b536149680d 100644
--- a/homeassistant/components/mitemp_bt/sensor.py
+++ b/homeassistant/components/mitemp_bt/sensor.py
@@ -1,21 +1,30 @@
"""Support for Xiaomi Mi Temp BLE environmental sensor."""
import logging
+import btlewrap
+from btlewrap.base import BluetoothBackendException
+from mitemp_bt import mitemp_bt_poller
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.helpers.entity import Entity
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_FORCE_UPDATE,
+ CONF_MAC,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
- CONF_MAC,
+ DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
- DEVICE_CLASS_BATTERY,
)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+try:
+ import bluepy.btle # noqa: F401 pylint: disable=unused-import
+
+ BACKEND = btlewrap.BluepyBackend
+except ImportError:
+ BACKEND = btlewrap.GatttoolBackend
_LOGGER = logging.getLogger(__name__)
@@ -60,17 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the MiTempBt sensor."""
- from mitemp_bt import mitemp_bt_poller
-
- try:
- import bluepy.btle # noqa: F401 pylint: disable=unused-import
- from btlewrap import BluepyBackend
-
- backend = BluepyBackend
- except ImportError:
- from btlewrap import GatttoolBackend
-
- backend = GatttoolBackend
+ backend = BACKEND
_LOGGER.debug("MiTempBt is using %s backend.", backend.__name__)
cache = config.get(CONF_CACHE)
@@ -152,8 +151,6 @@ class MiTempBtSensor(Entity):
This uses a rolling median over 3 values to filter out outliers.
"""
- from btlewrap.base import BluetoothBackendException
-
try:
_LOGGER.debug("Polling data for %s", self.name)
data = self.poller.parameter_value(self.parameter)
diff --git a/homeassistant/components/mobile_app/.translations/ko.json b/homeassistant/components/mobile_app/.translations/ko.json
index faf30e5f985..899845fcc2e 100644
--- a/homeassistant/components/mobile_app/.translations/ko.json
+++ b/homeassistant/components/mobile_app/.translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \ud1b5\ud569\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \uc5f0\ub3d9\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py
index 01877099201..ca2a58d1f96 100644
--- a/homeassistant/components/mobile_app/__init__.py
+++ b/homeassistant/components/mobile_app/__init__.py
@@ -1,6 +1,6 @@
"""Integrates Native Apps to Home Assistant."""
-from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.components.webhook import async_register as webhook_register
+from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.helpers import device_registry as dr, discovery
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
@@ -20,7 +20,6 @@ from .const import (
STORAGE_KEY,
STORAGE_VERSION,
)
-
from .http_api import RegistrationsView
from .webhook import handle_webhook
from .websocket_api import register_websocket_handlers
diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py
index 975c4c16c32..73bf925553e 100644
--- a/homeassistant/components/mobile_app/binary_sensor.py
+++ b/homeassistant/components/mobile_app/binary_sensor.py
@@ -1,9 +1,9 @@
"""Binary sensor platform for mobile_app."""
from functools import partial
+from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import callback
-from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
@@ -13,7 +13,6 @@ from .const import (
DATA_DEVICES,
DOMAIN,
)
-
from .entity import MobileAppEntity, sensor_id
diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py
index 96b0a35aae2..bc9c6167da8 100644
--- a/homeassistant/components/mobile_app/config_flow.py
+++ b/homeassistant/components/mobile_app/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for Mobile App."""
from homeassistant import config_entries
-from .const import DOMAIN, ATTR_DEVICE_NAME
+
+from .const import ATTR_DEVICE_NAME, DOMAIN
@config_entries.HANDLERS.register(DOMAIN)
diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py
index d01990b74b9..0b6a93a39ea 100644
--- a/homeassistant/components/mobile_app/const.py
+++ b/homeassistant/components/mobile_app/const.py
@@ -4,13 +4,13 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES as BINARY_SENSOR_CLASSES,
)
-from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES
from homeassistant.components.device_tracker import (
ATTR_BATTERY,
ATTR_GPS,
ATTR_GPS_ACCURACY,
ATTR_LOCATION_NAME,
)
+from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES
from homeassistant.const import ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA
from homeassistant.helpers import config_validation as cv
diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py
index 0e05c424609..f58f80aa5fc 100644
--- a/homeassistant/components/mobile_app/device_tracker.py
+++ b/homeassistant/components/mobile_app/device_tracker.py
@@ -1,19 +1,20 @@
"""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 TrackerEntity
+from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS
+from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_LATITUDE, ATTR_LONGITUDE
+from homeassistant.core import callback
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_GPS_ACCURACY,
ATTR_LOCATION_NAME,
ATTR_SPEED,
ATTR_VERTICAL_ACCURACY,
diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py
index 3be082951c5..2fb949720d6 100644
--- a/homeassistant/components/mobile_app/helpers.py
+++ b/homeassistant/components/mobile_app/helpers.py
@@ -1,9 +1,11 @@
"""Helpers for mobile_app."""
-import logging
import json
+import logging
from typing import Callable, Dict, Tuple
-from aiohttp.web import json_response, Response
+from aiohttp.web import Response, json_response
+from nacl.encoding import Base64Encoder
+from nacl.secret import SecretBox
from homeassistant.core import Context
from homeassistant.helpers.json import JSONEncoder
@@ -13,8 +15,8 @@ from .const import (
ATTR_APP_DATA,
ATTR_APP_ID,
ATTR_APP_NAME,
- ATTR_DEVICE_ID,
ATTR_APP_VERSION,
+ ATTR_DEVICE_ID,
ATTR_DEVICE_NAME,
ATTR_MANUFACTURER,
ATTR_MODEL,
@@ -36,8 +38,6 @@ def setup_decrypt() -> Tuple[int, Callable]:
Async friendly.
"""
- from nacl.secret import SecretBox
- from nacl.encoding import Base64Encoder
def decrypt(ciphertext, key):
"""Decrypt ciphertext using key."""
@@ -51,8 +51,6 @@ def setup_encrypt() -> Tuple[int, Callable]:
Async friendly.
"""
- from nacl.secret import SecretBox
- from nacl.encoding import Base64Encoder
def encrypt(ciphertext, key):
"""Encrypt ciphertext using key."""
diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py
index 67914ea7076..ee69f15fb11 100644
--- a/homeassistant/components/mobile_app/http_api.py
+++ b/homeassistant/components/mobile_app/http_api.py
@@ -1,18 +1,19 @@
"""Provides an HTTP API for mobile_app."""
-import uuid
from typing import Dict
+import uuid
-from aiohttp.web import Response, Request
+from aiohttp.web import Request, Response
+from nacl.secret import SecretBox
from homeassistant.auth.util import generate_secret
from homeassistant.components.cloud import (
+ CloudNotAvailable,
async_create_cloudhook,
async_remote_ui_url,
- CloudNotAvailable,
)
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
-from homeassistant.const import HTTP_CREATED, CONF_WEBHOOK_ID
+from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED
from .const import (
ATTR_DEVICE_ID,
@@ -24,7 +25,6 @@ from .const import (
DOMAIN,
REGISTRATION_SCHEMA,
)
-
from .helpers import supports_encryption
@@ -49,8 +49,6 @@ class RegistrationsView(HomeAssistantView):
data[CONF_WEBHOOK_ID] = webhook_id
if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption():
- from nacl.secret import SecretBox
-
data[CONF_SECRET] = generate_secret(SecretBox.KEY_SIZE)
data[CONF_USER_ID] = request["hass_user"].id
diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py
index 1e6a0517026..8ac34c9af1d 100644
--- a/homeassistant/components/mobile_app/notify.py
+++ b/homeassistant/components/mobile_app/notify.py
@@ -12,7 +12,6 @@ from homeassistant.components.notify import (
ATTR_TITLE_DEFAULT,
BaseNotificationService,
)
-
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.util.dt as dt_util
diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py
index b96a6f1e2f0..199ba968dd2 100644
--- a/homeassistant/components/mobile_app/sensor.py
+++ b/homeassistant/components/mobile_app/sensor.py
@@ -13,7 +13,6 @@ from .const import (
DATA_DEVICES,
DOMAIN,
)
-
from .entity import MobileAppEntity, sensor_id
diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py
index f95d5b993f0..66188500fd6 100644
--- a/homeassistant/components/mobile_app/webhook.py
+++ b/homeassistant/components/mobile_app/webhook.py
@@ -1,13 +1,12 @@
"""Webhook handlers for mobile_app."""
import logging
-from aiohttp.web import HTTPBadRequest, Response, Request
+from aiohttp.web import HTTPBadRequest, Request, Response
import voluptuous as vol
-from homeassistant.components.cloud import async_remote_ui_url, CloudNotAvailable
+from homeassistant.components.cloud import CloudNotAvailable, async_remote_ui_url
from homeassistant.components.frontend import MANIFEST_JSON
from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN
-
from homeassistant.const import (
ATTR_DOMAIN,
ATTR_SERVICE,
@@ -50,10 +49,10 @@ from .const import (
ERR_ENCRYPTION_REQUIRED,
ERR_SENSOR_DUPLICATE_UNIQUE_ID,
ERR_SENSOR_NOT_REGISTERED,
+ SIGNAL_LOCATION_UPDATE,
SIGNAL_SENSOR_UPDATE,
WEBHOOK_PAYLOAD_SCHEMA,
WEBHOOK_SCHEMAS,
- WEBHOOK_TYPES,
WEBHOOK_TYPE_CALL_SERVICE,
WEBHOOK_TYPE_FIRE_EVENT,
WEBHOOK_TYPE_GET_CONFIG,
@@ -63,10 +62,8 @@ from .const import (
WEBHOOK_TYPE_UPDATE_LOCATION,
WEBHOOK_TYPE_UPDATE_REGISTRATION,
WEBHOOK_TYPE_UPDATE_SENSOR_STATES,
- SIGNAL_LOCATION_UPDATE,
+ WEBHOOK_TYPES,
)
-
-
from .helpers import (
_decrypt_payload,
empty_okay_response,
@@ -77,7 +74,6 @@ from .helpers import (
webhook_response,
)
-
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py
index d0d13415b4d..813d0a9cf89 100644
--- a/homeassistant/components/mobile_app/websocket_api.py
+++ b/homeassistant/components/mobile_app/websocket_api.py
@@ -29,7 +29,6 @@ from .const import (
DATA_STORE,
DOMAIN,
)
-
from .helpers import safe_registration, savable_state
diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py
index 857dbab2a3b..21a3c3d16ea 100644
--- a/homeassistant/components/mopar/__init__.py
+++ b/homeassistant/components/mopar/__init__.py
@@ -1,17 +1,18 @@
"""Support for Mopar vehicles."""
-import logging
from datetime import timedelta
+import logging
+import motorparts
import voluptuous as vol
from homeassistant.components.lock import DOMAIN as LOCK
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.const import (
- CONF_USERNAME,
CONF_PASSWORD,
CONF_PIN,
CONF_SCAN_INTERVAL,
+ CONF_USERNAME,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import load_platform
@@ -53,8 +54,6 @@ SERVICE_HORN_SCHEMA = vol.Schema({vol.Required(ATTR_VEHICLE_INDEX): cv.positive_
def setup(hass, config):
"""Set up the Mopar component."""
- import motorparts
-
conf = config[DOMAIN]
cookie = hass.config.path(COOKIE_FILE)
try:
@@ -101,8 +100,6 @@ class MoparData:
def update(self, now, **kwargs):
"""Update data."""
- import motorparts
-
_LOGGER.debug("Updating vehicle data")
try:
self.vehicles = motorparts.get_summary(self._session)["vehicles"]
@@ -123,8 +120,6 @@ class MoparData:
@property
def attribution(self):
"""Get the attribution string from Mopar."""
- import motorparts
-
return motorparts.ATTRIBUTION
def get_vehicle_name(self, index):
@@ -136,8 +131,6 @@ class MoparData:
def actuate(self, command, index):
"""Run a command on the specified Mopar vehicle."""
- import motorparts
-
try:
response = getattr(motorparts, command)(self._session, index)
except motorparts.MoparError as error:
diff --git a/homeassistant/components/mopar/sensor.py b/homeassistant/components/mopar/sensor.py
index a29e9c5c739..2243fcdaa22 100644
--- a/homeassistant/components/mopar/sensor.py
+++ b/homeassistant/components/mopar/sensor.py
@@ -1,8 +1,8 @@
"""Support for the Mopar vehicle sensor platform."""
from homeassistant.components.mopar import (
- DOMAIN as MOPAR_DOMAIN,
- DATA_UPDATED,
ATTR_VEHICLE_INDEX,
+ DATA_UPDATED,
+ DOMAIN as MOPAR_DOMAIN,
)
from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS
from homeassistant.core import callback
diff --git a/homeassistant/components/mopar/switch.py b/homeassistant/components/mopar/switch.py
index bbada4ecee7..2dad56637ce 100644
--- a/homeassistant/components/mopar/switch.py
+++ b/homeassistant/components/mopar/switch.py
@@ -3,7 +3,7 @@ import logging
from homeassistant.components.mopar import DOMAIN as MOPAR_DOMAIN
from homeassistant.components.switch import SwitchDevice
-from homeassistant.const import STATE_ON, STATE_OFF
+from homeassistant.const import STATE_OFF, STATE_ON
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py
index c19f8f49226..2628815727c 100644
--- a/homeassistant/components/mpd/media_player.py
+++ b/homeassistant/components/mpd/media_player.py
@@ -3,9 +3,10 @@ from datetime import timedelta
import logging
import os
+import mpd
import voluptuous as vol
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_PLAYLIST,
@@ -85,8 +86,6 @@ class MpdDevice(MediaPlayerDevice):
# pylint: disable=no-member
def __init__(self, server, port, password, name):
"""Initialize the MPD device."""
- import mpd
-
self.server = server
self.port = port
self._name = name
@@ -107,8 +106,6 @@ class MpdDevice(MediaPlayerDevice):
def _connect(self):
"""Connect to MPD."""
- import mpd
-
try:
self._client.connect(self.server, self.port)
@@ -121,8 +118,6 @@ class MpdDevice(MediaPlayerDevice):
def _disconnect(self):
"""Disconnect from MPD."""
- import mpd
-
try:
self._client.disconnect()
except mpd.ConnectionError:
@@ -144,8 +139,6 @@ class MpdDevice(MediaPlayerDevice):
def update(self):
"""Get the latest data and update the state."""
- import mpd
-
try:
if not self._is_connected:
self._connect()
@@ -261,8 +254,6 @@ class MpdDevice(MediaPlayerDevice):
@Throttle(PLAYLIST_UPDATE_INTERVAL)
def _update_playlists(self, **kwargs):
"""Update available MPD playlists."""
- import mpd
-
try:
self._playlists = []
for playlist_data in self._client.listplaylists():
diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json
index ac27652cbdd..925b8cf5ab4 100644
--- a/homeassistant/components/mqtt/.translations/ru.json
+++ b/homeassistant/components/mqtt/.translations/ru.json
@@ -4,7 +4,7 @@
"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": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443"
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443."
},
"step": {
"broker": {
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 9b25a6ef6e4..2fab599ac3f 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -1,5 +1,6 @@
"""Support for MQTT message handling."""
import asyncio
+import sys
from functools import partial, wraps
import inspect
from itertools import groupby
@@ -15,6 +16,8 @@ from typing import Any, Callable, List, Optional, Union
import attr
import requests.certs
import voluptuous as vol
+import paho.mqtt.client as mqtt
+from paho.mqtt.matcher import MQTTMatcher
from homeassistant import config_entries
from homeassistant.components import websocket_api
@@ -36,6 +39,7 @@ from homeassistant.exceptions import (
ConfigEntryNotReady,
)
from homeassistant.helpers import config_validation as cv, template
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType
from homeassistant.loader import bind_hass
@@ -50,7 +54,12 @@ from .const import (
DEFAULT_DISCOVERY,
CONF_STATE_TOPIC,
ATTR_DISCOVERY_HASH,
+ PROTOCOL_311,
+ DEFAULT_QOS,
)
+from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash
+from .models import PublishPayloadType, Message, MessageCallbackType
+from .subscription import async_subscribe_topics, async_unsubscribe_topics
_LOGGER = logging.getLogger(__name__)
@@ -95,11 +104,9 @@ CONF_VIA_DEVICE = "via_device"
CONF_DEPRECATED_VIA_HUB = "via_hub"
PROTOCOL_31 = "3.1"
-PROTOCOL_311 = "3.1.1"
DEFAULT_PORT = 1883
DEFAULT_KEEPALIVE = 60
-DEFAULT_QOS = 0
DEFAULT_RETAIN = False
DEFAULT_PROTOCOL = PROTOCOL_311
DEFAULT_DISCOVERY_PREFIX = "homeassistant"
@@ -329,23 +336,9 @@ MQTT_PUBLISH_SCHEMA = vol.Schema(
# pylint: disable=invalid-name
-PublishPayloadType = Union[str, bytes, int, float, None]
SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None
-@attr.s(slots=True, frozen=True)
-class Message:
- """MQTT Message."""
-
- topic = attr.ib(type=str)
- payload = attr.ib(type=PublishPayloadType)
- qos = attr.ib(type=int)
- retain = attr.ib(type=bool)
-
-
-MessageCallbackType = Callable[[Message], None]
-
-
def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType:
"""Build the arguments for the publish service without the payload."""
data = {ATTR_TOPIC: topic}
@@ -629,8 +622,6 @@ async def async_setup_entry(hass, entry):
elif conf_tls_version == "1.0":
tls_version = ssl.PROTOCOL_TLSv1
else:
- import sys
-
# Python3.6 supports automatic negotiation of highest TLS version
if sys.hexversion >= 0x03060000:
tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member
@@ -735,8 +726,6 @@ class MQTT:
tls_version: Optional[int],
) -> None:
"""Initialize Home Assistant MQTT client."""
- import paho.mqtt.client as mqtt
-
self.hass = hass
self.broker = broker
self.port = port
@@ -776,7 +765,9 @@ class MQTT:
self._mqttc.on_message = self._mqtt_on_message
if will_message is not None:
- self._mqttc.will_set(*attr.astuple(will_message))
+ self._mqttc.will_set( # pylint: disable=no-value-for-parameter
+ *attr.astuple(will_message)
+ )
async def async_publish(
self, topic: str, payload: PublishPayloadType, qos: int, retain: bool
@@ -806,8 +797,6 @@ class MQTT:
return CONNECTION_FAILED_RECOVERABLE
if result != 0:
- import paho.mqtt.client as mqtt
-
_LOGGER.error("Failed to connect: %s", mqtt.error_string(result))
return CONNECTION_FAILED
@@ -889,8 +878,6 @@ class MQTT:
Resubscribe to all topics we were subscribed to and publish birth
message.
"""
- import paho.mqtt.client as mqtt
-
if result_code != mqtt.CONNACK_ACCEPTED:
_LOGGER.error(
"Unable to connect to the MQTT broker: %s",
@@ -909,7 +896,11 @@ class MQTT:
self.hass.add_job(self._async_perform_subscription, topic, max_qos)
if self.birth_message:
- self.hass.add_job(self.async_publish(*attr.astuple(self.birth_message)))
+ self.hass.add_job(
+ self.async_publish( # pylint: disable=no-value-for-parameter
+ *attr.astuple(self.birth_message)
+ )
+ )
def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None:
"""Message received callback."""
@@ -934,10 +925,11 @@ class MQTT:
payload = msg.payload.decode(subscription.encoding)
except (AttributeError, UnicodeDecodeError):
_LOGGER.warning(
- "Can't decode payload %s on %s with encoding %s",
+ "Can't decode payload %s on %s with encoding %s (for %s)",
msg.payload,
msg.topic,
subscription.encoding,
+ subscription.callback,
)
continue
@@ -978,8 +970,6 @@ class MQTT:
def _raise_on_error(result_code: int) -> None:
"""Raise error if error result."""
if result_code != 0:
- import paho.mqtt.client as mqtt
-
raise HomeAssistantError(
"Error talking to MQTT: {}".format(mqtt.error_string(result_code))
)
@@ -987,8 +977,6 @@ def _raise_on_error(result_code: int) -> None:
def _match_topic(subscription: str, topic: str) -> bool:
"""Test if topic matches subscription."""
- from paho.mqtt.matcher import MQTTMatcher
-
matcher = MQTTMatcher()
matcher[subscription] = True
try:
@@ -1022,8 +1010,6 @@ class MqttAttributes(Entity):
async def _attributes_subscribe_topics(self):
"""(Re)Subscribe to topics."""
- from .subscription import async_subscribe_topics
-
attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE)
if attr_tpl is not None:
attr_tpl.hass = self.hass
@@ -1059,8 +1045,6 @@ class MqttAttributes(Entity):
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
- from .subscription import async_unsubscribe_topics
-
self._attributes_sub_state = await async_unsubscribe_topics(
self.hass, self._attributes_sub_state
)
@@ -1096,7 +1080,6 @@ class MqttAvailability(Entity):
async def _availability_subscribe_topics(self):
"""(Re)Subscribe to topics."""
- from .subscription import async_subscribe_topics
@callback
def availability_message_received(msg: Message) -> None:
@@ -1122,8 +1105,6 @@ class MqttAvailability(Entity):
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
- from .subscription import async_unsubscribe_topics
-
self._availability_sub_state = await async_unsubscribe_topics(
self.hass, self._availability_sub_state
)
@@ -1148,9 +1129,6 @@ class MqttDiscoveryUpdate(Entity):
"""Subscribe to discovery updates."""
await super().async_added_to_hass()
- from homeassistant.helpers.dispatcher import async_dispatcher_connect
- from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash
-
@callback
def discovery_callback(payload):
"""Handle discovery update."""
diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py
index 2350dfc6634..5e995494a64 100644
--- a/homeassistant/components/mqtt/abbreviations.py
+++ b/homeassistant/components/mqtt/abbreviations.py
@@ -88,11 +88,13 @@ ABBREVIATIONS = {
"pl_cls": "payload_close",
"pl_disarm": "payload_disarm",
"pl_hi_spd": "payload_high_speed",
+ "pl_home": "payload_home",
"pl_lock": "payload_lock",
"pl_loc": "payload_locate",
"pl_lo_spd": "payload_low_speed",
"pl_med_spd": "payload_medium_speed",
"pl_not_avail": "payload_not_available",
+ "pl_not_home": "payload_not_home",
"pl_off": "payload_off",
"pl_off_spd": "payload_off_speed",
"pl_on": "payload_on",
@@ -128,6 +130,7 @@ ABBREVIATIONS = {
"spd_stat_t": "speed_state_topic",
"spd_val_tpl": "speed_value_template",
"spds": "speeds",
+ "src_type": "source_type",
"stat_clsd": "state_closed",
"stat_off": "state_off",
"stat_on": "state_on",
diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py
index d3c6ee819b5..a8a378e723c 100644
--- a/homeassistant/components/mqtt/config_flow.py
+++ b/homeassistant/components/mqtt/config_flow.py
@@ -3,6 +3,7 @@ from collections import OrderedDict
import queue
import voluptuous as vol
+import paho.mqtt.client as mqtt
from homeassistant import config_entries
from homeassistant.const import (
@@ -125,8 +126,6 @@ class FlowHandler(config_entries.ConfigFlow):
def try_connection(broker, port, username, password, protocol="3.1"):
"""Test if we can connect to an MQTT broker."""
- import paho.mqtt.client as mqtt
-
if protocol == "3.1":
proto = mqtt.MQTTv31
else:
diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py
index b365ee9d33e..3234bebbfc1 100644
--- a/homeassistant/components/mqtt/const.py
+++ b/homeassistant/components/mqtt/const.py
@@ -5,3 +5,5 @@ DEFAULT_DISCOVERY = False
ATTR_DISCOVERY_HASH = "discovery_hash"
CONF_STATE_TOPIC = "state_topic"
+PROTOCOL_311 = "3.1.1"
+DEFAULT_QOS = 0
diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py
index 66e14ca9a5a..e6cfab90c26 100644
--- a/homeassistant/components/mqtt/cover.py
+++ b/homeassistant/components/mqtt/cover.py
@@ -90,7 +90,7 @@ DEFAULT_TILT_MIN = 0
DEFAULT_TILT_OPEN_POSITION = 100
DEFAULT_TILT_OPTIMISTIC = False
-OPEN_CLOSE_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
+OPEN_CLOSE_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE
TILT_FEATURES = (
SUPPORT_OPEN_TILT
| SUPPORT_CLOSE_TILT
@@ -122,7 +122,9 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
- vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
+ vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): vol.Any(
+ cv.string, None
+ ),
vol.Optional(CONF_POSITION_CLOSED, default=DEFAULT_POSITION_CLOSED): int,
vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
@@ -396,6 +398,9 @@ class MqttCover(
if self._config.get(CONF_COMMAND_TOPIC) is not None:
supported_features = OPEN_CLOSE_FEATURES
+ if self._config.get(CONF_PAYLOAD_STOP) is not None:
+ supported_features |= SUPPORT_STOP
+
if self._config.get(CONF_SET_POSITION_TOPIC) is not None:
supported_features |= SUPPORT_SET_POSITION
diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py
index e9613e09a95..d25d7ce21d3 100644
--- a/homeassistant/components/mqtt/device_tracker.py
+++ b/homeassistant/components/mqtt/device_tracker.py
@@ -4,17 +4,26 @@ import logging
import voluptuous as vol
from homeassistant.components import mqtt
-from homeassistant.components.device_tracker import PLATFORM_SCHEMA
-from homeassistant.const import CONF_DEVICES
+from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_DEVICES, STATE_NOT_HOME, STATE_HOME
from . import CONF_QOS
_LOGGER = logging.getLogger(__name__)
+CONF_PAYLOAD_HOME = "payload_home"
+CONF_PAYLOAD_NOT_HOME = "payload_not_home"
+CONF_SOURCE_TYPE = "source_type"
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend(
- {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}}
+ {
+ vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic},
+ vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string,
+ vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string,
+ vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES),
+ }
)
@@ -22,13 +31,27 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up the MQTT tracker."""
devices = config[CONF_DEVICES]
qos = config[CONF_QOS]
+ payload_home = config[CONF_PAYLOAD_HOME]
+ payload_not_home = config[CONF_PAYLOAD_NOT_HOME]
+ source_type = config.get(CONF_SOURCE_TYPE)
for dev_id, topic in devices.items():
@callback
def async_message_received(msg, dev_id=dev_id):
"""Handle received MQTT message."""
- hass.async_create_task(async_see(dev_id=dev_id, location_name=msg.payload))
+ if msg.payload == payload_home:
+ location_name = STATE_HOME
+ elif msg.payload == payload_not_home:
+ location_name = STATE_NOT_HOME
+ else:
+ location_name = msg.payload
+
+ see_args = {"dev_id": dev_id, "location_name": location_name}
+ if source_type:
+ see_args["source_type"] = source_type
+
+ hass.async_create_task(async_see(**see_args))
await mqtt.async_subscribe(hass, topic, async_message_received, qos)
diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py
index 688cef03467..95a850fb9e8 100644
--- a/homeassistant/components/mqtt/light/__init__.py
+++ b/homeassistant/components/mqtt/light/__init__.py
@@ -16,34 +16,24 @@ from homeassistant.components.mqtt.discovery import (
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
+from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA
+from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic
+from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json
+from .schema_template import PLATFORM_SCHEMA_TEMPLATE, async_setup_entity_template
_LOGGER = logging.getLogger(__name__)
-CONF_SCHEMA = "schema"
-
def validate_mqtt_light(value):
"""Validate MQTT light schema."""
- from . import schema_basic
- from . import schema_json
- from . import schema_template
-
schemas = {
- "basic": schema_basic.PLATFORM_SCHEMA_BASIC,
- "json": schema_json.PLATFORM_SCHEMA_JSON,
- "template": schema_template.PLATFORM_SCHEMA_TEMPLATE,
+ "basic": PLATFORM_SCHEMA_BASIC,
+ "json": PLATFORM_SCHEMA_JSON,
+ "template": PLATFORM_SCHEMA_TEMPLATE,
}
return schemas[value[CONF_SCHEMA]](value)
-MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema(
- {
- vol.Optional(CONF_SCHEMA, default="basic"): vol.All(
- vol.Lower, vol.Any("basic", "json", "template")
- )
- }
-)
-
PLATFORM_SCHEMA = vol.All(
MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_light
)
@@ -81,14 +71,10 @@ async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_hash=None
):
"""Set up a MQTT Light."""
- from . import schema_basic
- from . import schema_json
- from . import schema_template
-
setup_entity = {
- "basic": schema_basic.async_setup_entity_basic,
- "json": schema_json.async_setup_entity_json,
- "template": schema_template.async_setup_entity_template,
+ "basic": async_setup_entity_basic,
+ "json": async_setup_entity_json,
+ "template": async_setup_entity_template,
}
await setup_entity[config[CONF_SCHEMA]](
config, async_add_entities, config_entry, discovery_hash
diff --git a/homeassistant/components/mqtt/light/schema.py b/homeassistant/components/mqtt/light/schema.py
new file mode 100644
index 00000000000..a7ab5e986a7
--- /dev/null
+++ b/homeassistant/components/mqtt/light/schema.py
@@ -0,0 +1,12 @@
+"""Shared schema code."""
+import voluptuous as vol
+
+CONF_SCHEMA = "schema"
+
+MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_SCHEMA, default="basic"): vol.All(
+ vol.Lower, vol.Any("basic", "json", "template")
+ )
+ }
+)
diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py
index 216762f9b2b..829809dd9c3 100644
--- a/homeassistant/components/mqtt/light/schema_basic.py
+++ b/homeassistant/components/mqtt/light/schema_basic.py
@@ -56,7 +56,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
-from . import MQTT_LIGHT_SCHEMA_SCHEMA
+from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py
index 1a46cd5e535..c4de1edbc3c 100644
--- a/homeassistant/components/mqtt/light/schema_json.py
+++ b/homeassistant/components/mqtt/light/schema_json.py
@@ -59,7 +59,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
import homeassistant.util.color as color_util
-from . import MQTT_LIGHT_SCHEMA_SCHEMA
+from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
from .schema_basic import CONF_BRIGHTNESS_SCALE
_LOGGER = logging.getLogger(__name__)
@@ -463,7 +463,7 @@ class MqttLightJson(
message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT]
if ATTR_TRANSITION in kwargs:
- message["transition"] = int(kwargs[ATTR_TRANSITION])
+ message["transition"] = kwargs[ATTR_TRANSITION]
if ATTR_BRIGHTNESS in kwargs and self._brightness is not None:
message["brightness"] = int(
@@ -521,7 +521,7 @@ class MqttLightJson(
message = {"state": "OFF"}
if ATTR_TRANSITION in kwargs:
- message["transition"] = int(kwargs[ATTR_TRANSITION])
+ message["transition"] = kwargs[ATTR_TRANSITION]
mqtt.async_publish(
self.hass,
diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py
index 410eff6143f..c80ab2f95a7 100644
--- a/homeassistant/components/mqtt/light/schema_template.py
+++ b/homeassistant/components/mqtt/light/schema_template.py
@@ -49,7 +49,7 @@ import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
from homeassistant.helpers.restore_state import RestoreEntity
-from . import MQTT_LIGHT_SCHEMA_SCHEMA
+from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py
new file mode 100644
index 00000000000..5f014aadd08
--- /dev/null
+++ b/homeassistant/components/mqtt/models.py
@@ -0,0 +1,20 @@
+"""Modesl used by multiple MQTT modules."""
+from typing import Union, Callable
+
+import attr
+
+# pylint: disable=invalid-name
+PublishPayloadType = Union[str, bytes, int, float, None]
+
+
+@attr.s(slots=True, frozen=True)
+class Message:
+ """MQTT Message."""
+
+ topic = attr.ib(type=str)
+ payload = attr.ib(type=PublishPayloadType)
+ qos = attr.ib(type=int)
+ retain = attr.ib(type=bool)
+
+
+MessageCallbackType = Callable[[Message], None]
diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py
index 2c70d18d772..f5d369a75c7 100644
--- a/homeassistant/components/mqtt/server.py
+++ b/homeassistant/components/mqtt/server.py
@@ -4,10 +4,14 @@ import logging
import tempfile
import voluptuous as vol
+from hbmqtt.broker import Broker, BrokerException
+from passlib.apps import custom_app_context
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv
+from .const import PROTOCOL_311
+
_LOGGER = logging.getLogger(__name__)
# None allows custom config to be created through generate_config
@@ -33,8 +37,6 @@ def async_start(hass, password, server_config):
This method is a coroutine.
"""
- from hbmqtt.broker import Broker, BrokerException
-
passwd = tempfile.NamedTemporaryFile()
gen_server_config, client_config = generate_config(hass, passwd, password)
@@ -63,8 +65,6 @@ def async_start(hass, password, server_config):
def generate_config(hass, passwd, password):
"""Generate a configuration based on current Home Assistant instance."""
- from . import PROTOCOL_311
-
config = {
"listeners": {
"default": {
@@ -83,8 +83,6 @@ def generate_config(hass, passwd, password):
username = "homeassistant"
# Encrypt with what hbmqtt uses to verify
- from passlib.apps import custom_app_context
-
passwd.write(
"homeassistant:{}\n".format(custom_app_context.encrypt(password)).encode(
"utf-8"
diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py
index d85399b5dcb..be48a769a23 100644
--- a/homeassistant/components/mqtt/subscription.py
+++ b/homeassistant/components/mqtt/subscription.py
@@ -8,7 +8,8 @@ from homeassistant.components import mqtt
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass
-from . import DEFAULT_QOS, MessageCallbackType
+from .const import DEFAULT_QOS
+from .models import MessageCallbackType
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py
index 5fdaa744ca9..12fd4c51693 100644
--- a/homeassistant/components/mqtt/vacuum/__init__.py
+++ b/homeassistant/components/mqtt/vacuum/__init__.py
@@ -15,51 +15,19 @@ from homeassistant.components.mqtt.discovery import (
clear_discovery_hash,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from .schema import CONF_SCHEMA, LEGACY, STATE, MQTT_VACUUM_SCHEMA
+from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy
+from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state
_LOGGER = logging.getLogger(__name__)
-CONF_SCHEMA = "schema"
-LEGACY = "legacy"
-STATE = "state"
-
def validate_mqtt_vacuum(value):
"""Validate MQTT vacuum schema."""
- from . import schema_legacy
- from . import schema_state
-
- schemas = {
- LEGACY: schema_legacy.PLATFORM_SCHEMA_LEGACY,
- STATE: schema_state.PLATFORM_SCHEMA_STATE,
- }
+ schemas = {LEGACY: PLATFORM_SCHEMA_LEGACY, STATE: PLATFORM_SCHEMA_STATE}
return schemas[value[CONF_SCHEMA]](value)
-def services_to_strings(services, service_to_string):
- """Convert SUPPORT_* service bitmask to list of service strings."""
- strings = []
- for service in service_to_string:
- if service & services:
- strings.append(service_to_string[service])
- return strings
-
-
-def strings_to_services(strings, string_to_service):
- """Convert service strings to SUPPORT_* service bitmask."""
- services = 0
- for string in strings:
- services |= string_to_service[string]
- return services
-
-
-MQTT_VACUUM_SCHEMA = vol.Schema(
- {
- vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All(
- vol.Lower, vol.Any(LEGACY, STATE)
- )
- }
-)
-
PLATFORM_SCHEMA = vol.All(
MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum
)
@@ -95,13 +63,7 @@ async def _async_setup_entity(
config, async_add_entities, config_entry, discovery_hash=None
):
"""Set up the MQTT vacuum."""
- from . import schema_legacy
- from . import schema_state
-
- setup_entity = {
- LEGACY: schema_legacy.async_setup_entity_legacy,
- STATE: schema_state.async_setup_entity_state,
- }
+ setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state}
await setup_entity[config[CONF_SCHEMA]](
config, async_add_entities, config_entry, discovery_hash
)
diff --git a/homeassistant/components/mqtt/vacuum/schema.py b/homeassistant/components/mqtt/vacuum/schema.py
new file mode 100644
index 00000000000..949b5cede9c
--- /dev/null
+++ b/homeassistant/components/mqtt/vacuum/schema.py
@@ -0,0 +1,31 @@
+"""Shared schema code."""
+import voluptuous as vol
+
+CONF_SCHEMA = "schema"
+LEGACY = "legacy"
+STATE = "state"
+
+MQTT_VACUUM_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All(
+ vol.Lower, vol.Any(LEGACY, STATE)
+ )
+ }
+)
+
+
+def services_to_strings(services, service_to_string):
+ """Convert SUPPORT_* service bitmask to list of service strings."""
+ strings = []
+ for service in service_to_string:
+ if service & services:
+ strings.append(service_to_string[service])
+ return strings
+
+
+def strings_to_services(strings, string_to_service):
+ """Convert service strings to SUPPORT_* service bitmask."""
+ services = 0
+ for string in strings:
+ services |= string_to_service[string]
+ return services
diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py
index f2fa8f8da66..d770cfbb7f8 100644
--- a/homeassistant/components/mqtt/vacuum/schema_legacy.py
+++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py
@@ -33,7 +33,7 @@ from homeassistant.components.mqtt import (
subscription,
)
-from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
+from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py
index 1ab415aef7b..40b3eeb752c 100644
--- a/homeassistant/components/mqtt/vacuum/schema_state.py
+++ b/homeassistant/components/mqtt/vacuum/schema_state.py
@@ -46,7 +46,7 @@ from homeassistant.components.mqtt import (
CONF_QOS,
)
-from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
+from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/msteams/__init__.py b/homeassistant/components/msteams/__init__.py
new file mode 100644
index 00000000000..42423887fa6
--- /dev/null
+++ b/homeassistant/components/msteams/__init__.py
@@ -0,0 +1 @@
+"""The Microsoft Teams component."""
diff --git a/homeassistant/components/msteams/manifest.json b/homeassistant/components/msteams/manifest.json
new file mode 100644
index 00000000000..f907cf570bb
--- /dev/null
+++ b/homeassistant/components/msteams/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "msteams",
+ "name": "Microsoft Teams",
+ "documentation": "https://www.home-assistant.io/integrations/msteams",
+ "requirements": ["pymsteams==0.1.12"],
+ "dependencies": [],
+ "codeowners": ["@peroyvind"]
+}
diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py
new file mode 100644
index 00000000000..c986f1d2363
--- /dev/null
+++ b/homeassistant/components/msteams/notify.py
@@ -0,0 +1,67 @@
+"""Microsoft Teams platform for notify component."""
+import logging
+
+import pymsteams
+import voluptuous as vol
+
+from homeassistant.components.notify import (
+ ATTR_DATA,
+ ATTR_TITLE,
+ ATTR_TITLE_DEFAULT,
+ PLATFORM_SCHEMA,
+ BaseNotificationService,
+)
+from homeassistant.const import CONF_URL
+import homeassistant.helpers.config_validation as cv
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_FILE_URL = "image_url"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_URL): cv.url})
+
+
+def get_service(hass, config, discovery_info=None):
+ """Get the Microsoft Teams notification service."""
+ webhook_url = config.get(CONF_URL)
+
+ try:
+ return MSTeamsNotificationService(webhook_url)
+
+ except RuntimeError as err:
+ _LOGGER.exception("Error in creating a new Microsoft Teams message: %s", err)
+ return None
+
+
+class MSTeamsNotificationService(BaseNotificationService):
+ """Implement the notification service for Microsoft Teams."""
+
+ def __init__(self, webhook_url):
+ """Initialize the service."""
+ self._webhook_url = webhook_url
+ self.teams_message = pymsteams.connectorcard(self._webhook_url)
+
+ def send_message(self, message=None, **kwargs):
+ """Send a message to the webhook."""
+ title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
+ data = kwargs.get(ATTR_DATA)
+
+ self.teams_message.title(title)
+
+ self.teams_message.text(message)
+
+ if data is not None:
+ file_url = data.get(ATTR_FILE_URL)
+
+ if file_url is not None:
+ if not file_url.startswith("http"):
+ _LOGGER.error("URL should start with http or https")
+ return
+
+ message_section = pymsteams.cardsection()
+ message_section.addImage(file_url)
+ self.teams_message.addSection(message_section)
+ try:
+ self.teams_message.send()
+ except RuntimeError as err:
+ _LOGGER.error("Could not send notification. Error: %s", err)
diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py
index 3c753d832e0..da1db0e02aa 100644
--- a/homeassistant/components/mvglive/sensor.py
+++ b/homeassistant/components/mvglive/sensor.py
@@ -1,14 +1,15 @@
"""Support for departure information for public transport in Munich."""
-import logging
-from datetime import timedelta
-
from copy import deepcopy
+from datetime import timedelta
+import logging
+
+import MVGLive
import voluptuous as vol
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
_LOGGER = logging.getLogger(__name__)
@@ -150,8 +151,6 @@ class MVGLiveData:
self, station, destinations, directions, lines, products, timeoffset, number
):
"""Initialize the sensor."""
- import MVGLive
-
self._station = station
self._destinations = destinations
self._directions = directions
diff --git a/homeassistant/components/mychevy/__init__.py b/homeassistant/components/mychevy/__init__.py
index 8ec83ed374b..0ec4d05a623 100644
--- a/homeassistant/components/mychevy/__init__.py
+++ b/homeassistant/components/mychevy/__init__.py
@@ -4,11 +4,11 @@ import logging
import threading
import time
+import mychevy.mychevy as mc
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers import discovery
+from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.util import Throttle
DOMAIN = "mychevy"
@@ -71,8 +71,6 @@ class EVBinarySensorConfig:
def setup(hass, base_config):
"""Set up the mychevy component."""
- import mychevy.mychevy as mc
-
config = base_config.get(DOMAIN)
email = config.get(CONF_USERNAME)
diff --git a/homeassistant/components/mythicbeastsdns/__init__.py b/homeassistant/components/mythicbeastsdns/__init__.py
index 993b62ac48d..d961c2e6e3d 100644
--- a/homeassistant/components/mythicbeastsdns/__init__.py
+++ b/homeassistant/components/mythicbeastsdns/__init__.py
@@ -1,10 +1,10 @@
"""Support for Mythic Beasts Dynamic DNS service."""
-import logging
from datetime import timedelta
+import logging
+import mbddns
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_DOMAIN,
CONF_HOST,
@@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__)
@@ -39,8 +40,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Initialize the Mythic Beasts component."""
- import mbddns
-
domain = config[DOMAIN][CONF_DOMAIN]
password = config[DOMAIN][CONF_PASSWORD]
host = config[DOMAIN][CONF_HOST]
diff --git a/homeassistant/components/namecheapdns/__init__.py b/homeassistant/components/namecheapdns/__init__.py
index 56c50ff52f8..fbc78f622a1 100644
--- a/homeassistant/components/namecheapdns/__init__.py
+++ b/homeassistant/components/namecheapdns/__init__.py
@@ -1,13 +1,14 @@
"""Support for namecheap DNS services."""
-import logging
from datetime import timedelta
+import logging
+import defusedxml.ElementTree as ET
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_DOMAIN
-from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.const import CONF_DOMAIN, CONF_HOST, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__)
@@ -55,8 +56,6 @@ async def async_setup(hass, config):
async def _update_namecheapdns(session, host, domain, password):
"""Update namecheap DNS entry."""
- import defusedxml.ElementTree as ET
-
params = {"host": host, "domain": domain, "password": password}
resp = await session.get(UPDATE_URL, params=params)
diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json
index 7f0d122f38b..ca180efa005 100644
--- a/homeassistant/components/neato/.translations/da.json
+++ b/homeassistant/components/neato/.translations/da.json
@@ -4,6 +4,9 @@
"already_configured": "Allerede konfigureret",
"invalid_credentials": "Ugyldige legitimationsoplysninger"
},
+ "create_entry": {
+ "default": "Se [Neato-dokumentation] ({docs_url})."
+ },
"error": {
"invalid_credentials": "Ugyldige legitimationsoplysninger",
"unexpected_error": "Uventet fejl"
diff --git a/homeassistant/components/neato/.translations/ko.json b/homeassistant/components/neato/.translations/ko.json
new file mode 100644
index 00000000000..aeb591f7b20
--- /dev/null
+++ b/homeassistant/components/neato/.translations/ko.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "create_entry": {
+ "default": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ },
+ "error": {
+ "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unexpected_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
+ "vendor": "\uacf5\uae09 \uc5c5\uccb4"
+ },
+ "description": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "title": "Neato \uacc4\uc815 \uc815\ubcf4"
+ }
+ },
+ "title": "Neato"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/.translations/nl.json b/homeassistant/components/neato/.translations/nl.json
new file mode 100644
index 00000000000..4846f0180f1
--- /dev/null
+++ b/homeassistant/components/neato/.translations/nl.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Al geconfigureerd",
+ "invalid_credentials": "Ongeldige gebruikersgegevens"
+ },
+ "create_entry": {
+ "default": "Zie [Neato-documentatie] ({docs_url})."
+ },
+ "error": {
+ "invalid_credentials": "Ongeldige inloggegevens",
+ "unexpected_error": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam",
+ "vendor": "Leverancier"
+ },
+ "description": "Zie [Neato-documentatie] ({docs_url}).",
+ "title": "Neato-account info"
+ }
+ },
+ "title": "Neato"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/.translations/pt-BR.json b/homeassistant/components/neato/.translations/pt-BR.json
new file mode 100644
index 00000000000..8c4c45f9c89
--- /dev/null
+++ b/homeassistant/components/neato/.translations/pt-BR.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": ""
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json
index 1a206258e24..999e45880cf 100644
--- a/homeassistant/components/neato/.translations/ru.json
+++ b/homeassistant/components/neato/.translations/ru.json
@@ -2,13 +2,13 @@
"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.",
- "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435"
+ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
},
"create_entry": {
"default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438."
},
"error": {
- "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435",
+ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py
index e17c562171a..ddf9789f678 100644
--- a/homeassistant/components/neato/__init__.py
+++ b/homeassistant/components/neato/__init__.py
@@ -1,230 +1,170 @@
"""Support for Neato botvac connected vacuum cleaners."""
+import asyncio
import logging
from datetime import timedelta
-from urllib.error import HTTPError
import voluptuous as vol
+from pybotvac import Account, Neato, Vorwerk
+from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException
-import homeassistant.helpers.config_validation as cv
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from homeassistant.helpers import discovery
+from homeassistant.helpers import config_validation as cv
from homeassistant.util import Throttle
+from .config_flow import NeatoConfigFlow
+from .const import (
+ CONF_VENDOR,
+ NEATO_CONFIG,
+ NEATO_DOMAIN,
+ NEATO_LOGIN,
+ NEATO_MAP_DATA,
+ NEATO_PERSISTENT_MAPS,
+ NEATO_ROBOTS,
+ SCAN_INTERVAL_MINUTES,
+ VALID_VENDORS,
+)
+
_LOGGER = logging.getLogger(__name__)
-CONF_VENDOR = "vendor"
-DOMAIN = "neato"
-NEATO_ROBOTS = "neato_robots"
-NEATO_LOGIN = "neato_login"
-NEATO_MAP_DATA = "neato_map_data"
-NEATO_PERSISTENT_MAPS = "neato_persistent_maps"
CONFIG_SCHEMA = vol.Schema(
{
- DOMAIN: vol.Schema(
+ NEATO_DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_VENDOR, default="neato"): vol.In(
- ["neato", "vorwerk"]
- ),
+ vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS),
}
)
},
extra=vol.ALLOW_EXTRA,
)
-MODE = {1: "Eco", 2: "Turbo"}
-ACTION = {
- 0: "Invalid",
- 1: "House Cleaning",
- 2: "Spot Cleaning",
- 3: "Manual Cleaning",
- 4: "Docking",
- 5: "User Menu Active",
- 6: "Suspended Cleaning",
- 7: "Updating",
- 8: "Copying logs",
- 9: "Recovering Location",
- 10: "IEC test",
- 11: "Map cleaning",
- 12: "Exploring map (creating a persistent map)",
- 13: "Acquiring Persistent Map IDs",
- 14: "Creating & Uploading Map",
- 15: "Suspended Exploration",
-}
-
-ERRORS = {
- "ui_error_battery_battundervoltlithiumsafety": "Replace battery",
- "ui_error_battery_critical": "Replace battery",
- "ui_error_battery_invalidsensor": "Replace battery",
- "ui_error_battery_lithiumadapterfailure": "Replace battery",
- "ui_error_battery_mismatch": "Replace battery",
- "ui_error_battery_nothermistor": "Replace battery",
- "ui_error_battery_overtemp": "Replace battery",
- "ui_error_battery_overvolt": "Replace battery",
- "ui_error_battery_undercurrent": "Replace battery",
- "ui_error_battery_undertemp": "Replace battery",
- "ui_error_battery_undervolt": "Replace battery",
- "ui_error_battery_unplugged": "Replace battery",
- "ui_error_brush_stuck": "Brush stuck",
- "ui_error_brush_overloaded": "Brush overloaded",
- "ui_error_bumper_stuck": "Bumper stuck",
- "ui_error_check_battery_switch": "Check battery",
- "ui_error_corrupt_scb": "Call customer service corrupt board",
- "ui_error_deck_debris": "Deck debris",
- "ui_error_dflt_app": "Check Neato app",
- "ui_error_disconnect_chrg_cable": "Disconnected charge cable",
- "ui_error_disconnect_usb_cable": "Disconnected USB cable",
- "ui_error_dust_bin_missing": "Dust bin missing",
- "ui_error_dust_bin_full": "Dust bin full",
- "ui_error_dust_bin_emptied": "Dust bin emptied",
- "ui_error_hardware_failure": "Hardware failure",
- "ui_error_ldrop_stuck": "Clear my path",
- "ui_error_lds_jammed": "Clear my path",
- "ui_error_lds_bad_packets": "Check Neato app",
- "ui_error_lds_disconnected": "Check Neato app",
- "ui_error_lds_missed_packets": "Check Neato app",
- "ui_error_lwheel_stuck": "Clear my path",
- "ui_error_navigation_backdrop_frontbump": "Clear my path",
- "ui_error_navigation_backdrop_leftbump": "Clear my path",
- "ui_error_navigation_backdrop_wheelextended": "Clear my path",
- "ui_error_navigation_noprogress": "Clear my path",
- "ui_error_navigation_origin_unclean": "Clear my path",
- "ui_error_navigation_pathproblems": "Cannot return to base",
- "ui_error_navigation_pinkycommsfail": "Clear my path",
- "ui_error_navigation_falling": "Clear my path",
- "ui_error_navigation_noexitstogo": "Clear my path",
- "ui_error_navigation_nomotioncommands": "Clear my path",
- "ui_error_navigation_rightdrop_leftbump": "Clear my path",
- "ui_error_navigation_undockingfailed": "Clear my path",
- "ui_error_picked_up": "Picked up",
- "ui_error_qa_fail": "Check Neato app",
- "ui_error_rdrop_stuck": "Clear my path",
- "ui_error_reconnect_failed": "Reconnect failed",
- "ui_error_rwheel_stuck": "Clear my path",
- "ui_error_stuck": "Stuck!",
- "ui_error_unable_to_return_to_base": "Unable to return to base",
- "ui_error_unable_to_see": "Clean vacuum sensors",
- "ui_error_vacuum_slip": "Clear my path",
- "ui_error_vacuum_stuck": "Clear my path",
- "ui_error_warning": "Error check app",
- "batt_base_connect_fail": "Battery failed to connect to base",
- "batt_base_no_power": "Battery base has no power",
- "batt_low": "Battery low",
- "batt_on_base": "Battery on base",
- "clean_tilt_on_start": "Clean the tilt on start",
- "dustbin_full": "Dust bin full",
- "dustbin_missing": "Dust bin missing",
- "gen_picked_up": "Picked up",
- "hw_fail": "Hardware failure",
- "hw_tof_sensor_sensor": "Hardware sensor disconnected",
- "lds_bad_packets": "Bad packets",
- "lds_deck_debris": "Debris on deck",
- "lds_disconnected": "Disconnected",
- "lds_jammed": "Jammed",
- "lds_missed_packets": "Missed packets",
- "maint_brush_stuck": "Brush stuck",
- "maint_brush_overload": "Brush overloaded",
- "maint_bumper_stuck": "Bumper stuck",
- "maint_customer_support_qa": "Contact customer support",
- "maint_vacuum_stuck": "Vacuum is stuck",
- "maint_vacuum_slip": "Vacuum is stuck",
- "maint_left_drop_stuck": "Vacuum is stuck",
- "maint_left_wheel_stuck": "Vacuum is stuck",
- "maint_right_drop_stuck": "Vacuum is stuck",
- "maint_right_wheel_stuck": "Vacuum is stuck",
- "not_on_charge_base": "Not on the charge base",
- "nav_robot_falling": "Clear my path",
- "nav_no_path": "Clear my path",
- "nav_path_problem": "Clear my path",
- "nav_backdrop_frontbump": "Clear my path",
- "nav_backdrop_leftbump": "Clear my path",
- "nav_backdrop_wheelextended": "Clear my path",
- "nav_mag_sensor": "Clear my path",
- "nav_no_exit": "Clear my path",
- "nav_no_movement": "Clear my path",
- "nav_rightdrop_leftbump": "Clear my path",
- "nav_undocking_failed": "Clear my path",
-}
-
-ALERTS = {
- "ui_alert_dust_bin_full": "Please empty dust bin",
- "ui_alert_recovering_location": "Returning to start",
- "ui_alert_battery_chargebasecommerr": "Battery error",
- "ui_alert_busy_charging": "Busy charging",
- "ui_alert_charging_base": "Base charging",
- "ui_alert_charging_power": "Charging power",
- "ui_alert_connect_chrg_cable": "Connect charge cable",
- "ui_alert_info_thank_you": "Thank you",
- "ui_alert_invalid": "Invalid check app",
- "ui_alert_old_error": "Old error",
- "ui_alert_swupdate_fail": "Update failed",
- "dustbin_full": "Please empty dust bin",
- "maint_brush_change": "Change the brush",
- "maint_filter_change": "Change the filter",
- "clean_completed_to_start": "Cleaning completed",
- "nav_floorplan_not_created": "No floorplan found",
- "nav_floorplan_load_fail": "Failed to load floorplan",
- "nav_floorplan_localization_fail": "Failed to load floorplan",
- "clean_incomplete_to_start": "Cleaning incomplete",
- "log_upload_failed": "Logs failed to upload",
-}
-
-
-def setup(hass, config):
+async def async_setup(hass, config):
"""Set up the Neato component."""
- from pybotvac import Account, Neato, Vorwerk
- if config[DOMAIN][CONF_VENDOR] == "neato":
- hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Neato)
- elif config[DOMAIN][CONF_VENDOR] == "vorwerk":
- hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Vorwerk)
+ if NEATO_DOMAIN not in config:
+ # There is an entry and nothing in configuration.yaml
+ return True
+
+ entries = hass.config_entries.async_entries(NEATO_DOMAIN)
+ hass.data[NEATO_CONFIG] = config[NEATO_DOMAIN]
+
+ if entries:
+ # There is an entry and something in the configuration.yaml
+ entry = entries[0]
+ conf = config[NEATO_DOMAIN]
+ if (
+ entry.data[CONF_USERNAME] == conf[CONF_USERNAME]
+ and entry.data[CONF_PASSWORD] == conf[CONF_PASSWORD]
+ and entry.data[CONF_VENDOR] == conf[CONF_VENDOR]
+ ):
+ # The entry is not outdated
+ return True
+
+ # The entry is outdated
+ error = await hass.async_add_executor_job(
+ NeatoConfigFlow.try_login,
+ conf[CONF_USERNAME],
+ conf[CONF_PASSWORD],
+ conf[CONF_VENDOR],
+ )
+ if error is not None:
+ _LOGGER.error(error)
+ return False
+
+ # Update the entry
+ hass.config_entries.async_update_entry(entry, data=config[NEATO_DOMAIN])
+ else:
+ # Create the new entry
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ NEATO_DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config[NEATO_DOMAIN],
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up config entry."""
+ hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account)
+
hub = hass.data[NEATO_LOGIN]
- if not hub.login():
+ await hass.async_add_executor_job(hub.login)
+ if not hub.logged_in:
_LOGGER.debug("Failed to login to Neato API")
return False
- hub.update_robots()
- for component in ("camera", "vacuum", "switch"):
- discovery.load_platform(hass, component, DOMAIN, {}, config)
+ try:
+ await hass.async_add_executor_job(hub.update_robots)
+ except NeatoRobotException:
+ _LOGGER.debug("Failed to connect to Neato API")
+ return False
+
+ for component in ("camera", "vacuum", "switch", "sensor"):
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload config entry."""
+ hass.data.pop(NEATO_LOGIN)
+ await asyncio.gather(
+ hass.config_entries.async_forward_entry_unload(entry, "camera"),
+ hass.config_entries.async_forward_entry_unload(entry, "vacuum"),
+ hass.config_entries.async_forward_entry_unload(entry, "switch"),
+ hass.config_entries.async_forward_entry_unload(entry, "sensor"),
+ )
return True
class NeatoHub:
"""A My Neato hub wrapper class."""
- def __init__(self, hass, domain_config, neato, vendor):
+ def __init__(self, hass, domain_config, neato):
"""Initialize the Neato hub."""
self.config = domain_config
self._neato = neato
self._hass = hass
- self._vendor = vendor
- self.my_neato = neato(
- domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD], vendor
- )
- self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
- self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps
- self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps
+ if self.config[CONF_VENDOR] == "vorwerk":
+ self._vendor = Vorwerk()
+ else: # Neato
+ self._vendor = Neato()
+
+ self.my_neato = None
+ self.logged_in = False
def login(self):
"""Login to My Neato."""
+ _LOGGER.debug("Trying to connect to Neato API")
try:
- _LOGGER.debug("Trying to connect to Neato API")
self.my_neato = self._neato(
self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor
)
- return True
- except HTTPError:
- _LOGGER.error("Unable to connect to Neato API")
- return False
+ except NeatoException as ex:
+ if isinstance(ex, NeatoLoginException):
+ _LOGGER.error("Invalid credentials")
+ else:
+ _LOGGER.error("Unable to connect to Neato API")
+ self.logged_in = False
+ return
- @Throttle(timedelta(seconds=300))
+ self.logged_in = True
+ _LOGGER.debug("Successfully connected to Neato API")
+
+ @Throttle(timedelta(minutes=SCAN_INTERVAL_MINUTES))
def update_robots(self):
"""Update the robot states."""
- _LOGGER.debug("Running HUB.update_robots %s", self._hass.data[NEATO_ROBOTS])
+ _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS))
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps
self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps
diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py
index 5d4e0057960..f60835b1146 100644
--- a/homeassistant/components/neato/camera.py
+++ b/homeassistant/components/neato/camera.py
@@ -2,35 +2,58 @@
from datetime import timedelta
import logging
+from pybotvac.exceptions import NeatoRobotException
+
from homeassistant.components.camera import Camera
-from . import NEATO_LOGIN, NEATO_MAP_DATA, NEATO_ROBOTS
+from .const import (
+ NEATO_DOMAIN,
+ NEATO_LOGIN,
+ NEATO_MAP_DATA,
+ NEATO_ROBOTS,
+ SCAN_INTERVAL_MINUTES,
+)
_LOGGER = logging.getLogger(__name__)
-SCAN_INTERVAL = timedelta(minutes=10)
+SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES)
+ATTR_GENERATED_AT = "generated_at"
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Neato Camera."""
+ pass
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Neato camera with config entry."""
dev = []
+ neato = hass.data.get(NEATO_LOGIN)
+ mapdata = hass.data.get(NEATO_MAP_DATA)
for robot in hass.data[NEATO_ROBOTS]:
if "maps" in robot.traits:
- dev.append(NeatoCleaningMap(hass, robot))
+ dev.append(NeatoCleaningMap(neato, robot, mapdata))
+
+ if not dev:
+ return
+
_LOGGER.debug("Adding robots for cleaning maps %s", dev)
- add_entities(dev, True)
+ async_add_entities(dev, True)
class NeatoCleaningMap(Camera):
"""Neato cleaning map for last clean."""
- def __init__(self, hass, robot):
+ def __init__(self, neato, robot, mapdata):
"""Initialize Neato cleaning map."""
super().__init__()
self.robot = robot
- self._robot_name = "{} {}".format(self.robot.name, "Cleaning Map")
+ self.neato = neato
+ self._mapdata = mapdata
+ self._available = self.neato.logged_in if self.neato is not None else False
+ self._robot_name = f"{self.robot.name} Cleaning Map"
self._robot_serial = self.robot.serial
- self.neato = hass.data[NEATO_LOGIN]
+ self._generated_at = None
self._image_url = None
self._image = None
@@ -41,16 +64,45 @@ class NeatoCleaningMap(Camera):
def update(self):
"""Check the contents of the map list."""
- self.neato.update_robots()
+ if self.neato is None:
+ _LOGGER.error("Error while updating camera")
+ self._image = None
+ self._image_url = None
+ self._available = False
+ return
+
+ _LOGGER.debug("Running camera update")
+ try:
+ self.neato.update_robots()
+ except NeatoRobotException as ex:
+ if self._available: # Print only once when available
+ _LOGGER.error("Neato camera connection error: %s", ex)
+ self._image = None
+ self._image_url = None
+ self._available = False
+ return
+
image_url = None
- map_data = self.hass.data[NEATO_MAP_DATA]
- image_url = map_data[self._robot_serial]["maps"][0]["url"]
+ map_data = self._mapdata[self._robot_serial]["maps"][0]
+ image_url = map_data["url"]
if image_url == self._image_url:
_LOGGER.debug("The map image_url is the same as old")
return
- image = self.neato.download_map(image_url)
+
+ try:
+ image = self.neato.download_map(image_url)
+ except NeatoRobotException as ex:
+ if self._available: # Print only once when available
+ _LOGGER.error("Neato camera connection error: %s", ex)
+ self._image = None
+ self._image_url = None
+ self._available = False
+ return
+
self._image = image.read()
self._image_url = image_url
+ self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ")
+ self._available = True
@property
def name(self):
@@ -61,3 +113,23 @@ class NeatoCleaningMap(Camera):
def unique_id(self):
"""Return unique ID."""
return self._robot_serial
+
+ @property
+ def available(self):
+ """Return if the robot is available."""
+ return self._available
+
+ @property
+ def device_info(self):
+ """Device info for neato robot."""
+ return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}}
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the vacuum cleaner."""
+ data = {}
+
+ if self._generated_at is not None:
+ data[ATTR_GENERATED_AT] = self._generated_at
+
+ return data
diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py
new file mode 100644
index 00000000000..56fba9047e7
--- /dev/null
+++ b/homeassistant/components/neato/config_flow.py
@@ -0,0 +1,112 @@
+"""Config flow to configure Neato integration."""
+
+import logging
+
+from pybotvac import Account, Neato, Vorwerk
+from pybotvac.exceptions import NeatoLoginException, NeatoRobotException
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+
+# pylint: disable=unused-import
+from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS
+
+DOCS_URL = "https://www.home-assistant.io/components/neato"
+DEFAULT_VENDOR = "neato"
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN):
+ """Neato integration config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def __init__(self):
+ """Initialize flow."""
+ self._username = vol.UNDEFINED
+ self._password = vol.UNDEFINED
+ self._vendor = vol.UNDEFINED
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ errors = {}
+
+ if self._async_current_entries():
+ return self.async_abort(reason="already_configured")
+
+ if user_input is not None:
+ self._username = user_input["username"]
+ self._password = user_input["password"]
+ self._vendor = user_input["vendor"]
+
+ error = await self.hass.async_add_executor_job(
+ self.try_login, self._username, self._password, self._vendor
+ )
+ if error:
+ errors["base"] = error
+ else:
+ return self.async_create_entry(
+ title=user_input[CONF_USERNAME],
+ data=user_input,
+ description_placeholders={"docs_url": DOCS_URL},
+ )
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS),
+ }
+ ),
+ description_placeholders={"docs_url": DOCS_URL},
+ errors=errors,
+ )
+
+ async def async_step_import(self, user_input):
+ """Import a config flow from configuration."""
+
+ if self._async_current_entries():
+ return self.async_abort(reason="already_configured")
+
+ username = user_input[CONF_USERNAME]
+ password = user_input[CONF_PASSWORD]
+ vendor = user_input[CONF_VENDOR]
+
+ error = await self.hass.async_add_executor_job(
+ self.try_login, username, password, vendor
+ )
+ if error is not None:
+ _LOGGER.error(error)
+ return self.async_abort(reason=error)
+
+ return self.async_create_entry(
+ title=f"{username} (from configuration)",
+ data={
+ CONF_USERNAME: username,
+ CONF_PASSWORD: password,
+ CONF_VENDOR: vendor,
+ },
+ )
+
+ @staticmethod
+ def try_login(username, password, vendor):
+ """Try logging in to device and return any errors."""
+ this_vendor = None
+ if vendor == "vorwerk":
+ this_vendor = Vorwerk()
+ else: # Neato
+ this_vendor = Neato()
+
+ try:
+ Account(username, password, this_vendor)
+ except NeatoLoginException:
+ return "invalid_credentials"
+ except NeatoRobotException:
+ return "unexpected_error"
+
+ return None
diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py
new file mode 100644
index 00000000000..6dbaeb10d36
--- /dev/null
+++ b/homeassistant/components/neato/const.py
@@ -0,0 +1,152 @@
+"""Constants for Neato integration."""
+
+NEATO_DOMAIN = "neato"
+
+CONF_VENDOR = "vendor"
+NEATO_CONFIG = "neato_config"
+NEATO_LOGIN = "neato_login"
+NEATO_MAP_DATA = "neato_map_data"
+NEATO_PERSISTENT_MAPS = "neato_persistent_maps"
+NEATO_ROBOTS = "neato_robots"
+
+SCAN_INTERVAL_MINUTES = 5
+
+VALID_VENDORS = ["neato", "vorwerk"]
+
+MODE = {1: "Eco", 2: "Turbo"}
+
+ACTION = {
+ 0: "Invalid",
+ 1: "House Cleaning",
+ 2: "Spot Cleaning",
+ 3: "Manual Cleaning",
+ 4: "Docking",
+ 5: "User Menu Active",
+ 6: "Suspended Cleaning",
+ 7: "Updating",
+ 8: "Copying logs",
+ 9: "Recovering Location",
+ 10: "IEC test",
+ 11: "Map cleaning",
+ 12: "Exploring map (creating a persistent map)",
+ 13: "Acquiring Persistent Map IDs",
+ 14: "Creating & Uploading Map",
+ 15: "Suspended Exploration",
+}
+
+ERRORS = {
+ "ui_error_battery_battundervoltlithiumsafety": "Replace battery",
+ "ui_error_battery_critical": "Replace battery",
+ "ui_error_battery_invalidsensor": "Replace battery",
+ "ui_error_battery_lithiumadapterfailure": "Replace battery",
+ "ui_error_battery_mismatch": "Replace battery",
+ "ui_error_battery_nothermistor": "Replace battery",
+ "ui_error_battery_overtemp": "Replace battery",
+ "ui_error_battery_overvolt": "Replace battery",
+ "ui_error_battery_undercurrent": "Replace battery",
+ "ui_error_battery_undertemp": "Replace battery",
+ "ui_error_battery_undervolt": "Replace battery",
+ "ui_error_battery_unplugged": "Replace battery",
+ "ui_error_brush_stuck": "Brush stuck",
+ "ui_error_brush_overloaded": "Brush overloaded",
+ "ui_error_bumper_stuck": "Bumper stuck",
+ "ui_error_check_battery_switch": "Check battery",
+ "ui_error_corrupt_scb": "Call customer service corrupt board",
+ "ui_error_deck_debris": "Deck debris",
+ "ui_error_dflt_app": "Check Neato app",
+ "ui_error_disconnect_chrg_cable": "Disconnected charge cable",
+ "ui_error_disconnect_usb_cable": "Disconnected USB cable",
+ "ui_error_dust_bin_missing": "Dust bin missing",
+ "ui_error_dust_bin_full": "Dust bin full",
+ "ui_error_dust_bin_emptied": "Dust bin emptied",
+ "ui_error_hardware_failure": "Hardware failure",
+ "ui_error_ldrop_stuck": "Clear my path",
+ "ui_error_lds_jammed": "Clear my path",
+ "ui_error_lds_bad_packets": "Check Neato app",
+ "ui_error_lds_disconnected": "Check Neato app",
+ "ui_error_lds_missed_packets": "Check Neato app",
+ "ui_error_lwheel_stuck": "Clear my path",
+ "ui_error_navigation_backdrop_frontbump": "Clear my path",
+ "ui_error_navigation_backdrop_leftbump": "Clear my path",
+ "ui_error_navigation_backdrop_wheelextended": "Clear my path",
+ "ui_error_navigation_noprogress": "Clear my path",
+ "ui_error_navigation_origin_unclean": "Clear my path",
+ "ui_error_navigation_pathproblems": "Cannot return to base",
+ "ui_error_navigation_pinkycommsfail": "Clear my path",
+ "ui_error_navigation_falling": "Clear my path",
+ "ui_error_navigation_noexitstogo": "Clear my path",
+ "ui_error_navigation_nomotioncommands": "Clear my path",
+ "ui_error_navigation_rightdrop_leftbump": "Clear my path",
+ "ui_error_navigation_undockingfailed": "Clear my path",
+ "ui_error_picked_up": "Picked up",
+ "ui_error_qa_fail": "Check Neato app",
+ "ui_error_rdrop_stuck": "Clear my path",
+ "ui_error_reconnect_failed": "Reconnect failed",
+ "ui_error_rwheel_stuck": "Clear my path",
+ "ui_error_stuck": "Stuck!",
+ "ui_error_unable_to_return_to_base": "Unable to return to base",
+ "ui_error_unable_to_see": "Clean vacuum sensors",
+ "ui_error_vacuum_slip": "Clear my path",
+ "ui_error_vacuum_stuck": "Clear my path",
+ "ui_error_warning": "Error check app",
+ "batt_base_connect_fail": "Battery failed to connect to base",
+ "batt_base_no_power": "Battery base has no power",
+ "batt_low": "Battery low",
+ "batt_on_base": "Battery on base",
+ "clean_tilt_on_start": "Clean the tilt on start",
+ "dustbin_full": "Dust bin full",
+ "dustbin_missing": "Dust bin missing",
+ "gen_picked_up": "Picked up",
+ "hw_fail": "Hardware failure",
+ "hw_tof_sensor_sensor": "Hardware sensor disconnected",
+ "lds_bad_packets": "Bad packets",
+ "lds_deck_debris": "Debris on deck",
+ "lds_disconnected": "Disconnected",
+ "lds_jammed": "Jammed",
+ "lds_missed_packets": "Missed packets",
+ "maint_brush_stuck": "Brush stuck",
+ "maint_brush_overload": "Brush overloaded",
+ "maint_bumper_stuck": "Bumper stuck",
+ "maint_customer_support_qa": "Contact customer support",
+ "maint_vacuum_stuck": "Vacuum is stuck",
+ "maint_vacuum_slip": "Vacuum is stuck",
+ "maint_left_drop_stuck": "Vacuum is stuck",
+ "maint_left_wheel_stuck": "Vacuum is stuck",
+ "maint_right_drop_stuck": "Vacuum is stuck",
+ "maint_right_wheel_stuck": "Vacuum is stuck",
+ "not_on_charge_base": "Not on the charge base",
+ "nav_robot_falling": "Clear my path",
+ "nav_no_path": "Clear my path",
+ "nav_path_problem": "Clear my path",
+ "nav_backdrop_frontbump": "Clear my path",
+ "nav_backdrop_leftbump": "Clear my path",
+ "nav_backdrop_wheelextended": "Clear my path",
+ "nav_mag_sensor": "Clear my path",
+ "nav_no_exit": "Clear my path",
+ "nav_no_movement": "Clear my path",
+ "nav_rightdrop_leftbump": "Clear my path",
+ "nav_undocking_failed": "Clear my path",
+}
+
+ALERTS = {
+ "ui_alert_dust_bin_full": "Please empty dust bin",
+ "ui_alert_recovering_location": "Returning to start",
+ "ui_alert_battery_chargebasecommerr": "Battery error",
+ "ui_alert_busy_charging": "Busy charging",
+ "ui_alert_charging_base": "Base charging",
+ "ui_alert_charging_power": "Charging power",
+ "ui_alert_connect_chrg_cable": "Connect charge cable",
+ "ui_alert_info_thank_you": "Thank you",
+ "ui_alert_invalid": "Invalid check app",
+ "ui_alert_old_error": "Old error",
+ "ui_alert_swupdate_fail": "Update failed",
+ "dustbin_full": "Please empty dust bin",
+ "maint_brush_change": "Change the brush",
+ "maint_filter_change": "Change the filter",
+ "clean_completed_to_start": "Cleaning completed",
+ "nav_floorplan_not_created": "No floorplan found",
+ "nav_floorplan_load_fail": "Failed to load floorplan",
+ "nav_floorplan_localization_fail": "Failed to load floorplan",
+ "clean_incomplete_to_start": "Cleaning incomplete",
+ "log_upload_failed": "Logs failed to upload",
+}
diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json
index 8b0c5acc723..03f8089159e 100644
--- a/homeassistant/components/neato/manifest.json
+++ b/homeassistant/components/neato/manifest.json
@@ -1,10 +1,14 @@
{
"domain": "neato",
"name": "Neato",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/neato",
"requirements": [
- "pybotvac==0.0.15"
+ "pybotvac==0.0.17"
],
"dependencies": [],
- "codeowners": []
-}
+ "codeowners": [
+ "@dshokouhi",
+ "@Santobert"
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py
new file mode 100644
index 00000000000..36175151e0e
--- /dev/null
+++ b/homeassistant/components/neato/sensor.py
@@ -0,0 +1,104 @@
+"""Support for Neato sensors."""
+from datetime import timedelta
+import logging
+
+from pybotvac.exceptions import NeatoRobotException
+
+from homeassistant.components.sensor import DEVICE_CLASS_BATTERY
+from homeassistant.helpers.entity import Entity
+
+from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES
+
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES)
+
+BATTERY = "Battery"
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the Neato sensor."""
+ pass
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the Neato sensor using config entry."""
+ dev = []
+ neato = hass.data.get(NEATO_LOGIN)
+ for robot in hass.data[NEATO_ROBOTS]:
+ dev.append(NeatoSensor(neato, robot))
+
+ if not dev:
+ return
+
+ _LOGGER.debug("Adding robots for sensors %s", dev)
+ async_add_entities(dev, True)
+
+
+class NeatoSensor(Entity):
+ """Neato sensor."""
+
+ def __init__(self, neato, robot):
+ """Initialize Neato sensor."""
+ self.robot = robot
+ self.neato = neato
+ self._available = self.neato.logged_in if self.neato is not None else False
+ self._robot_name = f"{self.robot.name} {BATTERY}"
+ self._robot_serial = self.robot.serial
+ self._state = None
+
+ def update(self):
+ """Update Neato Sensor."""
+ if self.neato is None:
+ _LOGGER.error("Error while updating sensor")
+ self._state = None
+ self._available = False
+ return
+
+ try:
+ self.neato.update_robots()
+ self._state = self.robot.state
+ except NeatoRobotException as ex:
+ if self._available:
+ _LOGGER.error("Neato sensor connection error: %s", ex)
+ self._state = None
+ self._available = False
+ return
+
+ self._available = True
+ _LOGGER.debug("self._state=%s", self._state)
+
+ @property
+ def name(self):
+ """Return the name of this sensor."""
+ return self._robot_name
+
+ @property
+ def unique_id(self):
+ """Return unique ID."""
+ return self._robot_serial
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return DEVICE_CLASS_BATTERY
+
+ @property
+ def available(self):
+ """Return availability."""
+ return self._available
+
+ @property
+ def state(self):
+ """Return the state."""
+ return self._state["details"]["charge"]
+
+ @property
+ def unit_of_measurement(self):
+ """Return unit of measurement."""
+ return "%"
+
+ @property
+ def device_info(self):
+ """Device info for neato robot."""
+ return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}}
diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json
new file mode 100644
index 00000000000..69cdb48a560
--- /dev/null
+++ b/homeassistant/components/neato/strings.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "title": "Neato",
+ "step": {
+ "user": {
+ "title": "Neato Account Info",
+ "data": {
+ "username": "Username",
+ "password": "Password",
+ "vendor": "Vendor"
+ },
+ "description": "See [Neato documentation]({docs_url})."
+ }
+ },
+ "error": {
+ "invalid_credentials": "Invalid credentials",
+ "unexpected_error": "Unexpected error"
+ },
+ "create_entry": {
+ "default": "See [Neato documentation]({docs_url})."
+ },
+ "abort": {
+ "already_configured": "Already configured",
+ "invalid_credentials": "Invalid credentials"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py
index 539e8cb748c..8536af63945 100644
--- a/homeassistant/components/neato/switch.py
+++ b/homeassistant/components/neato/switch.py
@@ -2,66 +2,77 @@
from datetime import timedelta
import logging
-import requests
+from pybotvac.exceptions import NeatoRobotException
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers.entity import ToggleEntity
-from . import NEATO_LOGIN, NEATO_ROBOTS
+from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES
_LOGGER = logging.getLogger(__name__)
-SCAN_INTERVAL = timedelta(minutes=10)
+SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES)
SWITCH_TYPE_SCHEDULE = "schedule"
SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]}
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Neato switches."""
+ pass
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Neato switch with config entry."""
dev = []
+ neato = hass.data.get(NEATO_LOGIN)
for robot in hass.data[NEATO_ROBOTS]:
for type_name in SWITCH_TYPES:
- dev.append(NeatoConnectedSwitch(hass, robot, type_name))
+ dev.append(NeatoConnectedSwitch(neato, robot, type_name))
+
+ if not dev:
+ return
+
_LOGGER.debug("Adding switches %s", dev)
- add_entities(dev)
+ async_add_entities(dev, True)
class NeatoConnectedSwitch(ToggleEntity):
"""Neato Connected Switches."""
- def __init__(self, hass, robot, switch_type):
+ def __init__(self, neato, robot, switch_type):
"""Initialize the Neato Connected switches."""
self.type = switch_type
self.robot = robot
- self.neato = hass.data[NEATO_LOGIN]
- self._robot_name = "{} {}".format(self.robot.name, SWITCH_TYPES[self.type][0])
- try:
- self._state = self.robot.state
- except (
- requests.exceptions.ConnectionError,
- requests.exceptions.HTTPError,
- ) as ex:
- _LOGGER.warning("Neato connection error: %s", ex)
- self._state = None
+ self.neato = neato
+ self._available = self.neato.logged_in if self.neato is not None else False
+ self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}"
+ self._state = None
self._schedule_state = None
self._clean_state = None
self._robot_serial = self.robot.serial
def update(self):
"""Update the states of Neato switches."""
- _LOGGER.debug("Running switch update")
- self.neato.update_robots()
- try:
- self._state = self.robot.state
- except (
- requests.exceptions.ConnectionError,
- requests.exceptions.HTTPError,
- ) as ex:
- _LOGGER.warning("Neato connection error: %s", ex)
+ if self.neato is None:
+ _LOGGER.error("Error while updating switches")
self._state = None
+ self._available = False
return
+
+ _LOGGER.debug("Running switch update")
+ try:
+ self.neato.update_robots()
+ self._state = self.robot.state
+ except NeatoRobotException as ex:
+ if self._available: # Print only once when available
+ _LOGGER.error("Neato switch connection error: %s", ex)
+ self._state = None
+ self._available = False
+ return
+
+ self._available = True
_LOGGER.debug("self._state=%s", self._state)
if self.type == SWITCH_TYPE_SCHEDULE:
_LOGGER.debug("State: %s", self._state)
@@ -79,7 +90,7 @@ class NeatoConnectedSwitch(ToggleEntity):
@property
def available(self):
"""Return True if entity is available."""
- return self._state
+ return self._available
@property
def unique_id(self):
@@ -94,12 +105,23 @@ class NeatoConnectedSwitch(ToggleEntity):
return True
return False
+ @property
+ def device_info(self):
+ """Device info for neato robot."""
+ return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}}
+
def turn_on(self, **kwargs):
"""Turn the switch on."""
if self.type == SWITCH_TYPE_SCHEDULE:
- self.robot.enable_schedule()
+ try:
+ self.robot.enable_schedule()
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato switch connection error: %s", ex)
def turn_off(self, **kwargs):
"""Turn the switch off."""
if self.type == SWITCH_TYPE_SCHEDULE:
- self.robot.disable_schedule()
+ try:
+ self.robot.disable_schedule()
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato switch connection error: %s", ex)
diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py
index f284b2eda1e..40ed79042c7 100644
--- a/homeassistant/components/neato/vacuum.py
+++ b/homeassistant/components/neato/vacuum.py
@@ -2,12 +2,10 @@
from datetime import timedelta
import logging
-import requests
+from pybotvac.exceptions import NeatoRobotException
import voluptuous as vol
from homeassistant.components.vacuum import (
- ATTR_BATTERY_ICON,
- ATTR_BATTERY_LEVEL,
ATTR_STATUS,
DOMAIN,
STATE_CLEANING,
@@ -31,20 +29,22 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.service import extract_entity_ids
-from . import (
+from .const import (
ACTION,
ALERTS,
ERRORS,
MODE,
+ NEATO_DOMAIN,
NEATO_LOGIN,
NEATO_MAP_DATA,
NEATO_PERSISTENT_MAPS,
NEATO_ROBOTS,
+ SCAN_INTERVAL_MINUTES,
)
_LOGGER = logging.getLogger(__name__)
-SCAN_INTERVAL = timedelta(minutes=5)
+SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES)
SUPPORT_NEATO = (
SUPPORT_BATTERY
@@ -65,6 +65,9 @@ ATTR_CLEAN_BATTERY_START = "battery_level_at_clean_start"
ATTR_CLEAN_BATTERY_END = "battery_level_at_clean_end"
ATTR_CLEAN_SUSP_COUNT = "clean_suspension_count"
ATTR_CLEAN_SUSP_TIME = "clean_suspension_time"
+ATTR_CLEAN_PAUSE_TIME = "clean_pause_time"
+ATTR_CLEAN_ERROR_TIME = "clean_error_time"
+ATTR_LAUNCHED_FROM = "launched_from"
ATTR_NAVIGATION = "navigation"
ATTR_CATEGORY = "category"
@@ -83,17 +86,25 @@ SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema(
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Neato vacuum."""
+ pass
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Neato vacuum with config entry."""
dev = []
+ neato = hass.data.get(NEATO_LOGIN)
+ mapdata = hass.data.get(NEATO_MAP_DATA)
+ persistent_maps = hass.data.get(NEATO_PERSISTENT_MAPS)
for robot in hass.data[NEATO_ROBOTS]:
- dev.append(NeatoConnectedVacuum(hass, robot))
+ dev.append(NeatoConnectedVacuum(neato, robot, mapdata, persistent_maps))
if not dev:
return
_LOGGER.debug("Adding vacuums %s", dev)
- add_entities(dev, True)
+ async_add_entities(dev, True)
def neato_custom_cleaning_service(call):
"""Zone cleaning service that allows user to change options."""
@@ -103,7 +114,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
navigation = call.data.get(ATTR_NAVIGATION)
category = call.data.get(ATTR_CATEGORY)
zone = call.data.get(ATTR_ZONE)
- robot.neato_custom_cleaning(mode, navigation, category, zone)
+ try:
+ robot.neato_custom_cleaning(mode, navigation, category, zone)
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato vacuum connection error: %s", ex)
def service_to_entities(call):
"""Return the known devices that a service call mentions."""
@@ -111,7 +125,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
entities = [entity for entity in dev if entity.entity_id in entity_ids]
return entities
- hass.services.register(
+ hass.services.async_register(
DOMAIN,
SERVICE_NEATO_CUSTOM_CLEANING,
neato_custom_cleaning_service,
@@ -122,44 +136,55 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class NeatoConnectedVacuum(StateVacuumDevice):
"""Representation of a Neato Connected Vacuum."""
- def __init__(self, hass, robot):
+ def __init__(self, neato, robot, mapdata, persistent_maps):
"""Initialize the Neato Connected Vacuum."""
self.robot = robot
- self.neato = hass.data[NEATO_LOGIN]
+ self.neato = neato
+ self._available = self.neato.logged_in if self.neato is not None else False
+ self._mapdata = mapdata
self._name = f"{self.robot.name}"
+ self._robot_has_map = self.robot.has_persistent_maps
+ self._robot_maps = persistent_maps
+ self._robot_serial = self.robot.serial
self._status_state = None
self._clean_state = None
self._state = None
- self._mapdata = hass.data[NEATO_MAP_DATA]
- self.clean_time_start = None
- self.clean_time_stop = None
- self.clean_area = None
- self.clean_battery_start = None
- self.clean_battery_end = None
- self.clean_suspension_charge_count = None
- self.clean_suspension_time = None
- self._available = False
+ self._clean_time_start = None
+ self._clean_time_stop = None
+ self._clean_area = None
+ self._clean_battery_start = None
+ self._clean_battery_end = None
+ self._clean_susp_charge_count = None
+ self._clean_susp_time = None
+ self._clean_pause_time = None
+ self._clean_error_time = None
+ self._launched_from = None
self._battery_level = None
- self._robot_serial = self.robot.serial
- self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS]
self._robot_boundaries = {}
- self._robot_has_map = self.robot.has_persistent_maps
+ self._robot_stats = None
def update(self):
"""Update the states of Neato Vacuums."""
- _LOGGER.debug("Running Neato Vacuums update")
- self.neato.update_robots()
- try:
- self._state = self.robot.state
- self._available = True
- except (
- requests.exceptions.ConnectionError,
- requests.exceptions.HTTPError,
- ) as ex:
- _LOGGER.warning("Neato connection error: %s", ex)
+ if self.neato is None:
+ _LOGGER.error("Error while updating vacuum")
self._state = None
self._available = False
return
+
+ _LOGGER.debug("Running Neato Vacuums update")
+ try:
+ if self._robot_stats is None:
+ self._robot_stats = self.robot.get_robot_info().json()
+ self.neato.update_robots()
+ self._state = self.robot.state
+ except NeatoRobotException as ex:
+ if self._available: # print only once when available
+ _LOGGER.error("Neato vacuum connection error: %s", ex)
+ self._state = None
+ self._available = False
+ return
+
+ self._available = True
_LOGGER.debug("self._state=%s", self._state)
if "alert" in self._state:
robot_alert = ALERTS.get(self._state["alert"])
@@ -202,34 +227,33 @@ class NeatoConnectedVacuum(StateVacuumDevice):
if not self._mapdata.get(self._robot_serial, {}).get("maps", []):
return
- self.clean_time_start = (
- self._mapdata[self._robot_serial]["maps"][0]["start_at"].strip("Z")
- ).replace("T", " ")
- self.clean_time_stop = (
- self._mapdata[self._robot_serial]["maps"][0]["end_at"].strip("Z")
- ).replace("T", " ")
- self.clean_area = self._mapdata[self._robot_serial]["maps"][0]["cleaned_area"]
- self.clean_suspension_charge_count = self._mapdata[self._robot_serial]["maps"][
- 0
- ]["suspended_cleaning_charging_count"]
- self.clean_suspension_time = self._mapdata[self._robot_serial]["maps"][0][
- "time_in_suspended_cleaning"
- ]
- self.clean_battery_start = self._mapdata[self._robot_serial]["maps"][0][
- "run_charge_at_start"
- ]
- self.clean_battery_end = self._mapdata[self._robot_serial]["maps"][0][
- "run_charge_at_end"
- ]
- if self._robot_has_map:
- if self._state["availableServices"]["maps"] != "basic-1":
- if self._robot_maps[self._robot_serial]:
- allmaps = self._robot_maps[self._robot_serial]
- for maps in allmaps:
- self._robot_boundaries = self.robot.get_map_boundaries(
- maps["id"]
- ).json()
+ mapdata = self._mapdata[self._robot_serial]["maps"][0]
+ self._clean_time_start = (mapdata["start_at"].strip("Z")).replace("T", " ")
+ self._clean_time_stop = (mapdata["end_at"].strip("Z")).replace("T", " ")
+ self._clean_area = mapdata["cleaned_area"]
+ self._clean_susp_charge_count = mapdata["suspended_cleaning_charging_count"]
+ self._clean_susp_time = mapdata["time_in_suspended_cleaning"]
+ self._clean_pause_time = mapdata["time_in_pause"]
+ self._clean_error_time = mapdata["time_in_error"]
+ self._clean_battery_start = mapdata["run_charge_at_start"]
+ self._clean_battery_end = mapdata["run_charge_at_end"]
+ self._launched_from = mapdata["launched_from"]
+
+ if (
+ self._robot_has_map
+ and self._state["availableServices"]["maps"] != "basic-1"
+ and self._robot_maps[self._robot_serial]
+ ):
+ allmaps = self._robot_maps[self._robot_serial]
+ for maps in allmaps:
+ try:
+ self._robot_boundaries = self.robot.get_map_boundaries(
+ maps["id"]
+ ).json()
+ except NeatoRobotException as ex:
+ _LOGGER.error("Could not fetch map boundaries: %s", ex)
+ self._robot_boundaries = {}
@property
def name(self):
@@ -251,6 +275,11 @@ class NeatoConnectedVacuum(StateVacuumDevice):
"""Return if the robot is available."""
return self._available
+ @property
+ def icon(self):
+ """Return neato specific icon."""
+ return "mdi:robot-vacuum-variant"
+
@property
def state(self):
"""Return the status of the vacuum cleaner."""
@@ -268,57 +297,87 @@ class NeatoConnectedVacuum(StateVacuumDevice):
if self._status_state is not None:
data[ATTR_STATUS] = self._status_state
-
- if self.battery_level is not None:
- data[ATTR_BATTERY_LEVEL] = self.battery_level
- data[ATTR_BATTERY_ICON] = self.battery_icon
-
- if self.clean_time_start is not None:
- data[ATTR_CLEAN_START] = self.clean_time_start
- if self.clean_time_stop is not None:
- data[ATTR_CLEAN_STOP] = self.clean_time_stop
- if self.clean_area is not None:
- data[ATTR_CLEAN_AREA] = self.clean_area
- if self.clean_suspension_charge_count is not None:
- data[ATTR_CLEAN_SUSP_COUNT] = self.clean_suspension_charge_count
- if self.clean_suspension_time is not None:
- data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time
- if self.clean_battery_start is not None:
- data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start
- if self.clean_battery_end is not None:
- data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end
+ if self._clean_time_start is not None:
+ data[ATTR_CLEAN_START] = self._clean_time_start
+ if self._clean_time_stop is not None:
+ data[ATTR_CLEAN_STOP] = self._clean_time_stop
+ if self._clean_area is not None:
+ data[ATTR_CLEAN_AREA] = self._clean_area
+ if self._clean_susp_charge_count is not None:
+ data[ATTR_CLEAN_SUSP_COUNT] = self._clean_susp_charge_count
+ if self._clean_susp_time is not None:
+ data[ATTR_CLEAN_SUSP_TIME] = self._clean_susp_time
+ if self._clean_pause_time is not None:
+ data[ATTR_CLEAN_PAUSE_TIME] = self._clean_pause_time
+ if self._clean_error_time is not None:
+ data[ATTR_CLEAN_ERROR_TIME] = self._clean_error_time
+ if self._clean_battery_start is not None:
+ data[ATTR_CLEAN_BATTERY_START] = self._clean_battery_start
+ if self._clean_battery_end is not None:
+ data[ATTR_CLEAN_BATTERY_END] = self._clean_battery_end
+ if self._launched_from is not None:
+ data[ATTR_LAUNCHED_FROM] = self._launched_from
return data
+ @property
+ def device_info(self):
+ """Device info for neato robot."""
+ return {
+ "identifiers": {(NEATO_DOMAIN, self._robot_serial)},
+ "name": self._name,
+ "manufacturer": self._robot_stats["data"]["mfg_name"],
+ "model": self._robot_stats["data"]["modelName"],
+ "sw_version": self._state["meta"]["firmware"],
+ }
+
def start(self):
"""Start cleaning or resume cleaning."""
- if self._state["state"] == 1:
- self.robot.start_cleaning()
- elif self._state["state"] == 3:
- self.robot.resume_cleaning()
+ try:
+ if self._state["state"] == 1:
+ self.robot.start_cleaning()
+ elif self._state["state"] == 3:
+ self.robot.resume_cleaning()
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato vacuum connection error: %s", ex)
def pause(self):
"""Pause the vacuum."""
- self.robot.pause_cleaning()
+ try:
+ self.robot.pause_cleaning()
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato vacuum connection error: %s", ex)
def return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock."""
- if self._clean_state == STATE_CLEANING:
- self.robot.pause_cleaning()
- self._clean_state = STATE_RETURNING
- self.robot.send_to_base()
+ try:
+ if self._clean_state == STATE_CLEANING:
+ self.robot.pause_cleaning()
+ self._clean_state = STATE_RETURNING
+ self.robot.send_to_base()
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato vacuum connection error: %s", ex)
def stop(self, **kwargs):
"""Stop the vacuum cleaner."""
- self.robot.stop_cleaning()
+ try:
+ self.robot.stop_cleaning()
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato vacuum connection error: %s", ex)
def locate(self, **kwargs):
"""Locate the robot by making it emit a sound."""
- self.robot.locate()
+ try:
+ self.robot.locate()
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato vacuum connection error: %s", ex)
def clean_spot(self, **kwargs):
"""Run a spot cleaning starting from the base."""
- self.robot.start_spot_cleaning()
+ try:
+ self.robot.start_spot_cleaning()
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato vacuum connection error: %s", ex)
def neato_custom_cleaning(self, mode, navigation, category, zone=None, **kwargs):
"""Zone cleaning service call."""
@@ -334,4 +393,7 @@ class NeatoConnectedVacuum(StateVacuumDevice):
return
self._clean_state = STATE_CLEANING
- self.robot.start_cleaning(mode, navigation, category, boundary_id)
+ try:
+ self.robot.start_cleaning(mode, navigation, category, boundary_id)
+ except NeatoRobotException as ex:
+ _LOGGER.error("Neato vacuum connection error: %s", ex)
diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json
index ac88ed224ed..ba49b788b9a 100644
--- a/homeassistant/components/nest/.translations/ru.json
+++ b/homeassistant/components/nest/.translations/ru.json
@@ -7,10 +7,10 @@
"no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)."
},
"error": {
- "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430",
- "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434",
+ "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.",
+ "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434.",
"timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.",
- "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430"
+ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430."
},
"step": {
"init": {
diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py
index cf1ba36aa89..32bbd009417 100644
--- a/homeassistant/components/nest/__init__.py
+++ b/homeassistant/components/nest/__init__.py
@@ -4,6 +4,8 @@ import socket
from datetime import datetime, timedelta
import threading
+from nest import Nest
+from nest.nest import AuthorizationError, APIError
import voluptuous as vol
from homeassistant import config_entries
@@ -142,7 +144,6 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Set up Nest from a config entry."""
- from nest import Nest
nest = Nest(access_token=entry.data["tokens"]["access_token"])
@@ -286,8 +287,6 @@ class NestDevice:
def initialize(self):
"""Initialize Nest."""
- from nest.nest import AuthorizationError, APIError
-
try:
# Do not optimize next statement, it is here for initialize
# persistence Nest API connection.
@@ -302,8 +301,6 @@ class NestDevice:
def structures(self):
"""Generate a list of structures."""
- from nest.nest import AuthorizationError, APIError
-
try:
for structure in self.nest.structures:
if structure.name not in self.local_structure:
@@ -332,8 +329,6 @@ class NestDevice:
def _devices(self, device_type):
"""Generate a list of Nest devices."""
- from nest.nest import AuthorizationError, APIError
-
try:
for structure in self.nest.structures:
if structure.name not in self.local_structure:
diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py
index eec7108cdea..795ce5c80e9 100644
--- a/homeassistant/components/nest/climate.py
+++ b/homeassistant/components/nest/climate.py
@@ -1,6 +1,7 @@
"""Support for Nest thermostats."""
import logging
+from nest.nest import APIError
import voluptuous as vol
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
@@ -232,7 +233,6 @@ class NestThermostat(ClimateDevice):
def set_temperature(self, **kwargs):
"""Set new target temperature."""
- import nest
temp = None
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
@@ -247,7 +247,7 @@ class NestThermostat(ClimateDevice):
try:
if temp is not None:
self.device.target = temp
- except nest.nest.APIError as api_error:
+ except APIError as api_error:
_LOGGER.error("An error occurred while setting temperature: %s", api_error)
# restore target temperature
self.schedule_update_ha_state(True)
diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py
index 51d826c242f..38d1827326d 100644
--- a/homeassistant/components/nest/local_auth.py
+++ b/homeassistant/components/nest/local_auth.py
@@ -2,6 +2,8 @@
import asyncio
from functools import partial
+from nest.nest import NestAuth, AUTHORIZE_URL, AuthorizationError
+
from homeassistant.core import callback
from . import config_flow
from .const import DOMAIN
@@ -21,14 +23,11 @@ def initialize(hass, client_id, client_secret):
async def generate_auth_url(client_id, flow_id):
"""Generate an authorize url."""
- from nest.nest import AUTHORIZE_URL
-
return AUTHORIZE_URL.format(client_id, flow_id)
async def resolve_auth_code(hass, client_id, client_secret, code):
"""Resolve an authorization code."""
- from nest.nest import NestAuth, AuthorizationError
result = asyncio.Future()
auth = NestAuth(
diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml
index 0015c83342d..e10e6264643 100644
--- a/homeassistant/components/nest/services.yaml
+++ b/homeassistant/components/nest/services.yaml
@@ -1,16 +1,37 @@
-set_mode:
- description: 'Set the home/away mode for a Nest structure. Set to away mode will
- also set Estimated Arrival Time if provided. Set ETA will cause the thermostat
- to begin warming or cooling the home before the user arrives. After ETA set other
- Automation can read ETA sensor as a signal to prepare the home for the user''s
- arrival.
+# Describes the format for available Nest services
- '
+set_away_mode:
+ description: Set the away mode for a Nest structure.
fields:
- eta: {description: Optional Estimated Arrival Time from now., example: '0:10'}
- eta_window: {description: Optional ETA window. Default is 1 minute., example: '0:5'}
- home_mode: {description: home or away, example: home}
- structure: {description: Optional structure name. Default set all structures managed
- by Home Assistant., example: My Home}
- trip_id: {description: Optional identity of a trip. Using the same trip_ID will
- update the estimation., example: trip_back_home}
+ away_mode:
+ description: New mode to set. Valid modes are "away" or "home".
+ example: "away"
+ structure:
+ description: Name(s) of structure(s) to change. Defaults to all structures if not specified.
+ example: "Apartment"
+
+set_eta:
+ description: Set or update the estimated time of arrival window for a Nest structure.
+ fields:
+ eta:
+ description: Estimated time of arrival from now.
+ example: "00:10:30"
+ eta_window:
+ description: Estimated time of arrival window. Default is 1 minute.
+ example: "00:05"
+ trip_id:
+ description: Unique ID for the trip. Default is auto-generated using a timestamp.
+ example: "Leave Work"
+ structure:
+ description: Name(s) of structure(s) to change. Defaults to all structures if not specified.
+ example: "Apartment"
+
+cancel_eta:
+ description: Cancel an existing estimated time of arrival window for a Nest structure.
+ fields:
+ trip_id:
+ description: Unique ID for the trip.
+ example: "Leave Work"
+ structure:
+ description: Name(s) of structure(s) to change. Defaults to all structures if not specified.
+ example: "Apartment"
diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py
index 28d422557da..4b9f0690ac5 100644
--- a/homeassistant/components/netatmo/__init__.py
+++ b/homeassistant/components/netatmo/__init__.py
@@ -3,6 +3,7 @@ import logging
from datetime import timedelta
from urllib.error import HTTPError
+import pyatmo
import voluptuous as vol
from homeassistant.const import (
@@ -89,7 +90,6 @@ SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({})
def setup(hass, config):
"""Set up the Netatmo devices."""
- import pyatmo
hass.data[DATA_PERSONS] = {}
try:
@@ -254,8 +254,6 @@ class CameraData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the Netatmo API to update the data."""
- import pyatmo
-
self.camera_data = pyatmo.CameraData(self.auth, size=100)
@Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES)
diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py
index 591cd790ecf..1a40d3952e9 100644
--- a/homeassistant/components/netatmo/binary_sensor.py
+++ b/homeassistant/components/netatmo/binary_sensor.py
@@ -1,6 +1,7 @@
"""Support for the Netatmo binary sensors."""
import logging
+from pyatmo import NoDevice
import voluptuous as vol
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
@@ -58,15 +59,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
module_name = None
- import pyatmo
-
auth = hass.data[DATA_NETATMO_AUTH]
try:
data = CameraData(hass, auth, home)
if not data.get_camera_names():
return None
- except pyatmo.NoDevice:
+ except NoDevice:
return None
welcome_sensors = config.get(CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES)
diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py
index 60428961cb9..ecc38add3b4 100644
--- a/homeassistant/components/netatmo/camera.py
+++ b/homeassistant/components/netatmo/camera.py
@@ -1,6 +1,7 @@
"""Support for the Netatmo cameras."""
import logging
+from pyatmo import NoDevice
import requests
import voluptuous as vol
@@ -38,7 +39,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
home = config.get(CONF_HOME)
verify_ssl = config.get(CONF_VERIFY_SSL, True)
quality = config.get(CONF_QUALITY, DEFAULT_QUALITY)
- import pyatmo
auth = hass.data[DATA_NETATMO_AUTH]
@@ -60,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
]
)
data.get_persons()
- except pyatmo.NoDevice:
+ except NoDevice:
return None
diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py
index 1465058652d..8ba13a03889 100644
--- a/homeassistant/components/netatmo/climate.py
+++ b/homeassistant/components/netatmo/climate.py
@@ -3,6 +3,7 @@ from datetime import timedelta
import logging
from typing import Optional, List
+import pyatmo
import requests
import voluptuous as vol
@@ -103,8 +104,6 @@ NA_VALVE = "NRV"
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the NetAtmo Thermostat."""
- import pyatmo
-
homes_conf = config.get(CONF_HOMES)
auth = hass.data[DATA_NETATMO_AUTH]
@@ -365,8 +364,6 @@ class HomeData:
def setup(self):
"""Retrieve HomeData by NetAtmo API."""
- import pyatmo
-
try:
self.homedata = pyatmo.HomeData(self.auth)
self.home_id = self.homedata.gethomeId(self.home)
@@ -408,8 +405,6 @@ class ThermostatData:
def setup(self):
"""Retrieve HomeData and HomeStatus by NetAtmo API."""
- import pyatmo
-
try:
self.homedata = pyatmo.HomeData(self.auth)
self.homestatus = pyatmo.HomeStatus(self.auth, home_id=self.home_id)
@@ -423,8 +418,6 @@ class ThermostatData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the NetAtmo API to update the data."""
- import pyatmo
-
try:
self.homestatus = pyatmo.HomeStatus(self.auth, home_id=self.home_id)
except TypeError:
diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json
index 83091368aff..efb2840216b 100644
--- a/homeassistant/components/netatmo/manifest.json
+++ b/homeassistant/components/netatmo/manifest.json
@@ -3,7 +3,7 @@
"name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": [
- "pyatmo==2.2.1"
+ "pyatmo==2.3.2"
],
"dependencies": [
"webhook"
diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py
index 9e68c078cdc..70b6297388c 100644
--- a/homeassistant/components/netatmo/sensor.py
+++ b/homeassistant/components/netatmo/sensor.py
@@ -4,6 +4,7 @@ import threading
from datetime import timedelta
from time import time
+import pyatmo
import requests
import voluptuous as vol
@@ -79,6 +80,7 @@ SENSOR_TYPES = {
"gustangle": ["Gust Angle", "", "mdi:compass", None],
"gustangle_value": ["Gust Angle Value", "º", "mdi:compass", None],
"guststrength": ["Gust Strength", "km/h", "mdi:weather-windy", None],
+ "reachable": ["Reachability", "", "mdi:signal", None],
"rf_status": ["Radio", "", "mdi:signal", None],
"rf_status_lvl": ["Radio_lvl", "", "mdi:signal", None],
"wifi_status": ["Wifi", "", "mdi:wifi", None],
@@ -174,8 +176,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if _dev:
add_entities(_dev, True)
- import pyatmo
-
for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]:
try:
data = NetatmoData(auth, data_class, config.get(CONF_STATION))
@@ -376,6 +376,8 @@ class NetatmoSensor(Entity):
self._state = "N (%d\xb0)" % data["GustAngle"]
elif self.type == "guststrength":
self._state = data["GustStrength"]
+ elif self.type == "reachable":
+ self._state = data["reachable"]
elif self.type == "rf_status_lvl":
self._state = data["rf_status"]
elif self.type == "rf_status":
@@ -512,8 +514,6 @@ class NetatmoPublicData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Request an update from the Netatmo API."""
- import pyatmo
-
data = pyatmo.PublicData(
self.auth,
LAT_NE=self.lat_ne,
@@ -559,12 +559,10 @@ class NetatmoData:
if time() < self._next_update or not self._update_in_progress.acquire(False):
return
try:
- from pyatmo import NoDevice
-
try:
self.station_data = self.data_class(self.auth)
_LOGGER.debug("%s detected!", str(self.data_class.__name__))
- except NoDevice:
+ except pyatmo.NoDevice:
_LOGGER.warning(
"No Weather or HomeCoach devices found for %s", str(self.station)
)
diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py
index b52e446ba5d..2e20f6423a5 100644
--- a/homeassistant/components/netgear/device_tracker.py
+++ b/homeassistant/components/netgear/device_tracker.py
@@ -1,6 +1,7 @@
"""Support for Netgear routers."""
import logging
+from pynetgear import Netgear
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -71,14 +72,13 @@ class NetgearDeviceScanner(DeviceScanner):
accesspoints,
):
"""Initialize the scanner."""
- import pynetgear
self.tracked_devices = devices
self.excluded_devices = excluded_devices
self.tracked_accesspoints = accesspoints
self.last_results = []
- self._api = pynetgear.Netgear(password, host, username, port, ssl)
+ self._api = Netgear(password, host, username, port, ssl)
_LOGGER.info("Logging in")
diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py
index 2514b37657f..4758a13c391 100644
--- a/homeassistant/components/netgear_lte/__init__.py
+++ b/homeassistant/components/netgear_lte/__init__.py
@@ -5,6 +5,7 @@ import logging
import aiohttp
import attr
+import eternalegypt
import voluptuous as vol
from homeassistant.const import (
@@ -139,7 +140,6 @@ class ModemData:
async def async_update(self):
"""Call the API to update the data."""
- import eternalegypt
try:
self.data = await self.modem.information()
@@ -264,7 +264,6 @@ async def async_setup(hass, config):
async def _setup_lte(hass, lte_config):
"""Set up a Netgear LTE modem."""
- import eternalegypt
host = lte_config[CONF_HOST]
password = lte_config[CONF_PASSWORD]
@@ -322,7 +321,6 @@ async def _login(hass, modem_data, password):
async def _retry_login(hass, modem_data, password):
"""Sleep and retry setup."""
- import eternalegypt
_LOGGER.warning("Could not connect to %s. Will keep trying", modem_data.host)
diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py
index 4f13662519d..9700ee3c715 100644
--- a/homeassistant/components/netgear_lte/notify.py
+++ b/homeassistant/components/netgear_lte/notify.py
@@ -2,6 +2,7 @@
import logging
import attr
+import eternalegypt
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService, DOMAIN
@@ -27,7 +28,6 @@ class NetgearNotifyService(BaseNotificationService):
async def async_send_message(self, message="", **kwargs):
"""Send a message to a user."""
- import eternalegypt
modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config)
if not modem_data:
diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py
index eac716573db..894bfae6180 100644
--- a/homeassistant/components/neurio_energy/sensor.py
+++ b/homeassistant/components/neurio_energy/sensor.py
@@ -1,15 +1,16 @@
"""Support for monitoring a Neurio energy sensor."""
-import logging
from datetime import timedelta
+import logging
+import neurio
import requests.exceptions
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_API_KEY, POWER_WATT, ENERGY_KILO_WATT_HOUR
+from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -69,8 +70,6 @@ class NeurioData:
def __init__(self, api_key, api_secret, sensor_id):
"""Initialize the data."""
- import neurio
-
self.api_key = api_key
self.api_secret = api_secret
self.sensor_id = sensor_id
diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py
index 4cb84956002..265e51d6e67 100644
--- a/homeassistant/components/niko_home_control/light.py
+++ b/homeassistant/components/niko_home_control/light.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+import nikohomecontrol
import voluptuous as vol
# Import the device class from the component that you want to support
@@ -20,8 +21,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string})
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Niko Home Control light platform."""
- import nikohomecontrol
-
host = config[CONF_HOST]
try:
diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py
index 8d3d61befd5..8e851592de3 100644
--- a/homeassistant/components/nilu/air_quality.py
+++ b/homeassistant/components/nilu/air_quality.py
@@ -2,6 +2,22 @@
from datetime import timedelta
import logging
+from niluclient import (
+ CO,
+ CO2,
+ NO,
+ NO2,
+ NOX,
+ OZONE,
+ PM1,
+ PM10,
+ PM25,
+ POLLUTION_INDEX,
+ SO2,
+ create_location_client,
+ create_station_client,
+ lookup_stations_in_area,
+)
import voluptuous as vol
from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity
@@ -95,8 +111,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the NILU air quality sensor."""
- import niluclient as nilu
-
name = config.get(CONF_NAME)
area = config.get(CONF_AREA)
stations = config.get(CONF_STATION)
@@ -105,15 +119,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sensors = []
if area:
- stations = nilu.lookup_stations_in_area(area)
+ stations = lookup_stations_in_area(area)
elif not area and not stations:
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
- location_client = nilu.create_location_client(latitude, longitude)
+ location_client = create_location_client(latitude, longitude)
stations = location_client.station_names
for station in stations:
- client = NiluData(nilu.create_station_client(station))
+ client = NiluData(create_station_client(station))
client.update()
if client.data.sensors:
sensors.append(NiluSensor(client, name, show_on_map))
@@ -178,71 +192,51 @@ class NiluSensor(AirQualityEntity):
@property
def carbon_monoxide(self) -> str:
"""Return the CO (carbon monoxide) level."""
- from niluclient import CO
-
return self.get_component_state(CO)
@property
def carbon_dioxide(self) -> str:
"""Return the CO2 (carbon dioxide) level."""
- from niluclient import CO2
-
return self.get_component_state(CO2)
@property
def nitrogen_oxide(self) -> str:
"""Return the N2O (nitrogen oxide) level."""
- from niluclient import NOX
-
return self.get_component_state(NOX)
@property
def nitrogen_monoxide(self) -> str:
"""Return the NO (nitrogen monoxide) level."""
- from niluclient import NO
-
return self.get_component_state(NO)
@property
def nitrogen_dioxide(self) -> str:
"""Return the NO2 (nitrogen dioxide) level."""
- from niluclient import NO2
-
return self.get_component_state(NO2)
@property
def ozone(self) -> str:
"""Return the O3 (ozone) level."""
- from niluclient import OZONE
-
return self.get_component_state(OZONE)
@property
def particulate_matter_2_5(self) -> str:
"""Return the particulate matter 2.5 level."""
- from niluclient import PM25
-
return self.get_component_state(PM25)
@property
def particulate_matter_10(self) -> str:
"""Return the particulate matter 10 level."""
- from niluclient import PM10
-
return self.get_component_state(PM10)
@property
def particulate_matter_0_1(self) -> str:
"""Return the particulate matter 0.1 level."""
- from niluclient import PM1
-
return self.get_component_state(PM1)
@property
def sulphur_dioxide(self) -> str:
"""Return the SO2 (sulphur dioxide) level."""
- from niluclient import SO2
-
return self.get_component_state(SO2)
def get_component_state(self, component_name: str) -> str:
@@ -254,14 +248,12 @@ class NiluSensor(AirQualityEntity):
def update(self) -> None:
"""Update the sensor."""
- import niluclient as nilu
-
self._api.update()
sensors = self._api.data.sensors.values()
if sensors:
max_index = max([s.pollution_index for s in sensors])
self._max_aqi = max_index
- self._attrs[ATTR_POLLUTION_INDEX] = nilu.POLLUTION_INDEX[self._max_aqi]
+ self._attrs[ATTR_POLLUTION_INDEX] = POLLUTION_INDEX[self._max_aqi]
self._attrs[ATTR_AREA] = self._api.data.area
diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py
index 38b7018af6c..0c72f4f43ea 100644
--- a/homeassistant/components/nissan_leaf/__init__.py
+++ b/homeassistant/components/nissan_leaf/__init__.py
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
import asyncio
import logging
import sys
-
+from pycarwings2 import CarwingsError, Session
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@@ -95,7 +95,6 @@ START_CHARGE_LEAF_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string})
def setup(hass, config):
"""Set up the Nissan Leaf component."""
- import pycarwings2
async def async_handle_update(service):
"""Handle service to update leaf data from Nissan servers."""
@@ -148,7 +147,7 @@ def setup(hass, config):
try:
# This might need to be made async (somehow) causes
# homeassistant to be slow to start
- sess = pycarwings2.Session(username, password, region)
+ sess = Session(username, password, region)
leaf = sess.get_leaf()
except KeyError:
_LOGGER.error(
@@ -156,7 +155,7 @@ def setup(hass, config):
" do you actually have a Leaf connected to your account?"
)
return False
- except pycarwings2.CarwingsError:
+ except CarwingsError:
_LOGGER.error(
"An unknown error occurred while connecting to Nissan: %s",
sys.exc_info()[0],
@@ -274,7 +273,6 @@ class LeafDataStore:
async def async_refresh_data(self, now):
"""Refresh the leaf data and update the datastore."""
- from pycarwings2 import CarwingsError
if self.request_in_progress:
_LOGGER.debug("Refresh currently in progress for %s", self.leaf.nickname)
@@ -339,7 +337,6 @@ class LeafDataStore:
async def async_get_battery(self):
"""Request battery update from Nissan servers."""
- from pycarwings2 import CarwingsError
try:
# Request battery update from the car
@@ -389,7 +386,6 @@ class LeafDataStore:
async def async_get_climate(self):
"""Request climate data from Nissan servers."""
- from pycarwings2 import CarwingsError
try:
return await self.hass.async_add_executor_job(
diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py
index 9b30ad5aaa8..8e6c13260e5 100644
--- a/homeassistant/components/norway_air/air_quality.py
+++ b/homeassistant/components/norway_air/air_quality.py
@@ -1,14 +1,14 @@
"""Sensor for checking the air quality forecast around Norway."""
+from datetime import timedelta
import logging
-from datetime import timedelta
+import metno
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -71,8 +71,6 @@ class AirSensor(AirQualityEntity):
def __init__(self, name, coordinates, forecast, session):
"""Initialize the sensor."""
- import metno
-
self._name = name
self._api = metno.AirQualityData(coordinates, forecast, session)
diff --git a/homeassistant/components/notion/.translations/nn.json b/homeassistant/components/notion/.translations/nn.json
new file mode 100644
index 00000000000..6d373424c28
--- /dev/null
+++ b/homeassistant/components/notion/.translations/nn.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Notion"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json
index 7345cf46295..6c1d5f5d8d7 100644
--- a/homeassistant/components/notion/.translations/ru.json
+++ b/homeassistant/components/notion/.translations/ru.json
@@ -2,14 +2,14 @@
"config": {
"error": {
"identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
- "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c",
- "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e"
+ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
+ "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b"
},
"title": "Notion"
}
diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py
index f83611d3e40..88e10270d18 100644
--- a/homeassistant/components/nuheat/__init__.py
+++ b/homeassistant/components/nuheat/__init__.py
@@ -1,11 +1,11 @@
"""Support for NuHeat thermostats."""
import logging
+import nuheat
import voluptuous as vol
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers import discovery
+from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.helpers import config_validation as cv, discovery
_LOGGER = logging.getLogger(__name__)
@@ -29,8 +29,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the NuHeat thermostat component."""
- import nuheat
-
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py
index 19780a35a20..5a4e4e233d1 100644
--- a/homeassistant/components/nuheat/climate.py
+++ b/homeassistant/components/nuheat/climate.py
@@ -9,9 +9,9 @@ from homeassistant.components.climate.const import (
HVAC_MODE_AUTO,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
+ PRESET_NONE,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
- PRESET_NONE,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py
index 0c16f3769d5..4bf6b395d5f 100644
--- a/homeassistant/components/oasa_telematics/sensor.py
+++ b/homeassistant/components/oasa_telematics/sensor.py
@@ -1,13 +1,14 @@
"""Support for OASA Telematics from telematics.oasa.gr."""
-import logging
from datetime import timedelta
+import logging
from operator import itemgetter
+import oasatelematics
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import dt as dt_util
@@ -128,8 +129,6 @@ class OASATelematicsData:
def __init__(self, stop_id, route_id):
"""Initialize the data object."""
- import oasatelematics
-
self.stop_id = stop_id
self.route_id = route_id
self.info = self.empty_result()
diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py
index 10b622d16c9..a9606e25bad 100644
--- a/homeassistant/components/ohmconnect/sensor.py
+++ b/homeassistant/components/ohmconnect/sensor.py
@@ -1,15 +1,16 @@
"""Support for OhmConnect."""
-import logging
from datetime import timedelta
+import logging
+import defusedxml.ElementTree as ET
import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -64,8 +65,6 @@ class OhmconnectSensor(Entity):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from OhmConnect."""
- import defusedxml.ElementTree as ET
-
try:
url = ("https://login.ohmconnect.com" "/verify-ohm-hour/{}").format(
self._ohmid
diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json
index 5bb116dece8..ef28c14a8b0 100644
--- a/homeassistant/components/onkyo/manifest.json
+++ b/homeassistant/components/onkyo/manifest.json
@@ -3,7 +3,7 @@
"name": "Onkyo",
"documentation": "https://www.home-assistant.io/integrations/onkyo",
"requirements": [
- "onkyo-eiscp==1.2.4"
+ "onkyo-eiscp==1.2.7"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py
index af92f6c5f05..86f0f418c3f 100644
--- a/homeassistant/components/onkyo/media_player.py
+++ b/homeassistant/components/onkyo/media_player.py
@@ -3,6 +3,8 @@ import logging
from typing import List
import voluptuous as vol
+import eiscp
+from eiscp import eISCP
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
from homeassistant.components.media_player.const import (
@@ -29,9 +31,11 @@ _LOGGER = logging.getLogger(__name__)
CONF_SOURCES = "sources"
CONF_MAX_VOLUME = "max_volume"
+CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume"
DEFAULT_NAME = "Onkyo Receiver"
-SUPPORTED_MAX_VOLUME = 80
+SUPPORTED_MAX_VOLUME = 100
+DEFAULT_RECEIVER_MAX_VOLUME = 80
SUPPORT_ONKYO = (
SUPPORT_VOLUME_SET
@@ -75,8 +79,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MAX_VOLUME, default=SUPPORTED_MAX_VOLUME): vol.All(
- vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME)
+ vol.Coerce(int), vol.Range(min=1, max=100)
),
+ vol.Optional(
+ CONF_RECEIVER_MAX_VOLUME, default=DEFAULT_RECEIVER_MAX_VOLUME
+ ): vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string},
}
)
@@ -133,9 +140,6 @@ def determine_zones(receiver):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Onkyo platform."""
- import eiscp
- from eiscp import eISCP
-
host = config.get(CONF_HOST)
hosts = []
@@ -164,6 +168,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
config.get(CONF_SOURCES),
name=config.get(CONF_NAME),
max_volume=config.get(CONF_MAX_VOLUME),
+ receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME),
)
)
KNOWN_HOSTS.append(host)
@@ -179,6 +184,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
receiver,
config.get(CONF_SOURCES),
name=f"{config[CONF_NAME]} Zone 2",
+ max_volume=config.get(CONF_MAX_VOLUME),
+ receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME),
)
)
# Add Zone3 if available
@@ -190,6 +197,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
receiver,
config.get(CONF_SOURCES),
name=f"{config[CONF_NAME]} Zone 3",
+ max_volume=config.get(CONF_MAX_VOLUME),
+ receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME),
)
)
except OSError:
@@ -205,7 +214,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class OnkyoDevice(MediaPlayerDevice):
"""Representation of an Onkyo device."""
- def __init__(self, receiver, sources, name=None, max_volume=SUPPORTED_MAX_VOLUME):
+ def __init__(
+ self,
+ receiver,
+ sources,
+ name=None,
+ max_volume=SUPPORTED_MAX_VOLUME,
+ receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME,
+ ):
"""Initialize the Onkyo Receiver."""
self._receiver = receiver
self._muted = False
@@ -215,6 +231,7 @@ class OnkyoDevice(MediaPlayerDevice):
name or f"{receiver.info['model_name']}_{receiver.info['identifier']}"
)
self._max_volume = max_volume
+ self._receiver_max_volume = receiver_max_volume
self._current_source = None
self._source_list = list(sources.values())
self._source_mapping = sources
@@ -264,14 +281,17 @@ class OnkyoDevice(MediaPlayerDevice):
if source in self._source_mapping:
self._current_source = self._source_mapping[source]
break
- self._current_source = "_".join([i for i in current_source_tuples[1]])
+ self._current_source = "_".join(current_source_tuples[1])
if preset_raw and self._current_source.lower() == "radio":
self._attributes[ATTR_PRESET] = preset_raw[1]
elif ATTR_PRESET in self._attributes:
del self._attributes[ATTR_PRESET]
self._muted = bool(mute_raw[1] == "on")
- self._volume = volume_raw[1] / self._max_volume
+ # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100)
+ self._volume = (
+ volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100)
+ )
if not hdmi_out_raw:
return
@@ -325,10 +345,15 @@ class OnkyoDevice(MediaPlayerDevice):
"""
Set volume level, input is range 0..1.
- Onkyo ranges from 1-80 however 80 is usually far too loud
- so allow the user to specify the upper range with CONF_MAX_VOLUME
+ However full volume on the amp is usually far too loud so allow the user to specify the upper range
+ with CONF_MAX_VOLUME. we change as per max_volume set by user. This means that if max volume is 80 then full
+ volume in HA will give 80% volume on the receiver. Then we convert
+ that to the correct scale for the receiver.
"""
- self.command(f"volume {int(volume * self._max_volume)}")
+ # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL
+ self.command(
+ f"volume {int(volume * (self._max_volume / 100) * self._receiver_max_volume)}"
+ )
def volume_up(self):
"""Increase volume by 1 step."""
@@ -369,11 +394,19 @@ class OnkyoDevice(MediaPlayerDevice):
class OnkyoDeviceZone(OnkyoDevice):
"""Representation of an Onkyo device's extra zone."""
- def __init__(self, zone, receiver, sources, name=None):
+ def __init__(
+ self,
+ zone,
+ receiver,
+ sources,
+ name=None,
+ max_volume=SUPPORTED_MAX_VOLUME,
+ receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME,
+ ):
"""Initialize the Zone with the zone identifier."""
self._zone = zone
self._supports_volume = True
- super().__init__(receiver, sources, name)
+ super().__init__(receiver, sources, name, max_volume, receiver_max_volume)
def update(self):
"""Get the latest state from the device."""
@@ -413,14 +446,17 @@ class OnkyoDeviceZone(OnkyoDevice):
if source in self._source_mapping:
self._current_source = self._source_mapping[source]
break
- self._current_source = "_".join([i for i in current_source_tuples[1]])
+ self._current_source = "_".join(current_source_tuples[1])
self._muted = bool(mute_raw[1] == "on")
if preset_raw and self._current_source.lower() == "radio":
self._attributes[ATTR_PRESET] = preset_raw[1]
elif ATTR_PRESET in self._attributes:
del self._attributes[ATTR_PRESET]
if self._supports_volume:
- self._volume = volume_raw[1] / 80.0
+ # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100)
+ self._volume = (
+ volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100)
+ )
@property
def supported_features(self):
@@ -434,8 +470,18 @@ class OnkyoDeviceZone(OnkyoDevice):
self.command(f"zone{self._zone}.power=standby")
def set_volume_level(self, volume):
- """Set volume level, input is range 0..1. Onkyo ranges from 1-80."""
- self.command(f"zone{self._zone}.volume={int(volume * 80)}")
+ """
+ Set volume level, input is range 0..1.
+
+ However full volume on the amp is usually far too loud so allow the user to specify the upper range
+ with CONF_MAX_VOLUME. we change as per max_volume set by user. This means that if max volume is 80 then full
+ volume in HA will give 80% volume on the receiver. Then we convert
+ that to the correct scale for the receiver.
+ """
+ # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL
+ self.command(
+ f"zone{self._zone}.volume={int(volume * (self._max_volume / 100) * self._receiver_max_volume)}"
+ )
def volume_up(self):
"""Increase volume by 1 step."""
diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py
index 29af1049fae..c73886c13c0 100644
--- a/homeassistant/components/onvif/camera.py
+++ b/homeassistant/components/onvif/camera.py
@@ -8,21 +8,29 @@ import asyncio
import datetime as dt
import logging
import os
-import voluptuous as vol
+from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedError
+from haffmpeg.camera import CameraMjpeg
+from haffmpeg.tools import IMAGE_JPEG, ImageFrame
+import onvif
+from onvif import ONVIFCamera, exceptions
+import voluptuous as vol
+from zeep.exceptions import Fault
+
+from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
+from homeassistant.components.camera.const import DOMAIN
+from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG
from homeassistant.const import (
- CONF_NAME,
+ ATTR_ENTITY_ID,
CONF_HOST,
- CONF_USERNAME,
+ CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
- ATTR_ENTITY_ID,
+ CONF_USERNAME,
)
-from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM
-from homeassistant.components.camera.const import DOMAIN
-from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS
-import homeassistant.helpers.config_validation as cv
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.service import async_extract_entity_ids
import homeassistant.util.dt as dt_util
@@ -122,9 +130,6 @@ class ONVIFHassCamera(Camera):
_LOGGER.debug("Importing dependencies")
- import onvif
- from onvif import ONVIFCamera
-
_LOGGER.debug("Setting up the ONVIF camera component")
self._username = config.get(CONF_USERNAME)
@@ -156,10 +161,6 @@ class ONVIFHassCamera(Camera):
Initializes the camera by obtaining the input uri and connecting to
the camera. Also retrieves the ONVIF profiles.
"""
- from aiohttp.client_exceptions import ClientConnectionError
- from homeassistant.exceptions import PlatformNotReady
- from zeep.exceptions import Fault
-
try:
_LOGGER.debug("Updating service addresses")
await self._camera.update_xaddrs()
@@ -169,7 +170,7 @@ class ONVIFHassCamera(Camera):
self.setup_ptz()
except ClientConnectionError as err:
_LOGGER.warning(
- "Couldn't connect to camera '%s', but will " "retry later. Error: %s",
+ "Couldn't connect to camera '%s', but will retry later. Error: %s",
self._name,
err,
)
@@ -184,8 +185,6 @@ class ONVIFHassCamera(Camera):
async def async_check_date_and_time(self):
"""Warns if camera and system date not synced."""
- from aiohttp.client_exceptions import ServerDisconnectedError
-
_LOGGER.debug("Setting up the ONVIF device management service")
devicemgmt = self._camera.create_devicemgmt_service()
@@ -228,8 +227,6 @@ class ONVIFHassCamera(Camera):
async def async_obtain_input_uri(self):
"""Set the input uri for the camera."""
- from onvif import exceptions
-
_LOGGER.debug(
"Connecting with ONVIF Camera: %s on port %s", self._host, self._port
)
@@ -289,8 +286,6 @@ class ONVIFHassCamera(Camera):
async def async_perform_ptz(self, pan, tilt, zoom):
"""Perform a PTZ action on the camera."""
- from onvif import exceptions
-
if self._ptz_service is None:
_LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name)
return
@@ -332,7 +327,6 @@ class ONVIFHassCamera(Camera):
async def async_camera_image(self):
"""Return a still image response from the camera."""
- from haffmpeg.tools import ImageFrame, IMAGE_JPEG
_LOGGER.debug("Retrieving image from camera '%s'", self._name)
@@ -347,8 +341,6 @@ class ONVIFHassCamera(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
- from haffmpeg.camera import CameraMjpeg
-
_LOGGER.debug("Handling mjpeg stream from camera '%s'", self._name)
ffmpeg_manager = self.hass.data[DATA_FFMPEG]
diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py
index 3c72af4f368..4a1b830a324 100644
--- a/homeassistant/components/opencv/image_processing.py
+++ b/homeassistant/components/opencv/image_processing.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+import numpy
import requests
import voluptuous as vol
@@ -15,6 +16,15 @@ from homeassistant.components.image_processing import (
from homeassistant.core import split_entity_id
import homeassistant.helpers.config_validation as cv
+try:
+ # Verify that the OpenCV python package is pre-installed
+ import cv2
+
+ CV2_IMPORTED = True
+except ImportError:
+ CV2_IMPORTED = False
+
+
_LOGGER = logging.getLogger(__name__)
ATTR_MATCHES = "matches"
@@ -86,11 +96,7 @@ def _get_default_classifier(dest_path):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the OpenCV image processing platform."""
- try:
- # Verify that the OpenCV python package is pre-installed
- # pylint: disable=unused-import,unused-variable
- import cv2 # noqa
- except ImportError:
+ if not CV2_IMPORTED:
_LOGGER.error(
"No OpenCV library found! Install or compile for your system "
"following instructions here: http://opencv.org/releases.html"
@@ -154,9 +160,6 @@ class OpenCVImageProcessor(ImageProcessingEntity):
def process_image(self, image):
"""Process the image."""
- import cv2 # pylint: disable=import-error
- import numpy
-
cv_image = cv2.imdecode(numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED)
for name, classifier in self._classifiers.items():
diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json
index 40421674a4b..bd82da000cf 100644
--- a/homeassistant/components/opencv/manifest.json
+++ b/homeassistant/components/opencv/manifest.json
@@ -3,7 +3,7 @@
"name": "Opencv",
"documentation": "https://www.home-assistant.io/integrations/opencv",
"requirements": [
- "numpy==1.17.1",
+ "numpy==1.17.3",
"opencv-python-headless==4.1.1.26"
],
"dependencies": [],
diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py
index d29dec224bd..0ac655cd448 100644
--- a/homeassistant/components/openevse/sensor.py
+++ b/homeassistant/components/openevse/sensor.py
@@ -1,17 +1,18 @@
"""Support for monitoring an OpenEVSE Charger."""
import logging
+import openevsewifi
from requests import RequestException
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- TEMP_CELSIUS,
CONF_HOST,
- ENERGY_KILO_WATT_HOUR,
CONF_MONITORED_VARIABLES,
+ ENERGY_KILO_WATT_HOUR,
+ TEMP_CELSIUS,
)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -38,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the OpenEVSE sensor."""
- import openevsewifi
-
host = config.get(CONF_HOST)
monitored_variables = config.get(CONF_MONITORED_VARIABLES)
diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py
index fc228ee26fb..0729943a770 100644
--- a/homeassistant/components/openhardwaremonitor/sensor.py
+++ b/homeassistant/components/openhardwaremonitor/sensor.py
@@ -7,6 +7,7 @@ import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
@@ -38,6 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Open Hardware Monitor platform."""
data = OpenHardwareMonitorData(config, hass)
+ if data.data is None:
+ raise PlatformNotReady
add_entities(data.devices, True)
@@ -130,7 +133,7 @@ class OpenHardwareMonitorData:
response = requests.get(data_url, timeout=30)
self.data = response.json()
except requests.exceptions.ConnectionError:
- _LOGGER.error("ConnectionError: Is OpenHardwareMonitor running?")
+ _LOGGER.debug("ConnectionError: Is OpenHardwareMonitor running?")
def initialize(self, now):
"""Parse of the sensors and adding of devices."""
diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json
index 0224d663a83..07567149063 100644
--- a/homeassistant/components/opentherm_gw/.translations/ca.json
+++ b/homeassistant/components/opentherm_gw/.translations/ca.json
@@ -19,5 +19,16 @@
}
},
"title": "Passarel\u00b7la d'OpenTherm"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Temperatura de la planta",
+ "precision": "Precisi\u00f3"
+ },
+ "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d\u2019OpenTherm"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json
index b8abb48af4e..152e38a5bba 100644
--- a/homeassistant/components/opentherm_gw/.translations/da.json
+++ b/homeassistant/components/opentherm_gw/.translations/da.json
@@ -16,5 +16,16 @@
}
},
"title": "OpenTherm Gateway"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Gulvtemperatur",
+ "precision": "Pr\u00e6cision"
+ },
+ "description": "Indstillinger for OpenTherm Gateway"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json
index 274dd46488b..3b18aa71b6c 100644
--- a/homeassistant/components/opentherm_gw/.translations/de.json
+++ b/homeassistant/components/opentherm_gw/.translations/de.json
@@ -10,8 +10,22 @@
"init": {
"data": {
"device": "Pfad oder URL",
+ "floor_temperature": "Boden-Temperatur",
"id": "ID",
- "name": "Name"
+ "name": "Name",
+ "precision": "Genauigkeit der Temperatur"
+ },
+ "title": "OpenTherm Gateway"
+ }
+ },
+ "title": "OpenTherm Gateway"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Boden-Temperatur",
+ "precision": "Genauigkeit"
}
}
}
diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json
index 65d7d9e92bb..a7e143505a8 100644
--- a/homeassistant/components/opentherm_gw/.translations/en.json
+++ b/homeassistant/components/opentherm_gw/.translations/en.json
@@ -19,5 +19,16 @@
}
},
"title": "OpenTherm Gateway"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Floor Temperature",
+ "precision": "Precision"
+ },
+ "description": "Options for the OpenTherm Gateway"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json
index 8ad9d89b07a..bb8a8b20f36 100644
--- a/homeassistant/components/opentherm_gw/.translations/es.json
+++ b/homeassistant/components/opentherm_gw/.translations/es.json
@@ -19,5 +19,16 @@
}
},
"title": "Gateway OpenTherm"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Temperatura del suelo",
+ "precision": "Precisi\u00f3n"
+ },
+ "description": "Opciones para OpenTherm Gateway"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json
index 82b9a7aee88..edde63d62b4 100644
--- a/homeassistant/components/opentherm_gw/.translations/fr.json
+++ b/homeassistant/components/opentherm_gw/.translations/fr.json
@@ -10,6 +10,7 @@
"init": {
"data": {
"device": "Chemin ou URL",
+ "floor_temperature": "Temp\u00e9rature du sol",
"id": "ID",
"name": "Nom",
"precision": "Pr\u00e9cision de la temp\u00e9rature climatique"
@@ -18,5 +19,16 @@
}
},
"title": "Passerelle OpenTherm"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Temp\u00e9rature du sol",
+ "precision": "Pr\u00e9cision"
+ },
+ "description": "Options pour la passerelle OpenTherm"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/ko.json b/homeassistant/components/opentherm_gw/.translations/ko.json
new file mode 100644
index 00000000000..f370427625d
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/.translations/ko.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "OpenTherm Gateway \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "id_exists": "OpenTherm Gateway id \uac00 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4",
+ "serial_error": "\uae30\uae30 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
+ "timeout": "\uc5f0\uacb0 \uc2dc\ub3c4 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "device": "\uacbd\ub85c \ub610\ub294 URL",
+ "floor_temperature": "\uc2e4\ub0b4\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc",
+ "id": "ID",
+ "name": "\uc774\ub984",
+ "precision": "\uc2e4\ub0b4\uc628\ub3c4 \uc815\ubc00\ub3c4"
+ },
+ "title": "OpenTherm Gateway"
+ }
+ },
+ "title": "OpenTherm Gateway"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc",
+ "precision": "\uc815\ubc00\ub3c4"
+ },
+ "description": "OpenTherm Gateway \uc635\uc158"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json
index ec1f719a6cc..505815dcb4d 100644
--- a/homeassistant/components/opentherm_gw/.translations/lb.json
+++ b/homeassistant/components/opentherm_gw/.translations/lb.json
@@ -19,5 +19,16 @@
}
},
"title": "OpenTherm Gateway"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Buedem Temperatur",
+ "precision": "Pr\u00e4zisioun"
+ },
+ "description": "Optioune fir OpenTherm Gateway"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json
index 4fec1baba7b..dbed3326b4a 100644
--- a/homeassistant/components/opentherm_gw/.translations/nl.json
+++ b/homeassistant/components/opentherm_gw/.translations/nl.json
@@ -1,14 +1,34 @@
{
"config": {
+ "error": {
+ "already_configured": "Gateway al geconfigureerd",
+ "id_exists": "Gateway id bestaat al",
+ "serial_error": "Fout bij het verbinden met het apparaat",
+ "timeout": "Er is een time-out opgetreden voor de verbindingspoging"
+ },
"step": {
"init": {
"data": {
"device": "Pad of URL",
- "id": "ID"
+ "floor_temperature": "Vloertemperatuur",
+ "id": "ID",
+ "name": "Naam",
+ "precision": "Klimaattemperatuur precisie"
},
"title": "OpenTherm Gateway"
}
},
"title": "OpenTherm Gateway"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Vloertemperatuur",
+ "precision": "Precisie"
+ },
+ "description": "Opties voor de OpenTherm Gateway"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json
index 6104aa7de72..9eb4444cbf1 100644
--- a/homeassistant/components/opentherm_gw/.translations/no.json
+++ b/homeassistant/components/opentherm_gw/.translations/no.json
@@ -19,5 +19,16 @@
}
},
"title": "OpenTherm Gateway"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Etasje Temperatur",
+ "precision": "Presisjon"
+ },
+ "description": "Alternativer for OpenTherm Gateway"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json
index 7e4a0eed013..e4403420b11 100644
--- a/homeassistant/components/opentherm_gw/.translations/pl.json
+++ b/homeassistant/components/opentherm_gw/.translations/pl.json
@@ -1,11 +1,33 @@
{
"config": {
+ "error": {
+ "already_configured": "Bramka jest ju\u017c skonfigurowana",
+ "id_exists": "Identyfikator bramki ju\u017c istnieje",
+ "serial_error": "B\u0142\u0105d po\u0142\u0105czenia z urz\u0105dzeniem",
+ "timeout": "Up\u0142yn\u0105\u0142 limit czasu pr\u00f3by po\u0142\u0105czenia"
+ },
"step": {
"init": {
"data": {
"device": "\u015acie\u017cka lub adres URL",
- "name": "Nazwa"
- }
+ "floor_temperature": "Temperatura pod\u0142ogi",
+ "id": "Identyfikator",
+ "name": "Nazwa",
+ "precision": "Precyzja temperatury"
+ },
+ "title": "Bramka OpenTherm"
+ }
+ },
+ "title": "Bramka OpenTherm"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Temperatura pod\u0142ogi",
+ "precision": "Precyzja"
+ },
+ "description": "Opcje dla bramki OpenTherm"
}
}
}
diff --git a/homeassistant/components/opentherm_gw/.translations/pt.json b/homeassistant/components/opentherm_gw/.translations/pt.json
new file mode 100644
index 00000000000..960e3a9cf5c
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/.translations/pt.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "init": {
+ "data": {
+ "id": "",
+ "name": "Nome"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json
index 718322ec171..0719857a7d3 100644
--- a/homeassistant/components/opentherm_gw/.translations/ru.json
+++ b/homeassistant/components/opentherm_gw/.translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"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_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.",
"serial_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.",
"timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f."
@@ -19,5 +19,16 @@
}
},
"title": "OpenTherm"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430",
+ "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c"
+ },
+ "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Opentherm"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json
index 5de551d5d0c..426459237aa 100644
--- a/homeassistant/components/opentherm_gw/.translations/sl.json
+++ b/homeassistant/components/opentherm_gw/.translations/sl.json
@@ -19,5 +19,16 @@
}
},
"title": "OpenTherm Prehod"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Temperatura nadstropja",
+ "precision": "Natan\u010dnost"
+ },
+ "description": "Mo\u017enosti za prehod OpenTherm"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json
index 648f156e864..0d2842ce767 100644
--- a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json
+++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json
@@ -19,5 +19,16 @@
}
},
"title": "OpenTherm \u9598\u9053\u5668"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6",
+ "precision": "\u6e96\u78ba\u5ea6"
+ },
+ "description": "OpenTherm \u9598\u9053\u5668\u9078\u9805"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py
index a32c375ac65..643f80ae8f9 100644
--- a/homeassistant/components/opentherm_gw/__init__.py
+++ b/homeassistant/components/opentherm_gw/__init__.py
@@ -6,6 +6,7 @@ import pyotgw
import pyotgw.vars as gw_vars
import voluptuous as vol
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR
from homeassistant.components.climate import DOMAIN as COMP_CLIMATE
from homeassistant.components.sensor import DOMAIN as COMP_SENSOR
@@ -16,13 +17,13 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
ATTR_TIME,
CONF_DEVICE,
+ CONF_ID,
CONF_NAME,
EVENT_HOMEASSISTANT_STOP,
PRECISION_HALVES,
PRECISION_TENTHS,
PRECISION_WHOLE,
)
-from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
import homeassistant.helpers.config_validation as cv
@@ -36,6 +37,7 @@ from .const import (
CONF_PRECISION,
DATA_GATEWAYS,
DATA_OPENTHERM_GW,
+ DOMAIN,
SERVICE_RESET_GATEWAY,
SERVICE_SET_CLOCK,
SERVICE_SET_CONTROL_SETPOINT,
@@ -50,8 +52,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "opentherm_gw"
-
CLIMATE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_PRECISION): vol.In(
@@ -75,25 +75,46 @@ CONFIG_SCHEMA = vol.Schema(
)
+async def options_updated(hass, entry):
+ """Handle options update."""
+ gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]]
+ async_dispatcher_send(hass, gateway.options_update_signal, entry)
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up the OpenTherm Gateway component."""
+ if DATA_OPENTHERM_GW not in hass.data:
+ hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}}
+
+ gateway = OpenThermGatewayDevice(hass, config_entry)
+ hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway
+
+ config_entry.add_update_listener(options_updated)
+
+ # Schedule directly on the loop to avoid blocking HA startup.
+ hass.loop.create_task(gateway.connect_and_subscribe())
+
+ for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, comp)
+ )
+
+ register_services(hass)
+ return True
+
+
async def async_setup(hass, config):
"""Set up the OpenTherm Gateway component."""
- conf = config[DOMAIN]
- hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}}
- for gw_id, cfg in conf.items():
- gateway = OpenThermGatewayDevice(hass, gw_id, cfg)
- hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] = gateway
- hass.async_create_task(
- async_load_platform(hass, COMP_CLIMATE, DOMAIN, gw_id, config)
- )
- hass.async_create_task(
- async_load_platform(hass, COMP_BINARY_SENSOR, DOMAIN, gw_id, config)
- )
- hass.async_create_task(
- async_load_platform(hass, COMP_SENSOR, DOMAIN, gw_id, config)
- )
- # Schedule directly on the loop to avoid blocking HA startup.
- hass.loop.create_task(gateway.connect_and_subscribe(cfg[CONF_DEVICE]))
- register_services(hass)
+ if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config:
+ conf = config[DOMAIN]
+ for device_id, device_config in conf.items():
+ device_config[CONF_ID] = device_id
+
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=device_config
+ )
+ )
return True
@@ -326,20 +347,22 @@ def register_services(hass):
class OpenThermGatewayDevice:
"""OpenTherm Gateway device class."""
- def __init__(self, hass, gw_id, config):
+ def __init__(self, hass, config_entry):
"""Initialize the OpenTherm Gateway."""
self.hass = hass
- self.gw_id = gw_id
- self.name = config.get(CONF_NAME, gw_id)
- self.climate_config = config[CONF_CLIMATE]
+ self.device_path = config_entry.data[CONF_DEVICE]
+ self.gw_id = config_entry.data[CONF_ID]
+ self.name = config_entry.data[CONF_NAME]
+ self.climate_config = config_entry.options
self.status = {}
- self.update_signal = f"{DATA_OPENTHERM_GW}_{gw_id}_update"
+ self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update"
+ self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update"
self.gateway = pyotgw.pyotgw()
- async def connect_and_subscribe(self, device_path):
+ async def connect_and_subscribe(self):
"""Connect to serial device and subscribe report handler."""
- await self.gateway.connect(self.hass.loop, device_path)
- _LOGGER.debug("Connected to OpenTherm Gateway at %s", device_path)
+ await self.gateway.connect(self.hass.loop, self.device_path)
+ _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path)
async def cleanup(event):
"""Reset overrides on the gateway."""
diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py
index 614829265e2..36867feda61 100644
--- a/homeassistant/components/opentherm_gw/binary_sensor.py
+++ b/homeassistant/components/opentherm_gw/binary_sensor.py
@@ -2,6 +2,7 @@
import logging
from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice
+from homeassistant.const import CONF_ID
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import async_generate_entity_id
@@ -12,18 +13,21 @@ from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW
_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the OpenTherm Gateway binary sensors."""
- if discovery_info is None:
- return
- gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info]
sensors = []
for var, info in BINARY_SENSOR_INFO.items():
device_class = info[0]
friendly_name_format = info[1]
sensors.append(
- OpenThermBinarySensor(gw_dev, var, device_class, friendly_name_format)
+ OpenThermBinarySensor(
+ hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]],
+ var,
+ device_class,
+ friendly_name_format,
+ )
)
+
async_add_entities(sensors)
diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py
index fab028560bb..44f143d64da 100644
--- a/homeassistant/components/opentherm_gw/climate.py
+++ b/homeassistant/components/opentherm_gw/climate.py
@@ -17,6 +17,7 @@ from homeassistant.components.climate.const import (
)
from homeassistant.const import (
ATTR_TEMPERATURE,
+ CONF_ID,
PRECISION_HALVES,
PRECISION_TENTHS,
PRECISION_WHOLE,
@@ -33,23 +34,28 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the opentherm_gw device."""
- gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info]
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up an OpenTherm Gateway climate entity."""
+ ents = []
+ ents.append(
+ OpenThermClimate(
+ hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]],
+ config_entry.options,
+ )
+ )
- gateway = OpenThermClimate(gw_dev)
- async_add_entities([gateway])
+ async_add_entities(ents)
class OpenThermClimate(ClimateDevice):
"""Representation of a climate device."""
- def __init__(self, gw_dev):
+ def __init__(self, gw_dev, options):
"""Initialize the device."""
self._gateway = gw_dev
self.friendly_name = gw_dev.name
- self.floor_temp = gw_dev.climate_config[CONF_FLOOR_TEMP]
- self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION)
+ self.floor_temp = options[CONF_FLOOR_TEMP]
+ self.temp_precision = options.get(CONF_PRECISION)
self._current_operation = None
self._current_temperature = None
self._hvac_mode = HVAC_MODE_HEAT
@@ -60,12 +66,22 @@ class OpenThermClimate(ClimateDevice):
self._away_state_a = False
self._away_state_b = False
+ @callback
+ def update_options(self, entry):
+ """Update climate entity options."""
+ self.floor_temp = entry.options[CONF_FLOOR_TEMP]
+ self.temp_precision = entry.options.get(CONF_PRECISION)
+ self.async_schedule_update_ha_state()
+
async def async_added_to_hass(self):
"""Connect to the OpenTherm Gateway device."""
- _LOGGER.debug("Added device %s", self.friendly_name)
+ _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name)
async_dispatcher_connect(
self.hass, self._gateway.update_signal, self.receive_report
)
+ async_dispatcher_connect(
+ self.hass, self._gateway.options_update_signal, self.update_options
+ )
@callback
def receive_report(self, status):
diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py
new file mode 100644
index 00000000000..2d7a65bbd84
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/config_flow.py
@@ -0,0 +1,142 @@
+"""OpenTherm Gateway config flow."""
+import asyncio
+from serial import SerialException
+
+import pyotgw
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import (
+ CONF_DEVICE,
+ CONF_ID,
+ CONF_NAME,
+ PRECISION_HALVES,
+ PRECISION_TENTHS,
+ PRECISION_WHOLE,
+)
+from homeassistant.core import callback
+
+import homeassistant.helpers.config_validation as cv
+
+from . import DOMAIN
+from .const import CONF_FLOOR_TEMP, CONF_PRECISION
+
+
+class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """OpenTherm Gateway Config Flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return OpenThermGwOptionsFlow(config_entry)
+
+ async def async_step_init(self, info=None):
+ """Handle config flow initiation."""
+ if info:
+ name = info[CONF_NAME]
+ device = info[CONF_DEVICE]
+ gw_id = cv.slugify(info.get(CONF_ID, name))
+
+ entries = [e.data for e in self.hass.config_entries.async_entries(DOMAIN)]
+
+ if gw_id in [e[CONF_ID] for e in entries]:
+ return self._show_form({"base": "id_exists"})
+
+ if device in [e[CONF_DEVICE] for e in entries]:
+ return self._show_form({"base": "already_configured"})
+
+ async def test_connection():
+ """Try to connect to the OpenTherm Gateway."""
+ otgw = pyotgw.pyotgw()
+ status = await otgw.connect(self.hass.loop, device)
+ await otgw.disconnect()
+ return status.get(pyotgw.OTGW_ABOUT)
+
+ try:
+ res = await asyncio.wait_for(test_connection(), timeout=10)
+ except asyncio.TimeoutError:
+ return self._show_form({"base": "timeout"})
+ except SerialException:
+ return self._show_form({"base": "serial_error"})
+
+ if res:
+ return self._create_entry(gw_id, name, device)
+
+ return self._show_form()
+
+ async def async_step_user(self, info=None):
+ """Handle manual initiation of the config flow."""
+ return await self.async_step_init(info)
+
+ async def async_step_import(self, import_config):
+ """
+ Import an OpenTherm Gateway device as a config entry.
+
+ This flow is triggered by `async_setup` for configured devices.
+ """
+ formatted_config = {
+ CONF_NAME: import_config.get(CONF_NAME, import_config[CONF_ID]),
+ CONF_DEVICE: import_config[CONF_DEVICE],
+ CONF_ID: import_config[CONF_ID],
+ }
+ return await self.async_step_init(info=formatted_config)
+
+ def _show_form(self, errors=None):
+ """Show the config flow form with possible errors."""
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_NAME): str,
+ vol.Required(CONF_DEVICE): str,
+ vol.Optional(CONF_ID): str,
+ }
+ ),
+ errors=errors or {},
+ )
+
+ def _create_entry(self, gw_id, name, device):
+ """Create entry for the OpenTherm Gateway device."""
+ return self.async_create_entry(
+ title=name, data={CONF_ID: gw_id, CONF_DEVICE: device, CONF_NAME: name}
+ )
+
+
+class OpenThermGwOptionsFlow(config_entries.OptionsFlow):
+ """Handle opentherm_gw options."""
+
+ def __init__(self, config_entry):
+ """Initialize the options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage the opentherm_gw options."""
+ if user_input is not None:
+ if user_input.get(CONF_PRECISION) == 0:
+ user_input[CONF_PRECISION] = None
+ return self.async_create_entry(title="", data=user_input)
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_PRECISION,
+ default=self.config_entry.options.get(CONF_PRECISION, 0),
+ ): vol.All(
+ vol.Coerce(float),
+ vol.In(
+ [0, PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
+ ),
+ ),
+ vol.Optional(
+ CONF_FLOOR_TEMP,
+ default=self.config_entry.options.get(CONF_FLOOR_TEMP, False),
+ ): bool,
+ }
+ ),
+ )
diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py
index 60042b92867..bd9b372de33 100644
--- a/homeassistant/components/opentherm_gw/const.py
+++ b/homeassistant/components/opentherm_gw/const.py
@@ -18,6 +18,8 @@ DEVICE_CLASS_COLD = "cold"
DEVICE_CLASS_HEAT = "heat"
DEVICE_CLASS_PROBLEM = "problem"
+DOMAIN = "opentherm_gw"
+
SERVICE_RESET_GATEWAY = "reset_gateway"
SERVICE_SET_CLOCK = "set_clock"
SERVICE_SET_CONTROL_SETPOINT = "set_control_setpoint"
diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json
index 9c7f165c6df..a632096cd75 100644
--- a/homeassistant/components/opentherm_gw/manifest.json
+++ b/homeassistant/components/opentherm_gw/manifest.json
@@ -3,10 +3,11 @@
"name": "Opentherm Gateway",
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"requirements": [
- "pyotgw==0.4b4"
+ "pyotgw==0.5b0"
],
"dependencies": [],
"codeowners": [
"@mvn23"
- ]
-}
+ ],
+ "config_flow": true
+}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py
index 1449caf5def..c77a73cd180 100644
--- a/homeassistant/components/opentherm_gw/sensor.py
+++ b/homeassistant/components/opentherm_gw/sensor.py
@@ -2,6 +2,7 @@
import logging
from homeassistant.components.sensor import ENTITY_ID_FORMAT
+from homeassistant.const import CONF_ID
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity, async_generate_entity_id
@@ -12,19 +13,23 @@ from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO
_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the OpenTherm Gateway sensors."""
- if discovery_info is None:
- return
- gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info]
sensors = []
for var, info in SENSOR_INFO.items():
device_class = info[0]
unit = info[1]
friendly_name_format = info[2]
sensors.append(
- OpenThermSensor(gw_dev, var, device_class, unit, friendly_name_format)
+ OpenThermSensor(
+ hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]],
+ var,
+ device_class,
+ unit,
+ friendly_name_format,
+ )
)
+
async_add_entities(sensors)
diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json
new file mode 100644
index 00000000000..1c246432fb1
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/strings.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "title": "OpenTherm Gateway",
+ "step": {
+ "init": {
+ "title": "OpenTherm Gateway",
+ "data": {
+ "name": "Name",
+ "device": "Path or URL",
+ "id": "ID"
+ }
+ }
+ },
+ "error": {
+ "already_configured": "Gateway already configured",
+ "id_exists": "Gateway id already exists",
+ "serial_error": "Error connecting to device",
+ "timeout": "Connection attempt timed out"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "description": "Options for the OpenTherm Gateway",
+ "data": {
+ "floor_temperature": "Floor Temperature",
+ "precision": "Precision"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json
index 58d57b28056..27d2921a7d4 100644
--- a/homeassistant/components/openuv/.translations/ru.json
+++ b/homeassistant/components/openuv/.translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"error": {
"identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.",
- "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API"
+ "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
},
"step": {
"user": {
diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py
index 51dc92623f3..23f88f59aad 100644
--- a/homeassistant/components/openweathermap/sensor.py
+++ b/homeassistant/components/openweathermap/sensor.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+from pyowm import OWM
+from pyowm.exceptions.api_call_error import APICallError
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -56,7 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the OpenWeatherMap sensor."""
- from pyowm import OWM
if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
@@ -127,8 +128,6 @@ class OpenWeatherMapSensor(Entity):
def update(self):
"""Get the latest data from OWM and updates the states."""
- from pyowm.exceptions.api_call_error import APICallError
-
try:
self.owa_client.update()
except APICallError:
@@ -201,8 +200,6 @@ class WeatherData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from OpenWeatherMap."""
- from pyowm.exceptions.api_call_error import APICallError
-
try:
obs = self.owm.weather_at_coords(self.latitude, self.longitude)
except (APICallError, TypeError):
diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py
index a51ea26607d..69ca965d660 100644
--- a/homeassistant/components/openweathermap/weather.py
+++ b/homeassistant/components/openweathermap/weather.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+from pyowm import OWM
+from pyowm.exceptions.api_call_error import APICallError
import voluptuous as vol
from homeassistant.components.weather import (
@@ -71,7 +73,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the OpenWeatherMap weather platform."""
- import pyowm
longitude = config.get(CONF_LONGITUDE, round(hass.config.longitude, 5))
latitude = config.get(CONF_LATITUDE, round(hass.config.latitude, 5))
@@ -79,8 +80,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
mode = config.get(CONF_MODE)
try:
- owm = pyowm.OWM(config.get(CONF_API_KEY))
- except pyowm.exceptions.api_call_error.APICallError:
+ owm = OWM(config.get(CONF_API_KEY))
+ except APICallError:
_LOGGER.error("Error while connecting to OpenWeatherMap")
return False
@@ -225,8 +226,6 @@ class OpenWeatherMapWeather(WeatherEntity):
def update(self):
"""Get the latest data from OWM and updates the states."""
- from pyowm.exceptions.api_call_error import APICallError
-
try:
self._owm.update()
self._owm.update_forecast()
@@ -263,8 +262,6 @@ class WeatherData:
@Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES)
def update_forecast(self):
"""Get the latest forecast from OpenWeatherMap."""
- from pyowm.exceptions.api_call_error import APICallError
-
try:
if self._mode == "daily":
fcd = self.owm.daily_forecast_at_coords(
diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py
index 7547342d898..71d8d65d8b8 100644
--- a/homeassistant/components/orangepi_gpio/__init__.py
+++ b/homeassistant/components/orangepi_gpio/__init__.py
@@ -1,18 +1,20 @@
"""Support for controlling GPIO pins of a Orange Pi."""
+
import logging
+from OPi import GPIO
+
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
+from .const import PIN_MODES
+
_LOGGER = logging.getLogger(__name__)
-CONF_PIN_MODE = "pin_mode"
DOMAIN = "orangepi_gpio"
-PIN_MODES = ["pc", "zeroplus", "zeroplus2", "deo", "neocore2"]
-def setup(hass, config):
+async def async_setup(hass, config):
"""Set up the Orange Pi GPIO component."""
- from OPi import GPIO
def cleanup_gpio(event):
"""Stuff to do before stopping."""
@@ -20,68 +22,31 @@ def setup(hass, config):
def prepare_gpio(event):
"""Stuff to do when home assistant starts."""
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio)
- hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio)
return True
def setup_mode(mode):
"""Set GPIO pin mode."""
- from OPi import GPIO
-
- if mode == "pc":
- import orangepi.pc
-
- GPIO.setmode(orangepi.pc.BOARD)
- elif mode == "zeroplus":
- import orangepi.zeroplus
-
- GPIO.setmode(orangepi.zeroplus.BOARD)
- elif mode == "zeroplus2":
- import orangepi.zeroplus
-
- GPIO.setmode(orangepi.zeroplus2.BOARD)
- elif mode == "duo":
- import nanopi.duo
-
- GPIO.setmode(nanopi.duo.BOARD)
- elif mode == "neocore2":
- import nanopi.neocore2
-
- GPIO.setmode(nanopi.neocore2.BOARD)
-
-
-def setup_output(port):
- """Set up a GPIO as output."""
- from OPi import GPIO
-
- GPIO.setup(port, GPIO.OUT)
+ _LOGGER.debug("Setting GPIO pin mode as %s", PIN_MODES[mode])
+ GPIO.setmode(PIN_MODES[mode])
def setup_input(port):
"""Set up a GPIO as input."""
- from OPi import GPIO
-
+ _LOGGER.debug("Setting up GPIO pin %i as input", port)
GPIO.setup(port, GPIO.IN)
-def write_output(port, value):
- """Write a value to a GPIO."""
- from OPi import GPIO
-
- GPIO.output(port, value)
-
-
def read_input(port):
"""Read a value from a GPIO."""
- from OPi import GPIO
-
+ _LOGGER.debug("Reading GPIO pin %i", port)
return GPIO.input(port)
def edge_detect(port, event_callback):
"""Add detection for RISING and FALLING events."""
- from OPi import GPIO
-
+ _LOGGER.debug("Add callback for GPIO pin %i", port)
GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback)
diff --git a/homeassistant/components/orangepi_gpio/binary_sensor.py b/homeassistant/components/orangepi_gpio/binary_sensor.py
index b89faf3e7d4..b89442a571c 100644
--- a/homeassistant/components/orangepi_gpio/binary_sensor.py
+++ b/homeassistant/components/orangepi_gpio/binary_sensor.py
@@ -1,50 +1,52 @@
"""Support for binary sensor using Orange Pi GPIO."""
-import logging
-from homeassistant.components import orangepi_gpio
-from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
-from homeassistant.const import DEVICE_DEFAULT_NAME
+from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
-from . import CONF_PIN_MODE
-from .const import CONF_INVERT_LOGIC, CONF_PORTS, PORT_SCHEMA
-
-_LOGGER = logging.getLogger(__name__)
+from . import edge_detect, read_input, setup_input, setup_mode
+from .const import CONF_INVERT_LOGIC, CONF_PIN_MODE, CONF_PORTS, PORT_SCHEMA
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PORT_SCHEMA)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Orange Pi GPIO devices."""
- pin_mode = config[CONF_PIN_MODE]
- orangepi_gpio.setup_mode(pin_mode)
-
- invert_logic = config[CONF_INVERT_LOGIC]
-
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the Orange Pi GPIO platform."""
binary_sensors = []
+ invert_logic = config[CONF_INVERT_LOGIC]
+ pin_mode = config[CONF_PIN_MODE]
ports = config[CONF_PORTS]
+
+ setup_mode(pin_mode)
+
for port_num, port_name in ports.items():
- binary_sensors.append(OPiGPIOBinarySensor(port_name, port_num, invert_logic))
- add_entities(binary_sensors, True)
+ binary_sensors.append(
+ OPiGPIOBinarySensor(hass, port_name, port_num, invert_logic)
+ )
+ async_add_entities(binary_sensors)
class OPiGPIOBinarySensor(BinarySensorDevice):
"""Represent a binary sensor that uses Orange Pi GPIO."""
- def __init__(self, name, port, invert_logic):
+ def __init__(self, hass, name, port, invert_logic):
"""Initialize the Orange Pi binary sensor."""
- self._name = name or DEVICE_DEFAULT_NAME
+ self._name = name
self._port = port
self._invert_logic = invert_logic
self._state = None
- orangepi_gpio.setup_input(self._port)
+ async def async_added_to_hass(self):
+ """Run when entity about to be added to hass."""
- def read_gpio(port):
- """Read state from GPIO."""
- self._state = orangepi_gpio.read_input(self._port)
- self.schedule_update_ha_state()
+ def gpio_edge_listener(port):
+ """Update GPIO when edge change is detected."""
+ self.schedule_update_ha_state(True)
- orangepi_gpio.edge_detect(self._port, read_gpio)
+ def setup_entity():
+ setup_input(self._port)
+ edge_detect(self._port, gpio_edge_listener)
+ self.schedule_update_ha_state(True)
+
+ await self.hass.async_add_executor_job(setup_entity)
@property
def should_poll(self):
@@ -62,5 +64,5 @@ class OPiGPIOBinarySensor(BinarySensorDevice):
return self._state != self._invert_logic
def update(self):
- """Update the GPIO state."""
- self._state = orangepi_gpio.read_input(self._port)
+ """Update state with new GPIO data."""
+ self._state = read_input(self._port)
diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py
index 6bb9ab1df1e..47ddf5b7085 100644
--- a/homeassistant/components/orangepi_gpio/const.py
+++ b/homeassistant/components/orangepi_gpio/const.py
@@ -1,19 +1,55 @@
"""Constants for Orange Pi GPIO."""
+
+from nanopi import duo, neocore2
+from orangepi import (
+ lite,
+ lite2,
+ one,
+ oneplus,
+ pc,
+ pc2,
+ pcplus,
+ pi3,
+ plus2e,
+ prime,
+ r1,
+ winplus,
+ zero,
+ zeroplus,
+ zeroplus2,
+)
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
-from . import CONF_PIN_MODE, PIN_MODES
-
CONF_INVERT_LOGIC = "invert_logic"
+CONF_PIN_MODE = "pin_mode"
CONF_PORTS = "ports"
-
DEFAULT_INVERT_LOGIC = False
+PIN_MODES = {
+ "lite": lite.BOARD,
+ "lite2": lite2.BOARD,
+ "one": one.BOARD,
+ "oneplus": oneplus.BOARD,
+ "pc": pc.BOARD,
+ "pc2": pc2.BOARD,
+ "pcplus": pcplus.BOARD,
+ "pi3": pi3.BOARD,
+ "plus2e": plus2e.BOARD,
+ "prime": prime.BOARD,
+ "r1": r1.BOARD,
+ "winplus": winplus.BOARD,
+ "zero": zero.BOARD,
+ "zeroplus": zeroplus.BOARD,
+ "zeroplus2": zeroplus2.BOARD,
+ "duo": duo.BOARD,
+ "neocore2": neocore2.BOARD,
+}
_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string})
PORT_SCHEMA = {
vol.Required(CONF_PORTS): _SENSORS_SCHEMA,
- vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES),
+ vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES.keys()),
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
}
diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json
index 51bca8fbbbe..52c8f8f509f 100644
--- a/homeassistant/components/orangepi_gpio/manifest.json
+++ b/homeassistant/components/orangepi_gpio/manifest.json
@@ -3,7 +3,7 @@
"name": "Orangepi GPIO",
"documentation": "https://www.home-assistant.io/integrations/orangepi_gpio",
"requirements": [
- "OPi.GPIO==0.3.6"
+ "OPi.GPIO==0.4.0"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/oru/__init__.py b/homeassistant/components/oru/__init__.py
new file mode 100644
index 00000000000..d1517ab0bf1
--- /dev/null
+++ b/homeassistant/components/oru/__init__.py
@@ -0,0 +1 @@
+"""The Orange and Rockland Utility smart energy meter integration."""
diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json
new file mode 100644
index 00000000000..ff5e74fd260
--- /dev/null
+++ b/homeassistant/components/oru/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "oru",
+ "name": "Orange and Rockland Utility Smart Energy Meter Sensor",
+ "documentation": "https://www.home-assistant.io/integrations/oru",
+ "dependencies": [],
+ "codeowners": ["@bvlaicu"],
+ "requirements": ["oru==0.1.9"]
+}
\ No newline at end of file
diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py
new file mode 100644
index 00000000000..e68d8e1c45a
--- /dev/null
+++ b/homeassistant/components/oru/sensor.py
@@ -0,0 +1,92 @@
+"""Platform for sensor integration."""
+from datetime import timedelta
+import logging
+
+import voluptuous as vol
+
+from oru import Meter
+from oru import MeterError
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import ENERGY_KILO_WATT_HOUR
+from homeassistant.helpers.entity import Entity
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_METER_NUMBER = "meter_number"
+
+SCAN_INTERVAL = timedelta(minutes=15)
+
+SENSOR_NAME = "ORU Current Energy Usage"
+SENSOR_ICON = "mdi:counter"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_METER_NUMBER): cv.string})
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the sensor platform."""
+
+ meter_number = config[CONF_METER_NUMBER]
+
+ try:
+ meter = Meter(meter_number)
+
+ except MeterError:
+ _LOGGER.error("Unable to create Oru meter")
+ return
+
+ add_entities([CurrentEnergyUsageSensor(meter)], True)
+
+ _LOGGER.debug("Oru meter_number = %s", meter_number)
+
+
+class CurrentEnergyUsageSensor(Entity):
+ """Representation of the sensor."""
+
+ def __init__(self, meter):
+ """Initialize the sensor."""
+ self._state = None
+ self._available = None
+ self.meter = meter
+
+ @property
+ def unique_id(self):
+ """Return a unique, HASS-friendly identifier for this entity."""
+ return self.meter.meter_id
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return SENSOR_NAME
+
+ @property
+ def icon(self):
+ """Return the icon of the sensor."""
+ return SENSOR_ICON
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return ENERGY_KILO_WATT_HOUR
+
+ def update(self):
+ """Fetch new state data for the sensor."""
+ try:
+ last_read = self.meter.last_read()
+
+ self._state = last_read
+ self._available = True
+
+ _LOGGER.debug(
+ "%s = %s %s", self.name, self._state, self.unit_of_measurement
+ )
+ except MeterError as err:
+ self._available = False
+
+ _LOGGER.error("Unexpected oru meter error: %s", err)
diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py
index 9a2da2bce06..05064861844 100644
--- a/homeassistant/components/osramlightify/light.py
+++ b/homeassistant/components/osramlightify/light.py
@@ -3,14 +3,15 @@ import logging
import random
import socket
+from lightify import Lightify
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
+ ATTR_EFFECT,
ATTR_HS_COLOR,
ATTR_TRANSITION,
- ATTR_EFFECT,
EFFECT_RANDOM,
PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
@@ -20,7 +21,6 @@ from homeassistant.components.light import (
SUPPORT_TRANSITION,
Light,
)
-
from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
@@ -71,11 +71,9 @@ DEFAULT_KELVIN = 2700
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Osram Lightify lights."""
- import lightify
-
host = config[CONF_HOST]
try:
- bridge = lightify.Lightify(host, log_level=logging.NOTSET)
+ bridge = Lightify(host, log_level=logging.NOTSET)
except socket.error as err:
msg = "Error connecting to bridge: {} due to: {}".format(host, str(err))
_LOGGER.exception(msg)
diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py
index a175155e6f2..3c4cd464d44 100644
--- a/homeassistant/components/otp/sensor.py
+++ b/homeassistant/components/otp/sensor.py
@@ -1,13 +1,14 @@
"""Support for One-Time Password (OTP)."""
-import time
import logging
+import time
+import pyotp
import voluptuous as vol
-from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_TOKEN
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -41,8 +42,6 @@ class TOTPSensor(Entity):
def __init__(self, name, token):
"""Initialize the sensor."""
- import pyotp
-
self._name = name
self._otp = pyotp.TOTP(token)
self._state = None
diff --git a/homeassistant/components/owntracks/.translations/ru.json b/homeassistant/components/owntracks/.translations/ru.json
index 6ebaa31cacf..31c3e77279d 100644
--- a/homeassistant/components/owntracks/.translations/ru.json
+++ b/homeassistant/components/owntracks/.translations/ru.json
@@ -4,7 +4,7 @@
"one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"create_entry": {
- "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u0435\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438."
+ "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py
index 67553ef608f..343a6d90b52 100644
--- a/homeassistant/components/owntracks/config_flow.py
+++ b/homeassistant/components/owntracks/config_flow.py
@@ -78,7 +78,10 @@ class OwnTracksFlow(config_entries.ConfigFlow):
async def _get_webhook_id(self):
"""Generate webhook ID."""
webhook_id = self.hass.components.webhook.async_generate_id()
- if self.hass.components.cloud.async_active_subscription():
+ if (
+ "cloud" in self.hass.config.components
+ and self.hass.components.cloud.async_active_subscription()
+ ):
webhook_url = await self.hass.components.cloud.async_create_cloudhook(
webhook_id
)
diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py
index 7ef31be1327..465d2762f74 100644
--- a/homeassistant/components/owntracks/messages.py
+++ b/homeassistant/components/owntracks/messages.py
@@ -79,6 +79,8 @@ def _parse_see_args(message, subscribe_topic):
kwargs["attributes"]["address"] = message["addr"]
if "cog" in message:
kwargs["attributes"]["course"] = message["cog"]
+ if "bs" in message:
+ kwargs["attributes"]["battery_status"] = message["bs"]
if "t" in message:
if message["t"] in ("c", "u"):
kwargs["source_type"] = SOURCE_TYPE_GPS
diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py
index 393ecb827cc..4a816252580 100644
--- a/homeassistant/components/panasonic_bluray/media_player.py
+++ b/homeassistant/components/panasonic_bluray/media_player.py
@@ -2,9 +2,10 @@
from datetime import timedelta
import logging
+from panacotta import PanasonicBD
import voluptuous as vol
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
SUPPORT_PAUSE,
SUPPORT_PLAY,
@@ -53,9 +54,7 @@ class PanasonicBluRay(MediaPlayerDevice):
def __init__(self, ip, name):
"""Initialize the Panasonic Blue-ray device."""
- import panacotta
-
- self._device = panacotta.PanasonicBD(ip)
+ self._device = PanasonicBD(ip)
self._name = name
self._state = STATE_OFF
self._position = 0
diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py
index d0b013c3bf3..0b19a8fa552 100644
--- a/homeassistant/components/panasonic_viera/media_player.py
+++ b/homeassistant/components/panasonic_viera/media_player.py
@@ -1,9 +1,11 @@
"""Support for interface with a Panasonic Viera TV."""
import logging
+from panasonic_viera import RemoteControl
import voluptuous as vol
+import wakeonlan
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
MEDIA_TYPE_URL,
SUPPORT_NEXT_TRACK,
@@ -62,8 +64,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Panasonic Viera TV platform."""
- from panasonic_viera import RemoteControl
-
mac = config.get(CONF_MAC)
name = config.get(CONF_NAME)
port = config.get(CONF_PORT)
@@ -95,8 +95,6 @@ class PanasonicVieraTVDevice(MediaPlayerDevice):
def __init__(self, mac, name, remote, host, app_power, uuid=None):
"""Initialize the Panasonic device."""
- import wakeonlan
-
# Save a reference to the imported class
self._wol = wakeonlan
self._mac = mac
diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py
index c242670ba48..417903c46e0 100644
--- a/homeassistant/components/pandora/media_player.py
+++ b/homeassistant/components/pandora/media_player.py
@@ -6,6 +6,8 @@ import re
import shutil
import signal
+import pexpect
+
from homeassistant import util
from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import (
@@ -104,8 +106,6 @@ class PandoraMediaPlayer(MediaPlayerDevice):
def turn_on(self):
"""Turn the media player on."""
- import pexpect
-
if self._player_state != STATE_OFF:
return
self._pianobar = pexpect.spawn("pianobar")
@@ -136,8 +136,6 @@ class PandoraMediaPlayer(MediaPlayerDevice):
def turn_off(self):
"""Turn the media player off."""
- import pexpect
-
if self._pianobar is None:
_LOGGER.info("Pianobar subprocess already stopped")
return
@@ -226,8 +224,6 @@ class PandoraMediaPlayer(MediaPlayerDevice):
def _send_station_list_command(self):
"""Send a station list command."""
- import pexpect
-
self._pianobar.send("s")
try:
self._pianobar.expect("Select station:", timeout=1)
@@ -248,8 +244,6 @@ class PandoraMediaPlayer(MediaPlayerDevice):
def _query_for_playing_status(self):
"""Query system for info about current track."""
- import pexpect
-
self._clear_buffer()
self._pianobar.send("i")
try:
@@ -372,8 +366,6 @@ class PandoraMediaPlayer(MediaPlayerDevice):
This is necessary because there are a bunch of 00:00 in the buffer
"""
- import pexpect
-
try:
while not self._pianobar.expect(".+", timeout=0.1):
pass
diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py
index 6b9c7c44ddf..33f17b18a80 100644
--- a/homeassistant/components/persistent_notification/__init__.py
+++ b/homeassistant/components/persistent_notification/__init__.py
@@ -52,11 +52,6 @@ STATE = "notifying"
STATUS_UNREAD = "unread"
STATUS_READ = "read"
-WS_TYPE_GET_NOTIFICATIONS = "persistent_notification/get"
-SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
- {vol.Required("type"): WS_TYPE_GET_NOTIFICATIONS}
-)
-
@bind_hass
def create(hass, message, title=None, notification_id=None):
@@ -198,14 +193,13 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
DOMAIN, SERVICE_MARK_READ, mark_read_service, SCHEMA_SERVICE_MARK_READ
)
- hass.components.websocket_api.async_register_command(
- WS_TYPE_GET_NOTIFICATIONS, websocket_get_notifications, SCHEMA_WS_GET
- )
+ hass.components.websocket_api.async_register_command(websocket_get_notifications)
return True
@callback
+@websocket_api.websocket_command({vol.Required("type"): "persistent_notification/get"})
def websocket_get_notifications(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
diff --git a/homeassistant/components/piglow/light.py b/homeassistant/components/piglow/light.py
index 31ece4a36a9..27bbb81d31f 100644
--- a/homeassistant/components/piglow/light.py
+++ b/homeassistant/components/piglow/light.py
@@ -2,18 +2,19 @@
import logging
import subprocess
+import piglow
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
- SUPPORT_BRIGHTNESS,
ATTR_HS_COLOR,
+ PLATFORM_SCHEMA,
+ SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
Light,
- PLATFORM_SCHEMA,
)
from homeassistant.const import CONF_NAME
+import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
@@ -29,23 +30,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Piglow Light platform."""
- import piglow
-
if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != "54":
_LOGGER.error("A Piglow device was not found")
return False
name = config.get(CONF_NAME)
- add_entities([PiglowLight(piglow, name)])
+ add_entities([PiglowLight(name)])
class PiglowLight(Light):
"""Representation of an Piglow Light."""
- def __init__(self, piglow, name):
+ def __init__(self, name):
"""Initialize an PiglowLight."""
- self._piglow = piglow
self._name = name
self._is_on = False
self._brightness = 255
@@ -88,7 +86,7 @@ class PiglowLight(Light):
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
- self._piglow.clear()
+ piglow.clear()
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
@@ -99,16 +97,16 @@ class PiglowLight(Light):
rgb = color_util.color_hsv_to_RGB(
self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100
)
- self._piglow.red(rgb[0])
- self._piglow.green(rgb[1])
- self._piglow.blue(rgb[2])
- self._piglow.show()
+ piglow.red(rgb[0])
+ piglow.green(rgb[1])
+ piglow.blue(rgb[2])
+ piglow.show()
self._is_on = True
self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
- self._piglow.clear()
- self._piglow.show()
+ piglow.clear()
+ piglow.show()
self._is_on = False
self.schedule_update_ha_state()
diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py
index 44b4055e032..6474165a6cd 100644
--- a/homeassistant/components/pjlink/media_player.py
+++ b/homeassistant/components/pjlink/media_player.py
@@ -105,10 +105,12 @@ class PjLinkDevice(MediaPlayerDevice):
pwstate = projector.get_power()
if pwstate in ("on", "warm-up"):
self._pwstate = STATE_ON
+ self._muted = projector.get_mute()[1]
+ self._current_source = format_input_source(*projector.get_input())
else:
self._pwstate = STATE_OFF
- self._muted = projector.get_mute()[1]
- self._current_source = format_input_source(*projector.get_input())
+ self._muted = False
+ self._current_source = None
except KeyError as err:
if str(err) == "'OK'":
self._pwstate = STATE_OFF
diff --git a/homeassistant/components/plaato/.translations/ru.json b/homeassistant/components/plaato/.translations/ru.json
index 59964fdedd6..dc06e3ddab0 100644
--- a/homeassistant/components/plaato/.translations/ru.json
+++ b/homeassistant/components/plaato/.translations/ru.json
@@ -5,7 +5,7 @@
"one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"create_entry": {
- "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Plaato Airlock\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438."
+ "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json
index a3ba5185371..7a8cf7a1424 100644
--- a/homeassistant/components/plex/.translations/ca.json
+++ b/homeassistant/components/plex/.translations/ca.json
@@ -4,6 +4,7 @@
"all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats",
"already_configured": "Aquest servidor Plex ja est\u00e0 configurat",
"already_in_progress": "S\u2019est\u00e0 configurant Plex",
+ "discovery_no_file": "No s'ha trobat cap fitxer de configuraci\u00f3 heretat",
"invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida",
"token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.",
"unknown": "Ha fallat per motiu desconegut"
diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json
index 1da4b4b4b49..99d5d4d1685 100644
--- a/homeassistant/components/plex/.translations/da.json
+++ b/homeassistant/components/plex/.translations/da.json
@@ -4,6 +4,7 @@
"all_configured": "Alle linkede servere er allerede konfigureret",
"already_configured": "Denne Plex-server er allerede konfigureret",
"already_in_progress": "Plex konfigureres",
+ "discovery_no_file": "Der blev ikke fundet nogen legacy konfigurationsfil",
"invalid_import": "Importeret konfiguration er ugyldig",
"token_request_timeout": "Timeout ved hentning af token",
"unknown": "Mislykkedes af ukendt \u00e5rsag"
@@ -32,6 +33,10 @@
"description": "Flere servere til r\u00e5dighed, v\u00e6lg en:",
"title": "V\u00e6lg Plex-server"
},
+ "start_website_auth": {
+ "description": "Forts\u00e6t for at autorisere p\u00e5 plex.tv.",
+ "title": "Tilslut Plex-server"
+ },
"user": {
"data": {
"manual_setup": "Manuel ops\u00e6tning",
diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json
index 95083102273..4b24e6c78a6 100644
--- a/homeassistant/components/plex/.translations/de.json
+++ b/homeassistant/components/plex/.translations/de.json
@@ -1,24 +1,60 @@
{
"config": {
"abort": {
- "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden"
+ "all_configured": "Alle verkn\u00fcpften Server sind bereits konfiguriert",
+ "already_configured": "Dieser Plex-Server ist bereits konfiguriert",
+ "already_in_progress": "Plex wird konfiguriert",
+ "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden",
+ "invalid_import": "Die importierte Konfiguration ist ung\u00fcltig",
+ "token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens",
+ "unknown": "Aus unbekanntem Grund fehlgeschlagen"
+ },
+ "error": {
+ "faulty_credentials": "Autorisation fehlgeschlagen",
+ "no_servers": "Keine Server sind mit dem Konto verbunden",
+ "no_token": "Bereitstellen eines Tokens oder Ausw\u00e4hlen der manuellen Einrichtung",
+ "not_found": "Plex-Server nicht gefunden"
},
"step": {
"manual_setup": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "ssl": "SSL verwenden",
+ "token": "Token (falls erforderlich)",
+ "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
+ },
"title": "Plex Server"
},
+ "select_server": {
+ "data": {
+ "server": "Server"
+ },
+ "description": "Mehrere Server verf\u00fcgbar, w\u00e4hlen Sie einen aus:",
+ "title": "Plex-Server ausw\u00e4hlen"
+ },
"start_website_auth": {
"description": "Weiter zur Autorisierung unter plex.tv.",
"title": "Plex Server verbinden"
},
"user": {
- "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell."
+ "data": {
+ "manual_setup": "Manuelle Einrichtung",
+ "token": "Plex Token"
+ },
+ "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell.",
+ "title": "Plex Server verbinden"
}
- }
+ },
+ "title": "Plex"
},
"options": {
"step": {
"plex_mp_settings": {
+ "data": {
+ "show_all_controls": "Alle Steuerelemente anzeigen",
+ "use_episode_art": "Episode-Bilder verwenden"
+ },
"description": "Optionen f\u00fcr Plex-Media-Player"
}
}
diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json
index c9e61dcf2e9..c06d314ec72 100644
--- a/homeassistant/components/plex/.translations/fr.json
+++ b/homeassistant/components/plex/.translations/fr.json
@@ -4,6 +4,7 @@
"all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s",
"already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9",
"already_in_progress": "Plex en cours de configuration",
+ "discovery_no_file": "Aucun fichier de configuration h\u00e9rit\u00e9 trouv\u00e9",
"invalid_import": "La configuration import\u00e9e est invalide",
"token_request_timeout": "D\u00e9lai d'obtention du jeton",
"unknown": "\u00c9chec pour une raison inconnue"
@@ -32,6 +33,10 @@
"description": "Plusieurs serveurs disponibles, s\u00e9lectionnez-en un:",
"title": "S\u00e9lectionnez le serveur Plex"
},
+ "start_website_auth": {
+ "description": "Continuer d'autoriser sur plex.tv.",
+ "title": "Connecter un serveur Plex"
+ },
"user": {
"data": {
"manual_setup": "Installation manuelle",
diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json
index 171c656566d..f8e78945802 100644
--- a/homeassistant/components/plex/.translations/ko.json
+++ b/homeassistant/components/plex/.translations/ko.json
@@ -4,15 +4,28 @@
"all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84",
"already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4",
+ "discovery_no_file": "\ub808\uac70\uc2dc \uad6c\uc131 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "token_request_timeout": "\ud1a0\ud070 \ud68d\ub4dd \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4",
"unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
"faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4",
"no_servers": "\uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "no_token": "\ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc218\ub3d9 \uc124\uc815\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
"not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
},
"step": {
+ "manual_setup": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8",
+ "ssl": "SSL \uc0ac\uc6a9",
+ "token": "\ud1a0\ud070 (\ud544\uc694\ud55c \uacbd\uc6b0)",
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d"
+ },
+ "title": "Plex \uc11c\ubc84"
+ },
"select_server": {
"data": {
"server": "\uc11c\ubc84"
@@ -20,14 +33,30 @@
"description": "\uc5ec\ub7ec \uc11c\ubc84\uac00 \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:",
"title": "Plex \uc11c\ubc84 \uc120\ud0dd"
},
+ "start_website_auth": {
+ "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud574\uc8fc\uc138\uc694.",
+ "title": "Plex \uc11c\ubc84 \uc5f0\uacb0"
+ },
"user": {
"data": {
+ "manual_setup": "\uc218\ub3d9 \uc124\uc815",
"token": "Plex \ud1a0\ud070"
},
- "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud574\uc8fc\uc138\uc694.",
+ "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uc124\uc815\ud574\uc8fc\uc138\uc694.",
"title": "Plex \uc11c\ubc84 \uc5f0\uacb0"
}
},
"title": "Plex"
+ },
+ "options": {
+ "step": {
+ "plex_mp_settings": {
+ "data": {
+ "show_all_controls": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864 \ud45c\uc2dc\ud558\uae30",
+ "use_episode_art": "\uc5d0\ud53c\uc18c\ub4dc \uc544\ud2b8 \uc0ac\uc6a9"
+ },
+ "description": "Plex \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4 \uc635\uc158"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json
new file mode 100644
index 00000000000..c971ebb4762
--- /dev/null
+++ b/homeassistant/components/plex/.translations/nl.json
@@ -0,0 +1,62 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Alle gekoppelde servers zijn al geconfigureerd",
+ "already_configured": "Deze Plex-server is al geconfigureerd",
+ "already_in_progress": "Plex wordt geconfigureerd",
+ "discovery_no_file": "Geen legacy configuratiebestand gevonden",
+ "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig",
+ "token_request_timeout": "Time-out verkrijgen van token",
+ "unknown": "Mislukt om onbekende reden"
+ },
+ "error": {
+ "faulty_credentials": "Autorisatie mislukt",
+ "no_servers": "Geen servers gekoppeld aan account",
+ "no_token": "Geef een token op of selecteer handmatige installatie",
+ "not_found": "Plex-server niet gevonden"
+ },
+ "step": {
+ "manual_setup": {
+ "data": {
+ "host": "Host",
+ "port": "Poort",
+ "ssl": "Gebruik SSL",
+ "token": "Token (indien nodig)",
+ "verify_ssl": "Controleer SSL-certificaat"
+ },
+ "title": "Plex server"
+ },
+ "select_server": {
+ "data": {
+ "server": "Server"
+ },
+ "description": "Meerdere servers beschikbaar, selecteer er een:",
+ "title": "Selecteer Plex server"
+ },
+ "start_website_auth": {
+ "description": "Ga verder met autoriseren bij plex.tv.",
+ "title": "Verbind de Plex server"
+ },
+ "user": {
+ "data": {
+ "manual_setup": "Handmatig setup",
+ "token": "Plex token"
+ },
+ "description": "Ga verder met autoriseren bij plex.tv of configureer een server.",
+ "title": "Verbind de Plex server"
+ }
+ },
+ "title": "Plex"
+ },
+ "options": {
+ "step": {
+ "plex_mp_settings": {
+ "data": {
+ "show_all_controls": "Toon alle bedieningselementen",
+ "use_episode_art": "Gebruik aflevering kunst"
+ },
+ "description": "Opties voor Plex-mediaspelers"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json
index 18c4e865a84..8ebd2b69bb9 100644
--- a/homeassistant/components/plex/.translations/no.json
+++ b/homeassistant/components/plex/.translations/no.json
@@ -24,7 +24,7 @@
"token": "Token (hvis n\u00f8dvendig)",
"verify_ssl": "Verifisere SSL-sertifikat"
},
- "title": "Plex server"
+ "title": "Plex-server"
},
"select_server": {
"data": {
@@ -35,7 +35,7 @@
},
"start_website_auth": {
"description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.",
- "title": "Koble til Plex server"
+ "title": "Koble til Plex-server"
},
"user": {
"data": {
diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json
index 9b75a0061e8..0b94e3eacb6 100644
--- a/homeassistant/components/plex/.translations/pl.json
+++ b/homeassistant/components/plex/.translations/pl.json
@@ -4,6 +4,7 @@
"all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.",
"already_configured": "Serwer Plex jest ju\u017c skonfigurowany",
"already_in_progress": "Plex jest konfigurowany",
+ "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego",
"invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa",
"token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena",
"unknown": "Nieznany b\u0142\u0105d"
@@ -32,6 +33,10 @@
"description": "Dost\u0119pnych jest wiele serwer\u00f3w, wybierz jeden:",
"title": "Wybierz serwer Plex"
},
+ "start_website_auth": {
+ "description": "Kontynuuj, by dokona\u0107 autoryzacji w plex.tv.",
+ "title": "Po\u0142\u0105cz z serwerem Plex"
+ },
"user": {
"data": {
"manual_setup": "Konfiguracja r\u0119czna",
@@ -48,7 +53,7 @@
"plex_mp_settings": {
"data": {
"show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce",
- "use_episode_art": "U\u017cyj grafiki episodu"
+ "use_episode_art": "U\u017cyj grafiki odcinka"
},
"description": "Opcje dla odtwarzaczy multimedialnych Plex"
}
diff --git a/homeassistant/components/plex/.translations/pt.json b/homeassistant/components/plex/.translations/pt.json
new file mode 100644
index 00000000000..4312910653f
--- /dev/null
+++ b/homeassistant/components/plex/.translations/pt.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "step": {
+ "manual_setup": {
+ "data": {
+ "host": "Servidor",
+ "port": "Porta"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "plex_mp_settings": {
+ "data": {
+ "show_all_controls": "Mostrar todos os controles"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json
index fe773f72be9..bce55d35baa 100644
--- a/homeassistant/components/plex/.translations/ru.json
+++ b/homeassistant/components/plex/.translations/ru.json
@@ -5,15 +5,15 @@
"already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.",
"already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.",
"discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.",
- "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430",
- "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430",
- "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435"
+ "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430.",
+ "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430.",
+ "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435."
},
"error": {
- "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438",
- "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e",
- "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443",
- "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d"
+ "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
+ "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.",
+ "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.",
+ "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d."
},
"step": {
"manual_setup": {
@@ -34,7 +34,7 @@
"title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex"
},
"start_website_auth": {
- "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.",
+ "description": "\u041f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.",
"title": "Plex"
},
"user": {
diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json
index 9be270a017c..7426e7f95ed 100644
--- a/homeassistant/components/plex/.translations/sl.json
+++ b/homeassistant/components/plex/.translations/sl.json
@@ -42,7 +42,7 @@
"manual_setup": "Ro\u010dna nastavitev",
"token": "Plex \u017eeton"
},
- "description": "Vnesite \u017eeton Plex za samodejno nastavitev ali ro\u010dno konfigurirajte stre\u017enik.",
+ "description": "Nadaljujte z avtorizacijo na plex.tv ali ro\u010dno konfigurirajte stre\u017enik.",
"title": "Pove\u017eite stre\u017enik Plex"
}
},
diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py
index ed94b6913bc..1aaa8a8e3aa 100644
--- a/homeassistant/components/plex/__init__.py
+++ b/homeassistant/components/plex/__init__.py
@@ -3,6 +3,7 @@ import asyncio
import logging
import plexapi.exceptions
+from plexwebsocket import PlexWebsocket
import requests.exceptions
import voluptuous as vol
@@ -15,8 +16,14 @@ from homeassistant.const import (
CONF_TOKEN,
CONF_URL,
CONF_VERIFY_SSL,
+ EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
from .const import (
CONF_USE_EPISODE_ART,
@@ -26,12 +33,14 @@ from .const import (
DEFAULT_PORT,
DEFAULT_SSL,
DEFAULT_VERIFY_SSL,
+ DISPATCHERS,
DOMAIN as PLEX_DOMAIN,
PLATFORMS,
PLEX_MEDIA_PLAYER_OPTIONS,
PLEX_SERVER_CONFIG,
- REFRESH_LISTENERS,
+ PLEX_UPDATE_PLATFORMS_SIGNAL,
SERVERS,
+ WEBSOCKETS,
)
from .server import PlexServer
@@ -64,7 +73,7 @@ _LOGGER = logging.getLogger(__package__)
def setup(hass, config):
"""Set up the Plex component."""
- hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}})
+ hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}})
plex_config = config.get(PLEX_DOMAIN, {})
if plex_config:
@@ -104,7 +113,7 @@ async def async_setup_entry(hass, entry):
)
hass.config_entries.async_update_entry(entry, options=options)
- plex_server = PlexServer(server_config, entry.options)
+ plex_server = PlexServer(hass, server_config, entry.options)
try:
await hass.async_add_executor_job(plex_server.connect)
except requests.exceptions.ConnectionError as error:
@@ -129,7 +138,8 @@ async def async_setup_entry(hass, entry):
_LOGGER.debug(
"Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use
)
- hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server
+ server_id = plex_server.machine_identifier
+ hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server
for platform in PLATFORMS:
hass.async_create_task(
@@ -138,6 +148,30 @@ async def async_setup_entry(hass, entry):
entry.add_update_listener(async_options_updated)
+ unsub = async_dispatcher_connect(
+ hass,
+ PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id),
+ plex_server.update_platforms,
+ )
+ hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, [])
+ hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
+
+ def update_plex():
+ async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
+
+ session = async_get_clientsession(hass)
+ websocket = PlexWebsocket(plex_server.plex_server, update_plex, session)
+ hass.loop.create_task(websocket.listen())
+ hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket
+
+ def close_websocket_session(_):
+ websocket.close()
+
+ unsub = hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, close_websocket_session
+ )
+ hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
+
return True
@@ -145,8 +179,12 @@ async def async_unload_entry(hass, entry):
"""Unload a config entry."""
server_id = entry.data[CONF_SERVER_IDENTIFIER]
- cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id)
- await hass.async_add_executor_job(cancel)
+ websocket = hass.data[PLEX_DOMAIN][WEBSOCKETS].pop(server_id)
+ websocket.close()
+
+ dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id)
+ for unsub in dispatchers:
+ unsub()
tasks = [
hass.config_entries.async_forward_entry_unload(entry, platform)
diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py
index 9e74756977d..c03b958b2da 100644
--- a/homeassistant/components/plex/config_flow.py
+++ b/homeassistant/components/plex/config_flow.py
@@ -19,6 +19,7 @@ from homeassistant.util.json import load_json
from .const import ( # pylint: disable=unused-import
AUTH_CALLBACK_NAME,
AUTH_CALLBACK_PATH,
+ CONF_CLIENT_IDENTIFIER,
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
CONF_USE_EPISODE_ART,
@@ -65,6 +66,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.available_servers = None
self.plexauth = None
self.token = None
+ self.client_id = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
@@ -79,7 +81,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
self.current_login = server_config
- plex_server = PlexServer(server_config)
+ plex_server = PlexServer(self.hass, server_config)
try:
await self.hass.async_add_executor_job(plex_server.connect)
@@ -116,6 +118,8 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
token = server_config.get(CONF_TOKEN)
entry_config = {CONF_URL: url}
+ if self.client_id:
+ entry_config[CONF_CLIENT_IDENTIFIER] = self.client_id
if token:
entry_config[CONF_TOKEN] = token
if url.startswith("https"):
@@ -216,6 +220,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_external_step_done(next_step_id="timed_out")
self.token = token
+ self.client_id = self.plexauth.client_identifier
return self.async_external_step_done(next_step_id="use_external_token")
async def async_step_timed_out(self, user_input=None):
diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py
index 0b436c4e208..d3c79e60bc4 100644
--- a/homeassistant/components/plex/const.py
+++ b/homeassistant/components/plex/const.py
@@ -2,20 +2,27 @@
from homeassistant.const import __version__
DOMAIN = "plex"
-NAME_FORMAT = "Plex {}"
+NAME_FORMAT = "Plex ({})"
DEFAULT_PORT = 32400
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
+DISPATCHERS = "dispatchers"
PLATFORMS = ["media_player", "sensor"]
-REFRESH_LISTENERS = "refresh_listeners"
SERVERS = "servers"
+WEBSOCKETS = "websockets"
PLEX_CONFIG_FILE = "plex.conf"
PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options"
PLEX_SERVER_CONFIG = "server_config"
+PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}"
+PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}"
+PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}"
+PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}"
+
+CONF_CLIENT_IDENTIFIER = "client_id"
CONF_SERVER = "server"
CONF_SERVER_IDENTIFIER = "server_id"
CONF_USE_EPISODE_ART = "use_episode_art"
diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json
index d4f2ae0517a..8edccda75e0 100644
--- a/homeassistant/components/plex/manifest.json
+++ b/homeassistant/components/plex/manifest.json
@@ -5,7 +5,8 @@
"documentation": "https://www.home-assistant.io/integrations/plex",
"requirements": [
"plexapi==3.0.6",
- "plexauth==0.0.4"
+ "plexauth==0.0.5",
+ "plexwebsocket==0.0.3"
],
"dependencies": [
"http"
diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py
index a49e4c9c057..32bf7b65fff 100644
--- a/homeassistant/components/plex/media_player.py
+++ b/homeassistant/components/plex/media_player.py
@@ -1,5 +1,4 @@
"""Support to interface with the Plex API."""
-from datetime import timedelta
import json
import logging
from xml.etree.ElementTree import ParseError
@@ -29,14 +28,17 @@ from homeassistant.const import (
STATE_PAUSED,
STATE_PLAYING,
)
-from homeassistant.helpers.event import track_time_interval
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util
from .const import (
CONF_SERVER_IDENTIFIER,
+ DISPATCHERS,
DOMAIN as PLEX_DOMAIN,
NAME_FORMAT,
- REFRESH_LISTENERS,
+ PLEX_NEW_MP_SIGNAL,
+ PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
SERVERS,
)
@@ -53,142 +55,55 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plex media_player from a config entry."""
-
- def add_entities(entities, update_before_add=False):
- """Sync version of async add entities."""
- hass.add_job(async_add_entities, entities, update_before_add)
-
- hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities)
-
-
-def _setup_platform(hass, config_entry, add_entities_callback):
- """Set up the Plex media_player platform."""
server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
- plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
- plex_clients = {}
- plex_sessions = {}
- hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval(
- hass, lambda now: update_devices(), timedelta(seconds=10)
+
+ def async_new_media_players(new_entities):
+ _async_add_entities(
+ hass, config_entry, async_add_entities, server_id, new_entities
+ )
+
+ unsub = async_dispatcher_connect(
+ hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players
)
-
- def update_devices():
- """Update the devices objects."""
- try:
- devices = plexserver.clients()
- except plexapi.exceptions.BadRequest:
- _LOGGER.exception("Error listing plex devices")
- return
- except requests.exceptions.RequestException as ex:
- _LOGGER.warning(
- "Could not connect to Plex server: %s (%s)",
- plexserver.friendly_name,
- ex,
- )
- return
-
- new_plex_clients = []
- available_client_ids = []
- for device in devices:
- # For now, let's allow all deviceClass types
- if device.deviceClass in ["badClient"]:
- continue
-
- available_client_ids.append(device.machineIdentifier)
-
- if device.machineIdentifier not in plex_clients:
- new_client = PlexClient(
- plexserver, device, None, plex_sessions, update_devices
- )
- plex_clients[device.machineIdentifier] = new_client
- _LOGGER.debug("New device: %s", device.machineIdentifier)
- new_plex_clients.append(new_client)
- else:
- _LOGGER.debug("Refreshing device: %s", device.machineIdentifier)
- plex_clients[device.machineIdentifier].refresh(device, None)
-
- # add devices with a session and no client (ex. PlexConnect Apple TV's)
- try:
- sessions = plexserver.sessions()
- except plexapi.exceptions.BadRequest:
- _LOGGER.exception("Error listing plex sessions")
- return
- except requests.exceptions.RequestException as ex:
- _LOGGER.warning(
- "Could not connect to Plex server: %s (%s)",
- plexserver.friendly_name,
- ex,
- )
- return
-
- plex_sessions.clear()
- for session in sessions:
- for player in session.players:
- plex_sessions[player.machineIdentifier] = session, player
-
- for machine_identifier, (session, player) in plex_sessions.items():
- if machine_identifier in available_client_ids:
- # Avoid using session if already added as a device.
- _LOGGER.debug("Skipping session, device exists: %s", machine_identifier)
- continue
-
- if (
- machine_identifier not in plex_clients
- and machine_identifier is not None
- ):
- new_client = PlexClient(
- plexserver, player, session, plex_sessions, update_devices
- )
- plex_clients[machine_identifier] = new_client
- _LOGGER.debug("New session: %s", machine_identifier)
- new_plex_clients.append(new_client)
- else:
- _LOGGER.debug("Refreshing session: %s", machine_identifier)
- plex_clients[machine_identifier].refresh(None, session)
-
- for client in plex_clients.values():
- # force devices to idle that do not have a valid session
- if client.session is None:
- client.force_idle()
-
- client.set_availability(
- client.machine_identifier in available_client_ids
- or client.machine_identifier in plex_sessions
- )
-
- if client not in new_plex_clients:
- client.schedule_update_ha_state()
-
- if new_plex_clients:
- add_entities_callback(new_plex_clients)
+ hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
-class PlexClient(MediaPlayerDevice):
+@callback
+def _async_add_entities(
+ hass, config_entry, async_add_entities, server_id, new_entities
+):
+ """Set up Plex media_player entities."""
+ entities = []
+ plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
+ for entity_params in new_entities:
+ plex_mp = PlexMediaPlayer(plexserver, **entity_params)
+ entities.append(plex_mp)
+
+ async_add_entities(entities, True)
+
+
+class PlexMediaPlayer(MediaPlayerDevice):
"""Representation of a Plex device."""
- def __init__(self, plex_server, device, session, plex_sessions, update_devices):
+ def __init__(self, plex_server, device, session=None):
"""Initialize the Plex device."""
+ self.plex_server = plex_server
+ self.device = device
+ self.session = session
self._app_name = ""
- self._device = None
self._available = False
- self._marked_unavailable = None
self._device_protocol_capabilities = None
self._is_player_active = False
- self._is_player_available = False
- self._player = None
- self._machine_identifier = None
+ self._machine_identifier = device.machineIdentifier
self._make = ""
self._name = None
self._player_state = "idle"
self._previous_volume_level = 1 # Used in fake muting
- self._session = None
self._session_type = None
self._session_username = None
self._state = STATE_IDLE
self._volume_level = 1 # since we can't retrieve remotely
self._volume_muted = False # since we can't retrieve remotely
- self.plex_server = plex_server
- self.plex_sessions = plex_sessions
- self.update_devices = update_devices
# General
self._media_content_id = None
self._media_content_rating = None
@@ -208,7 +123,22 @@ class PlexClient(MediaPlayerDevice):
self._media_season = None
self._media_series_title = None
- self.refresh(device, session)
+ async def async_added_to_hass(self):
+ """Run when about to be added to hass."""
+ server_id = self.plex_server.machine_identifier
+ unsub = async_dispatcher_connect(
+ self.hass,
+ PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id),
+ self.async_refresh_media_player,
+ )
+ self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
+
+ @callback
+ def async_refresh_media_player(self, device, session):
+ """Set instance objects and trigger an entity state update."""
+ self.device = device
+ self.session = session
+ self.async_schedule_update_ha_state(True)
def _clear_media_details(self):
"""Set all Media Items to None."""
@@ -232,52 +162,46 @@ class PlexClient(MediaPlayerDevice):
# Clear library Name
self._app_name = ""
- def refresh(self, device, session):
+ def update(self):
"""Refresh key device data."""
self._clear_media_details()
- if session: # Not being triggered by Chrome or FireTablet Plex App
- self._session = session
- if device:
- self._device = device
+ self._available = self.device or self.session
+ name_base = None
+
+ if self.device:
try:
- device_url = self._device.url("/")
+ device_url = self.device.url("/")
except plexapi.exceptions.BadRequest:
device_url = "127.0.0.1"
if "127.0.0.1" in device_url:
- self._device.proxyThroughServer()
- self._session = None
- self._machine_identifier = self._device.machineIdentifier
- self._name = NAME_FORMAT.format(self._device.title or DEVICE_DEFAULT_NAME)
- self._device_protocol_capabilities = self._device.protocolCapabilities
+ self.device.proxyThroughServer()
+ name_base = self.device.title or self.device.product
+ self._device_protocol_capabilities = self.device.protocolCapabilities
+ self._player_state = self.device.state
- # set valid session, preferring device session
- if self._device.machineIdentifier in self.plex_sessions:
- self._session = self.plex_sessions.get(
- self._device.machineIdentifier, [None, None]
- )[0]
-
- if self._session:
- if (
- self._device is not None
- and self._device.machineIdentifier is not None
- and self._session.players
- ):
- self._is_player_available = True
- self._player = [
+ if not self.session:
+ self.force_idle()
+ else:
+ session_device = next(
+ (
p
- for p in self._session.players
- if p.machineIdentifier == self._device.machineIdentifier
- ][0]
- self._name = NAME_FORMAT.format(self._player.title)
- self._player_state = self._player.state
- self._session_username = self._session.usernames[0]
- self._make = self._player.device
+ for p in self.session.players
+ if p.machineIdentifier == self.device.machineIdentifier
+ ),
+ None,
+ )
+ if session_device:
+ self._make = session_device.device or ""
+ self._player_state = session_device.state
+ name_base = name_base or session_device.title or session_device.product
else:
- self._is_player_available = False
+ _LOGGER.warning("No player associated with active session")
+
+ self._session_username = self.session.usernames[0]
# Calculate throttled position for proper progress display.
- position = int(self._session.viewOffset / 1000)
+ position = int(self.session.viewOffset / 1000)
now = dt_util.utcnow()
if self._media_position is not None:
pos_diff = position - self._media_position
@@ -289,21 +213,22 @@ class PlexClient(MediaPlayerDevice):
self._media_position_updated_at = now
self._media_position = position
- self._media_content_id = self._session.ratingKey
- self._media_content_rating = getattr(self._session, "contentRating", None)
+ self._media_content_id = self.session.ratingKey
+ self._media_content_rating = getattr(self.session, "contentRating", None)
+ self._name = self._name or NAME_FORMAT.format(name_base or DEVICE_DEFAULT_NAME)
self._set_player_state()
- if self._is_player_active and self._session is not None:
- self._session_type = self._session.type
- self._media_duration = int(self._session.duration / 1000)
+ if self._is_player_active and self.session is not None:
+ self._session_type = self.session.type
+ self._media_duration = int(self.session.duration / 1000)
# title (movie name, tv episode name, music song name)
- self._media_title = self._session.title
+ self._media_title = self.session.title
# media type
self._set_media_type()
self._app_name = (
- self._session.section().title
- if self._session.section() is not None
+ self.session.section().title
+ if self.session.section() is not None
else ""
)
self._set_media_image()
@@ -311,33 +236,21 @@ class PlexClient(MediaPlayerDevice):
self._session_type = None
def _set_media_image(self):
- thumb_url = self._session.thumbUrl
+ thumb_url = self.session.thumbUrl
if (
self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.plex_server.use_episode_art
):
- thumb_url = self._session.url(self._session.grandparentThumb)
+ thumb_url = self.session.url(self.session.grandparentThumb)
if thumb_url is None:
_LOGGER.debug(
- "Using media art because media thumb " "was not found: %s",
- self.entity_id,
+ "Using media art because media thumb was not found: %s", self.name
)
- thumb_url = self.session.url(self._session.art)
+ thumb_url = self.session.url(self.session.art)
self._media_image_url = thumb_url
- def set_availability(self, available):
- """Set the device as available/unavailable noting time."""
- if not available:
- self._clear_media_details()
- if self._marked_unavailable is None:
- self._marked_unavailable = dt_util.utcnow()
- else:
- self._marked_unavailable = None
-
- self._available = available
-
def _set_player_state(self):
if self._player_state == "playing":
self._is_player_active = True
@@ -357,41 +270,41 @@ class PlexClient(MediaPlayerDevice):
self._media_content_type = MEDIA_TYPE_TVSHOW
# season number (00)
- if callable(self._session.season):
- self._media_season = str((self._session.season()).index).zfill(2)
- elif self._session.parentIndex is not None:
- self._media_season = self._session.parentIndex.zfill(2)
+ if callable(self.session.season):
+ self._media_season = str((self.session.season()).index).zfill(2)
+ elif self.session.parentIndex is not None:
+ self._media_season = self.session.parentIndex.zfill(2)
else:
self._media_season = None
# show name
- self._media_series_title = self._session.grandparentTitle
+ self._media_series_title = self.session.grandparentTitle
# episode number (00)
- if self._session.index is not None:
- self._media_episode = str(self._session.index).zfill(2)
+ if self.session.index is not None:
+ self._media_episode = str(self.session.index).zfill(2)
elif self._session_type == "movie":
self._media_content_type = MEDIA_TYPE_MOVIE
- if self._session.year is not None and self._media_title is not None:
- self._media_title += " (" + str(self._session.year) + ")"
+ if self.session.year is not None and self._media_title is not None:
+ self._media_title += " (" + str(self.session.year) + ")"
elif self._session_type == "track":
self._media_content_type = MEDIA_TYPE_MUSIC
- self._media_album_name = self._session.parentTitle
- self._media_album_artist = self._session.grandparentTitle
- self._media_track = self._session.index
- self._media_artist = self._session.originalTitle
+ self._media_album_name = self.session.parentTitle
+ self._media_album_artist = self.session.grandparentTitle
+ self._media_track = self.session.index
+ self._media_artist = self.session.originalTitle
# use album artist if track artist is missing
if self._media_artist is None:
_LOGGER.debug(
- "Using album artist because track artist " "was not found: %s",
- self.entity_id,
+ "Using album artist because track artist was not found: %s",
+ self.name,
)
self._media_artist = self._media_album_artist
def force_idle(self):
"""Force client to idle."""
self._state = STATE_IDLE
- self._session = None
+ self.session = None
self._clear_media_details()
@property
@@ -402,7 +315,7 @@ class PlexClient(MediaPlayerDevice):
@property
def unique_id(self):
"""Return the id of this plex client."""
- return self.machine_identifier
+ return self._machine_identifier
@property
def available(self):
@@ -414,31 +327,11 @@ class PlexClient(MediaPlayerDevice):
"""Return the name of the device."""
return self._name
- @property
- def machine_identifier(self):
- """Return the machine identifier of the device."""
- return self._machine_identifier
-
@property
def app_name(self):
"""Return the library name of playing media."""
return self._app_name
- @property
- def device(self):
- """Return the device, if any."""
- return self._device
-
- @property
- def marked_unavailable(self):
- """Return time device was marked unavailable."""
- return self._marked_unavailable
-
- @property
- def session(self):
- """Return the session, if any."""
- return self._session
-
@property
def state(self):
"""Return the state of the device."""
@@ -462,8 +355,7 @@ class PlexClient(MediaPlayerDevice):
"""Return the content type of current playing media."""
if self._session_type == "clip":
_LOGGER.debug(
- "Clip content type detected, " "compatibility may vary: %s",
- self.entity_id,
+ "Clip content type detected, compatibility may vary: %s", self.name
)
return MEDIA_TYPE_TVSHOW
if self._session_type == "episode":
@@ -560,8 +452,8 @@ class PlexClient(MediaPlayerDevice):
# no mute support
if self.make.lower() == "shield android tv":
_LOGGER.debug(
- "Shield Android TV client detected, disabling mute " "controls: %s",
- self.entity_id,
+ "Shield Android TV client detected, disabling mute controls: %s",
+ self.name,
)
return (
SUPPORT_PAUSE
@@ -579,7 +471,7 @@ class PlexClient(MediaPlayerDevice):
_LOGGER.debug(
"Tivo client detected, only enabling pause, play, "
"stop, and off controls: %s",
- self.entity_id,
+ self.name,
)
return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF
@@ -603,7 +495,7 @@ class PlexClient(MediaPlayerDevice):
if self.device and "playback" in self._device_protocol_capabilities:
self.device.setVolume(int(volume * 100), self._active_media_plexapi_type)
self._volume_level = volume # store since we can't retrieve
- self.update_devices()
+ self.plex_server.update_platforms()
@property
def volume_level(self):
@@ -642,19 +534,19 @@ class PlexClient(MediaPlayerDevice):
"""Send play command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.play(self._active_media_plexapi_type)
- self.update_devices()
+ self.plex_server.update_platforms()
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()
+ self.plex_server.update_platforms()
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()
+ self.plex_server.update_platforms()
def turn_off(self):
"""Turn the client off."""
@@ -665,13 +557,13 @@ class PlexClient(MediaPlayerDevice):
"""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()
+ self.plex_server.update_platforms()
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()
+ self.plex_server.update_platforms()
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
@@ -706,7 +598,7 @@ class PlexClient(MediaPlayerDevice):
except requests.exceptions.ConnectTimeout:
_LOGGER.error("Timed out playing on %s", self.name)
- self.update_devices()
+ self.plex_server.update_platforms()
def _get_music_media(self, library_name, src):
"""Find music media and return a Plex media object."""
diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py
index 7d5b54356a0..287f0edf39a 100644
--- a/homeassistant/components/plex/sensor.py
+++ b/homeassistant/components/plex/sensor.py
@@ -1,19 +1,21 @@
"""Support for Plex media server monitoring."""
-from datetime import timedelta
import logging
-import plexapi.exceptions
-import requests.exceptions
-
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
-from .const import CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, SERVERS
+from .const import (
+ CONF_SERVER_IDENTIFIER,
+ DISPATCHERS,
+ DOMAIN as PLEX_DOMAIN,
+ NAME_FORMAT,
+ PLEX_UPDATE_SENSOR_SIGNAL,
+ SERVERS,
+)
_LOGGER = logging.getLogger(__name__)
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Plex sensor platform.
@@ -26,8 +28,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plex sensor from a config entry."""
server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
- sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id])
- async_add_entities([sensor], True)
+ plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
+ sensor = PlexSensor(plexserver)
+ async_add_entities([sensor])
class PlexSensor(Entity):
@@ -35,12 +38,29 @@ class PlexSensor(Entity):
def __init__(self, plex_server):
"""Initialize the sensor."""
+ self.sessions = []
self._state = None
self._now_playing = []
self._server = plex_server
- self._name = f"Plex ({plex_server.friendly_name})"
+ self._name = NAME_FORMAT.format(plex_server.friendly_name)
self._unique_id = f"sensor-{plex_server.machine_identifier}"
+ async def async_added_to_hass(self):
+ """Run when about to be added to hass."""
+ server_id = self._server.machine_identifier
+ unsub = async_dispatcher_connect(
+ self.hass,
+ PLEX_UPDATE_SENSOR_SIGNAL.format(server_id),
+ self.async_refresh_sensor,
+ )
+ self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
+
+ @callback
+ def async_refresh_sensor(self, sessions):
+ """Set instance object and trigger an entity state update."""
+ self.sessions = sessions
+ self.async_schedule_update_ha_state(True)
+
@property
def name(self):
"""Return the name of the sensor."""
@@ -51,6 +71,11 @@ class PlexSensor(Entity):
"""Return the id of this plex client."""
return self._unique_id
+ @property
+ def should_poll(self):
+ """Return True if entity has to be polled for state."""
+ return False
+
@property
def state(self):
"""Return the state of the sensor."""
@@ -66,24 +91,10 @@ class PlexSensor(Entity):
"""Return the state attributes."""
return {content[0]: content[1] for content in self._now_playing}
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update method for Plex sensor."""
- try:
- sessions = self._server.sessions()
- except plexapi.exceptions.BadRequest:
- _LOGGER.error(
- "Error listing current Plex sessions on %s", self._server.friendly_name
- )
- return
- except requests.exceptions.RequestException as ex:
- _LOGGER.warning(
- "Temporary error connecting to %s (%s)", self._server.friendly_name, ex
- )
- return
-
now_playing = []
- for sess in sessions:
+ for sess in self.sessions:
user = sess.usernames[0]
device = sess.players[0].title
now_playing_user = f"{user} - {device}"
@@ -120,5 +131,5 @@ class PlexSensor(Entity):
now_playing_title += f" ({sess.year})"
now_playing.append((now_playing_user, now_playing_title))
- self._state = len(sessions)
+ self._state = len(self.sessions)
self._now_playing = now_playing
diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py
index d9ddc28c89a..e6f77a310f1 100644
--- a/homeassistant/components/plex/server.py
+++ b/homeassistant/components/plex/server.py
@@ -1,17 +1,25 @@
"""Shared class to maintain Plex server instances."""
+import logging
+
import plexapi.myplex
import plexapi.playqueue
import plexapi.server
from requests import Session
+import requests.exceptions
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
+from homeassistant.helpers.dispatcher import dispatcher_send
from .const import (
+ CONF_CLIENT_IDENTIFIER,
CONF_SERVER,
CONF_SHOW_ALL_CONTROLS,
CONF_USE_EPISODE_ART,
DEFAULT_VERIFY_SSL,
+ PLEX_NEW_MP_SIGNAL,
+ PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
+ PLEX_UPDATE_SENSOR_SIGNAL,
X_PLEX_DEVICE_NAME,
X_PLEX_PLATFORM,
X_PLEX_PRODUCT,
@@ -19,21 +27,23 @@ from .const import (
)
from .errors import NoServersFound, ServerNotSpecified
+_LOGGER = logging.getLogger(__name__)
+
# Set default headers sent by plexapi
plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME
plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM
plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT
plexapi.X_PLEX_VERSION = X_PLEX_VERSION
-plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers()
-plexapi.server.BASE_HEADERS = plexapi.reset_base_headers()
class PlexServer:
"""Manages a single Plex server connection."""
- def __init__(self, server_config, options=None):
+ def __init__(self, hass, server_config, options=None):
"""Initialize a Plex server instance."""
+ self._hass = hass
self._plex_server = None
+ self._known_clients = set()
self._url = server_config.get(CONF_URL)
self._token = server_config.get(CONF_TOKEN)
self._server_name = server_config.get(CONF_SERVER)
@@ -41,6 +51,12 @@ class PlexServer:
self.options = options
self.server_choice = None
+ # Header conditionally added as it is not available in config entry v1
+ if CONF_CLIENT_IDENTIFIER in server_config:
+ plexapi.X_PLEX_IDENTIFIER = server_config[CONF_CLIENT_IDENTIFIER]
+ plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers()
+ plexapi.server.BASE_HEADERS = plexapi.reset_base_headers()
+
def connect(self):
"""Connect to a Plex server directly, obtaining direct URL if necessary."""
@@ -76,13 +92,84 @@ class PlexServer:
else:
_connect_with_token()
- def clients(self):
- """Pass through clients call to plexapi."""
- return self._plex_server.clients()
+ def refresh_entity(self, machine_identifier, device, session):
+ """Forward refresh dispatch to media_player."""
+ dispatcher_send(
+ self._hass,
+ PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(machine_identifier),
+ device,
+ session,
+ )
- def sessions(self):
- """Pass through sessions call to plexapi."""
- return self._plex_server.sessions()
+ def update_platforms(self):
+ """Update the platform entities."""
+ _LOGGER.debug("Updating devices")
+
+ available_clients = {}
+ new_clients = set()
+
+ try:
+ devices = self._plex_server.clients()
+ sessions = self._plex_server.sessions()
+ except plexapi.exceptions.BadRequest:
+ _LOGGER.exception("Error requesting Plex client data from server")
+ return
+ except requests.exceptions.RequestException as ex:
+ _LOGGER.warning(
+ "Could not connect to Plex server: %s (%s)", self.friendly_name, ex
+ )
+ return
+
+ for device in devices:
+ available_clients[device.machineIdentifier] = {"device": device}
+
+ if device.machineIdentifier not in self._known_clients:
+ new_clients.add(device.machineIdentifier)
+ _LOGGER.debug("New device: %s", device.machineIdentifier)
+
+ for session in sessions:
+ for player in session.players:
+ available_clients.setdefault(
+ player.machineIdentifier, {"device": player}
+ )
+ available_clients[player.machineIdentifier]["session"] = session
+
+ if player.machineIdentifier not in self._known_clients:
+ new_clients.add(player.machineIdentifier)
+ _LOGGER.debug("New session: %s", player.machineIdentifier)
+
+ new_entity_configs = []
+ for client_id, client_data in available_clients.items():
+ if client_id in new_clients:
+ new_entity_configs.append(client_data)
+ else:
+ self.refresh_entity(
+ client_id, client_data["device"], client_data.get("session")
+ )
+
+ self._known_clients.update(new_clients)
+
+ idle_clients = self._known_clients.difference(available_clients)
+ for client_id in idle_clients:
+ self.refresh_entity(client_id, None, None)
+
+ if new_entity_configs:
+ dispatcher_send(
+ self._hass,
+ PLEX_NEW_MP_SIGNAL.format(self.machine_identifier),
+ new_entity_configs,
+ )
+
+ dispatcher_send(
+ self._hass,
+ PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier),
+ sessions,
+ )
+
+ @property
+ def plex_server(self):
+ """Return the plexapi PlexServer instance."""
+ return self._plex_server
@property
def friendly_name(self):
diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py
index 815e2688009..05a8f96bda7 100644
--- a/homeassistant/components/pocketcasts/sensor.py
+++ b/homeassistant/components/pocketcasts/sensor.py
@@ -1,13 +1,13 @@
"""Support for Pocket Casts."""
+from datetime import timedelta
import logging
-from datetime import timedelta
-
+import pocketcasts
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -25,8 +25,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the pocketcasts platform for sensors."""
- import pocketcasts
-
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py
index 666f8c28241..44e31e24fb0 100644
--- a/homeassistant/components/proliphix/climate.py
+++ b/homeassistant/components/proliphix/climate.py
@@ -1,7 +1,8 @@
"""Support for Proliphix NT10e Thermostats."""
+import proliphix
import voluptuous as vol
-from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
+from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
from homeassistant.components.climate.const import (
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
@@ -9,12 +10,12 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import (
+ ATTR_TEMPERATURE,
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
PRECISION_TENTHS,
TEMP_FAHRENHEIT,
- ATTR_TEMPERATURE,
)
import homeassistant.helpers.config_validation as cv
@@ -35,8 +36,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
- import proliphix
-
pdp = proliphix.PDP(host, username, password)
add_entities([ProliphixThermostat(pdp)], True)
diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py
index 82db5f6725f..8eeb9325bc0 100644
--- a/homeassistant/components/prometheus/__init__.py
+++ b/homeassistant/components/prometheus/__init__.py
@@ -3,24 +3,25 @@ import logging
import string
from aiohttp import web
+import prometheus_client
import voluptuous as vol
from homeassistant import core as hacore
from homeassistant.components.climate.const import ATTR_CURRENT_TEMPERATURE
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT,
- ATTR_DEVICE_CLASS,
CONTENT_TYPE_TEXT_PLAIN,
EVENT_STATE_CHANGED,
- TEMP_FAHRENHEIT,
TEMP_CELSIUS,
+ TEMP_FAHRENHEIT,
)
from homeassistant.helpers import entityfilter, state as state_helper
import homeassistant.helpers.config_validation as cv
-from homeassistant.util.temperature import fahrenheit_to_celsius
from homeassistant.helpers.entity_values import EntityValues
+from homeassistant.util.temperature import fahrenheit_to_celsius
_LOGGER = logging.getLogger(__name__)
@@ -64,8 +65,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Activate Prometheus component."""
- import prometheus_client
-
hass.http.register_view(PrometheusView(prometheus_client))
conf = config[DOMAIN]
@@ -99,7 +98,7 @@ class PrometheusMetrics:
def __init__(
self,
- prometheus_client,
+ prometheus_cli,
entity_filter,
namespace,
climate_units,
@@ -108,7 +107,7 @@ class PrometheusMetrics:
default_metric,
):
"""Initialize Prometheus Metrics."""
- self.prometheus_client = prometheus_client
+ self.prometheus_cli = prometheus_cli
self._component_config = component_config
self._override_metric = override_metric
self._default_metric = default_metric
@@ -147,9 +146,7 @@ class PrometheusMetrics:
getattr(self, handler)(state)
metric = self._metric(
- "state_change",
- self.prometheus_client.Counter,
- "The number of state changes",
+ "state_change", self.prometheus_cli.Counter, "The number of state changes"
)
metric.labels(**self._labels(state)).inc()
@@ -199,7 +196,7 @@ class PrometheusMetrics:
if "battery_level" in state.attributes:
metric = self._metric(
"battery_level_percent",
- self.prometheus_client.Gauge,
+ self.prometheus_cli.Gauge,
"Battery level as a percentage of its capacity",
)
try:
@@ -211,7 +208,7 @@ class PrometheusMetrics:
def _handle_binary_sensor(self, state):
metric = self._metric(
"binary_sensor_state",
- self.prometheus_client.Gauge,
+ self.prometheus_cli.Gauge,
"State of the binary sensor (0/1)",
)
value = self.state_as_number(state)
@@ -220,7 +217,7 @@ class PrometheusMetrics:
def _handle_input_boolean(self, state):
metric = self._metric(
"input_boolean_state",
- self.prometheus_client.Gauge,
+ self.prometheus_cli.Gauge,
"State of the input boolean (0/1)",
)
value = self.state_as_number(state)
@@ -229,7 +226,7 @@ class PrometheusMetrics:
def _handle_device_tracker(self, state):
metric = self._metric(
"device_tracker_state",
- self.prometheus_client.Gauge,
+ self.prometheus_cli.Gauge,
"State of the device tracker (0/1)",
)
value = self.state_as_number(state)
@@ -237,14 +234,14 @@ class PrometheusMetrics:
def _handle_person(self, state):
metric = self._metric(
- "person_state", self.prometheus_client.Gauge, "State of the person (0/1)"
+ "person_state", self.prometheus_cli.Gauge, "State of the person (0/1)"
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
def _handle_light(self, state):
metric = self._metric(
- "light_state", self.prometheus_client.Gauge, "Load level of a light (0..1)"
+ "light_state", self.prometheus_cli.Gauge, "Load level of a light (0..1)"
)
try:
@@ -259,7 +256,7 @@ class PrometheusMetrics:
def _handle_lock(self, state):
metric = self._metric(
- "lock_state", self.prometheus_client.Gauge, "State of the lock (0/1)"
+ "lock_state", self.prometheus_cli.Gauge, "State of the lock (0/1)"
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
@@ -271,7 +268,7 @@ class PrometheusMetrics:
temp = fahrenheit_to_celsius(temp)
metric = self._metric(
"temperature_c",
- self.prometheus_client.Gauge,
+ self.prometheus_cli.Gauge,
"Temperature in degrees Celsius",
)
metric.labels(**self._labels(state)).set(temp)
@@ -282,15 +279,13 @@ class PrometheusMetrics:
current_temp = fahrenheit_to_celsius(current_temp)
metric = self._metric(
"current_temperature_c",
- self.prometheus_client.Gauge,
+ self.prometheus_cli.Gauge,
"Current Temperature in degrees Celsius",
)
metric.labels(**self._labels(state)).set(current_temp)
metric = self._metric(
- "climate_state",
- self.prometheus_client.Gauge,
- "State of the thermostat (0/1)",
+ "climate_state", self.prometheus_cli.Gauge, "State of the thermostat (0/1)"
)
try:
value = self.state_as_number(state)
@@ -308,7 +303,7 @@ class PrometheusMetrics:
if metric is not None:
_metric = self._metric(
- metric, self.prometheus_client.Gauge, f"Sensor data measured in {unit}"
+ metric, self.prometheus_cli.Gauge, f"Sensor data measured in {unit}"
)
try:
@@ -368,7 +363,7 @@ class PrometheusMetrics:
def _handle_switch(self, state):
metric = self._metric(
- "switch_state", self.prometheus_client.Gauge, "State of the switch (0/1)"
+ "switch_state", self.prometheus_cli.Gauge, "State of the switch (0/1)"
)
try:
@@ -383,7 +378,7 @@ class PrometheusMetrics:
def _handle_automation(self, state):
metric = self._metric(
"automation_triggered_count",
- self.prometheus_client.Counter,
+ self.prometheus_cli.Counter,
"Count of times an automation has been triggered",
)
@@ -396,15 +391,15 @@ class PrometheusView(HomeAssistantView):
url = API_ENDPOINT
name = "api:prometheus"
- def __init__(self, prometheus_client):
+ def __init__(self, prometheus_cli):
"""Initialize Prometheus view."""
- self.prometheus_client = prometheus_client
+ self.prometheus_cli = prometheus_cli
async def get(self, request):
"""Handle request for Prometheus metrics."""
_LOGGER.debug("Received Prometheus metrics request")
return web.Response(
- body=self.prometheus_client.generate_latest(),
+ body=self.prometheus_cli.generate_latest(),
content_type=CONTENT_TYPE_TEXT_PLAIN,
)
diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py
index b1ce8ad7ac0..90487120ffe 100644
--- a/homeassistant/components/proxy/camera.py
+++ b/homeassistant/components/proxy/camera.py
@@ -1,12 +1,14 @@
"""Proxy camera platform that enables image processing of camera data."""
import asyncio
+from datetime import timedelta
+import io
import logging
-from datetime import timedelta
+from PIL import Image
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
-from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE
+from homeassistant.const import CONF_ENTITY_ID, CONF_MODE, CONF_NAME
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
import homeassistant.util.dt as dt_util
@@ -58,9 +60,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
def _precheck_image(image, opts):
"""Perform some pre-checks on the given image."""
- from PIL import Image
- import io
-
if not opts:
raise ValueError()
try:
@@ -77,9 +76,6 @@ def _precheck_image(image, opts):
def _resize_image(image, opts):
"""Resize image."""
- from PIL import Image
- import io
-
try:
img = _precheck_image(image, opts)
except ValueError:
@@ -125,8 +121,6 @@ def _resize_image(image, opts):
def _crop_image(image, opts):
"""Crop image."""
- import io
-
try:
img = _precheck_image(image, opts)
except ValueError:
diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json
index c67fd4afc09..e3f62514801 100644
--- a/homeassistant/components/proxy/manifest.json
+++ b/homeassistant/components/proxy/manifest.json
@@ -3,7 +3,7 @@
"name": "Proxy",
"documentation": "https://www.home-assistant.io/integrations/proxy",
"requirements": [
- "pillow==6.1.0"
+ "pillow==6.2.0"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py
index 60635bba525..205059be608 100644
--- a/homeassistant/components/ps4/__init__.py
+++ b/homeassistant/components/ps4/__init__.py
@@ -3,8 +3,8 @@ import logging
import os
import voluptuous as vol
-from pyps4_homeassistant.ddp import async_create_ddp_endpoint
-from pyps4_homeassistant.media_art import COUNTRIES
+from pyps4_2ndscreen.ddp import async_create_ddp_endpoint
+from pyps4_2ndscreen.media_art import COUNTRIES
from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_TYPE,
@@ -172,12 +172,8 @@ def load_games(hass: HomeAssistantType) -> dict:
_LOGGER.error("Games file was not parsed correctly")
games = {}
- # If file does not exist, create empty file.
- if not os.path.isfile(g_file):
- _LOGGER.info("Creating PS4 Games File")
- games = {}
- save_games(hass, games)
- else:
+ # If file exists
+ if os.path.isfile(g_file):
games = _reformat_data(hass, games)
return games
diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py
index a4b74077793..44523aea85a 100644
--- a/homeassistant/components/ps4/config_flow.py
+++ b/homeassistant/components/ps4/config_flow.py
@@ -2,6 +2,9 @@
from collections import OrderedDict
import logging
+from pyps4_2ndscreen.errors import CredentialTimeout
+from pyps4_2ndscreen.helpers import Helper
+from pyps4_2ndscreen.media_art import COUNTRIES
import voluptuous as vol
from homeassistant import config_entries
@@ -37,8 +40,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
def __init__(self):
"""Initialize the config flow."""
- from pyps4_homeassistant import Helper
-
self.helper = Helper()
self.creds = None
self.name = None
@@ -61,8 +62,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
async def async_step_creds(self, user_input=None):
"""Return PS4 credentials from 2nd Screen App."""
- from pyps4_homeassistant.errors import CredentialTimeout
-
errors = {}
if user_input is not None:
try:
@@ -103,8 +102,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
async def async_step_link(self, user_input=None):
"""Prompt user input. Create or edit entry."""
- from pyps4_homeassistant.media_art import COUNTRIES
-
regions = sorted(COUNTRIES.keys())
default_region = None
errors = {}
diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json
index 98a14d877e8..361711c400c 100644
--- a/homeassistant/components/ps4/manifest.json
+++ b/homeassistant/components/ps4/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ps4",
"requirements": [
- "pyps4-homeassistant==0.8.7"
+ "pyps4-2ndscreen==1.0.1"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py
index e1ec32ddd1f..3e8b667cd13 100644
--- a/homeassistant/components/ps4/media_player.py
+++ b/homeassistant/components/ps4/media_player.py
@@ -2,8 +2,8 @@
import logging
import asyncio
-import pyps4_homeassistant.ps4 as pyps4
-from pyps4_homeassistant.errors import NotReady
+import pyps4_2ndscreen.ps4 as pyps4
+from pyps4_2ndscreen.errors import NotReady
from homeassistant.core import callback
from homeassistant.components.media_player import ENTITY_IMAGE_URL, MediaPlayerDevice
@@ -254,7 +254,7 @@ class PS4Device(MediaPlayerDevice):
async def async_get_title_data(self, title_id, name):
"""Get PS Store Data."""
- from pyps4_homeassistant.errors import PSDataIncomplete
+ from pyps4_2ndscreen.errors import PSDataIncomplete
app_name = None
art = None
diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py
index 869987bfe4b..55cef1405d9 100644
--- a/homeassistant/components/ptvsd/__init__.py
+++ b/homeassistant/components/ptvsd/__init__.py
@@ -4,9 +4,9 @@ Enable ptvsd debugger to attach to HA.
Attach ptvsd debugger by default to port 5678.
"""
+from asyncio import Event
import logging
from threading import Thread
-from asyncio import Event
import voluptuous as vol
@@ -36,7 +36,11 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up ptvsd debugger."""
- import ptvsd
+
+ # This is a local import, since importing this at the top, will cause
+ # ptvsd to hook into `sys.settrace`. So does `coverage` to generate
+ # coverage, resulting in a battle and incomplete code test coverage.
+ import ptvsd # pylint: disable=import-outside-toplevel
conf = config[DOMAIN]
host = conf[CONF_HOST]
diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py
index 70738965340..76c1e14e5a5 100644
--- a/homeassistant/components/pushbullet/notify.py
+++ b/homeassistant/components/pushbullet/notify.py
@@ -2,6 +2,9 @@
import logging
import mimetypes
+from pushbullet import PushBullet
+from pushbullet import InvalidKeyError
+from pushbullet import PushError
import voluptuous as vol
from homeassistant.const import CONF_API_KEY
@@ -28,8 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}
def get_service(hass, config, discovery_info=None):
"""Get the Pushbullet notification service."""
- from pushbullet import PushBullet
- from pushbullet import InvalidKeyError
try:
pushbullet = PushBullet(config[CONF_API_KEY])
@@ -124,7 +125,6 @@ class PushBulletNotificationService(BaseNotificationService):
def _push_data(self, message, title, data, pusher, email=None):
"""Create the message content."""
- from pushbullet import PushError
if data is None:
data = {}
diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py
index 3ed53fb01f6..600b38b6eaf 100644
--- a/homeassistant/components/pushbullet/sensor.py
+++ b/homeassistant/components/pushbullet/sensor.py
@@ -1,6 +1,10 @@
"""Pushbullet platform for sensor component."""
import logging
+import threading
+from pushbullet import PushBullet
+from pushbullet import InvalidKeyError
+from pushbullet import Listener
import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS
@@ -35,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Pushbullet Sensor platform."""
- from pushbullet import PushBullet
- from pushbullet import InvalidKeyError
try:
pushbullet = PushBullet(config.get(CONF_API_KEY))
@@ -95,7 +97,6 @@ class PushBulletNotificationProvider:
def __init__(self, pb):
"""Start to retrieve pushes from the given Pushbullet instance."""
- import threading
self.pushbullet = pb
self._data = None
@@ -123,7 +124,6 @@ class PushBulletNotificationProvider:
Spawn a new Listener and links it to self.on_push.
"""
- from pushbullet import Listener
self.listener = Listener(account=self.pushbullet, on_push=self.on_push)
_LOGGER.debug("Getting pushes")
diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py
index 83da9a657fe..3f78897838d 100644
--- a/homeassistant/components/pushover/notify.py
+++ b/homeassistant/components/pushover/notify.py
@@ -1,7 +1,9 @@
"""Pushover platform for notify component."""
import logging
+import requests
import voluptuous as vol
+from pushover import InitError, Client, RequestError
from homeassistant.const import CONF_API_KEY
import homeassistant.helpers.config_validation as cv
@@ -28,8 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the Pushover notification service."""
- from pushover import InitError
-
try:
return PushoverNotificationService(
hass, config[CONF_USER_KEY], config[CONF_API_KEY]
@@ -44,8 +44,6 @@ class PushoverNotificationService(BaseNotificationService):
def __init__(self, hass, user_key, api_token):
"""Initialize the service."""
- from pushover import Client
-
self._hass = hass
self._user_key = user_key
self._api_token = api_token
@@ -53,8 +51,6 @@ class PushoverNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
- from pushover import RequestError
-
# Make a copy and use empty dict if necessary
data = dict(kwargs.get(ATTR_DATA) or {})
@@ -65,8 +61,6 @@ class PushoverNotificationService(BaseNotificationService):
# If attachment is a URL, use requests to open it as a stream.
if data[ATTR_ATTACHMENT].startswith("http"):
try:
- import requests
-
response = requests.get(
data[ATTR_ATTACHMENT], stream=True, timeout=5
)
diff --git a/homeassistant/components/qrcode/__init__.py b/homeassistant/components/qrcode/__init__.py
index bcc1985a2dc..55b1a2a9d6b 100644
--- a/homeassistant/components/qrcode/__init__.py
+++ b/homeassistant/components/qrcode/__init__.py
@@ -1 +1 @@
-"""The qrcode component."""
+"""The QR code component."""
diff --git a/homeassistant/components/qrcode/image_processing.py b/homeassistant/components/qrcode/image_processing.py
index 5e1b7c11b25..018f074a6d2 100644
--- a/homeassistant/components/qrcode/image_processing.py
+++ b/homeassistant/components/qrcode/image_processing.py
@@ -1,15 +1,20 @@
-"""Support for the QR image processing."""
-from homeassistant.core import split_entity_id
+"""Support for the QR code image processing."""
+import io
+
+from PIL import Image
+from pyzbar import pyzbar
+
from homeassistant.components.image_processing import (
- ImageProcessingEntity,
- CONF_SOURCE,
CONF_ENTITY_ID,
CONF_NAME,
+ CONF_SOURCE,
+ ImageProcessingEntity,
)
+from homeassistant.core import split_entity_id
def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the demo image processing platform."""
+ """Set up the QR code image processing platform."""
# pylint: disable=unused-argument
entities = []
for camera in config[CONF_SOURCE]:
@@ -19,7 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class QrEntity(ImageProcessingEntity):
- """QR image processing entity."""
+ """A QR image processing entity."""
def __init__(self, camera_entity, name):
"""Initialize QR image processing entity."""
@@ -49,10 +54,6 @@ class QrEntity(ImageProcessingEntity):
def process_image(self, image):
"""Process image."""
- import io
- from pyzbar import pyzbar
- from PIL import Image
-
stream = io.BytesIO(image)
img = Image.open(stream)
diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json
index 87e16f62987..a3130070cc3 100644
--- a/homeassistant/components/qrcode/manifest.json
+++ b/homeassistant/components/qrcode/manifest.json
@@ -3,7 +3,7 @@
"name": "Qrcode",
"documentation": "https://www.home-assistant.io/integrations/qrcode",
"requirements": [
- "pillow==6.1.0",
+ "pillow==6.2.0",
"pyzbar==0.1.7"
],
"dependencies": [],
diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json
index 6248890389d..afaa55424d2 100644
--- a/homeassistant/components/rainmachine/.translations/ru.json
+++ b/homeassistant/components/rainmachine/.translations/ru.json
@@ -2,12 +2,12 @@
"config": {
"error": {
"identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
- "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435"
+ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
},
"step": {
"user": {
"data": {
- "ip_address": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441",
+ "ip_address": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442"
},
diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py
index 963c2624362..8b7ea0a38d7 100644
--- a/homeassistant/components/raspihats/__init__.py
+++ b/homeassistant/components/raspihats/__init__.py
@@ -120,7 +120,9 @@ class I2CHatsManager(threading.Thread):
with self._lock:
i2c_hat = self._i2c_hats.get(address)
if i2c_hat is None:
- # pylint: disable=import-error,no-name-in-module
+ # This is a Pi module and can't be installed in CI without
+ # breaking the build.
+ # pylint: disable=import-outside-toplevel,import-error
import raspihats.i2c_hats as module
constructor = getattr(module, board)
@@ -138,7 +140,9 @@ class I2CHatsManager(threading.Thread):
def run(self):
"""Keep alive for I2C-HATs."""
- # pylint: disable=import-error,no-name-in-module
+ # This is a Pi module and can't be installed in CI without
+ # breaking the build.
+ # pylint: disable=import-outside-toplevel,import-error
from raspihats.i2c_hats import ResponseException
_LOGGER.info(log_message(self, "starting"))
@@ -199,7 +203,9 @@ class I2CHatsManager(threading.Thread):
def read_di(self, address, channel):
"""Read a value from a I2C-HAT digital input."""
- # pylint: disable=import-error,no-name-in-module
+ # This is a Pi module and can't be installed in CI without
+ # breaking the build.
+ # pylint: disable=import-outside-toplevel,import-error
from raspihats.i2c_hats import ResponseException
with self._lock:
@@ -212,7 +218,9 @@ class I2CHatsManager(threading.Thread):
def write_dq(self, address, channel, value):
"""Write a value to a I2C-HAT digital output."""
- # pylint: disable=import-error,no-name-in-module
+ # This is a Pi module and can't be installed in CI without
+ # breaking the build.
+ # pylint: disable=import-outside-toplevel,import-error
from raspihats.i2c_hats import ResponseException
with self._lock:
@@ -224,7 +232,9 @@ class I2CHatsManager(threading.Thread):
def read_dq(self, address, channel):
"""Read a value from a I2C-HAT digital output."""
- # pylint: disable=import-error,no-name-in-module
+ # This is a Pi module and can't be installed in CI without
+ # breaking the build.
+ # pylint: disable=import-outside-toplevel,import-error
from raspihats.i2c_hats import ResponseException
with self._lock:
diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py
index 118b6fb3709..17496f3d361 100644
--- a/homeassistant/components/recollect_waste/sensor.py
+++ b/homeassistant/components/recollect_waste/sensor.py
@@ -1,11 +1,12 @@
"""Support for Recollect Waste curbside collection pickup."""
import logging
+import recollect_waste
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -29,9 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Recollect Waste platform."""
- import recollect_waste
-
- # pylint: disable=no-member
client = recollect_waste.RecollectWasteClient(
config[CONF_PLACE_ID], config[CONF_SERVICE_ID]
)
@@ -40,7 +38,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
# with given place_id and service_id.
try:
client.get_next_pickup()
- # pylint: disable=no-member
except recollect_waste.RecollectWasteException as ex:
_LOGGER.error("Recollect Waste platform error. %s", ex)
return
@@ -85,8 +82,6 @@ class RecollectWasteSensor(Entity):
def update(self):
"""Update device state."""
- import recollect_waste
-
try:
pickup_event = self.client.get_next_pickup()
self._state = pickup_event.event_date
@@ -96,6 +91,5 @@ class RecollectWasteSensor(Entity):
ATTR_AREA_NAME: pickup_event.area_name,
}
)
- # pylint: disable=no-member
except recollect_waste.RecollectWasteException as ex:
_LOGGER.error("Recollect Waste platform error. %s", ex)
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index b36e0a34fa4..10b1d04304f 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -8,8 +8,14 @@ import queue
import threading
import time
from typing import Any, Dict, Optional
+from sqlite3 import Connection
import voluptuous as vol
+from sqlalchemy import exc, create_engine
+from sqlalchemy.engine import Engine
+from sqlalchemy.event import listens_for
+from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy.pool import StaticPool
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -23,6 +29,7 @@ from homeassistant.const import (
EVENT_TIME_CHANGED,
MATCH_ALL,
)
+from homeassistant.components import persistent_notification
from homeassistant.core import CoreState, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import generate_filter
@@ -31,6 +38,7 @@ import homeassistant.util.dt as dt_util
from . import migration, purge
from .const import DATA_INSTANCE
+from .models import Base, Events, RecorderRuns, States
from .util import session_scope
_LOGGER = logging.getLogger(__name__)
@@ -100,11 +108,9 @@ def run_information(hass, point_in_time: Optional[datetime] = None):
There is also the run that covers point_in_time.
"""
- from . import models
-
ins = hass.data[DATA_INSTANCE]
- recorder_runs = models.RecorderRuns
+ recorder_runs = RecorderRuns
if point_in_time is None or point_in_time > ins.recording_start:
return ins.run_info
@@ -208,10 +214,6 @@ class Recorder(threading.Thread):
def run(self):
"""Start processing events to save."""
- from .models import States, Events
- from homeassistant.components import persistent_notification
- from sqlalchemy import exc
-
tries = 1
connected = False
@@ -393,18 +395,10 @@ class Recorder(threading.Thread):
def _setup_connection(self):
"""Ensure database is ready to fly."""
- from sqlalchemy import create_engine, event
- from sqlalchemy.engine import Engine
- from sqlalchemy.orm import scoped_session
- from sqlalchemy.orm import sessionmaker
- from sqlite3 import Connection
-
- from . import models
-
kwargs = {}
# pylint: disable=unused-variable
- @event.listens_for(Engine, "connect")
+ @listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
"""Set sqlite's WAL mode."""
if isinstance(dbapi_connection, Connection):
@@ -416,8 +410,6 @@ class Recorder(threading.Thread):
dbapi_connection.isolation_level = old_isolation
if self.db_url == "sqlite://" or ":memory:" in self.db_url:
- from sqlalchemy.pool import StaticPool
-
kwargs["connect_args"] = {"check_same_thread": False}
kwargs["poolclass"] = StaticPool
kwargs["pool_reset_on_return"] = None
@@ -428,7 +420,7 @@ class Recorder(threading.Thread):
self.engine.dispose()
self.engine = create_engine(self.db_url, **kwargs)
- models.Base.metadata.create_all(self.engine)
+ Base.metadata.create_all(self.engine)
self.get_session = scoped_session(sessionmaker(bind=self.engine))
def _close_connection(self):
@@ -439,8 +431,6 @@ class Recorder(threading.Thread):
def _setup_run(self):
"""Log the start of the current run."""
- from .models import RecorderRuns
-
with session_scope(session=self.get_session()) as session:
for run in session.query(RecorderRuns).filter_by(end=None):
run.closed_incorrect = True
diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json
index cdb09d66067..1f00cf89f15 100644
--- a/homeassistant/components/recorder/manifest.json
+++ b/homeassistant/components/recorder/manifest.json
@@ -3,8 +3,8 @@
"name": "Recorder",
"documentation": "https://www.home-assistant.io/integrations/recorder",
"requirements": [
- "sqlalchemy==1.3.8"
+ "sqlalchemy==1.3.10"
],
"dependencies": [],
"codeowners": []
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py
index 3de0430d8f3..33a01ea1ac0 100644
--- a/homeassistant/components/recorder/migration.py
+++ b/homeassistant/components/recorder/migration.py
@@ -2,6 +2,11 @@
import logging
import os
+from sqlalchemy import Table, text
+from sqlalchemy.engine import reflection
+from sqlalchemy.exc import OperationalError, SQLAlchemyError
+
+from .models import SchemaChanges, SCHEMA_VERSION, Base
from .util import session_scope
_LOGGER = logging.getLogger(__name__)
@@ -10,8 +15,6 @@ PROGRESS_FILE = ".migration_progress"
def migrate_schema(instance):
"""Check if the schema needs to be upgraded."""
- from .models import SchemaChanges, SCHEMA_VERSION
-
progress_path = instance.hass.config.path(PROGRESS_FILE)
with session_scope(session=instance.get_session()) as session:
@@ -60,11 +63,7 @@ def _create_index(engine, table_name, index_name):
The index name should match the name given for the index
within the table definition described in the models
"""
- from sqlalchemy import Table
- from sqlalchemy.exc import OperationalError
- from . import models
-
- table = Table(table_name, models.Base.metadata)
+ table = Table(table_name, Base.metadata)
_LOGGER.debug("Looking up index for table %s", table_name)
# Look up the index object by name from the table is the models
index = next(idx for idx in table.indexes if idx.name == index_name)
@@ -99,9 +98,6 @@ def _drop_index(engine, table_name, index_name):
string here is generated from the method parameters without sanitizing.
DO NOT USE THIS FUNCTION IN ANY OPERATION THAT TAKES USER INPUT.
"""
- from sqlalchemy import text
- from sqlalchemy.exc import SQLAlchemyError
-
_LOGGER.debug("Dropping index %s from table %s", index_name, table_name)
success = False
@@ -159,9 +155,6 @@ def _drop_index(engine, table_name, index_name):
def _add_columns(engine, table_name, columns_def):
"""Add columns to a table."""
- from sqlalchemy import text
- from sqlalchemy.exc import OperationalError
-
_LOGGER.info(
"Adding columns %s to table %s. Note: this can take several "
"minutes on large databases and slow computers. Please "
@@ -277,9 +270,6 @@ def _inspect_schema_version(engine, session):
version 1 are present to make the determination. Eventually this logic
can be removed and we can assume a new db is being created.
"""
- from sqlalchemy.engine import reflection
- from .models import SchemaChanges, SCHEMA_VERSION
-
inspector = reflection.Inspector.from_engine(engine)
indexes = inspector.get_indexes("events")
diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py
index 12f4a9065af..b512bfc8204 100644
--- a/homeassistant/components/recorder/models.py
+++ b/homeassistant/components/recorder/models.py
@@ -15,6 +15,7 @@ from sqlalchemy import (
distinct,
)
from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm.session import Session
import homeassistant.util.dt as dt_util
from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id
@@ -164,8 +165,6 @@ class RecorderRuns(Base): # type: ignore
Specify point_in_time if you want to know which existed at that point
in time inside the run.
"""
- from sqlalchemy.orm.session import Session
-
session = Session.object_session(self)
assert session is not None, "RecorderRuns need to be persisted"
diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py
index 81426d65f06..089476245fe 100644
--- a/homeassistant/components/recorder/purge.py
+++ b/homeassistant/components/recorder/purge.py
@@ -2,7 +2,10 @@
from datetime import timedelta
import logging
+from sqlalchemy.exc import SQLAlchemyError
+
import homeassistant.util.dt as dt_util
+from .models import Events, States
from .util import session_scope
@@ -11,9 +14,6 @@ _LOGGER = logging.getLogger(__name__)
def purge_old_data(instance, purge_days, repack):
"""Purge events and states older than purge_days ago."""
- from .models import States, Events
- from sqlalchemy.exc import SQLAlchemyError
-
purge_before = dt_util.utcnow() - timedelta(days=purge_days)
_LOGGER.debug("Purging events before %s", purge_before)
@@ -34,8 +34,8 @@ def purge_old_data(instance, purge_days, repack):
_LOGGER.debug("Deleted %s events", deleted_rows)
# Execute sqlite vacuum command to free up space on disk
- if repack and instance.engine.driver == "pysqlite":
- _LOGGER.debug("Vacuuming SQLite to free space")
+ if repack and instance.engine.driver in ("pysqlite", "postgresql"):
+ _LOGGER.debug("Vacuuming SQL DB to free space")
instance.engine.execute("VACUUM")
except SQLAlchemyError as err:
diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py
index 674d687ec14..8cfcafea79d 100644
--- a/homeassistant/components/recorder/util.py
+++ b/homeassistant/components/recorder/util.py
@@ -3,6 +3,8 @@ from contextlib import contextmanager
import logging
import time
+from sqlalchemy.exc import OperationalError, SQLAlchemyError
+
from .const import DATA_INSTANCE
_LOGGER = logging.getLogger(__name__)
@@ -37,8 +39,6 @@ def session_scope(*, hass=None, session=None):
def commit(session, work):
"""Commit & retry work: Either a model or in a function."""
- import sqlalchemy.exc
-
for _ in range(0, RETRIES):
try:
if callable(work):
@@ -47,7 +47,7 @@ def commit(session, work):
session.add(work)
session.commit()
return True
- except sqlalchemy.exc.OperationalError as err:
+ except OperationalError as err:
_LOGGER.error("Error executing query: %s", err)
session.rollback()
time.sleep(QUERY_RETRY_WAIT)
@@ -59,8 +59,6 @@ def execute(qry):
This method also retries a few times in the case of stale connections.
"""
- from sqlalchemy.exc import SQLAlchemyError
-
for tryno in range(0, RETRIES):
try:
timer_start = time.perf_counter()
diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py
index 61cb319fd11..b7d36010714 100644
--- a/homeassistant/components/rejseplanen/sensor.py
+++ b/homeassistant/components/rejseplanen/sensor.py
@@ -7,17 +7,18 @@ https://help.rejseplanen.dk/hc/en-us/articles/214174465-Rejseplanen-s-API
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.rejseplanen/
"""
+from datetime import datetime, timedelta
import logging
-from datetime import timedelta, datetime
from operator import itemgetter
+import rjpl
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-import homeassistant.util.dt as dt_util
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
+import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -166,8 +167,6 @@ class PublicTransportData:
def update(self):
"""Get the latest data from rejseplanen."""
- import rjpl
-
self.info = []
def intersection(lst1, lst2):
diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py
index c92a246da14..fdfbdfd5cdc 100644
--- a/homeassistant/components/remember_the_milk/__init__.py
+++ b/homeassistant/components/remember_the_milk/__init__.py
@@ -3,6 +3,7 @@ import json
import logging
import os
+from rtmapi import Rtm, RtmRequestFailedException
import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME, CONF_TOKEN, STATE_OK
@@ -102,8 +103,6 @@ def _create_instance(
def _register_new_account(
hass, account_name, api_key, shared_secret, stored_rtm_config, component
):
- from rtmapi import Rtm
-
request_id = None
configurator = hass.components.configurator
api = Rtm(api_key, shared_secret, "write", None)
@@ -240,14 +239,12 @@ class RememberTheMilk(Entity):
def __init__(self, name, api_key, shared_secret, token, rtm_config):
"""Create new instance of Remember The Milk component."""
- import rtmapi
-
self._name = name
self._api_key = api_key
self._shared_secret = shared_secret
self._token = token
self._rtm_config = rtm_config
- self._rtm_api = rtmapi.Rtm(api_key, shared_secret, "delete", token)
+ self._rtm_api = Rtm(api_key, shared_secret, "delete", token)
self._token_valid = None
self._check_token()
_LOGGER.debug("Instance created for account %s", self._name)
@@ -277,8 +274,6 @@ class RememberTheMilk(Entity):
e.g. "my task #some_tag ^today" will add tag "some_tag" and set the
due date to today.
"""
- import rtmapi
-
try:
task_name = call.data.get(CONF_NAME)
hass_id = call.data.get(CONF_ID)
@@ -316,7 +311,7 @@ class RememberTheMilk(Entity):
self.name,
task_name,
)
- except rtmapi.RtmRequestFailedException as rtm_exception:
+ except RtmRequestFailedException as rtm_exception:
_LOGGER.error(
"Error creating new Remember The Milk task for " "account %s: %s",
self._name,
@@ -327,8 +322,6 @@ class RememberTheMilk(Entity):
def complete_task(self, call):
"""Complete a task that was previously created by this component."""
- import rtmapi
-
hass_id = call.data.get(CONF_ID)
rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id)
if rtm_id is None:
@@ -352,7 +345,7 @@ class RememberTheMilk(Entity):
_LOGGER.debug(
"Completed task with id %s in account %s", hass_id, self._name
)
- except rtmapi.RtmRequestFailedException as rtm_exception:
+ except RtmRequestFailedException as rtm_exception:
_LOGGER.error(
"Error creating new Remember The Milk task for " "account %s: %s",
self._name,
diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json
index 4979fe29e0e..6ec9dc6f8f4 100644
--- a/homeassistant/components/remember_the_milk/manifest.json
+++ b/homeassistant/components/remember_the_milk/manifest.json
@@ -3,7 +3,7 @@
"name": "Remember the milk",
"documentation": "https://www.home-assistant.io/integrations/remember_the_milk",
"requirements": [
- "RtmAPI==0.7.0",
+ "RtmAPI==0.7.2",
"httplib2==0.10.3"
],
"dependencies": ["configurator"],
diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py
index 6f72a6b7ddc..12975baca91 100644
--- a/homeassistant/components/repetier/__init__.py
+++ b/homeassistant/components/repetier/__init__.py
@@ -1,7 +1,8 @@
"""Support for Repetier-Server sensors."""
-import logging
from datetime import timedelta
+import logging
+import pyrepetier
import voluptuous as vol
from homeassistant.const import (
@@ -160,8 +161,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Repetier Server component."""
- import pyrepetier
-
hass.data[REPETIER_API] = {}
for repetier in config[DOMAIN]:
diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py
index 01d974e7006..41adb855903 100644
--- a/homeassistant/components/rest/sensor.py
+++ b/homeassistant/components/rest/sensor.py
@@ -16,6 +16,7 @@ from homeassistant.const import (
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_RESOURCE,
+ CONF_RESOURCE_TEMPLATE,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
CONF_TIMEOUT,
@@ -42,7 +43,8 @@ METHODS = ["POST", "GET"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
- vol.Required(CONF_RESOURCE): cv.url,
+ vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url,
+ vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template,
vol.Optional(CONF_AUTHENTICATION): vol.In(
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
),
@@ -62,11 +64,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
}
)
+PLATFORM_SCHEMA = vol.All(
+ cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA
+)
+
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the RESTful sensor."""
name = config.get(CONF_NAME)
resource = config.get(CONF_RESOURCE)
+ resource_template = config.get(CONF_RESOURCE_TEMPLATE)
method = config.get(CONF_METHOD)
payload = config.get(CONF_PAYLOAD)
verify_ssl = config.get(CONF_VERIFY_SSL)
@@ -83,6 +90,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if value_template is not None:
value_template.hass = hass
+ if resource_template is not None:
+ resource_template.hass = hass
+ resource = resource_template.render()
+
if username and password:
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
auth = HTTPDigestAuth(username, password)
@@ -108,6 +119,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
value_template,
json_attrs,
force_update,
+ resource_template,
)
],
True,
@@ -127,6 +139,7 @@ class RestSensor(Entity):
value_template,
json_attrs,
force_update,
+ resource_template,
):
"""Initialize the REST sensor."""
self._hass = hass
@@ -139,6 +152,7 @@ class RestSensor(Entity):
self._json_attrs = json_attrs
self._attributes = None
self._force_update = force_update
+ self._resource_template = resource_template
@property
def name(self):
@@ -172,6 +186,9 @@ class RestSensor(Entity):
def update(self):
"""Get the latest data from REST API and update the state."""
+ if self._resource_template is not None:
+ self.rest.set_url(self._resource_template.render())
+
self.rest.update()
value = self.rest.data
@@ -217,6 +234,10 @@ class RestData:
self._timeout = timeout
self.data = None
+ def set_url(self, url):
+ """Set url."""
+ self._request.prepare_url(url, None)
+
def update(self):
"""Get the latest data from REST service with provided method."""
_LOGGER.debug("Updating from %s", self._request.url)
diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py
index 1607000e8d9..223dc7da7cc 100644
--- a/homeassistant/components/rest_command/__init__.py
+++ b/homeassistant/components/rest_command/__init__.py
@@ -28,7 +28,7 @@ DEFAULT_TIMEOUT = 10
DEFAULT_METHOD = "get"
DEFAULT_VERIFY_SSL = True
-SUPPORT_REST_METHODS = ["get", "post", "put", "delete"]
+SUPPORT_REST_METHODS = ["get", "patch", "post", "put", "delete"]
CONF_CONTENT_TYPE = "content_type"
diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py
index c218bc271ce..b3e1d2b16b7 100644
--- a/homeassistant/components/rflink/__init__.py
+++ b/homeassistant/components/rflink/__init__.py
@@ -2,8 +2,10 @@
import asyncio
from collections import defaultdict
import logging
-import async_timeout
+import async_timeout
+from rflink.protocol import create_rflink_connection
+from serial import SerialException
import voluptuous as vol
from homeassistant.const import (
@@ -11,18 +13,18 @@ from homeassistant.const import (
CONF_COMMAND,
CONF_HOST,
CONF_PORT,
- STATE_ON,
EVENT_HOMEASSISTANT_STOP,
+ STATE_ON,
)
from homeassistant.core import CoreState, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.dispatcher import (
- async_dispatcher_send,
async_dispatcher_connect,
+ async_dispatcher_send,
)
+from homeassistant.helpers.entity import Entity
from homeassistant.helpers.restore_state import RestoreEntity
_LOGGER = logging.getLogger(__name__)
@@ -118,9 +120,6 @@ def identify_event_type(event):
async def async_setup(hass, config):
"""Set up the Rflink component."""
- from rflink.protocol import create_rflink_connection
- import serial
-
# Allow entities to register themselves by device_id to be looked up when
# new rflink events arrive to be handled
hass.data[DATA_ENTITY_LOOKUP] = {
@@ -239,7 +238,7 @@ async def async_setup(hass, config):
transport, protocol = await connection
except (
- serial.serialutil.SerialException,
+ SerialException,
ConnectionRefusedError,
TimeoutError,
OSError,
diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py
index 48484621c4d..aa0ef4f9c62 100644
--- a/homeassistant/components/rflink/sensor.py
+++ b/homeassistant/components/rflink/sensor.py
@@ -1,6 +1,7 @@
"""Support for Rflink sensors."""
import logging
+from rflink.parser import PACKET_FIELDS, UNITS
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -66,8 +67,6 @@ def lookup_unit_for_sensor_type(sensor_type):
Async friendly.
"""
- from rflink.parser import UNITS, PACKET_FIELDS
-
field_abbrev = {v: k for k, v in PACKET_FIELDS.items()}
return UNITS.get(field_abbrev.get(sensor_type))
diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py
index 79b3054ecf2..1515ce33c6e 100644
--- a/homeassistant/components/rfxtrx/__init__.py
+++ b/homeassistant/components/rfxtrx/__init__.py
@@ -1,7 +1,9 @@
"""Support for RFXtrx devices."""
+import binascii
from collections import OrderedDict
import logging
+import RFXtrx as rfxtrxmod
import voluptuous as vol
from homeassistant.const import (
@@ -12,8 +14,8 @@ from homeassistant.const import (
CONF_DEVICES,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
- TEMP_CELSIUS,
POWER_WATT,
+ TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -113,9 +115,6 @@ def setup(hass, config):
for subscriber in RECEIVED_EVT_SUBSCRIBERS:
subscriber(event)
- # Try to load the RFXtrx module.
- import RFXtrx as rfxtrxmod
-
device = config[DOMAIN][ATTR_DEVICE]
debug = config[DOMAIN][ATTR_DEBUG]
dummy_connection = config[DOMAIN][ATTR_DUMMY]
@@ -144,8 +143,6 @@ def setup(hass, config):
def get_rfx_object(packetid):
"""Return the RFXObject with the packetid."""
- import RFXtrx as rfxtrxmod
-
try:
binarypacket = bytearray.fromhex(packetid)
except ValueError:
@@ -167,7 +164,6 @@ def get_pt2262_deviceid(device_id, nb_data_bits):
"""Extract and return the address bits from a Lighting4/PT2262 packet."""
if nb_data_bits is None:
return
- import binascii
try:
data = bytearray.fromhex(device_id)
diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py
index 8f1c7e6fa55..6465dc36326 100644
--- a/homeassistant/components/rfxtrx/binary_sensor.py
+++ b/homeassistant/components/rfxtrx/binary_sensor.py
@@ -1,6 +1,7 @@
"""Support for RFXtrx binary sensors."""
import logging
+import RFXtrx as rfxtrxmod
import voluptuous as vol
from homeassistant.components import rfxtrx
@@ -54,8 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Binary Sensor platform to RFXtrx."""
- import RFXtrx as rfxtrxmod
-
sensors = []
for packet_id, entity in config[CONF_DEVICES].items():
diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py
index 3d420981685..7aff22bd124 100644
--- a/homeassistant/components/rfxtrx/cover.py
+++ b/homeassistant/components/rfxtrx/cover.py
@@ -1,4 +1,5 @@
"""Support for RFXtrx covers."""
+import RFXtrx as rfxtrxmod
import voluptuous as vol
from homeassistant.components import rfxtrx
@@ -34,8 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the RFXtrx cover."""
- import RFXtrx as rfxtrxmod
-
covers = rfxtrx.get_devices_from_config(config, RfxtrxCover)
add_entities(covers)
diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py
index d2d2e842c0a..a745a11388a 100644
--- a/homeassistant/components/rfxtrx/light.py
+++ b/homeassistant/components/rfxtrx/light.py
@@ -1,6 +1,7 @@
"""Support for RFXtrx lights."""
import logging
+import RFXtrx as rfxtrxmod
import voluptuous as vol
from homeassistant.components import rfxtrx
@@ -45,8 +46,6 @@ SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the RFXtrx platform."""
- import RFXtrx as rfxtrxmod
-
lights = rfxtrx.get_devices_from_config(config, RfxtrxLight)
add_entities(lights)
diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py
index 5941b00764b..5429943a7a6 100644
--- a/homeassistant/components/rfxtrx/sensor.py
+++ b/homeassistant/components/rfxtrx/sensor.py
@@ -1,6 +1,7 @@
"""Support for RFXtrx sensors."""
import logging
+from RFXtrx import SensorEvent
import voluptuous as vol
from homeassistant.components import rfxtrx
@@ -43,8 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the RFXtrx platform."""
- from RFXtrx import SensorEvent
-
sensors = []
for packet_id, entity_info in config[CONF_DEVICES].items():
event = rfxtrx.get_rfx_object(packet_id)
diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py
index bb5d5fe6d43..6d91b261a4f 100644
--- a/homeassistant/components/rfxtrx/switch.py
+++ b/homeassistant/components/rfxtrx/switch.py
@@ -1,6 +1,7 @@
"""Support for RFXtrx switches."""
import logging
+import RFXtrx as rfxtrxmod
import voluptuous as vol
from homeassistant.components import rfxtrx
@@ -38,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities_callback, discovery_info=None):
"""Set up the RFXtrx platform."""
- import RFXtrx as rfxtrxmod
-
# Add switch from config file
switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch)
add_entities_callback(switches)
diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json
index 1f06daf0623..ed33caa1264 100644
--- a/homeassistant/components/rmvtransport/manifest.json
+++ b/homeassistant/components/rmvtransport/manifest.json
@@ -3,7 +3,7 @@
"name": "Rmvtransport",
"documentation": "https://www.home-assistant.io/integrations/rmvtransport",
"requirements": [
- "PyRMVtransport==0.1.3"
+ "PyRMVtransport==0.2.9"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py
index d7d075f48f7..190274518cd 100644
--- a/homeassistant/components/rmvtransport/sensor.py
+++ b/homeassistant/components/rmvtransport/sensor.py
@@ -3,6 +3,8 @@ import asyncio
import logging
from datetime import timedelta
+from RMVtransport import RMVtransport
+from RMVtransport.rmvtransport import RMVtransportApiConnectionError
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -157,7 +159,7 @@ class RMVDepartureSensor(Entity):
"""Return the state attributes."""
try:
return {
- "next_departures": [val for val in self.data.departures[1:]],
+ "next_departures": self.data.departures[1:],
"direction": self.data.departures[0].get("direction"),
"line": self.data.departures[0].get("line"),
"minutes": self.data.departures[0].get("minutes"),
@@ -208,8 +210,6 @@ class RMVDepartureData:
timeout,
):
"""Initialize the sensor."""
- from RMVtransport import RMVtransport
-
self.station = None
self._station_id = station_id
self._destinations = destinations
@@ -224,14 +224,12 @@ class RMVDepartureData:
@Throttle(SCAN_INTERVAL)
async def async_update(self):
"""Update the connection data."""
- from RMVtransport.rmvtransport import RMVtransportApiConnectionError
-
try:
_data = await self.rmv.get_departures(
self._station_id,
products=self._products,
- directionId=self._direction,
- maxJourneys=50,
+ direction_id=self._direction,
+ max_journeys=50,
)
except RMVtransportApiConnectionError:
self.departures = []
diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py
index d69b0eddb71..12aca141510 100644
--- a/homeassistant/components/roku/media_player.py
+++ b/homeassistant/components/roku/media_player.py
@@ -20,7 +20,7 @@ from homeassistant.const import (
STATE_HOME,
STATE_IDLE,
STATE_PLAYING,
- STATE_OFF,
+ STATE_STANDBY,
)
DEFAULT_PORT = 8060
@@ -98,7 +98,7 @@ class RokuDevice(MediaPlayerDevice):
def state(self):
"""Return the state of the device."""
if self._power_state == "Off":
- return STATE_OFF
+ return STATE_STANDBY
if self.current_app is None:
return None
diff --git a/homeassistant/components/route53/services.yaml b/homeassistant/components/route53/services.yaml
index e69de29bb2d..20dbfa77f7a 100644
--- a/homeassistant/components/route53/services.yaml
+++ b/homeassistant/components/route53/services.yaml
@@ -0,0 +1,2 @@
+update_records:
+ description: Trigger update of records.
\ No newline at end of file
diff --git a/homeassistant/components/rpi_gpio/__init__.py b/homeassistant/components/rpi_gpio/__init__.py
index 31509614df4..ed7eefbb1fe 100644
--- a/homeassistant/components/rpi_gpio/__init__.py
+++ b/homeassistant/components/rpi_gpio/__init__.py
@@ -1,6 +1,8 @@
"""Support for controlling GPIO pins of a Raspberry Pi."""
import logging
+from RPi import GPIO # pylint: disable=import-error
+
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
_LOGGER = logging.getLogger(__name__)
@@ -10,7 +12,6 @@ DOMAIN = "rpi_gpio"
def setup(hass, config):
"""Set up the Raspberry PI GPIO component."""
- from RPi import GPIO # pylint: disable=import-error
def cleanup_gpio(event):
"""Stuff to do before stopping."""
@@ -27,34 +28,24 @@ def setup(hass, config):
def setup_output(port):
"""Set up a GPIO as output."""
- from RPi import GPIO # pylint: disable=import-error
-
GPIO.setup(port, GPIO.OUT)
def setup_input(port, pull_mode):
"""Set up a GPIO as input."""
- from RPi import GPIO # pylint: disable=import-error
-
GPIO.setup(port, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP)
def write_output(port, value):
"""Write a value to a GPIO."""
- from RPi import GPIO # pylint: disable=import-error
-
GPIO.output(port, value)
def read_input(port):
"""Read a value from a GPIO."""
- from RPi import GPIO # pylint: disable=import-error
-
return GPIO.input(port)
def edge_detect(port, event_callback, bounce):
"""Add detection for RISING and FALLING events."""
- from RPi import GPIO # pylint: disable=import-error
-
GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py
index 4acbed9a0fa..3e38da47eed 100644
--- a/homeassistant/components/rpi_gpio/binary_sensor.py
+++ b/homeassistant/components/rpi_gpio/binary_sensor.py
@@ -4,7 +4,7 @@ import logging
import voluptuous as vol
from homeassistant.components import rpi_gpio
-from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
+from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
from homeassistant.const import DEVICE_DEFAULT_NAME
import homeassistant.helpers.config_validation as cv
diff --git a/homeassistant/components/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py
index 83cc497324d..648171b9738 100644
--- a/homeassistant/components/rpi_gpio/cover.py
+++ b/homeassistant/components/rpi_gpio/cover.py
@@ -4,9 +4,9 @@ from time import sleep
import voluptuous as vol
-from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME
from homeassistant.components import rpi_gpio
+from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice
+from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json
index 0bee2baeddf..4d3ea4da010 100644
--- a/homeassistant/components/rpi_gpio/manifest.json
+++ b/homeassistant/components/rpi_gpio/manifest.json
@@ -3,7 +3,7 @@
"name": "Rpi gpio",
"documentation": "https://www.home-assistant.io/integrations/rpi_gpio",
"requirements": [
- "RPi.GPIO==0.6.5"
+ "RPi.GPIO==0.7.0"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/rpi_pfio/__init__.py b/homeassistant/components/rpi_pfio/__init__.py
index d51785daf9c..72be34e0f45 100644
--- a/homeassistant/components/rpi_pfio/__init__.py
+++ b/homeassistant/components/rpi_pfio/__init__.py
@@ -1,6 +1,8 @@
"""Support for controlling the PiFace Digital I/O module on a RPi."""
import logging
+import pifacedigitalio as PFIO
+
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
_LOGGER = logging.getLogger(__name__)
@@ -12,8 +14,6 @@ DATA_PFIO_LISTENER = "pfio_listener"
def setup(hass, config):
"""Set up the Raspberry PI PFIO component."""
- import pifacedigitalio as PFIO
-
pifacedigital = PFIO.PiFaceDigital()
hass.data[DATA_PFIO_LISTENER] = PFIO.InputEventListener(chip=pifacedigital)
@@ -33,22 +33,16 @@ def setup(hass, config):
def write_output(port, value):
"""Write a value to a PFIO."""
- import pifacedigitalio as PFIO
-
PFIO.digital_write(port, value)
def read_input(port):
"""Read a value from a PFIO."""
- import pifacedigitalio as PFIO
-
return PFIO.digital_read(port)
def edge_detect(hass, port, event_callback, settle):
"""Add detection for RISING and FALLING events."""
- import pifacedigitalio as PFIO
-
hass.data[DATA_PFIO_LISTENER].register(
port, PFIO.IODIR_BOTH, event_callback, settle_time=settle
)
diff --git a/homeassistant/components/rpi_pfio/binary_sensor.py b/homeassistant/components/rpi_pfio/binary_sensor.py
index 44da251732b..89d44a0e8db 100644
--- a/homeassistant/components/rpi_pfio/binary_sensor.py
+++ b/homeassistant/components/rpi_pfio/binary_sensor.py
@@ -3,8 +3,8 @@ import logging
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
from homeassistant.components import rpi_pfio
+from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME
import homeassistant.helpers.config_validation as cv
diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py
index 58624c758d9..21ac9eefdb2 100644
--- a/homeassistant/components/sabnzbd/sensor.py
+++ b/homeassistant/components/sabnzbd/sensor.py
@@ -49,6 +49,7 @@ class SabnzbdSensor(Entity):
"""Return the state of the sensor."""
return self._state
+ @property
def should_poll(self):
"""Don't poll. Will be updated by dispatcher signal."""
return False
diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json
index e42b37195a4..2dd701e9c7c 100644
--- a/homeassistant/components/saj/manifest.json
+++ b/homeassistant/components/saj/manifest.json
@@ -3,7 +3,7 @@
"name": "SAJ",
"documentation": "https://www.home-assistant.io/integrations/saj",
"requirements": [
- "pysaj==0.0.9"
+ "pysaj==0.0.12"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py
index fa06b2b9125..5605866908e 100644
--- a/homeassistant/components/saj/sensor.py
+++ b/homeassistant/components/saj/sensor.py
@@ -9,6 +9,9 @@ import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST,
+ CONF_PASSWORD,
+ CONF_TYPE,
+ CONF_USERNAME,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
@@ -31,6 +34,8 @@ MAX_INTERVAL = 300
UNIT_OF_MEASUREMENT_HOURS = "h"
+INVERTER_TYPES = ["ethernet", "wifi"]
+
SAJ_UNIT_MAPPINGS = {
"W": POWER_WATT,
"kWh": ENERGY_KILO_WATT_HOUR,
@@ -40,16 +45,24 @@ SAJ_UNIT_MAPPINGS = {
"": None,
}
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string})
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_TYPE, default=INVERTER_TYPES[0]): vol.In(INVERTER_TYPES),
+ vol.Inclusive(CONF_USERNAME, "credentials"): cv.string,
+ vol.Inclusive(CONF_PASSWORD, "credentials"): cv.string,
+ }
+)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up SAJ sensors."""
remove_interval_update = None
+ wifi = config[CONF_TYPE] == INVERTER_TYPES[1]
# Init all sensors
- sensor_def = pysaj.Sensors()
+ sensor_def = pysaj.Sensors(wifi)
# Use all sensors by default
hass_sensors = []
@@ -57,7 +70,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
for sensor in sensor_def:
hass_sensors.append(SAJsensor(sensor))
- saj = pysaj.SAJ(config[CONF_HOST])
+ kwargs = {}
+
+ if wifi:
+ kwargs["wifi"] = True
+ if config.get(CONF_USERNAME) and config.get(CONF_PASSWORD):
+ kwargs["username"] = config[CONF_USERNAME]
+ kwargs["password"] = config[CONF_PASSWORD]
+
+ try:
+ saj = pysaj.SAJ(config[CONF_HOST], **kwargs)
+ await saj.read(sensor_def)
+ except pysaj.UnauthorizedException:
+ _LOGGER.error("Username and/or password is wrong.")
+ return
+ except pysaj.UnexpectedResponseException as err:
+ _LOGGER.error(
+ "Error in SAJ, please check host/ip address. Original error: %s", err
+ )
+ return
async_add_entities(hass_sensors)
diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py
index 1f71a24c304..ec2dc3118a9 100644
--- a/homeassistant/components/scene/__init__.py
+++ b/homeassistant/components/scene/__init__.py
@@ -1,5 +1,4 @@
"""Allow users to set and activate scenes."""
-import asyncio
import importlib
import logging
@@ -7,7 +6,6 @@ import voluptuous as vol
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON
-from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.state import HASS_DOMAIN
@@ -69,20 +67,7 @@ async def async_setup(hass, config):
HA_DOMAIN, {"platform": "homeasistant", STATES: []}
)
- async def async_handle_scene_service(service):
- """Handle calls to the switch services."""
- target_scenes = await component.async_extract_from_service(service)
-
- tasks = [scene.async_activate() for scene in target_scenes]
- if tasks:
- await asyncio.wait(tasks)
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_TURN_ON,
- async_handle_scene_service,
- schema=ENTITY_SERVICE_SCHEMA,
- )
+ component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_activate")
return True
diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml
index ee255affe44..0f1e7103aaf 100644
--- a/homeassistant/components/scene/services.yaml
+++ b/homeassistant/components/scene/services.yaml
@@ -5,4 +5,18 @@ turn_on:
fields:
entity_id:
description: Name(s) of scenes to turn on
- example: 'scene.romantic'
+ example: "scene.romantic"
+
+reload:
+ description: Reload the scene configuration
+
+apply:
+ description: Activate a scene. Takes same data as the entities field from a single scene in the config.
+ fields:
+ entities:
+ description: The entities and the state that they need to be.
+ example:
+ light.kitchen: "on"
+ light.ceiling:
+ state: "on"
+ brightness: 80
diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json
index 989070900ca..5fdcca372b9 100644
--- a/homeassistant/components/scrape/manifest.json
+++ b/homeassistant/components/scrape/manifest.json
@@ -3,7 +3,7 @@
"name": "Scrape",
"documentation": "https://www.home-assistant.io/integrations/scrape",
"requirements": [
- "beautifulsoup4==4.8.0"
+ "beautifulsoup4==4.8.1"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py
index b6a6fdf4896..0bfb7351c88 100644
--- a/homeassistant/components/scrape/sensor.py
+++ b/homeassistant/components/scrape/sensor.py
@@ -1,6 +1,7 @@
"""Support for getting data from websites with scraping."""
import logging
+from bs4 import BeautifulSoup
import voluptuous as vol
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
@@ -124,8 +125,6 @@ class ScrapeSensor(Entity):
_LOGGER.error("Unable to retrieve data")
return
- from bs4 import BeautifulSoup
-
raw_data = BeautifulSoup(self.rest.data, "html.parser")
_LOGGER.debug(raw_data)
diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py
index cdd6af57617..46d2291cf81 100644
--- a/homeassistant/components/season/sensor.py
+++ b/homeassistant/components/season/sensor.py
@@ -2,6 +2,7 @@
import logging
from datetime import datetime
+import ephem
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -67,7 +68,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def get_season(date, hemisphere, season_tracking_type):
"""Calculate the current season."""
- import ephem
if hemisphere == "equator":
return None
diff --git a/homeassistant/components/sensor/.translations/ca.json b/homeassistant/components/sensor/.translations/ca.json
index 59db5a62f86..94d95e7ddf8 100644
--- a/homeassistant/components/sensor/.translations/ca.json
+++ b/homeassistant/components/sensor/.translations/ca.json
@@ -4,6 +4,7 @@
"is_battery_level": "Nivell de bateria de {entity_name}",
"is_humidity": "Humitat de {entity_name}",
"is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}",
+ "is_power": "Pot\u00e8ncia de {entity_name}",
"is_pressure": "Pressi\u00f3 de {entity_name}",
"is_signal_strength": "For\u00e7a del senyal de {entity_name}",
"is_temperature": "Temperatura de {entity_name}",
@@ -14,6 +15,7 @@
"battery_level": "Nivell de bateria de {entity_name}",
"humidity": "Humitat de {entity_name}",
"illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}",
+ "power": "Pot\u00e8ncia de {entity_name}",
"pressure": "Pressi\u00f3 de {entity_name}",
"signal_strength": "For\u00e7a del senyal de {entity_name}",
"temperature": "Temperatura de {entity_name}",
diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json
index 1f248099df3..bf28653c0ce 100644
--- a/homeassistant/components/sensor/.translations/de.json
+++ b/homeassistant/components/sensor/.translations/de.json
@@ -1,7 +1,10 @@
{
"device_automation": {
"condition_type": {
+ "is_battery_level": "{entity_name} Batteriestand",
"is_humidity": "{entity_name} Feuchtigkeit",
+ "is_illuminance": "{entity_name} Beleuchtungsst\u00e4rke",
+ "is_power": "{entity_name} Leistung",
"is_pressure": "{entity_name} Druck",
"is_signal_strength": "{entity_name} Signalst\u00e4rke",
"is_temperature": "{entity_name} Temperatur",
@@ -11,6 +14,8 @@
"trigger_type": {
"battery_level": "{entity_name} Batteriestatus",
"humidity": "{entity_name} Feuchtigkeit",
+ "illuminance": "{entity_name} Beleuchtungsst\u00e4rke",
+ "power": "{entity_name} Leistung",
"pressure": "{entity_name} Druck",
"signal_strength": "{entity_name} Signalst\u00e4rke",
"temperature": "{entity_name} Temperatur",
diff --git a/homeassistant/components/sensor/.translations/en.json b/homeassistant/components/sensor/.translations/en.json
index 7bbbe660feb..07411b885b8 100644
--- a/homeassistant/components/sensor/.translations/en.json
+++ b/homeassistant/components/sensor/.translations/en.json
@@ -1,26 +1,26 @@
{
"device_automation": {
"condition_type": {
- "is_battery_level": "{entity_name} battery level",
- "is_humidity": "{entity_name} humidity",
- "is_illuminance": "{entity_name} illuminance",
- "is_power": "{entity_name} power",
- "is_pressure": "{entity_name} pressure",
- "is_signal_strength": "{entity_name} signal strength",
- "is_temperature": "{entity_name} temperature",
- "is_timestamp": "{entity_name} timestamp",
- "is_value": "{entity_name} value"
+ "is_battery_level": "Current {entity_name} battery level",
+ "is_humidity": "Current {entity_name} humidity",
+ "is_illuminance": "Current {entity_name} illuminance",
+ "is_power": "Current {entity_name} power",
+ "is_pressure": "Current {entity_name} pressure",
+ "is_signal_strength": "Current {entity_name} signal strength",
+ "is_temperature": "Current {entity_name} temperature",
+ "is_timestamp": "Current {entity_name} timestamp",
+ "is_value": "Current {entity_name} value"
},
"trigger_type": {
- "battery_level": "{entity_name} battery level",
- "humidity": "{entity_name} humidity",
- "illuminance": "{entity_name} illuminance",
- "power": "{entity_name} power",
- "pressure": "{entity_name} pressure",
- "signal_strength": "{entity_name} signal strength",
- "temperature": "{entity_name} temperature",
- "timestamp": "{entity_name} timestamp",
- "value": "{entity_name} value"
+ "battery_level": "{entity_name} battery level changes",
+ "humidity": "{entity_name} humidity changes",
+ "illuminance": "{entity_name} illuminance changes",
+ "power": "{entity_name} power changes",
+ "pressure": "{entity_name} pressure changes",
+ "signal_strength": "{entity_name} signal strength changes",
+ "temperature": "{entity_name} temperature changes",
+ "timestamp": "{entity_name} timestamp changes",
+ "value": "{entity_name} value changes"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json
index 676a5aa413f..56725a59e21 100644
--- a/homeassistant/components/sensor/.translations/fr.json
+++ b/homeassistant/components/sensor/.translations/fr.json
@@ -1,24 +1,24 @@
{
"device_automation": {
"condition_type": {
- "is_battery_level": "{entity_name} niveau batterie",
- "is_humidity": "{entity_name} humidit\u00e9",
- "is_illuminance": "{entity_name} \u00e9clairement",
+ "is_battery_level": "Le niveau de la batterie de {entity_name}",
+ "is_humidity": "L'humidit\u00e9 de {entity_name}",
+ "is_illuminance": "L'\u00e9clairement de {entity_name}",
"is_power": "{entity_name} puissance",
"is_pressure": "{entity_name} pression",
"is_signal_strength": "{entity_name} force du signal",
- "is_temperature": "{entity_name} temp\u00e9rature",
+ "is_temperature": "La temp\u00e9rature de {entity_name}",
"is_timestamp": "{entity_name} horodatage",
"is_value": "{entity_name} valeur"
},
"trigger_type": {
- "battery_level": "{entity_name} niveau batterie",
- "humidity": "{entity_name} humidit\u00e9",
- "illuminance": "{entity_name} \u00e9clairement",
+ "battery_level": "Le niveau de la batterie de {entity_name}",
+ "humidity": "L'humidit\u00e9 de {entity_name}",
+ "illuminance": "L'\u00e9clairement de {entity_name}",
"power": "{entity_name} puissance",
"pressure": "{entity_name} pression",
"signal_strength": "{entity_name} force du signal",
- "temperature": "{entity_name} temp\u00e9rature",
+ "temperature": "La temp\u00e9rature de {entity_name}",
"timestamp": "{entity_name} horodatage",
"value": "{entity_name} valeur"
}
diff --git a/homeassistant/components/sensor/.translations/hu.json b/homeassistant/components/sensor/.translations/hu.json
new file mode 100644
index 00000000000..78ea3e5e89b
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/hu.json
@@ -0,0 +1,26 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_battery_level": "{entity_name} akku szint",
+ "is_humidity": "{entity_name} p\u00e1ratartalom",
+ "is_illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s",
+ "is_power": "{entity_name} teljes\u00edtm\u00e9ny",
+ "is_pressure": "{entity_name} nyom\u00e1s",
+ "is_signal_strength": "{entity_name} jeler\u0151ss\u00e9g",
+ "is_temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet",
+ "is_timestamp": "{entity_name} id\u0151b\u00e9lyeg",
+ "is_value": "{entity_name} \u00e9rt\u00e9k"
+ },
+ "trigger_type": {
+ "battery_level": "{entity_name} akku szint",
+ "humidity": "{entity_name} p\u00e1ratartalom",
+ "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s",
+ "power": "{entity_name} teljes\u00edtm\u00e9ny",
+ "pressure": "{entity_name} nyom\u00e1s",
+ "signal_strength": "{entity_name} jeler\u0151ss\u00e9g",
+ "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet",
+ "timestamp": "{entity_name} id\u0151b\u00e9lyeg",
+ "value": "{entity_name} \u00e9rt\u00e9k"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/ko.json b/homeassistant/components/sensor/.translations/ko.json
new file mode 100644
index 00000000000..d24a4058343
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/ko.json
@@ -0,0 +1,26 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9",
+ "is_humidity": "{entity_name} \uc2b5\ub3c4",
+ "is_illuminance": "{entity_name} \uc870\ub3c4",
+ "is_power": "{entity_name} \uc18c\ube44 \uc804\ub825",
+ "is_pressure": "{entity_name} \uc555\ub825",
+ "is_signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4",
+ "is_temperature": "{entity_name} \uc628\ub3c4",
+ "is_timestamp": "{entity_name} \uc2dc\uac01",
+ "is_value": "{entity_name} \uac12"
+ },
+ "trigger_type": {
+ "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9",
+ "humidity": "{entity_name} \uc2b5\ub3c4",
+ "illuminance": "{entity_name} \uc870\ub3c4",
+ "power": "{entity_name} \uc18c\ube44 \uc804\ub825",
+ "pressure": "{entity_name} \uc555\ub825",
+ "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4",
+ "temperature": "{entity_name} \uc628\ub3c4",
+ "timestamp": "{entity_name} \uc2dc\uac01",
+ "value": "{entity_name} \uac12"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/moon.hu.json b/homeassistant/components/sensor/.translations/moon.hu.json
index 0fcd02a6961..fff9f51f50d 100644
--- a/homeassistant/components/sensor/.translations/moon.hu.json
+++ b/homeassistant/components/sensor/.translations/moon.hu.json
@@ -4,9 +4,9 @@
"full_moon": "Telihold",
"last_quarter": "Utols\u00f3 negyed",
"new_moon": "\u00dajhold",
- "waning_crescent": "Fogy\u00f3 Hold (sarl\u00f3)",
- "waning_gibbous": "Fogy\u00f3 Hold",
- "waxing_crescent": "N\u00f6v\u0151 Hold (sarl\u00f3)",
- "waxing_gibbous": "N\u00f6v\u0151 Hold"
+ "waning_crescent": "Fogy\u00f3 holdsarl\u00f3",
+ "waning_gibbous": "Fogy\u00f3 hold",
+ "waxing_crescent": "N\u00f6v\u0151 holdsarl\u00f3",
+ "waxing_gibbous": "N\u00f6v\u0151 hold"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json
new file mode 100644
index 00000000000..33a7d837d55
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/nl.json
@@ -0,0 +1,26 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_battery_level": "Huidige batterijniveau {entity_name}",
+ "is_humidity": "{entity_name} vochtigheidsgraad",
+ "is_illuminance": "{entity_name} verlichtingssterkte",
+ "is_power": "{entity_name}\nvermogen",
+ "is_pressure": "{entity_name} druk",
+ "is_signal_strength": "{entity_name} signaalsterkte",
+ "is_temperature": "{entity_name} temperatuur",
+ "is_timestamp": "{entity_name} tijdstip",
+ "is_value": "{entity_name} waarde"
+ },
+ "trigger_type": {
+ "battery_level": "{entity_name} batterijniveau",
+ "humidity": "{entity_name} vochtigheidsgraad",
+ "illuminance": "{entity_name} verlichtingssterkte",
+ "power": "{entity_name} vermogen",
+ "pressure": "{entity_name} druk",
+ "signal_strength": "{entity_name} signaalsterkte",
+ "temperature": "{entity_name} temperatuur",
+ "timestamp": "{entity_name} tijdstip",
+ "value": "{entity_name} waarde"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json
index 5f5eeaacd11..6709e4eb28c 100644
--- a/homeassistant/components/sensor/.translations/no.json
+++ b/homeassistant/components/sensor/.translations/no.json
@@ -4,7 +4,7 @@
"is_battery_level": "{entity_name} batteriniv\u00e5",
"is_humidity": "{entity_name} fuktighet",
"is_illuminance": "{entity_name} belysningsstyrke",
- "is_power": "{entity_name} str\u00f8m",
+ "is_power": "{entity_name} effekt",
"is_pressure": "{entity_name} trykk",
"is_signal_strength": "{entity_name} signalstyrke",
"is_temperature": "{entity_name} temperatur",
diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json
index da1dcc1d6fd..68a3a0fecfd 100644
--- a/homeassistant/components/sensor/.translations/pl.json
+++ b/homeassistant/components/sensor/.translations/pl.json
@@ -3,7 +3,24 @@
"condition_type": {
"is_battery_level": "{entity_name} poziom na\u0142adowania baterii",
"is_humidity": "{entity_name} wilgotno\u015b\u0107",
- "is_temperature": "{entity_name} temperatura"
+ "is_illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}",
+ "is_power": "moc {entity_name}",
+ "is_pressure": "ci\u015bnienie {entity_name}",
+ "is_signal_strength": "si\u0142a sygna\u0142u {entity_name}",
+ "is_temperature": "temperatura {entity_name}",
+ "is_timestamp": "znacznik czasu {entity_name}",
+ "is_value": "warto\u015b\u0107 {entity_name}"
+ },
+ "trigger_type": {
+ "battery_level": "poziom baterii {entity_name}",
+ "humidity": "wilgotno\u015b\u0107 {entity_name}",
+ "illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}",
+ "power": "moc {entity_name}",
+ "pressure": "ci\u015bnienie {entity_name}",
+ "signal_strength": "si\u0142a sygna\u0142u {entity_name}",
+ "temperature": "temperatura {entity_name}",
+ "timestamp": "znacznik czasu {entity_name}",
+ "value": "warto\u015b\u0107 {entity_name}"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/pt.json b/homeassistant/components/sensor/.translations/pt.json
new file mode 100644
index 00000000000..801b22f0c45
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/pt.json
@@ -0,0 +1,21 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_humidity": "humidade {entity_name}",
+ "is_power": "pot\u00eancia {entity_name}",
+ "is_timestamp": "momento temporal de {entity_name}",
+ "is_value": "valor {entity_name}"
+ },
+ "trigger_type": {
+ "battery_level": "n\u00edvel da bateria {entity_name}",
+ "humidity": "humidade {entity_name}",
+ "illuminance": "ilumin\u00e2ncia {entity_name}",
+ "power": "pot\u00eancia {entity_name}",
+ "pressure": "press\u00e3o {entity_name}",
+ "signal_strength": "for\u00e7a do sinal de {entity_name}",
+ "temperature": "temperatura de {entity_name}",
+ "timestamp": "momento temporal de {entity_name}",
+ "value": "valor {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/ru.json b/homeassistant/components/sensor/.translations/ru.json
new file mode 100644
index 00000000000..8c70f41fcb7
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/ru.json
@@ -0,0 +1,15 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "illuminance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "pressure": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "signal_strength": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "temperature": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "timestamp": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py
new file mode 100644
index 00000000000..259fb5dbab9
--- /dev/null
+++ b/homeassistant/components/sensor/device_condition.py
@@ -0,0 +1,170 @@
+"""Provides device conditions for sensors."""
+from typing import Dict, List
+import voluptuous as vol
+
+from homeassistant.components.device_automation.exceptions import (
+ InvalidDeviceAutomationConfig,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_UNIT_OF_MEASUREMENT,
+ CONF_ABOVE,
+ CONF_BELOW,
+ CONF_ENTITY_ID,
+ CONF_TYPE,
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_ILLUMINANCE,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_PRESSURE,
+ DEVICE_CLASS_SIGNAL_STRENGTH,
+ DEVICE_CLASS_TEMPERATURE,
+ DEVICE_CLASS_TIMESTAMP,
+)
+from homeassistant.helpers.entity_registry import (
+ async_entries_for_device,
+ async_get_registry,
+)
+from homeassistant.helpers import condition, config_validation as cv
+from homeassistant.helpers.typing import ConfigType
+
+from . import DOMAIN
+
+
+# mypy: allow-untyped-defs, no-check-untyped-defs
+
+DEVICE_CLASS_NONE = "none"
+
+CONF_IS_BATTERY_LEVEL = "is_battery_level"
+CONF_IS_HUMIDITY = "is_humidity"
+CONF_IS_ILLUMINANCE = "is_illuminance"
+CONF_IS_POWER = "is_power"
+CONF_IS_PRESSURE = "is_pressure"
+CONF_IS_SIGNAL_STRENGTH = "is_signal_strength"
+CONF_IS_TEMPERATURE = "is_temperature"
+CONF_IS_TIMESTAMP = "is_timestamp"
+CONF_IS_VALUE = "is_value"
+
+ENTITY_CONDITIONS = {
+ DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}],
+ DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}],
+ DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}],
+ DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}],
+ DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}],
+ DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}],
+ DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}],
+ DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_IS_TIMESTAMP}],
+ DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}],
+}
+
+CONDITION_SCHEMA = vol.All(
+ cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(
+ [
+ CONF_IS_BATTERY_LEVEL,
+ CONF_IS_HUMIDITY,
+ CONF_IS_ILLUMINANCE,
+ CONF_IS_POWER,
+ CONF_IS_PRESSURE,
+ CONF_IS_SIGNAL_STRENGTH,
+ CONF_IS_TEMPERATURE,
+ CONF_IS_TIMESTAMP,
+ CONF_IS_VALUE,
+ ]
+ ),
+ vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)),
+ vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)),
+ }
+ ),
+ cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
+)
+
+
+async def async_get_conditions(
+ hass: HomeAssistant, device_id: str
+) -> List[Dict[str, str]]:
+ """List device conditions."""
+ conditions: List[Dict[str, str]] = []
+ entity_registry = await async_get_registry(hass)
+ entries = [
+ entry
+ for entry in async_entries_for_device(entity_registry, device_id)
+ if entry.domain == DOMAIN
+ ]
+
+ for entry in entries:
+ device_class = DEVICE_CLASS_NONE
+ state = hass.states.get(entry.entity_id)
+ unit_of_measurement = (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None
+ )
+
+ if not state or not unit_of_measurement:
+ continue
+
+ if ATTR_DEVICE_CLASS in state.attributes:
+ device_class = state.attributes[ATTR_DEVICE_CLASS]
+
+ templates = ENTITY_CONDITIONS.get(
+ device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE]
+ )
+
+ conditions.extend(
+ (
+ {
+ **template,
+ "condition": "device",
+ "device_id": device_id,
+ "entity_id": entry.entity_id,
+ "domain": DOMAIN,
+ }
+ for template in templates
+ )
+ )
+
+ return conditions
+
+
+def async_condition_from_config(
+ config: ConfigType, config_validation: bool
+) -> condition.ConditionCheckerType:
+ """Evaluate state based on configuration."""
+ if config_validation:
+ config = CONDITION_SCHEMA(config)
+ numeric_state_config = {
+ condition.CONF_CONDITION: "numeric_state",
+ condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ }
+ if CONF_ABOVE in config:
+ numeric_state_config[condition.CONF_ABOVE] = config[CONF_ABOVE]
+ if CONF_BELOW in config:
+ numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW]
+
+ return condition.async_numeric_state_from_config(numeric_state_config)
+
+
+async def async_get_condition_capabilities(hass, config):
+ """List condition capabilities."""
+ state = hass.states.get(config[CONF_ENTITY_ID])
+ unit_of_measurement = (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None
+ )
+
+ if not state or not unit_of_measurement:
+ raise InvalidDeviceAutomationConfig
+
+ return {
+ "extra_fields": vol.Schema(
+ {
+ vol.Optional(
+ CONF_ABOVE, description={"suffix": unit_of_measurement}
+ ): vol.Coerce(float),
+ vol.Optional(
+ CONF_BELOW, description={"suffix": unit_of_measurement}
+ ): vol.Coerce(float),
+ }
+ )
+ }
diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py
index 50fb1dd5c14..73e55340da9 100644
--- a/homeassistant/components/sensor/device_trigger.py
+++ b/homeassistant/components/sensor/device_trigger.py
@@ -3,6 +3,9 @@ import voluptuous as vol
import homeassistant.components.automation.numeric_state as numeric_state_automation
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation.exceptions import (
+ InvalidDeviceAutomationConfig,
+)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
@@ -72,11 +75,6 @@ TRIGGER_SCHEMA = vol.All(
),
vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)),
vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)),
- vol.Optional(CONF_FOR): vol.Any(
- vol.All(cv.time_period, cv.positive_timedelta),
- cv.template,
- cv.template_complex,
- ),
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
),
@@ -87,14 +85,17 @@ TRIGGER_SCHEMA = vol.All(
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
numeric_state_config = {
+ numeric_state_automation.CONF_PLATFORM: "numeric_state",
numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE),
- numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW),
- numeric_state_automation.CONF_FOR: config.get(CONF_FOR),
}
+ if CONF_ABOVE in config:
+ numeric_state_config[numeric_state_automation.CONF_ABOVE] = config[CONF_ABOVE]
+ if CONF_BELOW in config:
+ numeric_state_config[numeric_state_automation.CONF_BELOW] = config[CONF_BELOW]
if CONF_FOR in config:
numeric_state_config[CONF_FOR] = config[CONF_FOR]
+ numeric_state_config = numeric_state_automation.TRIGGER_SCHEMA(numeric_state_config)
return await numeric_state_automation.async_attach_trigger(
hass, numeric_state_config, action, automation_info, platform_type="device"
)
@@ -121,7 +122,8 @@ async def async_get_triggers(hass, device_id):
if not state or not unit_of_measurement:
continue
- device_class = state.attributes.get(ATTR_DEVICE_CLASS)
+ if ATTR_DEVICE_CLASS in state.attributes:
+ device_class = state.attributes[ATTR_DEVICE_CLASS]
templates = ENTITY_TRIGGERS.get(
device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE]
@@ -143,13 +145,25 @@ async def async_get_triggers(hass, device_id):
return triggers
-async def async_get_trigger_capabilities(hass, trigger):
+async def async_get_trigger_capabilities(hass, config):
"""List trigger capabilities."""
+ state = hass.states.get(config[CONF_ENTITY_ID])
+ unit_of_measurement = (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None
+ )
+
+ if not state or not unit_of_measurement:
+ raise InvalidDeviceAutomationConfig
+
return {
"extra_fields": vol.Schema(
{
- vol.Optional(CONF_ABOVE): vol.Coerce(float),
- vol.Optional(CONF_BELOW): vol.Coerce(float),
+ vol.Optional(
+ CONF_ABOVE, description={"suffix": unit_of_measurement}
+ ): vol.Coerce(float),
+ vol.Optional(
+ CONF_BELOW, description={"suffix": unit_of_measurement}
+ ): vol.Coerce(float),
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)
diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json
index 7df239facde..a05f57f4584 100644
--- a/homeassistant/components/sensor/strings.json
+++ b/homeassistant/components/sensor/strings.json
@@ -1,26 +1,26 @@
{
"device_automation": {
"condition_type": {
- "is_battery_level": "{entity_name} battery level",
- "is_humidity": "{entity_name} humidity",
- "is_illuminance": "{entity_name} illuminance",
- "is_power": "{entity_name} power",
- "is_pressure": "{entity_name} pressure",
- "is_signal_strength": "{entity_name} signal strength",
- "is_temperature": "{entity_name} temperature",
- "is_timestamp": "{entity_name} timestamp",
- "is_value": "{entity_name} value"
+ "is_battery_level": "Current {entity_name} battery level",
+ "is_humidity": "Current {entity_name} humidity",
+ "is_illuminance": "Current {entity_name} illuminance",
+ "is_power": "Current {entity_name} power",
+ "is_pressure": "Current {entity_name} pressure",
+ "is_signal_strength": "Current {entity_name} signal strength",
+ "is_temperature": "Current {entity_name} temperature",
+ "is_timestamp": "Current {entity_name} timestamp",
+ "is_value": "Current {entity_name} value"
},
"trigger_type": {
- "battery_level": "{entity_name} battery level",
- "humidity": "{entity_name} humidity",
- "illuminance": "{entity_name} illuminance",
- "power": "{entity_name} power",
- "pressure": "{entity_name} pressure",
- "signal_strength": "{entity_name} signal strength",
- "temperature": "{entity_name} temperature",
- "timestamp": "{entity_name} timestamp",
- "value": "{entity_name} value"
+ "battery_level": "{entity_name} battery level changes",
+ "humidity": "{entity_name} humidity changes",
+ "illuminance": "{entity_name} illuminance changes",
+ "power": "{entity_name} power changes",
+ "pressure": "{entity_name} pressure changes",
+ "signal_strength": "{entity_name} signal strength changes",
+ "temperature": "{entity_name} temperature changes",
+ "timestamp": "{entity_name} timestamp changes",
+ "value": "{entity_name} value changes"
}
}
}
diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py
index 27775b8c702..a08f9522c4b 100644
--- a/homeassistant/components/serial/sensor.py
+++ b/homeassistant/components/serial/sensor.py
@@ -1,12 +1,13 @@
"""Support for reading data from a serial port."""
-import logging
import json
+import logging
+import serial_asyncio
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -64,8 +65,6 @@ class SerialSensor(Entity):
async def serial_read(self, device, rate, **kwargs):
"""Read the data from the port."""
- import serial_asyncio
-
reader, _ = await serial_asyncio.open_serial_connection(
url=device, baudrate=rate, **kwargs
)
diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py
index f8698ac6bd8..fa12ff7a1b2 100644
--- a/homeassistant/components/sesame/lock.py
+++ b/homeassistant/components/sesame/lock.py
@@ -1,15 +1,17 @@
"""Support for Sesame, by CANDY HOUSE."""
from typing import Callable
+
+import pysesame2
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA
+from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
CONF_API_KEY,
STATE_LOCKED,
STATE_UNLOCKED,
)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
ATTR_DEVICE_ID = "device_id"
@@ -22,8 +24,6 @@ def setup_platform(
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
):
"""Set up the Sesame platform."""
- import pysesame2
-
api_key = config.get(CONF_API_KEY)
add_entities(
diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py
index 4b96cc50ecc..315b5c39fec 100644
--- a/homeassistant/components/seven_segments/image_processing.py
+++ b/homeassistant/components/seven_segments/image_processing.py
@@ -1,19 +1,21 @@
"""Optical character recognition processing of seven segments displays."""
-import logging
import io
+import logging
import os
+import subprocess
+from PIL import Image
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.core import split_entity_id
from homeassistant.components.image_processing import (
- PLATFORM_SCHEMA,
- ImageProcessingEntity,
- CONF_SOURCE,
CONF_ENTITY_ID,
CONF_NAME,
+ CONF_SOURCE,
+ PLATFORM_SCHEMA,
+ ImageProcessingEntity,
)
+from homeassistant.core import split_entity_id
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -120,9 +122,6 @@ class ImageProcessingSsocr(ImageProcessingEntity):
def process_image(self, image):
"""Process the image."""
- from PIL import Image
- import subprocess
-
stream = io.BytesIO(image)
img = Image.open(stream)
img.save(self.filepath, "png")
diff --git a/homeassistant/components/shiftr/__init__.py b/homeassistant/components/shiftr/__init__.py
index 8e698d283cf..1c3cddac256 100644
--- a/homeassistant/components/shiftr/__init__.py
+++ b/homeassistant/components/shiftr/__init__.py
@@ -1,16 +1,17 @@
"""Support for Shiftr.io."""
import logging
+import paho.mqtt.client as mqtt
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_PASSWORD,
CONF_USERNAME,
- EVENT_STATE_CHANGED,
EVENT_HOMEASSISTANT_STOP,
+ EVENT_STATE_CHANGED,
)
from homeassistant.helpers import state as state_helper
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -33,8 +34,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Initialize the Shiftr.io MQTT consumer."""
- import paho.mqtt.client as mqtt
-
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py
index 7b1360b0b01..d2a6a28fbe4 100644
--- a/homeassistant/components/shodan/sensor.py
+++ b/homeassistant/components/shodan/sensor.py
@@ -1,12 +1,13 @@
"""Sensor for displaying the number of result on Shodan.io."""
-import logging
from datetime import timedelta
+import logging
+import shodan
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -32,8 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Shodan sensor."""
- import shodan
-
api_key = config.get(CONF_API_KEY)
name = config.get(CONF_NAME)
query = config.get(CONF_QUERY)
diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py
index 3c9cb4391a7..a5e901b8c6e 100644
--- a/homeassistant/components/shopping_list/__init__.py
+++ b/homeassistant/components/shopping_list/__init__.py
@@ -101,13 +101,6 @@ def async_setup(hass, config):
hass.http.register_view(UpdateShoppingListItemView)
hass.http.register_view(ClearCompletedItemsView)
- hass.components.conversation.async_register(
- INTENT_ADD_ITEM, ["Add [the] [a] [an] {item} to my shopping list"]
- )
- hass.components.conversation.async_register(
- INTENT_LAST_ITEMS, ["What is on my shopping list"]
- )
-
hass.components.frontend.async_register_built_in_panel(
"shopping-list", "shopping_list", "mdi:cart"
)
diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json
index e82172f92f8..721ba69d67e 100644
--- a/homeassistant/components/simplisafe/.translations/ru.json
+++ b/homeassistant/components/simplisafe/.translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"error": {
"identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
- "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435"
+ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
},
"step": {
"user": {
diff --git a/homeassistant/components/sinch/__init__.py b/homeassistant/components/sinch/__init__.py
new file mode 100644
index 00000000000..43a5f2b2a5c
--- /dev/null
+++ b/homeassistant/components/sinch/__init__.py
@@ -0,0 +1 @@
+"""Component to integrate with Sinch SMS API."""
diff --git a/homeassistant/components/sinch/manifest.json b/homeassistant/components/sinch/manifest.json
new file mode 100644
index 00000000000..a1864428fee
--- /dev/null
+++ b/homeassistant/components/sinch/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "sinch",
+ "name": "Sinch",
+ "documentation": "https://www.home-assistant.io/components/sinch",
+ "dependencies": [],
+ "codeowners": [
+ "@bendikrb"
+ ],
+ "requirements": [
+ "clx-sdk-xms==1.0.0"
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py
new file mode 100644
index 00000000000..173873c0a6c
--- /dev/null
+++ b/homeassistant/components/sinch/notify.py
@@ -0,0 +1,97 @@
+"""Support for Sinch notifications."""
+import logging
+
+import voluptuous as vol
+from clx.xms.api import MtBatchTextSmsResult
+from clx.xms.client import Client
+from clx.xms.exceptions import (
+ ErrorResponseException,
+ UnexpectedResponseException,
+ UnauthorizedException,
+ NotFoundException,
+)
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.notify import (
+ ATTR_MESSAGE,
+ ATTR_DATA,
+ ATTR_TARGET,
+ PLATFORM_SCHEMA,
+ BaseNotificationService,
+)
+from homeassistant.const import CONF_API_KEY, CONF_SENDER
+
+DOMAIN = "sinch"
+
+CONF_SERVICE_PLAN_ID = "service_plan_id"
+CONF_DEFAULT_RECIPIENTS = "default_recipients"
+
+ATTR_SENDER = CONF_SENDER
+
+DEFAULT_SENDER = "Home Assistant"
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Required(CONF_SERVICE_PLAN_ID): cv.string,
+ vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string,
+ vol.Optional(CONF_DEFAULT_RECIPIENTS, default=[]): vol.All(
+ cv.ensure_list, [cv.string]
+ ),
+ }
+)
+
+
+def get_service(hass, config, discovery_info=None):
+ """Get the Sinch notification service."""
+ return SinchNotificationService(config)
+
+
+class SinchNotificationService(BaseNotificationService):
+ """Send Notifications to Sinch SMS recipients."""
+
+ def __init__(self, config):
+ """Initialize the service."""
+ self.default_recipients = config[CONF_DEFAULT_RECIPIENTS]
+ self.sender = config[CONF_SENDER]
+ self.client = Client(config[CONF_SERVICE_PLAN_ID], config[CONF_API_KEY])
+
+ def send_message(self, message="", **kwargs):
+ """Send a message to a user."""
+ targets = kwargs.get(ATTR_TARGET, self.default_recipients)
+ data = kwargs.get(ATTR_DATA, {})
+
+ clx_args = {ATTR_MESSAGE: message, ATTR_SENDER: self.sender}
+
+ if ATTR_SENDER in data:
+ clx_args[ATTR_SENDER] = data[ATTR_SENDER]
+
+ if not targets:
+ _LOGGER.error("At least 1 target is required")
+ return
+
+ try:
+ for target in targets:
+ result: MtBatchTextSmsResult = self.client.create_text_message(
+ clx_args[ATTR_SENDER], target, clx_args[ATTR_MESSAGE]
+ )
+ batch_id = result.batch_id
+ _LOGGER.debug(
+ 'Successfully sent SMS to "%s" (batch_id: %s)', target, batch_id
+ )
+ except ErrorResponseException as ex:
+ _LOGGER.error(
+ "Caught ErrorResponseException. Response code: %d (%s)",
+ ex.error_code,
+ ex,
+ )
+ except NotFoundException as ex:
+ _LOGGER.error("Caught NotFoundException (request URL: %s)", ex.url)
+ except UnauthorizedException as ex:
+ _LOGGER.error(
+ "Caught UnauthorizedException (service plan: %s)", ex.service_plan_id
+ )
+ except UnexpectedResponseException as ex:
+ _LOGGER.error("Caught UnexpectedResponseException: %s", ex)
diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json
index a3cb97cdc2d..7ab42c5da87 100644
--- a/homeassistant/components/skybeacon/manifest.json
+++ b/homeassistant/components/skybeacon/manifest.json
@@ -3,7 +3,7 @@
"name": "Skybeacon",
"documentation": "https://www.home-assistant.io/integrations/skybeacon",
"requirements": [
- "pygatt[GATTTOOL]==4.0.1"
+ "pygatt[GATTTOOL]==4.0.5"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py
index 1c098409610..cbf394edf47 100644
--- a/homeassistant/components/skybeacon/sensor.py
+++ b/homeassistant/components/skybeacon/sensor.py
@@ -3,6 +3,9 @@ import logging
import threading
from uuid import UUID
+from pygatt import BLEAddressType
+from pygatt.backends import Characteristic, GATTToolBackend
+from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -132,13 +135,8 @@ class Monitor(threading.Thread):
def run(self):
"""Thread that keeps connection alive."""
- # pylint: disable=import-error
- import pygatt
- from pygatt.backends import Characteristic
- from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout
-
cached_char = Characteristic(BLE_TEMP_UUID, BLE_TEMP_HANDLE)
- adapter = pygatt.backends.GATTToolBackend()
+ adapter = GATTToolBackend()
while True:
try:
_LOGGER.debug("Connecting to %s", self.name)
@@ -147,7 +145,7 @@ class Monitor(threading.Thread):
# Seems only one connection can be initiated at a time
with CONNECT_LOCK:
device = adapter.connect(
- self.mac, CONNECT_TIMEOUT, pygatt.BLEAddressType.random
+ self.mac, CONNECT_TIMEOUT, BLEAddressType.random
)
if SKIP_HANDLE_LOOKUP:
# HACK: inject handle mapping collected offline
diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py
index 1b9895aab76..b645a590c3c 100644
--- a/homeassistant/components/slack/notify.py
+++ b/homeassistant/components/slack/notify.py
@@ -3,11 +3,10 @@ import logging
import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
+import slacker
+from slacker import Slacker
import voluptuous as vol
-from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME
-import homeassistant.helpers.config_validation as cv
-
from homeassistant.components.notify import (
ATTR_DATA,
ATTR_TARGET,
@@ -15,6 +14,8 @@ from homeassistant.components.notify import (
PLATFORM_SCHEMA,
BaseNotificationService,
)
+from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -45,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the Slack notification service."""
- import slacker
channel = config.get(CONF_CHANNEL)
api_key = config.get(CONF_API_KEY)
@@ -67,7 +67,6 @@ class SlackNotificationService(BaseNotificationService):
def __init__(self, default_channel, api_token, username, icon, is_allowed_path):
"""Initialize the service."""
- from slacker import Slacker
self._default_channel = default_channel
self._api_token = api_token
@@ -84,7 +83,6 @@ class SlackNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
- import slacker
if kwargs.get(ATTR_TARGET) is None:
targets = [self._default_channel]
diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py
index 56e10b03d2a..ff1c48a141d 100644
--- a/homeassistant/components/sma/sensor.py
+++ b/homeassistant/components/sma/sensor.py
@@ -3,17 +3,18 @@ import asyncio
from datetime import timedelta
import logging
+import pysma
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
+ CONF_PATH,
CONF_SCAN_INTERVAL,
CONF_SSL,
CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP,
- CONF_PATH,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@@ -35,8 +36,6 @@ GROUPS = ["user", "installer"]
def _check_sensor_schema(conf):
"""Check sensors and attributes are valid."""
try:
- import pysma
-
valid = [s.name for s in pysma.Sensors()]
except (ImportError, AttributeError):
return conf
@@ -87,7 +86,6 @@ PLATFORM_SCHEMA = vol.All(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up SMA WebConnect sensor."""
- import pysma
# Check config again during load - dependency available
config = _check_sensor_schema(config)
diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py
index 0da0b29fbc2..ecab09f6ff9 100644
--- a/homeassistant/components/smappee/__init__.py
+++ b/homeassistant/components/smappee/__init__.py
@@ -1,13 +1,16 @@
"""Support for Smappee energy monitor."""
-import logging
from datetime import datetime, timedelta
+import logging
import re
-import voluptuous as vol
+
from requests.exceptions import RequestException
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_HOST
-from homeassistant.util import Throttle
-from homeassistant.helpers.discovery import load_platform
+import smappy
+import voluptuous as vol
+
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.discovery import load_platform
+from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -72,7 +75,6 @@ class Smappee:
self, client_id, client_secret, username, password, host, host_password
):
"""Initialize the data."""
- import smappy
self._remote_active = False
self._local_active = False
diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py
index 7206bea110b..ef2da4e9a1d 100644
--- a/homeassistant/components/smarthab/__init__.py
+++ b/homeassistant/components/smarthab/__init__.py
@@ -6,11 +6,12 @@ https://home-assistant.io/integrations/smarthab/
"""
import logging
+import pysmarthab
import voluptuous as vol
-from homeassistant.helpers.discovery import load_platform
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.discovery import load_platform
DOMAIN = "smarthab"
DATA_HUB = "hub"
@@ -32,7 +33,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config) -> bool:
"""Set up the SmartHab platform."""
- import pysmarthab
sh_conf = config.get(DOMAIN)
diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py
index 3d5b4259aa9..9bcb89b7ab4 100644
--- a/homeassistant/components/smarthab/cover.py
+++ b/homeassistant/components/smarthab/cover.py
@@ -4,18 +4,21 @@ Support for SmartHab device integration.
For more details about this component, please refer to the documentation at
https://home-assistant.io/integrations/smarthab/
"""
-import logging
from datetime import timedelta
+import logging
+
+import pysmarthab
from requests.exceptions import Timeout
from homeassistant.components.cover import (
- CoverDevice,
- SUPPORT_OPEN,
- SUPPORT_CLOSE,
- SUPPORT_SET_POSITION,
ATTR_POSITION,
+ SUPPORT_CLOSE,
+ SUPPORT_OPEN,
+ SUPPORT_SET_POSITION,
+ CoverDevice,
)
-from . import DOMAIN, DATA_HUB
+
+from . import DATA_HUB, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -24,7 +27,6 @@ SCAN_INTERVAL = timedelta(seconds=60)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the SmartHab roller shutters platform."""
- import pysmarthab
hub = hass.data[DOMAIN][DATA_HUB]
devices = hub.get_device_list()
diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py
index a8a55dea48a..bc6eb31fd04 100644
--- a/homeassistant/components/smarthab/light.py
+++ b/homeassistant/components/smarthab/light.py
@@ -4,12 +4,15 @@ Support for SmartHab device integration.
For more details about this component, please refer to the documentation at
https://home-assistant.io/integrations/smarthab/
"""
-import logging
from datetime import timedelta
+import logging
+
+import pysmarthab
from requests.exceptions import Timeout
from homeassistant.components.light import Light
-from . import DOMAIN, DATA_HUB
+
+from . import DATA_HUB, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -18,7 +21,6 @@ SCAN_INTERVAL = timedelta(seconds=60)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the SmartHab lights platform."""
- import pysmarthab
hub = hass.data[DOMAIN][DATA_HUB]
devices = hub.get_device_list()
diff --git a/homeassistant/components/smartthings/.translations/nn.json b/homeassistant/components/smartthings/.translations/nn.json
new file mode 100644
index 00000000000..929e95dc2ff
--- /dev/null
+++ b/homeassistant/components/smartthings/.translations/nn.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "SmartThings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smartthings/.translations/pl.json b/homeassistant/components/smartthings/.translations/pl.json
index 33803994764..849ad174134 100644
--- a/homeassistant/components/smartthings/.translations/pl.json
+++ b/homeassistant/components/smartthings/.translations/pl.json
@@ -2,7 +2,7 @@
"config": {
"error": {
"app_not_installed": "Upewnij si\u0119, \u017ce zainstalowa\u0142e\u015b i autoryzowa\u0142e\u015b Home Assistant SmartApp i spr\u00f3buj ponownie.",
- "app_setup_error": "Nie mo\u017cna skonfigurowa\u0107 SmartApp. Prosz\u0119 spr\u00f3buj ponownie.",
+ "app_setup_error": "Nie mo\u017cna skonfigurowa\u0107 SmartApp. Spr\u00f3buj ponownie.",
"base_url_not_https": "Parametr `base_url` dla komponentu `http` musi by\u0107 skonfigurowany i rozpoczyna\u0107 si\u0119 od `https://`.",
"token_already_setup": "Token zosta\u0142 ju\u017c skonfigurowany.",
"token_forbidden": "Token nie ma wymaganych zakres\u00f3w OAuth.",
diff --git a/homeassistant/components/smartthings/.translations/ru.json b/homeassistant/components/smartthings/.translations/ru.json
index 575c593d5a4..f07586c16e3 100644
--- a/homeassistant/components/smartthings/.translations/ru.json
+++ b/homeassistant/components/smartthings/.translations/ru.json
@@ -6,7 +6,7 @@
"base_url_not_https": "\u0412 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0435 `http` \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 `base_url`, \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u0441 `https://`.",
"token_already_setup": "\u0422\u043e\u043a\u0435\u043d \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.",
"token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f OAuth.",
- "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID",
+ "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID.",
"token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u043b\u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d.",
"webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443, \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0432 `base_url`. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043a \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443."
},
diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py
index d205c1d245c..ecd4da5dcab 100644
--- a/homeassistant/components/smartthings/smartapp.py
+++ b/homeassistant/components/smartthings/smartapp.py
@@ -22,7 +22,7 @@ from pysmartthings import (
SubscriptionEntity,
)
-from homeassistant.components import cloud, webhook
+from homeassistant.components import webhook
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import (
@@ -88,7 +88,10 @@ async def validate_installed_app(api, installed_app_id: str):
def validate_webhook_requirements(hass: HomeAssistantType) -> bool:
"""Ensure HASS is setup properly to receive webhooks."""
- if cloud.async_active_subscription(hass):
+ if (
+ "cloud" in hass.config.components
+ and hass.components.cloud.async_active_subscription()
+ ):
return True
if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None:
return True
@@ -102,7 +105,11 @@ def get_webhook_url(hass: HomeAssistantType) -> str:
Return the cloudhook if available, otherwise local webhook.
"""
cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL]
- if cloud.async_active_subscription(hass) and cloudhook_url is not None:
+ if (
+ "cloud" in hass.config.components
+ and hass.components.cloud.async_active_subscription()
+ and cloudhook_url is not None
+ ):
return cloudhook_url
return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID])
@@ -222,10 +229,11 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType):
cloudhook_url = config.get(CONF_CLOUDHOOK_URL)
if (
cloudhook_url is None
- and cloud.async_active_subscription(hass)
+ and "cloud" in hass.config.components
+ and hass.components.cloud.async_active_subscription()
and not hass.config_entries.async_entries(DOMAIN)
):
- cloudhook_url = await cloud.async_create_cloudhook(
+ cloudhook_url = await hass.components.cloud.async_create_cloudhook(
hass, config[CONF_WEBHOOK_ID]
)
config[CONF_CLOUDHOOK_URL] = cloudhook_url
@@ -273,8 +281,14 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType):
return
# Remove the cloudhook if it was created
cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL]
- if cloudhook_url and cloud.async_is_logged_in(hass):
- await cloud.async_delete_cloudhook(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID])
+ if (
+ cloudhook_url
+ and "cloud" in hass.config.components
+ and hass.components.cloud.async_is_logged_in()
+ ):
+ await hass.components.cloud.async_delete_cloudhook(
+ hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]
+ )
# Remove cloudhook from storage
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
await store.async_save(
diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json
index 03b17b3ba8b..f3ba34adac3 100644
--- a/homeassistant/components/smhi/.translations/ru.json
+++ b/homeassistant/components/smhi/.translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"error": {
"name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.",
- "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438"
+ "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py
index 8a96865ab8d..d592f25a61d 100644
--- a/homeassistant/components/smtp/notify.py
+++ b/homeassistant/components/smtp/notify.py
@@ -136,16 +136,15 @@ class MailNotificationService(BaseNotificationService):
server = None
try:
server = self.connect()
- except smtplib.socket.gaierror:
+ except (smtplib.socket.gaierror, ConnectionRefusedError):
_LOGGER.exception(
- "SMTP server not found (%s:%s). "
- "Please check the IP address or hostname of your SMTP server",
+ "SMTP server not found or refused connection (%s:%s). "
+ "Please check the IP address, hostname, and availability of your SMTP server.",
self._server,
self._port,
)
- return False
- except (smtplib.SMTPAuthenticationError, ConnectionRefusedError):
+ except smtplib.SMTPAuthenticationError:
_LOGGER.exception(
"Login not possible. "
"Please check your setting and/or your credentials"
diff --git a/homeassistant/components/snapcast/__init__.py b/homeassistant/components/snapcast/__init__.py
index 9e41bd8ff38..e6c574b7b2b 100644
--- a/homeassistant/components/snapcast/__init__.py
+++ b/homeassistant/components/snapcast/__init__.py
@@ -1,6 +1,7 @@
"""The snapcast component."""
import asyncio
+
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py
index 81cd6538578..c3c9138eb89 100644
--- a/homeassistant/components/snapcast/media_player.py
+++ b/homeassistant/components/snapcast/media_player.py
@@ -2,9 +2,11 @@
import logging
import socket
+import snapcast.control
+from snapcast.control.server import CONTROL_PORT
import voluptuous as vol
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
SUPPORT_SELECT_SOURCE,
SUPPORT_VOLUME_MUTE,
@@ -24,12 +26,12 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import (
- DOMAIN,
- SERVICE_SNAPSHOT,
- SERVICE_RESTORE,
- SERVICE_JOIN,
- SERVICE_UNJOIN,
ATTR_MASTER,
+ DOMAIN,
+ SERVICE_JOIN,
+ SERVICE_RESTORE,
+ SERVICE_SNAPSHOT,
+ SERVICE_UNJOIN,
)
_LOGGER = logging.getLogger(__name__)
@@ -55,8 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Snapcast platform."""
- import snapcast.control
- from snapcast.control.server import CONTROL_PORT
host = config.get(CONF_HOST)
port = config.get(CONF_PORT, CONTROL_PORT)
diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py
index a628c426e0f..eafae9537e5 100644
--- a/homeassistant/components/snmp/device_tracker.py
+++ b/homeassistant/components/snmp/device_tracker.py
@@ -2,6 +2,8 @@
import binascii
import logging
+from pysnmp.entity import config as cfg
+from pysnmp.entity.rfc3413.oneliner import cmdgen
import voluptuous as vol
from homeassistant.components.device_tracker import (
@@ -45,8 +47,6 @@ class SnmpScanner(DeviceScanner):
def __init__(self, config):
"""Initialize the scanner."""
- from pysnmp.entity.rfc3413.oneliner import cmdgen
- from pysnmp.entity import config as cfg
self.snmp = cmdgen.CommandGenerator()
diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py
index 5e6b5ed1f28..b369ec83c58 100644
--- a/homeassistant/components/snmp/sensor.py
+++ b/homeassistant/components/snmp/sensor.py
@@ -2,6 +2,17 @@
from datetime import timedelta
import logging
+import pysnmp.hlapi.asyncio as hlapi
+from pysnmp.hlapi.asyncio import (
+ CommunityData,
+ ContextData,
+ ObjectIdentity,
+ ObjectType,
+ SnmpEngine,
+ UdpTransportTarget,
+ UsmUserData,
+ getCmd,
+)
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -70,16 +81,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the SNMP sensor."""
- from pysnmp.hlapi.asyncio import (
- getCmd,
- CommunityData,
- SnmpEngine,
- UdpTransportTarget,
- ContextData,
- ObjectType,
- ObjectIdentity,
- UsmUserData,
- )
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
@@ -101,7 +102,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
value_template.hass = hass
if version == "3":
- import pysnmp.hlapi.asyncio as hlapi
if not authkey:
authproto = "none"
@@ -194,7 +194,6 @@ class SnmpData:
async def async_update(self):
"""Get the latest data from the remote SNMP capable host."""
- from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity
errindication, errstatus, errindex, restable = await getCmd(
*self._request_args, ObjectType(ObjectIdentity(self._baseoid))
diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py
index 95496cb6a45..aac43208a1f 100644
--- a/homeassistant/components/snmp/switch.py
+++ b/homeassistant/components/snmp/switch.py
@@ -1,6 +1,18 @@
"""Support for SNMP enabled switch."""
import logging
+import pysnmp.hlapi.asyncio as hlapi
+from pysnmp.hlapi.asyncio import (
+ CommunityData,
+ ContextData,
+ ObjectIdentity,
+ ObjectType,
+ SnmpEngine,
+ UdpTransportTarget,
+ UsmUserData,
+ getCmd,
+ setCmd,
+)
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
@@ -136,13 +148,6 @@ class SnmpSwitch(SwitchDevice):
command_payload_off,
):
"""Initialize the switch."""
- from pysnmp.hlapi.asyncio import (
- CommunityData,
- ContextData,
- SnmpEngine,
- UdpTransportTarget,
- UsmUserData,
- )
self._name = name
self._baseoid = baseoid
@@ -157,7 +162,6 @@ class SnmpSwitch(SwitchDevice):
self._payload_off = payload_off
if version == "3":
- import pysnmp.hlapi.asyncio as hlapi
if not authkey:
authproto = "none"
@@ -186,20 +190,14 @@ class SnmpSwitch(SwitchDevice):
async def async_turn_on(self, **kwargs):
"""Turn on the switch."""
- from pyasn1.type.univ import Integer
-
- await self._set(Integer(self._command_payload_on))
+ await self._set(self._command_payload_on)
async def async_turn_off(self, **kwargs):
"""Turn off the switch."""
- from pyasn1.type.univ import Integer
-
- await self._set(Integer(self._command_payload_off))
+ await self._set(self._command_payload_off)
async def async_update(self):
"""Update the state."""
- from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity
- from pyasn1.type.univ import Integer
errindication, errstatus, errindex, restable = await getCmd(
*self._request_args, ObjectType(ObjectIdentity(self._baseoid))
@@ -215,9 +213,9 @@ class SnmpSwitch(SwitchDevice):
)
else:
for resrow in restable:
- if resrow[-1] == Integer(self._payload_on):
+ if resrow[-1] == self._payload_on:
self._state = True
- elif resrow[-1] == Integer(self._payload_off):
+ elif resrow[-1] == self._payload_off:
self._state = False
else:
self._state = None
@@ -233,7 +231,6 @@ class SnmpSwitch(SwitchDevice):
return self._state
async def _set(self, value):
- from pysnmp.hlapi.asyncio import setCmd, ObjectType, ObjectIdentity
await setCmd(
*self._request_args, ObjectType(ObjectIdentity(self._commandoid), value)
diff --git a/homeassistant/components/socialblade/sensor.py b/homeassistant/components/socialblade/sensor.py
index 0acfb63a629..3d53e76a27a 100644
--- a/homeassistant/components/socialblade/sensor.py
+++ b/homeassistant/components/socialblade/sensor.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+import socialbladeclient
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -71,7 +72,6 @@ class SocialBladeSensor(Entity):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from Social Blade."""
- import socialbladeclient
try:
data = socialbladeclient.get_data(self.channel_id)
diff --git a/homeassistant/components/solaredge/.translations/de.json b/homeassistant/components/solaredge/.translations/de.json
new file mode 100644
index 00000000000..cbe913e131c
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/de.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Diese site_id ist bereits konfiguriert"
+ },
+ "error": {
+ "site_exists": "Diese site_id ist bereits konfiguriert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Der API-Schl\u00fcssel f\u00fcr diese Site",
+ "name": "Der Name dieser Installation",
+ "site_id": "Die SolarEdge-Site-ID"
+ },
+ "title": "Definiere die API-Parameter f\u00fcr diese Installation"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json
index d8622cdd2c1..e6e7094648d 100644
--- a/homeassistant/components/solaredge/.translations/ru.json
+++ b/homeassistant/components/solaredge/.translations/ru.json
@@ -9,9 +9,9 @@
"step": {
"user": {
"data": {
- "api_key": "\u041a\u043b\u044e\u0447 API \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0430\u0439\u0442\u0430",
+ "api_key": "\u041a\u043b\u044e\u0447 API",
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
- "site_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0430\u0439\u0442\u0430 SolarEdge"
+ "site_id": "site-id"
},
"title": "SolarEdge"
}
diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py
index 4fc62e44921..917fb86ddcb 100644
--- a/homeassistant/components/solaredge_local/sensor.py
+++ b/homeassistant/components/solaredge_local/sensor.py
@@ -2,6 +2,7 @@
import logging
from datetime import timedelta
import statistics
+from copy import deepcopy
from requests.exceptions import HTTPError, ConnectTimeout
from solaredge_local import SolarEdge
@@ -14,6 +15,7 @@ from homeassistant.const import (
POWER_WATT,
ENERGY_WATT_HOUR,
TEMP_CELSIUS,
+ TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -22,63 +24,107 @@ from homeassistant.util import Throttle
DOMAIN = "solaredge_local"
UPDATE_DELAY = timedelta(seconds=10)
+INVERTER_MODES = (
+ "SHUTTING_DOWN",
+ "ERROR",
+ "STANDBY",
+ "PAIRING",
+ "POWER_PRODUCTION",
+ "AC_CHARGING",
+ "NOT_PAIRED",
+ "NIGHT_MODE",
+ "GRID_MONITORING",
+ "IDLE",
+)
+
# Supported sensor types:
-# Key: ['json_key', 'name', unit, icon]
+# Key: ['json_key', 'name', unit, icon, attribute name]
SENSOR_TYPES = {
- "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"],
+ "current_AC_voltage": ["gridvoltage", "Grid Voltage", "V", "mdi:current-ac", None],
+ "current_DC_voltage": ["dcvoltage", "DC Voltage", "V", "mdi:current-dc", None],
+ "current_frequency": [
+ "gridfrequency",
+ "Grid Frequency",
+ "Hz",
+ "mdi:current-ac",
+ None,
+ ],
+ "current_power": [
+ "currentPower",
+ "Current Power",
+ POWER_WATT,
+ "mdi:solar-power",
+ None,
+ ],
"energy_this_month": [
"energyThisMonth",
- "Energy this month",
+ "Energy This Month",
ENERGY_WATT_HOUR,
"mdi:solar-power",
+ None,
],
"energy_this_year": [
"energyThisYear",
- "Energy this year",
+ "Energy This Year",
ENERGY_WATT_HOUR,
"mdi:solar-power",
+ None,
],
"energy_today": [
"energyToday",
- "Energy today",
+ "Energy Today",
ENERGY_WATT_HOUR,
"mdi:solar-power",
+ None,
],
"inverter_temperature": [
"invertertemperature",
"Inverter Temperature",
TEMP_CELSIUS,
"mdi:thermometer",
+ "operating_mode",
],
"lifetime_energy": [
"energyTotal",
- "Lifetime energy",
+ "Lifetime Energy",
ENERGY_WATT_HOUR,
"mdi:solar-power",
+ None,
+ ],
+ "optimizer_connected": [
+ "optimizers",
+ "Optimizers Online",
+ "optimizers",
+ "mdi:solar-panel",
+ "optimizers_connected",
],
"optimizer_current": [
"optimizercurrent",
- "Avrage Optimizer Current",
+ "Average Optimizer Current",
"A",
"mdi:solar-panel",
+ None,
],
"optimizer_power": [
"optimizerpower",
- "Avrage Optimizer Power",
+ "Average Optimizer Power",
POWER_WATT,
"mdi:solar-panel",
+ None,
],
"optimizer_temperature": [
"optimizertemperature",
- "Avrage Optimizer Temperature",
+ "Average Optimizer Temperature",
TEMP_CELSIUS,
"mdi:solar-panel",
+ None,
],
"optimizer_voltage": [
"optimizervoltage",
- "Avrage Optimizer Voltage",
+ "Average Optimizer Voltage",
"V",
"mdi:solar-panel",
+ None,
],
}
@@ -112,13 +158,71 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.error("Could not retrieve details from SolarEdge API")
return
+ # Changing inverter temperature unit.
+ sensors = deepcopy(SENSOR_TYPES)
+ if status.inverters.primary.temperature.units.farenheit:
+ sensors["inverter_temperature"] = [
+ "invertertemperature",
+ "Inverter Temperature",
+ TEMP_FAHRENHEIT,
+ "mdi:thermometer",
+ "operating_mode",
+ None,
+ ]
+
+ try:
+ if status.metersList[0]:
+ sensors["import_current_power"] = [
+ "currentPowerimport",
+ "current import Power",
+ POWER_WATT,
+ "mdi:arrow-collapse-down",
+ None,
+ ]
+ sensors["import_meter_reading"] = [
+ "totalEnergyimport",
+ "total import Energy",
+ ENERGY_WATT_HOUR,
+ "mdi:counter",
+ None,
+ ]
+ except IndexError:
+ _LOGGER.debug("Import meter sensors are not created")
+
+ try:
+ if status.metersList[1]:
+ sensors["export_current_power"] = [
+ "currentPowerexport",
+ "current export Power",
+ POWER_WATT,
+ "mdi:arrow-expand-up",
+ None,
+ ]
+ sensors["export_meter_reading"] = [
+ "totalEnergyexport",
+ "total export Energy",
+ ENERGY_WATT_HOUR,
+ "mdi:counter",
+ None,
+ ]
+ except IndexError:
+ _LOGGER.debug("Export meter sensors are not created")
+
# 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)
+ for sensor_info in sensors.values():
+ sensor = SolarEdgeSensor(
+ platform_name,
+ data,
+ sensor_info[0],
+ sensor_info[1],
+ sensor_info[2],
+ sensor_info[3],
+ sensor_info[4],
+ )
entities.append(sensor)
add_entities(entities, True)
@@ -127,30 +231,42 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class SolarEdgeSensor(Entity):
"""Representation of an SolarEdge Monitoring API sensor."""
- def __init__(self, platform_name, sensor_key, data):
+ def __init__(self, platform_name, data, json_key, name, unit, icon, attr):
"""Initialize the sensor."""
- self.platform_name = platform_name
- self.sensor_key = sensor_key
- self.data = data
+ self._platform_name = platform_name
+ 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]
+ self._json_key = json_key
+ self._name = name
+ self._unit_of_measurement = unit
+ self._icon = icon
+ self._attr = attr
@property
def name(self):
"""Return the name."""
- return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})"
+ return f"{self._platform_name} ({self._name})"
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if self._attr:
+ try:
+ return {self._attr: self._data.info[self._json_key]}
+ except KeyError:
+ return None
+ return None
+
@property
def icon(self):
"""Return the sensor icon."""
- return SENSOR_TYPES[self.sensor_key][3]
+ return self._icon
@property
def state(self):
@@ -159,8 +275,8 @@ class SolarEdgeSensor(Entity):
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]
+ self._data.update()
+ self._state = self._data.data[self._json_key]
class SolarEdgeData:
@@ -171,6 +287,7 @@ class SolarEdgeData:
self.hass = hass
self.api = api
self.data = {}
+ self.info = {}
@Throttle(UPDATE_DELAY)
def update(self):
@@ -220,11 +337,33 @@ class SolarEdgeData:
self.data["energyThisMonth"] = round(status.energy.thisMonth, 2)
self.data["energyToday"] = round(status.energy.today, 2)
self.data["currentPower"] = round(status.powerWatt, 2)
- self.data[
- "invertertemperature"
- ] = status.inverters.primary.temperature.value
+ self.data["invertertemperature"] = round(
+ status.inverters.primary.temperature.value, 2
+ )
+ self.data["dcvoltage"] = round(status.inverters.primary.voltage, 2)
+ self.data["gridfrequency"] = round(status.frequencyHz, 2)
+ self.data["gridvoltage"] = round(status.voltage, 2)
+ self.data["optimizers"] = status.optimizersStatus.online
+
+ self.info["optimizers"] = status.optimizersStatus.total
+ self.info["invertertemperature"] = INVERTER_MODES[status.status]
+
+ try:
+ if status.metersList[1]:
+ self.data["currentPowerimport"] = status.metersList[1].currentPower
+ self.data["totalEnergyimport"] = status.metersList[1].totalEnergy
+ except IndexError:
+ pass
+
+ try:
+ if status.metersList[0]:
+ self.data["currentPowerexport"] = status.metersList[0].currentPower
+ self.data["totalEnergyexport"] = status.metersList[0].totalEnergy
+ except IndexError:
+ pass
+
if maintenance.system.name:
- self.data["optimizertemperature"] = statistics.mean(temperature)
- self.data["optimizervoltage"] = statistics.mean(voltage)
- self.data["optimizercurrent"] = statistics.mean(current)
- self.data["optimizerpower"] = power
+ self.data["optimizertemperature"] = round(statistics.mean(temperature), 2)
+ self.data["optimizervoltage"] = round(statistics.mean(voltage), 2)
+ self.data["optimizercurrent"] = round(statistics.mean(current), 2)
+ self.data["optimizerpower"] = round(power, 2)
diff --git a/homeassistant/components/solarlog/.translations/ca.json b/homeassistant/components/solarlog/.translations/ca.json
new file mode 100644
index 00000000000..6a041c7ea4f
--- /dev/null
+++ b/homeassistant/components/solarlog/.translations/ca.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "No s'ha pogut connectar, verifica l'adre\u00e7a de l'amfitri\u00f3"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP del dispositiu Solar-Log",
+ "name": "Prefix utilitzat pels sensors de Solar-Log"
+ },
+ "title": "Configuraci\u00f3 de la connexi\u00f3 amb Solar-Log"
+ }
+ },
+ "title": "Solar-Log"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/.translations/da.json b/homeassistant/components/solarlog/.translations/da.json
new file mode 100644
index 00000000000..a344832c61c
--- /dev/null
+++ b/homeassistant/components/solarlog/.translations/da.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheden er allerede konfigureret"
+ },
+ "error": {
+ "already_configured": "Enheden er allerede konfigureret",
+ "cannot_connect": "Kunne ikke oprette forbindelse, verificer v\u00e6rtsadressen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e6rtsnavnet eller ip-adressen p\u00e5 din Solar-Log-enhed",
+ "name": "Pr\u00e6fikset, der skal bruges til dine Solar-Log sensorer"
+ },
+ "title": "Angiv dit Solar-Log forbindelse"
+ }
+ },
+ "title": "Solar-Log"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/.translations/en.json b/homeassistant/components/solarlog/.translations/en.json
new file mode 100644
index 00000000000..f1396045819
--- /dev/null
+++ b/homeassistant/components/solarlog/.translations/en.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect, please verify host address"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "The hostname or ip-address of your Solar-Log device",
+ "name": "The prefix to be used for your Solar-Log sensors"
+ },
+ "title": "Define your Solar-Log connection"
+ }
+ },
+ "title": "Solar-Log"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/.translations/fr.json b/homeassistant/components/solarlog/.translations/fr.json
new file mode 100644
index 00000000000..0f1b4944ed9
--- /dev/null
+++ b/homeassistant/components/solarlog/.translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "cannot_connect": "\u00c9chec de la connexion, veuillez v\u00e9rifier l'adresse de l'h\u00f4te."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Le nom d'h\u00f4te ou l'adresse IP de votre p\u00e9riph\u00e9rique Solar-Log",
+ "name": "Le pr\u00e9fixe \u00e0 utiliser pour vos capteurs Solar-Log"
+ },
+ "title": "D\u00e9finissez votre connexion Solar-Log"
+ }
+ },
+ "title": "Solar-Log"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/.translations/nl.json b/homeassistant/components/solarlog/.translations/nl.json
new file mode 100644
index 00000000000..3965f71e992
--- /dev/null
+++ b/homeassistant/components/solarlog/.translations/nl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_connect": "Verbinding mislukt, controleer het host-adres"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "De hostnaam of het IP-adres van uw Solar-Log apparaat",
+ "name": "Het voorvoegsel dat moet worden gebruikt voor uw Solar-Log sensoren"
+ },
+ "title": "Definieer uw Solar-Log verbinding"
+ }
+ },
+ "title": "Solar-Log"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/.translations/no.json b/homeassistant/components/solarlog/.translations/no.json
new file mode 100644
index 00000000000..017e886c817
--- /dev/null
+++ b/homeassistant/components/solarlog/.translations/no.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Kunne ikke koble til, vennligst bekreft vertsadresse"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vertsnavnet eller ip-adressen til din Solar-Log-enhet",
+ "name": "Prefikset som skal brukes til dine Solar-Log sensorer"
+ },
+ "title": "Definer din Solar-Log tilkobling"
+ }
+ },
+ "title": "Solar-Log"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/.translations/ru.json b/homeassistant/components/solarlog/.translations/ru.json
new file mode 100644
index 00000000000..7f40935e5a5
--- /dev/null
+++ b/homeassistant/components/solarlog/.translations/ru.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "error": {
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441",
+ "name": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 Solar-Log"
+ },
+ "title": "Solar-Log"
+ }
+ },
+ "title": "Solar-Log"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py
new file mode 100644
index 00000000000..c8035e1f7e6
--- /dev/null
+++ b/homeassistant/components/solarlog/__init__.py
@@ -0,0 +1,21 @@
+"""Solar-Log integration."""
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+async def async_setup(hass, config):
+ """Component setup, do nothing."""
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
+ """Set up a config entry for solarlog."""
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, "sensor")
+ )
+ return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ return await hass.config_entries.async_forward_entry_unload(entry, "sensor")
diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py
new file mode 100644
index 00000000000..5cb2d5deec1
--- /dev/null
+++ b/homeassistant/components/solarlog/config_flow.py
@@ -0,0 +1,107 @@
+"""Config flow for solarlog integration."""
+import logging
+from urllib.parse import ParseResult, urlparse
+
+from requests.exceptions import HTTPError, Timeout
+from sunwatcher.solarlog.solarlog import SolarLog
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.util import slugify
+
+from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@callback
+def solarlog_entries(hass: HomeAssistant):
+ """Return the hosts already configured."""
+ return set(
+ entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)
+ )
+
+
+class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for solarlog."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self) -> None:
+ """Initialize the config flow."""
+ self._errors = {}
+
+ def _host_in_configuration_exists(self, host) -> bool:
+ """Return True if host exists in configuration."""
+ if host in solarlog_entries(self.hass):
+ return True
+ return False
+
+ async def _test_connection(self, host):
+ """Check if we can connect to the Solar-Log device."""
+ try:
+ await self.hass.async_add_executor_job(SolarLog, host)
+ return True
+ except (OSError, HTTPError, Timeout):
+ self._errors[CONF_HOST] = "cannot_connect"
+ _LOGGER.error(
+ "Could not connect to Solar-Log device at %s, check host ip address",
+ host,
+ )
+ return False
+
+ async def async_step_user(self, user_input=None):
+ """Step when user intializes a integration."""
+ self._errors = {}
+ if user_input is not None:
+ # set some defaults in case we need to return to the form
+ name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME))
+ host_entry = user_input.get(CONF_HOST, DEFAULT_HOST)
+
+ url = urlparse(host_entry, "http")
+ netloc = url.netloc or url.path
+ path = url.path if url.netloc else ""
+ url = ParseResult("http", netloc, path, *url[3:])
+ host = url.geturl()
+
+ if self._host_in_configuration_exists(host):
+ self._errors[CONF_HOST] = "already_configured"
+ else:
+ if await self._test_connection(host):
+ return self.async_create_entry(title=name, data={CONF_HOST: host})
+ else:
+ user_input = {}
+ user_input[CONF_NAME] = DEFAULT_NAME
+ user_input[CONF_HOST] = DEFAULT_HOST
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)
+ ): str,
+ vol.Required(
+ CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST)
+ ): str,
+ }
+ ),
+ errors=self._errors,
+ )
+
+ async def async_step_import(self, user_input=None):
+ """Import a config entry."""
+ host_entry = user_input.get(CONF_HOST, DEFAULT_HOST)
+
+ url = urlparse(host_entry, "http")
+ netloc = url.netloc or url.path
+ path = url.path if url.netloc else ""
+ url = ParseResult("http", netloc, path, *url[3:])
+ host = url.geturl()
+
+ if self._host_in_configuration_exists(host):
+ return self.async_abort(reason="already_configured")
+ return await self.async_step_user(user_input)
diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py
new file mode 100644
index 00000000000..67eb8006cec
--- /dev/null
+++ b/homeassistant/components/solarlog/const.py
@@ -0,0 +1,89 @@
+"""Constants for the Solar-Log integration."""
+from datetime import timedelta
+
+from homeassistant.const import POWER_WATT, ENERGY_KILO_WATT_HOUR
+
+DOMAIN = "solarlog"
+
+"""Default config for solarlog."""
+DEFAULT_HOST = "http://solar-log"
+DEFAULT_NAME = "solarlog"
+
+"""Fixed constants."""
+SCAN_INTERVAL = timedelta(seconds=60)
+
+"""Supported sensor types."""
+SENSOR_TYPES = {
+ "time": ["TIME", "last update", None, "mdi:calendar-clock"],
+ "power_ac": ["powerAC", "power AC", POWER_WATT, "mdi:solar-power"],
+ "power_dc": ["powerDC", "power DC", POWER_WATT, "mdi:solar-power"],
+ "voltage_ac": ["voltageAC", "voltage AC", "V", "mdi:flash"],
+ "voltage_dc": ["voltageDC", "voltage DC", "V", "mdi:flash"],
+ "yield_day": ["yieldDAY", "yield day", ENERGY_KILO_WATT_HOUR, "mdi:solar-power"],
+ "yield_yesterday": [
+ "yieldYESTERDAY",
+ "yield yesterday",
+ ENERGY_KILO_WATT_HOUR,
+ "mdi:solar-power",
+ ],
+ "yield_month": [
+ "yieldMONTH",
+ "yield month",
+ ENERGY_KILO_WATT_HOUR,
+ "mdi:solar-power",
+ ],
+ "yield_year": ["yieldYEAR", "yield year", ENERGY_KILO_WATT_HOUR, "mdi:solar-power"],
+ "yield_total": [
+ "yieldTOTAL",
+ "yield total",
+ ENERGY_KILO_WATT_HOUR,
+ "mdi:solar-power",
+ ],
+ "consumption_ac": ["consumptionAC", "consumption AC", POWER_WATT, "mdi:power-plug"],
+ "consumption_day": [
+ "consumptionDAY",
+ "consumption day",
+ ENERGY_KILO_WATT_HOUR,
+ "mdi:power-plug",
+ ],
+ "consumption_yesterday": [
+ "consumptionYESTERDAY",
+ "consumption yesterday",
+ ENERGY_KILO_WATT_HOUR,
+ "mdi:power-plug",
+ ],
+ "consumption_month": [
+ "consumptionMONTH",
+ "consumption month",
+ ENERGY_KILO_WATT_HOUR,
+ "mdi:power-plug",
+ ],
+ "consumption_year": [
+ "consumptionYEAR",
+ "consumption year",
+ ENERGY_KILO_WATT_HOUR,
+ "mdi:power-plug",
+ ],
+ "consumption_total": [
+ "consumptionTOTAL",
+ "consumption total",
+ ENERGY_KILO_WATT_HOUR,
+ "mdi:power-plug",
+ ],
+ "total_power": ["totalPOWER", "total power", "Wp", "mdi:solar-power"],
+ "alternator_loss": [
+ "alternatorLOSS",
+ "alternator loss",
+ POWER_WATT,
+ "mdi:solar-power",
+ ],
+ "capacity": ["CAPACITY", "capacity", "%", "mdi:solar-power"],
+ "efficiency": ["EFFICIENCY", "efficiency", "% W/Wp", "mdi:solar-power"],
+ "power_available": [
+ "powerAVAILABLE",
+ "power available",
+ POWER_WATT,
+ "mdi:solar-power",
+ ],
+ "usage": ["USAGE", "usage", None, "mdi:solar-power"],
+}
diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json
new file mode 100644
index 00000000000..9331628e027
--- /dev/null
+++ b/homeassistant/components/solarlog/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "solarlog",
+ "name": "Solar-Log",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integration/solarlog",
+ "dependencies": [],
+ "codeowners": ["@Ernst79"],
+ "requirements": ["sunwatcher==0.2.1"]
+}
diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py
new file mode 100644
index 00000000000..583529ffe87
--- /dev/null
+++ b/homeassistant/components/solarlog/sensor.py
@@ -0,0 +1,159 @@
+"""Platform for solarlog sensors."""
+import logging
+from urllib.parse import ParseResult, urlparse
+
+from requests.exceptions import HTTPError, Timeout
+from sunwatcher.solarlog.solarlog import SolarLog
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+
+from .const import DOMAIN, DEFAULT_HOST, DEFAULT_NAME, SCAN_INTERVAL, SENSOR_TYPES
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ }
+)
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Import YAML configuration when available."""
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config)
+ )
+ )
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Add solarlog entry."""
+ host_entry = entry.data[CONF_HOST]
+
+ url = urlparse(host_entry, "http")
+ netloc = url.netloc or url.path
+ path = url.path if url.netloc else ""
+ url = ParseResult("http", netloc, path, *url[3:])
+ host = url.geturl()
+
+ platform_name = entry.title
+
+ try:
+ api = await hass.async_add_executor_job(SolarLog, host)
+ _LOGGER.debug("Connected to Solar-Log device, setting up entries")
+ except (OSError, HTTPError, Timeout):
+ _LOGGER.error(
+ "Could not connect to Solar-Log device at %s, check host ip address", host
+ )
+ return
+
+ # Create solarlog data service which will retrieve and update the data.
+ data = await hass.async_add_executor_job(SolarlogData, hass, api, host)
+
+ # Create a new sensor for each sensor type.
+ entities = []
+ for sensor_key in SENSOR_TYPES:
+ sensor = SolarlogSensor(platform_name, sensor_key, data)
+ entities.append(sensor)
+
+ async_add_entities(entities, True)
+ return True
+
+
+class SolarlogSensor(Entity):
+ """Representation of a 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 of the sensor."""
+ return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1])
+
+ @property
+ def unit_of_measurement(self):
+ """Return the state of the sensor."""
+ 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 SolarlogData:
+ """Get and update the latest data."""
+
+ def __init__(self, hass, api, host):
+ """Initialize the data object."""
+ self.api = api
+ self.hass = hass
+ self.host = host
+ self.update = Throttle(SCAN_INTERVAL)(self._update)
+ self.data = {}
+
+ def _update(self):
+ """Update the data from the SolarLog device."""
+ try:
+ self.api = SolarLog(self.host)
+ response = self.api.time
+ _LOGGER.debug(
+ "Connection to Solarlog successful. Retrieving latest Solarlog update of %s",
+ response,
+ )
+ except (OSError, Timeout, HTTPError):
+ _LOGGER.error("Connection error, Could not retrieve data, skipping update")
+ return
+
+ try:
+ self.data["TIME"] = self.api.time
+ self.data["powerAC"] = self.api.power_ac
+ self.data["powerDC"] = self.api.power_dc
+ self.data["voltageAC"] = self.api.voltage_ac
+ self.data["voltageDC"] = self.api.voltage_dc
+ self.data["yieldDAY"] = self.api.yield_day / 1000
+ self.data["yieldYESTERDAY"] = self.api.yield_yesterday / 1000
+ self.data["yieldMONTH"] = self.api.yield_month / 1000
+ self.data["yieldYEAR"] = self.api.yield_year / 1000
+ self.data["yieldTOTAL"] = self.api.yield_total / 1000
+ self.data["consumptionAC"] = self.api.consumption_ac
+ self.data["consumptionDAY"] = self.api.consumption_day / 1000
+ self.data["consumptionYESTERDAY"] = self.api.consumption_yesterday / 1000
+ self.data["consumptionMONTH"] = self.api.consumption_month / 1000
+ self.data["consumptionYEAR"] = self.api.consumption_year / 1000
+ self.data["consumptionTOTAL"] = self.api.consumption_total / 1000
+ self.data["totalPOWER"] = self.api.total_power
+ self.data["alternatorLOSS"] = self.api.alternator_loss
+ self.data["CAPACITY"] = round(self.api.capacity * 100, 0)
+ self.data["EFFICIENCY"] = round(self.api.efficiency * 100, 0)
+ self.data["powerAVAILABLE"] = self.api.power_available
+ self.data["USAGE"] = self.api.usage
+ _LOGGER.debug("Updated Solarlog overview data: %s", self.data)
+ except AttributeError:
+ _LOGGER.error("Missing details data in Solarlog response")
diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json
new file mode 100644
index 00000000000..5399d5176c9
--- /dev/null
+++ b/homeassistant/components/solarlog/strings.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "title": "Solar-Log",
+ "step": {
+ "user": {
+ "title": "Define your Solar-Log connection",
+ "data": {
+ "host": "The hostname or ip-address of your Solar-Log device",
+ "name": "The prefix to be used for your Solar-Log sensors"
+ }
+ }
+ },
+ "error": {
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect, please verify host address"
+ },
+ "abort": {
+ "already_configured": "Device is already configured"
+ }
+ }
+}
diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json
index 6bd4737d6fc..18b33d1bc9b 100644
--- a/homeassistant/components/soma/.translations/ca.json
+++ b/homeassistant/components/soma/.translations/ca.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Autenticaci\u00f3 exitosa amb Soma."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "port": "Port"
+ },
+ "description": "Introdueix la informaci\u00f3 de connexi\u00f3 de SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json
index a82da0ce24d..557eeab55b1 100644
--- a/homeassistant/components/soma/.translations/da.json
+++ b/homeassistant/components/soma/.translations/da.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Godkendt med Soma."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e6rt",
+ "port": "Port"
+ },
+ "description": "Indtast forbindelsesindstillinger for din SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json
index d93eec8aed7..cb08613c07b 100644
--- a/homeassistant/components/soma/.translations/de.json
+++ b/homeassistant/components/soma/.translations/de.json
@@ -4,6 +4,20 @@
"already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.",
"authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
"missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation."
- }
+ },
+ "create_entry": {
+ "default": "Erfolgreich bei Soma authentifiziert."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "description": "Bitte gib die Verbindungsinformationen f\u00fcr SOMA Connect ein.",
+ "title": "SOMA Connect"
+ }
+ },
+ "title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json
index 5dea73fcc22..42e09a8762c 100644
--- a/homeassistant/components/soma/.translations/en.json
+++ b/homeassistant/components/soma/.translations/en.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Successfully authenticated with Soma."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "description": "Please enter connection settings of your SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json
index 8126b6ea5ae..86922622704 100644
--- a/homeassistant/components/soma/.translations/es.json
+++ b/homeassistant/components/soma/.translations/es.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Autenticado con \u00e9xito con Soma."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Puerto"
+ },
+ "description": "Por favor, introduzca los ajustes de conexi\u00f3n de SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json
index e990fb98dc2..a758ab0f615 100644
--- a/homeassistant/components/soma/.translations/fr.json
+++ b/homeassistant/components/soma/.translations/fr.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Authentifi\u00e9 avec succ\u00e8s avec Soma."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "port": "Port"
+ },
+ "description": "Veuillez entrer les param\u00e8tres de connexion de votre SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json
index ce8e950dacc..1398b2a66be 100644
--- a/homeassistant/components/soma/.translations/it.json
+++ b/homeassistant/components/soma/.translations/it.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Autenticato con successo con Soma."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Porta"
+ },
+ "description": "Inserisci le impostazioni di connessione del tuo SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json
index 53146bebf83..90995ebc9f2 100644
--- a/homeassistant/components/soma/.translations/ko.json
+++ b/homeassistant/components/soma/.translations/ko.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8"
+ },
+ "description": "SOMA Connect \uc640\uc758 \uc5f0\uacb0 \uc124\uc815\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json
index d8aba082537..93e9a1e66c4 100644
--- a/homeassistant/components/soma/.translations/lb.json
+++ b/homeassistant/components/soma/.translations/lb.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "Apparat",
+ "port": "Port"
+ },
+ "description": "Gitt Verbindungs Informatioune vun \u00e4rem SOMA Connect an.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/nl.json b/homeassistant/components/soma/.translations/nl.json
new file mode 100644
index 00000000000..c1188b0ac63
--- /dev/null
+++ b/homeassistant/components/soma/.translations/nl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren.",
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen."
+ },
+ "create_entry": {
+ "default": "Succesvol geverifieerd met Soma."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Poort"
+ },
+ "description": "Voer de verbindingsinstellingen van uw SOMA Connect in.",
+ "title": "SOMA Connect"
+ }
+ },
+ "title": "Soma"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json
index 1ea53b778ea..b2d80208b83 100644
--- a/homeassistant/components/soma/.translations/no.json
+++ b/homeassistant/components/soma/.translations/no.json
@@ -1,13 +1,23 @@
{
"config": {
"abort": {
- "already_setup": "Du kan bare konfigurere en Soma-konto.",
+ "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.",
"authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.",
"missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen."
},
"create_entry": {
"default": "Vellykket autentisering med Somfy."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "port": "Port"
+ },
+ "description": "Vennligst skriv tilkoblingsinnstillingene for din SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json
index 0ed881853b8..4d783f3f0a0 100644
--- a/homeassistant/components/soma/.translations/pl.json
+++ b/homeassistant/components/soma/.translations/pl.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Pomy\u015blnie uwierzytelniono z Soma"
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/pt-BR.json b/homeassistant/components/soma/.translations/pt-BR.json
new file mode 100644
index 00000000000..da05e3b43ae
--- /dev/null
+++ b/homeassistant/components/soma/.translations/pt-BR.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Porta"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/pt.json b/homeassistant/components/soma/.translations/pt.json
new file mode 100644
index 00000000000..f681da4210f
--- /dev/null
+++ b/homeassistant/components/soma/.translations/pt.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Servidor",
+ "port": "Porta"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json
index 5ab3af0ecf8..f7e6574b113 100644
--- a/homeassistant/components/soma/.translations/ru.json
+++ b/homeassistant/components/soma/.translations/ru.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a SOMA Connect.",
+ "title": "Soma"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json
index 7dd523f366c..b3075208d2c 100644
--- a/homeassistant/components/soma/.translations/sl.json
+++ b/homeassistant/components/soma/.translations/sl.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Uspe\u0161no overjen s Soma."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "Gostitelj",
+ "port": "Vrata"
+ },
+ "description": "Prosimo, vnesite nastavitve povezave za va\u0161 SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json
index 3d28389ff91..893abe82ee1 100644
--- a/homeassistant/components/soma/.translations/zh-Hant.json
+++ b/homeassistant/components/soma/.translations/zh-Hant.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002"
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "port": "\u901a\u8a0a\u57e0"
+ },
+ "description": "\u8acb\u8f38\u5165 SOMA Connect \u9023\u7dda\u8a2d\u5b9a\u3002",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py
index 5bf51e743e9..b4daa28b5b2 100644
--- a/homeassistant/components/soma/__init__.py
+++ b/homeassistant/components/soma/__init__.py
@@ -3,6 +3,7 @@ import logging
import voluptuous as vol
from api.soma_api import SomaApi
+from requests import RequestException
import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries
@@ -75,6 +76,12 @@ class SomaEntity(Entity):
self.device = device
self.api = api
self.current_position = 50
+ self.is_available = True
+
+ @property
+ def available(self):
+ """Return true if the last API commands returned successfully."""
+ return self.is_available
@property
def unique_id(self):
@@ -100,12 +107,19 @@ class SomaEntity(Entity):
async def async_update(self):
"""Update the device with the latest data."""
- response = await self.hass.async_add_executor_job(
- self.api.get_shade_state, self.device["mac"]
- )
+ try:
+ response = await self.hass.async_add_executor_job(
+ self.api.get_shade_state, self.device["mac"]
+ )
+ except RequestException:
+ _LOGGER.error("Connection to SOMA Connect failed")
+ self.is_available = False
+ return
if response["result"] != "success":
_LOGGER.error(
"Unable to reach device %s (%s)", self.device["name"], response["msg"]
)
+ self.is_available = False
return
self.current_position = 100 - response["position"]
+ self.is_available = True
diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json
index eac817ce119..aa2f92f0be6 100644
--- a/homeassistant/components/soma/strings.json
+++ b/homeassistant/components/soma/strings.json
@@ -8,6 +8,16 @@
"create_entry": {
"default": "Successfully authenticated with Soma."
},
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "description": "Please enter connection settings of your SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
"title": "Soma"
}
}
diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py
index 2c7c71d7a69..cd5960bf6b1 100644
--- a/homeassistant/components/somfy/__init__.py
+++ b/homeassistant/components/somfy/__init__.py
@@ -4,21 +4,21 @@ Support for Somfy hubs.
For more details about this component, please refer to the documentation at
https://home-assistant.io/integrations/somfy/
"""
+import asyncio
import logging
from datetime import timedelta
-from functools import partial
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant import config_entries
+from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow
from homeassistant.components.somfy import config_flow
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_TOKEN
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle
+from . import api
+
API = "api"
DEVICES = "devices"
@@ -52,19 +52,21 @@ SOMFY_COMPONENTS = ["cover"]
async def async_setup(hass, config):
"""Set up the Somfy component."""
+ hass.data[DOMAIN] = {}
+
if DOMAIN not in config:
return True
- hass.data[DOMAIN] = {}
-
- config_flow.register_flow_implementation(
- hass, config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET]
- )
-
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
- )
+ config_flow.SomfyFlowHandler.async_register_implementation(
+ hass,
+ config_entry_oauth2_flow.LocalOAuth2Implementation(
+ hass,
+ DOMAIN,
+ config[DOMAIN][CONF_CLIENT_ID],
+ config[DOMAIN][CONF_CLIENT_SECRET],
+ "https://accounts.somfy.com/oauth/oauth/v2/auth",
+ "https://accounts.somfy.com/oauth/oauth/v2/token",
+ ),
)
return True
@@ -72,25 +74,18 @@ async def async_setup(hass, config):
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Set up Somfy from a config entry."""
-
- def token_saver(token):
- _LOGGER.debug("Saving updated token")
- entry.data[CONF_TOKEN] = token
- update_entry = partial(
- hass.config_entries.async_update_entry, data={**entry.data}
+ # Backwards compat
+ if "auth_implementation" not in entry.data:
+ hass.config_entries.async_update_entry(
+ entry, data={**entry.data, "auth_implementation": DOMAIN}
)
- hass.add_job(update_entry, entry)
- # Force token update.
- from pymfy.api.somfy_api import SomfyApi
-
- hass.data[DOMAIN][API] = SomfyApi(
- entry.data["refresh_args"]["client_id"],
- entry.data["refresh_args"]["client_secret"],
- token=entry.data[CONF_TOKEN],
- token_updater=token_saver,
+ implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
+ hass, entry
)
+ hass.data[DOMAIN][API] = api.ConfigEntrySomfyApi(hass, entry, implementation)
+
await update_all_devices(hass)
for component in SOMFY_COMPONENTS:
@@ -104,16 +99,22 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Unload a config entry."""
hass.data[DOMAIN].pop(API, None)
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in SOMFY_COMPONENTS
+ ]
+ )
return True
class SomfyEntity(Entity):
"""Representation of a generic Somfy device."""
- def __init__(self, device, api):
+ def __init__(self, device, somfy_api):
"""Initialize the Somfy device."""
self.device = device
- self.api = api
+ self.api = somfy_api
@property
def unique_id(self):
diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py
new file mode 100644
index 00000000000..3e7bcf9deb4
--- /dev/null
+++ b/homeassistant/components/somfy/api.py
@@ -0,0 +1,55 @@
+"""API for Somfy bound to HASS OAuth."""
+from asyncio import run_coroutine_threadsafe
+from functools import partial
+
+import requests
+from pymfy.api import somfy_api
+
+from homeassistant import core, config_entries
+from homeassistant.helpers import config_entry_oauth2_flow
+
+
+class ConfigEntrySomfyApi(somfy_api.AbstractSomfyApi):
+ """Provide a Somfy API tied into an OAuth2 based config entry."""
+
+ def __init__(
+ self,
+ hass: core.HomeAssistant,
+ config_entry: config_entries.ConfigEntry,
+ implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
+ ):
+ """Initialize the Config Entry Somfy API."""
+ self.hass = hass
+ self.config_entry = config_entry
+ self.session = config_entry_oauth2_flow.OAuth2Session(
+ hass, config_entry, implementation
+ )
+
+ def get(self, path):
+ """Fetch a URL from the Somfy API."""
+ return run_coroutine_threadsafe(
+ self._request("get", path), self.hass.loop
+ ).result()
+
+ def post(self, path, *, json):
+ """Post data to the Somfy API."""
+ return run_coroutine_threadsafe(
+ self._request("post", path, json=json), self.hass.loop
+ ).result()
+
+ async def _request(self, method, path, **kwargs):
+ """Make a request."""
+ await self.session.async_ensure_token_valid()
+
+ return await self.hass.async_add_executor_job(
+ partial(
+ requests.request,
+ method,
+ f"{self.base_url}{path}",
+ **kwargs,
+ headers={
+ **kwargs.get("headers", {}),
+ "authorization": f"Bearer {self.config_entry.data['token']['access_token']}",
+ },
+ )
+ )
diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py
index 9f3c58c8ffb..cb180d4e247 100644
--- a/homeassistant/components/somfy/config_flow.py
+++ b/homeassistant/components/somfy/config_flow.py
@@ -1,141 +1,28 @@
"""Config flow for Somfy."""
-import asyncio
import logging
-import async_timeout
-
from homeassistant import config_entries
-from homeassistant.components.http import HomeAssistantView
-from homeassistant.core import callback
-from .const import CLIENT_ID, CLIENT_SECRET, DOMAIN
-
-AUTH_CALLBACK_PATH = "/auth/somfy/callback"
-AUTH_CALLBACK_NAME = "auth:somfy:callback"
+from homeassistant.helpers import config_entry_oauth2_flow
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-@callback
-def register_flow_implementation(hass, client_id, client_secret):
- """Register a flow implementation.
+@config_entries.HANDLERS.register(DOMAIN)
+class SomfyFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
+ """Config flow to handle Somfy OAuth2 authentication."""
- client_id: Client id.
- client_secret: Client secret.
- """
- hass.data[DOMAIN][CLIENT_ID] = client_id
- hass.data[DOMAIN][CLIENT_SECRET] = client_secret
-
-
-@config_entries.HANDLERS.register("somfy")
-class SomfyFlowHandler(config_entries.ConfigFlow):
- """Handle a config flow."""
-
- VERSION = 1
+ DOMAIN = DOMAIN
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
- def __init__(self):
- """Instantiate config flow."""
- self.code = None
-
- async def async_step_import(self, user_input=None):
- """Handle external yaml configuration."""
- if self.hass.config_entries.async_entries(DOMAIN):
- return self.async_abort(reason="already_setup")
- return await self.async_step_auth()
+ @property
+ def logger(self) -> logging.Logger:
+ """Return logger."""
+ return logging.getLogger(__name__)
async def async_step_user(self, user_input=None):
"""Handle a flow start."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason="already_setup")
- if DOMAIN not in self.hass.data:
- return self.async_abort(reason="missing_configuration")
-
- return await self.async_step_auth()
-
- async def async_step_auth(self, user_input=None):
- """Create an entry for auth."""
- # Flow has been triggered from Somfy website
- if user_input:
- return await self.async_step_code(user_input)
-
- try:
- with async_timeout.timeout(10):
- url, _ = await self._get_authorization_url()
- except asyncio.TimeoutError:
- return self.async_abort(reason="authorize_url_timeout")
-
- return self.async_external_step(step_id="auth", url=url)
-
- async def _get_authorization_url(self):
- """Get Somfy authorization url."""
- from pymfy.api.somfy_api import SomfyApi
-
- client_id = self.hass.data[DOMAIN][CLIENT_ID]
- client_secret = self.hass.data[DOMAIN][CLIENT_SECRET]
- redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}"
- api = SomfyApi(client_id, client_secret, redirect_uri)
-
- self.hass.http.register_view(SomfyAuthCallbackView())
- # Thanks to the state, we can forward the flow id to Somfy that will
- # add it in the callback.
- return await self.hass.async_add_executor_job(
- api.get_authorization_url, self.flow_id
- )
-
- async def async_step_code(self, code):
- """Received code for authentication."""
- self.code = code
- return self.async_external_step_done(next_step_id="creation")
-
- async def async_step_creation(self, user_input=None):
- """Create Somfy api and entries."""
- client_id = self.hass.data[DOMAIN][CLIENT_ID]
- client_secret = self.hass.data[DOMAIN][CLIENT_SECRET]
- code = self.code
- from pymfy.api.somfy_api import SomfyApi
-
- redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}"
- api = SomfyApi(client_id, client_secret, redirect_uri)
- token = await self.hass.async_add_executor_job(api.request_token, None, code)
- _LOGGER.info("Successfully authenticated Somfy")
- return self.async_create_entry(
- title="Somfy",
- data={
- "token": token,
- "refresh_args": {
- "client_id": client_id,
- "client_secret": client_secret,
- },
- },
- )
-
-
-class SomfyAuthCallbackView(HomeAssistantView):
- """Somfy Authorization Callback View."""
-
- requires_auth = False
- url = AUTH_CALLBACK_PATH
- name = AUTH_CALLBACK_NAME
-
- @staticmethod
- async def get(request):
- """Receive authorization code."""
- from aiohttp import web_response
-
- if "code" not in request.query or "state" not in request.query:
- return web_response.Response(
- text="Missing code or state parameter in " + request.url
- )
-
- hass = request.app["hass"]
- hass.async_create_task(
- hass.config_entries.flow.async_configure(
- flow_id=request.query["state"], user_input=request.query["code"]
- )
- )
-
- return web_response.Response(
- headers={"content-type": "text/html"},
- text="",
- )
+ return await super().async_step_user(user_input)
diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py
index 99fafb71bff..8765e37e6d6 100644
--- a/homeassistant/components/somfy/const.py
+++ b/homeassistant/components/somfy/const.py
@@ -1,5 +1,3 @@
"""Define constants for the Somfy component."""
DOMAIN = "somfy"
-CLIENT_ID = "client_id"
-CLIENT_SECRET = "client_secret"
diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json
index 83b50684fda..a34023f76ff 100644
--- a/homeassistant/components/somfy/manifest.json
+++ b/homeassistant/components/somfy/manifest.json
@@ -3,11 +3,7 @@
"name": "Somfy Open API",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/somfy",
- "dependencies": [],
- "codeowners": [
- "@tetienne"
- ],
- "requirements": [
- "pymfy==0.5.2"
- ]
-}
\ No newline at end of file
+ "dependencies": ["http"],
+ "codeowners": ["@tetienne"],
+ "requirements": ["pymfy==0.6.0"]
+}
diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py
index 9f5ae9d1ac6..0567cd0ea6a 100644
--- a/homeassistant/components/songpal/media_player.py
+++ b/homeassistant/components/songpal/media_player.py
@@ -4,6 +4,14 @@ import logging
from collections import OrderedDict
import voluptuous as vol
+from songpal import (
+ Device,
+ SongpalException,
+ VolumeChange,
+ ContentChange,
+ PowerChange,
+ ConnectChange,
+)
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
from homeassistant.components.media_player.const import (
@@ -60,8 +68,6 @@ SET_SOUND_SCHEMA = vol.Schema(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Songpal platform."""
- from songpal import SongpalException
-
if PLATFORM not in hass.data:
hass.data[PLATFORM] = {}
@@ -117,8 +123,6 @@ class SongpalDevice(MediaPlayerDevice):
def __init__(self, name, endpoint, poll=False):
"""Init."""
- from songpal import Device
-
self._name = name
self._endpoint = endpoint
self._poll = poll
@@ -151,7 +155,6 @@ class SongpalDevice(MediaPlayerDevice):
async def async_activate_websocket(self):
"""Activate websocket for listening if wanted."""
_LOGGER.info("Activating websocket connection..")
- from songpal import VolumeChange, ContentChange, PowerChange, ConnectChange
async def _volume_changed(volume: VolumeChange):
_LOGGER.debug("Volume changed: %s", volume)
@@ -230,8 +233,6 @@ class SongpalDevice(MediaPlayerDevice):
async def async_update(self):
"""Fetch updates from the device."""
- from songpal import SongpalException
-
try:
volumes = await self.dev.get_volume_information()
if not volumes:
diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py
index bd16cfe353a..d2c6210f01c 100644
--- a/homeassistant/components/sonos/__init__.py
+++ b/homeassistant/components/sonos/__init__.py
@@ -1,16 +1,16 @@
"""Support to embed Sonos."""
import asyncio
+
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
-from homeassistant.const import CONF_HOSTS, ATTR_ENTITY_ID, ATTR_TIME
+from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import DOMAIN
-
CONF_ADVERTISE_ADDR = "advertise_addr"
CONF_INTERFACE_ADDR = "interface_addr"
diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py
index 3ce62f54a2f..42ac32163a4 100644
--- a/homeassistant/components/sonos/config_flow.py
+++ b/homeassistant/components/sonos/config_flow.py
@@ -1,13 +1,14 @@
"""Config flow for SONOS."""
-from homeassistant.helpers import config_entry_flow
+import pysonos
+
from homeassistant import config_entries
+from homeassistant.helpers import config_entry_flow
+
from .const import DOMAIN
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
- import pysonos
-
return await hass.async_add_executor_job(pysonos.discover)
diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json
index 6d636f36b3f..7b0c041b2a9 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/integrations/sonos",
"requirements": [
- "pysonos==0.0.23"
+ "pysonos==0.0.24"
],
"dependencies": [],
"ssdp": {
diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py
index 41472413a07..94d252e9fee 100644
--- a/homeassistant/components/sonos/media_player.py
+++ b/homeassistant/components/sonos/media_player.py
@@ -8,8 +8,8 @@ import urllib
import async_timeout
import pysonos
+from pysonos.exceptions import SoCoException, SoCoUPnPException
import pysonos.snapshot
-from pysonos.exceptions import SoCoUPnPException, SoCoException
from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import (
@@ -40,11 +40,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.dt import utcnow
from . import (
- CONF_ADVERTISE_ADDR,
- CONF_HOSTS,
- CONF_INTERFACE_ADDR,
- DATA_SERVICE_EVENT,
- DOMAIN as SONOS_DOMAIN,
ATTR_ALARM_ID,
ATTR_ENABLED,
ATTR_INCLUDE_LINKED_ZONES,
@@ -56,6 +51,11 @@ from . import (
ATTR_TIME,
ATTR_VOLUME,
ATTR_WITH_GROUP,
+ CONF_ADVERTISE_ADDR,
+ CONF_HOSTS,
+ CONF_INTERFACE_ADDR,
+ DATA_SERVICE_EVENT,
+ DOMAIN as SONOS_DOMAIN,
SERVICE_CLEAR_TIMER,
SERVICE_JOIN,
SERVICE_PLAY_QUEUE,
@@ -1161,10 +1161,9 @@ class SonosEntity(MediaPlayerDevice):
@soco_coordinator
def set_alarm(self, data):
"""Set the alarm clock on the player."""
- from pysonos import alarms
alarm = None
- for one_alarm in alarms.get_alarms(self.soco):
+ for one_alarm in pysonos.alarms.get_alarms(self.soco):
# pylint: disable=protected-access
if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]):
alarm = one_alarm
diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py
index 43a4b7bc0fe..e68bed34cfa 100644
--- a/homeassistant/components/sony_projector/switch.py
+++ b/homeassistant/components/sony_projector/switch.py
@@ -1,10 +1,11 @@
"""Support for Sony projectors via SDCP network control."""
import logging
+import pysdcp
import voluptuous as vol
-from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
-from homeassistant.const import STATE_ON, STATE_OFF, CONF_NAME, CONF_HOST
+from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
+from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -21,7 +22,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Connect to Sony projector using network."""
- import pysdcp
host = config[CONF_HOST]
name = config[CONF_NAME]
diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py
index 029356cb082..afccc71d285 100644
--- a/homeassistant/components/speedtestdotnet/__init__.py
+++ b/homeassistant/components/speedtestdotnet/__init__.py
@@ -1,15 +1,17 @@
"""Support for testing internet speed via Speedtest.net."""
-import logging
from datetime import timedelta
+import logging
+import speedtest
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
+
from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
@@ -72,7 +74,6 @@ class SpeedtestData:
def update(self, now=None):
"""Get the latest data from speedtest.net."""
- import speedtest
_LOGGER.debug("Executing speedtest.net speed test")
speed = speedtest.Speedtest()
diff --git a/homeassistant/components/spotcrime/sensor.py b/homeassistant/components/spotcrime/sensor.py
index fc3a7592af3..2edaa3cf933 100644
--- a/homeassistant/components/spotcrime/sensor.py
+++ b/homeassistant/components/spotcrime/sensor.py
@@ -1,27 +1,28 @@
"""Sensor for Spot Crime."""
-from datetime import timedelta
from collections import defaultdict
+from datetime import timedelta
import logging
+import spotcrime
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_API_KEY,
- CONF_INCLUDE,
- CONF_EXCLUDE,
- CONF_NAME,
- CONF_LATITUDE,
- CONF_LONGITUDE,
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
ATTR_LONGITUDE,
+ CONF_API_KEY,
+ CONF_EXCLUDE,
+ CONF_INCLUDE,
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_NAME,
CONF_RADIUS,
)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
-import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -75,7 +76,6 @@ class SpotCrimeSensor(Entity):
self, name, latitude, longitude, radius, include, exclude, api_key, days
):
"""Initialize the Spot Crime sensor."""
- import spotcrime
self._name = name
self._include = include
diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py
index 31fdc09af80..236c8b8db89 100644
--- a/homeassistant/components/spotify/media_player.py
+++ b/homeassistant/components/spotify/media_player.py
@@ -3,11 +3,14 @@ from datetime import timedelta
import logging
import random
+import spotipy
+import spotipy.oauth2
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
+ ATTR_MEDIA_CONTENT_ID,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_PLAYLIST,
SUPPORT_NEXT_TRACK,
@@ -18,7 +21,6 @@ from homeassistant.components.media_player.const import (
SUPPORT_SELECT_SOURCE,
SUPPORT_SHUFFLE_SET,
SUPPORT_VOLUME_SET,
- ATTR_MEDIA_CONTENT_ID,
)
from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING
from homeassistant.core import callback
@@ -97,7 +99,6 @@ def request_configuration(hass, config, add_entities, oauth):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Spotify platform."""
- import spotipy.oauth2
callback_url = f"{hass.config.api.base_url}{AUTH_CALLBACK_PATH}"
cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH))
@@ -181,7 +182,6 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
def refresh_spotify_instance(self):
"""Fetch a new spotify instance."""
- import spotipy
token_refreshed = False
need_token = self._token_info is None or self._oauth.is_token_expired(
diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json
index 41d80ebccf9..fa641adc839 100644
--- a/homeassistant/components/sql/manifest.json
+++ b/homeassistant/components/sql/manifest.json
@@ -3,10 +3,10 @@
"name": "Sql",
"documentation": "https://www.home-assistant.io/integrations/sql",
"requirements": [
- "sqlalchemy==1.3.8"
+ "sqlalchemy==1.3.10"
],
"dependencies": [],
"codeowners": [
"@dgomes"
]
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py
index 3b32f2747f1..52899c7da80 100644
--- a/homeassistant/components/sql/sensor.py
+++ b/homeassistant/components/sql/sensor.py
@@ -1,13 +1,15 @@
"""Sensor from an SQL Query."""
-import decimal
import datetime
+import decimal
import logging
+import sqlalchemy
+from sqlalchemy.orm import scoped_session, sessionmaker
import voluptuous as vol
+from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE
-from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_URL, DEFAULT_DB_FILE
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -46,20 +48,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if not db_url:
db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE))
- import sqlalchemy
- from sqlalchemy.orm import sessionmaker, scoped_session
-
try:
engine = sqlalchemy.create_engine(db_url)
- sessionmaker = scoped_session(sessionmaker(bind=engine))
+ sessmaker = scoped_session(sessionmaker(bind=engine))
# Run a dummy query just to test the db_url
- sess = sessionmaker()
+ sess = sessmaker()
sess.execute("SELECT 1;")
except sqlalchemy.exc.SQLAlchemyError as err:
_LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err)
return
+ finally:
+ sess.close()
queries = []
@@ -74,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
value_template.hass = hass
sensor = SQLSensor(
- name, sessionmaker, query_str, column_name, unit, value_template
+ name, sessmaker, query_str, column_name, unit, value_template
)
queries.append(sensor)
@@ -120,7 +121,6 @@ class SQLSensor(Entity):
def update(self):
"""Retrieve sensor data from the query."""
- import sqlalchemy
try:
sess = self.sessionmaker()
diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py
index 6540fca1405..d8574223307 100644
--- a/homeassistant/components/squeezebox/media_player.py
+++ b/homeassistant/components/squeezebox/media_player.py
@@ -2,13 +2,14 @@
import asyncio
import json
import logging
+import socket
import urllib.parse
import aiohttp
import async_timeout
import voluptuous as vol
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
from homeassistant.components.media_player.const import (
ATTR_MEDIA_ENQUEUE,
DOMAIN,
@@ -40,6 +41,7 @@ from homeassistant.const import (
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.util.dt import utcnow
_LOGGER = logging.getLogger(__name__)
@@ -100,7 +102,6 @@ SERVICE_TO_METHOD = {
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the squeezebox platform."""
- import socket
known_servers = hass.data.get(KNOWN_SERVERS)
if known_servers is None:
@@ -126,18 +127,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
# Get IP of host, to prevent duplication of same host (different DNS names)
try:
ipaddr = socket.gethostbyname(host)
- except (OSError) as error:
+ except OSError as error:
_LOGGER.error("Could not communicate with %s:%d: %s", host, port, error)
- return False
+ raise PlatformNotReady from error
if ipaddr in known_servers:
return
- known_servers.add(ipaddr)
_LOGGER.debug("Creating LMS object for %s", ipaddr)
lms = LogitechMediaServer(hass, host, port, username, password)
players = await lms.create_players()
+ if players is None:
+ raise PlatformNotReady
+
+ known_servers.add(ipaddr)
hass.data[DATA_SQUEEZEBOX].extend(players)
async_add_entities(players)
@@ -194,7 +198,7 @@ class LogitechMediaServer:
result = []
data = await self.async_query("players", "status")
if data is False:
- return result
+ return None
for players in data.get("players_loop", []):
player = SqueezeBoxDevice(self, players["playerid"], players["name"])
await player.async_update()
diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py
index 1b567c58b45..55ae15cede7 100644
--- a/homeassistant/components/startca/sensor.py
+++ b/homeassistant/components/startca/sensor.py
@@ -1,10 +1,11 @@
"""Support for Start.ca Bandwidth Monitor."""
from datetime import timedelta
-from xml.parsers.expat import ExpatError
import logging
-import async_timeout
+from xml.parsers.expat import ExpatError
+import async_timeout
import voluptuous as vol
+import xmltodict
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME
@@ -138,8 +139,6 @@ class StartcaData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Get the Start.ca bandwidth data from the web service."""
- import xmltodict
-
_LOGGER.debug("Updating Start.ca usage data")
url = "https://www.start.ca/support/usage/api?key=" + self.api_key
with async_timeout.timeout(REQUEST_TIMEOUT):
diff --git a/homeassistant/components/statsd/__init__.py b/homeassistant/components/statsd/__init__.py
index 714de88dd87..79065f7ba53 100644
--- a/homeassistant/components/statsd/__init__.py
+++ b/homeassistant/components/statsd/__init__.py
@@ -1,11 +1,12 @@
"""Support for sending data to StatsD."""
import logging
+import statsd
import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_STATE_CHANGED
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import state as state_helper
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -40,7 +41,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the StatsD component."""
- import statsd
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py
index 6c9c5ac6079..85e5c49fb2c 100644
--- a/homeassistant/components/steam_online/sensor.py
+++ b/homeassistant/components/steam_online/sensor.py
@@ -1,15 +1,16 @@
"""Sensor for Steam account status."""
-import logging
from datetime import timedelta
+import logging
+import steam
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_API_KEY
from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
-from homeassistant.const import CONF_API_KEY
-import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -38,13 +39,12 @@ BASE_INTERVAL = timedelta(minutes=1)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Steam platform."""
- import steam as steamod
- steamod.api.key.set(config.get(CONF_API_KEY))
+ steam.api.key.set(config.get(CONF_API_KEY))
# Initialize steammods app list before creating sensors
# to benefit from internal caching of the list.
- hass.data[APP_LIST_KEY] = steamod.apps.app_list()
- entities = [SteamSensor(account, steamod) for account in config.get(CONF_ACCOUNTS)]
+ hass.data[APP_LIST_KEY] = steam.apps.app_list()
+ entities = [SteamSensor(account, steam) for account in config.get(CONF_ACCOUNTS)]
if not entities:
return
add_entities(entities, True)
diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py
index 2ae8dd5f714..a83f05820e2 100644
--- a/homeassistant/components/stream/__init__.py
+++ b/homeassistant/components/stream/__init__.py
@@ -4,31 +4,30 @@ import threading
import voluptuous as vol
+from homeassistant.auth.util import generate_secret
+from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import callback
+from homeassistant.exceptions import HomeAssistantError
+import homeassistant.helpers.config_validation as cv
+from homeassistant.loader import bind_hass
+
+from .const import (
+ ATTR_ENDPOINTS,
+ ATTR_STREAMS,
+ CONF_DURATION,
+ CONF_LOOKBACK,
+ CONF_STREAM_SOURCE,
+ DOMAIN,
+ SERVICE_RECORD,
+)
+from .core import PROVIDERS
+from .hls import async_setup_hls
+
try:
import uvloop
except ImportError:
uvloop = None
-from homeassistant.auth.util import generate_secret
-import homeassistant.helpers.config_validation as cv
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_FILENAME
-from homeassistant.core import callback
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.loader import bind_hass
-
-from .const import (
- DOMAIN,
- ATTR_STREAMS,
- ATTR_ENDPOINTS,
- CONF_STREAM_SOURCE,
- CONF_DURATION,
- CONF_LOOKBACK,
- SERVICE_RECORD,
-)
-from .core import PROVIDERS
-from .worker import stream_worker
-from .hls import async_setup_hls
-from .recorder import async_setup_recorder
_LOGGER = logging.getLogger(__name__)
@@ -104,6 +103,9 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N
async def async_setup(hass, config):
"""Set up stream."""
+ # Keep import here so that we can import stream integration without installing reqs
+ from .recorder import async_setup_recorder
+
hass.data[DOMAIN] = {}
hass.data[DOMAIN][ATTR_ENDPOINTS] = {}
hass.data[DOMAIN][ATTR_STREAMS] = {}
@@ -181,6 +183,9 @@ class Stream:
def start(self):
"""Start a stream."""
+ # Keep import here so that we can import stream integration without installing reqs
+ from .worker import stream_worker
+
if self._thread is None or not self._thread.isAlive():
self._thread_quit = threading.Event()
self._thread = threading.Thread(
diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py
index 81335783e1a..9282c2cb855 100644
--- a/homeassistant/components/stream/core.py
+++ b/homeassistant/components/stream/core.py
@@ -2,17 +2,17 @@
import asyncio
from collections import deque
import io
-from typing import List, Any
+from typing import Any, List
-import attr
from aiohttp import web
+import attr
-from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
+from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later
from homeassistant.util.decorator import Registry
-from .const import DOMAIN, ATTR_STREAMS
+from .const import ATTR_STREAMS, DOMAIN
PROVIDERS = Registry()
diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py
index c9e62f53a57..2cd98c0a00f 100644
--- a/homeassistant/components/stream/hls.py
+++ b/homeassistant/components/stream/hls.py
@@ -5,7 +5,7 @@ from homeassistant.core import callback
from homeassistant.util.dt import utcnow
from .const import FORMAT_CONTENT_TYPE
-from .core import StreamView, StreamOutput, PROVIDERS
+from .core import PROVIDERS, StreamOutput, StreamView
@callback
diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py
index cd25896aff3..1dd90b8b804 100644
--- a/homeassistant/components/stream/recorder.py
+++ b/homeassistant/components/stream/recorder.py
@@ -1,10 +1,13 @@
"""Provide functionality to record stream."""
+
import threading
from typing import List
+import av
+
from homeassistant.core import callback
-from .core import Segment, StreamOutput, PROVIDERS
+from .core import PROVIDERS, Segment, StreamOutput
@callback
@@ -14,8 +17,6 @@ def async_setup_recorder(hass):
def recorder_save_worker(file_out: str, segments: List[Segment]):
"""Handle saving stream."""
- import av
-
output = av.open(file_out, "w", options={"movflags": "frag_keyframe"})
output_v = None
diff --git a/homeassistant/components/stream/services.yaml b/homeassistant/components/stream/services.yaml
index e69de29bb2d..c3b25e06348 100644
--- a/homeassistant/components/stream/services.yaml
+++ b/homeassistant/components/stream/services.yaml
@@ -0,0 +1,15 @@
+record:
+ description: Make a .mp4 recording from a provided stream.
+ fields:
+ stream_source:
+ description: The input source for the stream.
+ example: "rtsp://my.stream.feed:554"
+ filename:
+ description: The file name string.
+ example: "/tmp/my_stream.mp4"
+ duration:
+ description: "Target recording length (in seconds). Default: 30"
+ example: 30
+ lookback:
+ description: "Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream for stream_source. Default: 0"
+ example: 5
\ No newline at end of file
diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py
index e87221304a3..99ffd833eb3 100644
--- a/homeassistant/components/stream/worker.py
+++ b/homeassistant/components/stream/worker.py
@@ -3,6 +3,8 @@ from fractions import Fraction
import io
import logging
+import av
+
from .const import AUDIO_SAMPLE_RATE
from .core import Segment, StreamBuffer
@@ -11,9 +13,8 @@ _LOGGER = logging.getLogger(__name__)
def generate_audio_frame():
"""Generate a blank audio frame."""
- from av import AudioFrame
- audio_frame = AudioFrame(format="dbl", layout="mono", samples=1024)
+ audio_frame = av.AudioFrame(format="dbl", layout="mono", samples=1024)
# audio_bytes = b''.join(b'\x00\x00\x00\x00\x00\x00\x00\x00'
# for i in range(0, 1024))
audio_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" * 1024
@@ -25,7 +26,6 @@ def generate_audio_frame():
def create_stream_buffer(stream_output, video_stream, audio_frame):
"""Create a new StreamBuffer."""
- import av
a_packet = None
segment = io.BytesIO()
@@ -45,7 +45,6 @@ def create_stream_buffer(stream_output, video_stream, audio_frame):
def stream_worker(hass, stream, quit_event):
"""Handle consuming streams."""
- import av
container = av.open(stream.source, options=stream.options)
try:
diff --git a/homeassistant/components/stride/__init__.py b/homeassistant/components/stride/__init__.py
deleted file mode 100644
index 461a3ee744f..00000000000
--- a/homeassistant/components/stride/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The stride component."""
diff --git a/homeassistant/components/stride/manifest.json b/homeassistant/components/stride/manifest.json
deleted file mode 100644
index 840984ad073..00000000000
--- a/homeassistant/components/stride/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "domain": "stride",
- "name": "Stride",
- "documentation": "https://www.home-assistant.io/integrations/stride",
- "requirements": [
- "pystride==0.1.7"
- ],
- "dependencies": [],
- "codeowners": []
-}
diff --git a/homeassistant/components/stride/notify.py b/homeassistant/components/stride/notify.py
deleted file mode 100644
index 082d986491a..00000000000
--- a/homeassistant/components/stride/notify.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""Stride platform for notify component."""
-import logging
-
-import voluptuous as vol
-
-from homeassistant.const import CONF_ROOM, CONF_TOKEN
-import homeassistant.helpers.config_validation as cv
-
-from homeassistant.components.notify import (
- ATTR_DATA,
- ATTR_TARGET,
- PLATFORM_SCHEMA,
- BaseNotificationService,
-)
-
-_LOGGER = logging.getLogger(__name__)
-
-CONF_PANEL = "panel"
-CONF_CLOUDID = "cloudid"
-
-DEFAULT_PANEL = None
-
-VALID_PANELS = {"info", "note", "tip", "warning", None}
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_CLOUDID): cv.string,
- vol.Required(CONF_ROOM): cv.string,
- vol.Required(CONF_TOKEN): cv.string,
- vol.Optional(CONF_PANEL, default=DEFAULT_PANEL): vol.In(VALID_PANELS),
- }
-)
-
-
-def get_service(hass, config, discovery_info=None):
- """Get the Stride notification service."""
- return StrideNotificationService(
- config[CONF_TOKEN], config[CONF_ROOM], config[CONF_PANEL], config[CONF_CLOUDID]
- )
-
-
-class StrideNotificationService(BaseNotificationService):
- """Implement the notification service for Stride."""
-
- def __init__(self, token, default_room, default_panel, cloudid):
- """Initialize the service."""
- self._token = token
- self._default_room = default_room
- self._default_panel = default_panel
- self._cloudid = cloudid
-
- from stride import Stride
-
- self._stride = Stride(self._cloudid, access_token=self._token)
-
- def send_message(self, message="", **kwargs):
- """Send a message."""
- panel = self._default_panel
-
- if kwargs.get(ATTR_DATA) is not None:
- data = kwargs.get(ATTR_DATA)
- if (data.get(CONF_PANEL) is not None) and (
- data.get(CONF_PANEL) in VALID_PANELS
- ):
- panel = data.get(CONF_PANEL)
-
- message_text = {
- "type": "paragraph",
- "content": [{"type": "text", "text": message}],
- }
- panel_text = message_text
- if panel is not None:
- panel_text = {
- "type": "panel",
- "attrs": {"panelType": panel},
- "content": [message_text],
- }
-
- message_doc = {"body": {"version": 1, "type": "doc", "content": [panel_text]}}
-
- targets = kwargs.get(ATTR_TARGET, [self._default_room])
-
- for target in targets:
- self._stride.message_room(target, message_doc)
diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py
index 7d883e273e5..e848449e61e 100644
--- a/homeassistant/components/sun/__init__.py
+++ b/homeassistant/components/sun/__init__.py
@@ -18,7 +18,7 @@ from homeassistant.helpers.sun import (
from homeassistant.util import dt as dt_util
-# mypy: allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py
index 86e763142e6..4293f187f5b 100644
--- a/homeassistant/components/supla/__init__.py
+++ b/homeassistant/components/supla/__init__.py
@@ -9,8 +9,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ["pysupla==0.0.3"]
-
_LOGGER = logging.getLogger(__name__)
DOMAIN = "supla"
diff --git a/homeassistant/components/switch/.translations/de.json b/homeassistant/components/switch/.translations/de.json
new file mode 100644
index 00000000000..5396facadd7
--- /dev/null
+++ b/homeassistant/components/switch/.translations/de.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "{entity_name} umschalten",
+ "turn_off": "Schalte {entity_name} aus.",
+ "turn_on": "Schalte {entity_name} ein."
+ },
+ "condition_type": {
+ "is_off": "{entity_name} ist ausgeschaltet",
+ "is_on": "{entity_name} ist eingeschaltet",
+ "turn_off": "{entity_name} ausgeschaltet",
+ "turn_on": "{entity_name} eingeschaltet"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} ausgeschaltet",
+ "turned_on": "{entity_name} eingeschaltet"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/lv.json b/homeassistant/components/switch/.translations/lv.json
new file mode 100644
index 00000000000..784a9a37afa
--- /dev/null
+++ b/homeassistant/components/switch/.translations/lv.json
@@ -0,0 +1,12 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "turn_off": "{entity_name} tika izsl\u0113gta",
+ "turn_on": "{entity_name} tika iesl\u0113gta"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} tika izsl\u0113gta",
+ "turned_on": "{entity_name} tika iesl\u0113gta"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json
index 09b43f4100d..3d352aa2b58 100644
--- a/homeassistant/components/switch/.translations/pl.json
+++ b/homeassistant/components/switch/.translations/pl.json
@@ -12,8 +12,8 @@
"turn_on": "prze\u0142\u0105cznik {entity_name} w\u0142\u0105czony"
},
"trigger_type": {
- "turned_off": "wy\u0142\u0105czenie {entity_name}",
- "turned_on": "w\u0142\u0105czenie {entity_name}"
+ "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}",
+ "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json
index cd5cbc0d6a1..74503eea60b 100644
--- a/homeassistant/components/switch/.translations/ru.json
+++ b/homeassistant/components/switch/.translations/ru.json
@@ -6,14 +6,14 @@
"turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}"
},
"condition_type": {
- "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
- "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
- "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
- "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e"
+ "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "turn_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "turn_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438"
},
"trigger_type": {
- "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
- "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435"
+ "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py
index 5825a3ba91a..56f8f6c196e 100644
--- a/homeassistant/components/switch/device_condition.py
+++ b/homeassistant/components/switch/device_condition.py
@@ -1,5 +1,5 @@
"""Provides device conditions for switches."""
-from typing import List
+from typing import Dict, List
import voluptuous as vol
from homeassistant.core import HomeAssistant
@@ -21,9 +21,16 @@ def async_condition_from_config(
"""Evaluate state based on configuration."""
if config_validation:
config = CONDITION_SCHEMA(config)
- return toggle_entity.async_condition_from_config(config, config_validation)
+ return toggle_entity.async_condition_from_config(config)
-async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_conditions(
+ hass: HomeAssistant, device_id: str
+) -> List[Dict[str, str]]:
"""List device conditions."""
return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)
+
+
+async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict:
+ """List condition capabilities."""
+ return await toggle_entity.async_get_condition_capabilities(hass, config)
diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py
index 22a016e49b9..7f0458b3e9f 100644
--- a/homeassistant/components/switch/device_trigger.py
+++ b/homeassistant/components/switch/device_trigger.py
@@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
-async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict:
+async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List trigger capabilities."""
- return await toggle_entity.async_get_trigger_capabilities(hass, trigger)
+ return await toggle_entity.async_get_trigger_capabilities(hass, config)
diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py
index 8f3b5d87f8c..b0abf957991 100644
--- a/homeassistant/components/switch/light.py
+++ b/homeassistant/components/switch/light.py
@@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.components.light import PLATFORM_SCHEMA, Light
-# mypy: allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py
new file mode 100644
index 00000000000..7ed1f70cb97
--- /dev/null
+++ b/homeassistant/components/switch/reproduce_state.py
@@ -0,0 +1,61 @@
+"""Reproduce an Switch state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ STATE_ON,
+ STATE_OFF,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+)
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+VALID_STATES = {STATE_ON, STATE_OFF}
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if state.state not in VALID_STATES:
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state:
+ return
+
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+
+ if state.state == STATE_ON:
+ service = SERVICE_TURN_ON
+ elif state.state == STATE_OFF:
+ service = SERVICE_TURN_OFF
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Switch states."""
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py
index 950a8a67930..6abbfd5fae5 100644
--- a/homeassistant/components/switchmate/switch.py
+++ b/homeassistant/components/switchmate/switch.py
@@ -1,12 +1,14 @@
"""Support for Switchmate."""
-import logging
from datetime import timedelta
+import logging
+# pylint: disable=import-error, no-member, no-value-for-parameter
+import switchmate
import voluptuous as vol
+from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
+from homeassistant.const import CONF_MAC, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, CONF_MAC
_LOGGER = logging.getLogger(__name__)
@@ -37,8 +39,6 @@ class SwitchmateEntity(SwitchDevice):
def __init__(self, mac, name, flip_on_off) -> None:
"""Initialize the Switchmate."""
- # pylint: disable=import-error, no-member, no-value-for-parameter
- import switchmate
self._mac = mac
self._name = name
diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json
index 79e6f8e5571..41ac1024a85 100644
--- a/homeassistant/components/syncthru/manifest.json
+++ b/homeassistant/components/syncthru/manifest.json
@@ -3,7 +3,7 @@
"name": "Syncthru",
"documentation": "https://www.home-assistant.io/integrations/syncthru",
"requirements": [
- "pysyncthru==0.4.3"
+ "pysyncthru==0.5.0"
],
"dependencies": [],
"codeowners": ["@nielstron"]
diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py
index 5594a4b3c9a..8c176f48803 100644
--- a/homeassistant/components/synology/camera.py
+++ b/homeassistant/components/synology/camera.py
@@ -105,6 +105,7 @@ class SynologyCamera(Camera):
"""Return true if the device is recording."""
return self._camera.is_recording
+ @property
def should_poll(self):
"""Update the recording state periodically."""
return True
diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py
index b45b393e332..36306efa93e 100644
--- a/homeassistant/components/synology_srm/device_tracker.py
+++ b/homeassistant/components/synology_srm/device_tracker.py
@@ -4,9 +4,10 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.synology_srm/
"""
import logging
+
+import synology_srm
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN,
PLATFORM_SCHEMA,
@@ -14,12 +15,13 @@ from homeassistant.components.device_tracker import (
)
from homeassistant.const import (
CONF_HOST,
- CONF_USERNAME,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
+ CONF_USERNAME,
CONF_VERIFY_SSL,
)
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -52,7 +54,6 @@ class SynologySrmDeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialize the scanner."""
- import synology_srm
self.client = synology_srm.Client(
host=config[CONF_HOST],
diff --git a/homeassistant/components/syslog/notify.py b/homeassistant/components/syslog/notify.py
index d7696e43bef..67d4882e5c3 100644
--- a/homeassistant/components/syslog/notify.py
+++ b/homeassistant/components/syslog/notify.py
@@ -1,5 +1,6 @@
"""Syslog notification service."""
import logging
+import syslog
import voluptuous as vol
@@ -67,7 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the syslog notification service."""
- import syslog
facility = getattr(syslog, SYSLOG_FACILITY[config.get(CONF_FACILITY)])
option = getattr(syslog, SYSLOG_OPTION[config.get(CONF_OPTION)])
@@ -87,7 +87,6 @@ class SyslogNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
- import syslog
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py
index ad2072baaa5..53c5c104cd1 100644
--- a/homeassistant/components/systemmonitor/sensor.py
+++ b/homeassistant/components/systemmonitor/sensor.py
@@ -3,6 +3,7 @@ import logging
import os
import socket
+import psutil
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -134,8 +135,6 @@ class SystemMonitorSensor(Entity):
def update(self):
"""Get the latest system information."""
- import psutil
-
if self.type == "disk_use_percent":
self._state = psutil.disk_usage(self.argument).percent
elif self.type == "disk_use":
@@ -219,8 +218,8 @@ class SystemMonitorSensor(Entity):
dt_util.utc_from_timestamp(psutil.boot_time())
).isoformat()
elif self.type == "load_1m":
- self._state = os.getloadavg()[0]
+ self._state = round(os.getloadavg()[0], 2)
elif self.type == "load_5m":
- self._state = os.getloadavg()[1]
+ self._state = round(os.getloadavg()[1], 2)
elif self.type == "load_15m":
- self._state = os.getloadavg()[2]
+ self._state = round(os.getloadavg()[2], 2)
diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py
index 4400df6db96..6bcc783400c 100644
--- a/homeassistant/components/tahoma/__init__.py
+++ b/homeassistant/components/tahoma/__init__.py
@@ -42,6 +42,7 @@ TAHOMA_TYPES = {
"io:RollerShutterUnoIOComponent": "cover",
"io:RollerShutterVeluxIOComponent": "cover",
"io:RollerShutterWithLowSpeedManagementIOComponent": "cover",
+ "io:SomfyBasicContactIOSystemSensor": "sensor",
"io:SomfyContactIOSystemSensor": "sensor",
"io:VerticalExteriorAwningIOComponent": "cover",
"io:WindowOpenerVeluxIOComponent": "cover",
diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py
index 0ed3879cc7a..5279b160d9c 100644
--- a/homeassistant/components/tahoma/sensor.py
+++ b/homeassistant/components/tahoma/sensor.py
@@ -44,6 +44,8 @@ class TahomaSensor(TahomaDevice, Entity):
return None
if self.tahoma_device.type == "io:SomfyContactIOSystemSensor":
return None
+ if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor":
+ return None
if self.tahoma_device.type == "io:LightIOSystemSensor":
return "lx"
if self.tahoma_device.type == "Humidity Sensor":
@@ -66,6 +68,11 @@ class TahomaSensor(TahomaDevice, Entity):
self._available = bool(
self.tahoma_device.active_states.get("core:StatusState") == "available"
)
+ if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor":
+ self.current_value = self.tahoma_device.active_states["core:ContactState"]
+ self._available = bool(
+ self.tahoma_device.active_states.get("core:StatusState") == "available"
+ )
if self.tahoma_device.type == "rtds:RTDSContactSensor":
self.current_value = self.tahoma_device.active_states["core:ContactState"]
self._available = True
diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py
index ea0963a092e..e0025a050c3 100644
--- a/homeassistant/components/ted5000/sensor.py
+++ b/homeassistant/components/ted5000/sensor.py
@@ -1,9 +1,10 @@
-"""Support gathering ted500 information."""
-import logging
+"""Support gathering ted5000 information."""
from datetime import timedelta
+import logging
import requests
import voluptuous as vol
+import xmltodict
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, POWER_WATT
@@ -94,7 +95,6 @@ class Ted5000Gateway:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from the Ted5000 XML API."""
- import xmltodict
try:
request = requests.get(self.url, timeout=10)
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index a36f41edf3b..7acf4985def 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -7,6 +7,16 @@ import logging
import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
+from telegram import (
+ Bot,
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+)
+from telegram.error import TelegramError
+from telegram.parsemode import ParseMode
+from telegram.utils.request import Request
import voluptuous as vol
from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE
@@ -375,8 +385,6 @@ async def async_setup(hass, config):
def initialize_bot(p_config):
"""Initialize telegram bot with proxy support."""
- from telegram import Bot
- from telegram.utils.request import Request
api_key = p_config.get(CONF_API_KEY)
proxy_url = p_config.get(CONF_PROXY_URL)
@@ -396,7 +404,6 @@ class TelegramNotificationService:
def __init__(self, hass, bot, allowed_chat_ids, parser):
"""Initialize the service."""
- from telegram.parsemode import ParseMode
self.allowed_chat_ids = allowed_chat_ids
self._default_user = self.allowed_chat_ids[0]
@@ -457,7 +464,6 @@ class TelegramNotificationService:
- a string like: `/cmd1, /cmd2, /cmd3`
- or a string like: `text_b1:/cmd1, text_b2:/cmd2`
"""
- from telegram import InlineKeyboardButton
buttons = []
if isinstance(row_keyboard, str):
@@ -507,8 +513,6 @@ class TelegramNotificationService:
params[ATTR_REPLY_TO_MSGID] = data[ATTR_REPLY_TO_MSGID]
# Keyboards:
if ATTR_KEYBOARD in data:
- from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove
-
keys = data.get(ATTR_KEYBOARD)
keys = keys if isinstance(keys, list) else [keys]
if keys:
@@ -517,9 +521,8 @@ class TelegramNotificationService:
)
else:
params[ATTR_REPLYMARKUP] = ReplyKeyboardRemove(True)
- elif ATTR_KEYBOARD_INLINE in data:
- from telegram import InlineKeyboardMarkup
+ elif ATTR_KEYBOARD_INLINE in data:
keys = data.get(ATTR_KEYBOARD_INLINE)
keys = keys if isinstance(keys, list) else [keys]
params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup(
@@ -529,7 +532,6 @@ class TelegramNotificationService:
def _send_msg(self, func_send, msg_error, *args_msg, **kwargs_msg):
"""Send one message."""
- from telegram.error import TelegramError
try:
out = func_send(*args_msg, **kwargs_msg)
diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py
index 7ca486e33b2..314cb31a373 100644
--- a/homeassistant/components/telegram_bot/polling.py
+++ b/homeassistant/components/telegram_bot/polling.py
@@ -1,6 +1,10 @@
"""Support for Telegram bot using polling."""
import logging
+from telegram import Update
+from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter
+from telegram.ext import Updater, Handler
+
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
@@ -32,8 +36,6 @@ async def async_setup_platform(hass, config):
def process_error(bot, update, error):
"""Telegram bot error handler."""
- from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter
-
try:
raise error
except (TimedOut, NetworkError, RetryAfter):
@@ -45,8 +47,6 @@ def process_error(bot, update, error):
def message_handler(handler):
"""Create messages handler."""
- from telegram import Update
- from telegram.ext import Handler
class MessageHandler(Handler):
"""Telegram bot message handler."""
@@ -72,7 +72,6 @@ class TelegramPoll(BaseTelegramBotEntity):
def __init__(self, bot, hass, allowed_chat_ids):
"""Initialize the polling instance."""
- from telegram.ext import Updater
BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids)
diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py
index c71510eddd9..16da2e741e4 100644
--- a/homeassistant/components/telegram_bot/webhooks.py
+++ b/homeassistant/components/telegram_bot/webhooks.py
@@ -2,6 +2,8 @@
import datetime as dt
import logging
+from telegram.error import TimedOut
+
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.const import KEY_REAL_IP
from homeassistant.const import (
@@ -26,7 +28,6 @@ REMOVE_HANDLER_URL = ""
async def async_setup_platform(hass, config):
"""Set up the Telegram webhooks platform."""
- import telegram
bot = initialize_bot(config)
@@ -55,7 +56,7 @@ async def async_setup_platform(hass, config):
while retry_num < 3:
try:
return bot.setWebhook(handler_url, timeout=5)
- except telegram.error.TimedOut:
+ except TimedOut:
retry_num += 1
_LOGGER.warning("Timeout trying to set webhook (retry #%d)", retry_num)
diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json
index 9d3c97ad902..41dc39146e8 100644
--- a/homeassistant/components/tellduslive/.translations/ru.json
+++ b/homeassistant/components/tellduslive/.translations/ru.json
@@ -4,15 +4,15 @@
"already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
- "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430"
+ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
- "auth_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443"
+ "auth_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443."
},
"step": {
"auth": {
- "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442 TelldusLive:\n 1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u043d\u0438\u0436\u0435\n 2. \u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Telldus Live\n 3. Authorize **{app_name}** (\u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Yes**).\n 4. \u0412\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.\n\n [\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 TelldusLive]({auth_url})",
- "title": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 TelldusLive"
+ "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442 Telldus Live:\n 1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u043d\u0438\u0436\u0435\n 2. \u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Telldus Live\n 3. Authorize **{app_name}** (\u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Yes**).\n 4. \u0412\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.\n\n [\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 Telldus Live]({auth_url})",
+ "title": "Telldus Live"
},
"user": {
"data": {
diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py
index 526db5e73df..e7f341c90b2 100644
--- a/homeassistant/components/tellstick/__init__.py
+++ b/homeassistant/components/tellstick/__init__.py
@@ -2,13 +2,22 @@
import logging
import threading
+from tellcore.constants import (
+ TELLSTICK_DIM,
+ TELLSTICK_TURNOFF,
+ TELLSTICK_TURNON,
+ TELLSTICK_UP,
+)
+from tellcore.library import TelldusError
+from tellcore.telldus import AsyncioCallbackDispatcher, TelldusCore
+from tellcorenet import TellCoreClient
import voluptuous as vol
-from homeassistant.helpers import discovery
+from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT
-from homeassistant.helpers.entity import Entity
+from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -73,10 +82,6 @@ def _discover(hass, config, component_name, found_tellcore_devices):
def setup(hass, config):
"""Set up the Tellstick component."""
- from tellcore.constants import TELLSTICK_DIM, TELLSTICK_UP
- from tellcore.telldus import AsyncioCallbackDispatcher
- from tellcore.telldus import TelldusCore
- from tellcorenet import TellCoreClient
conf = config.get(DOMAIN, {})
net_host = conf.get(CONF_HOST)
@@ -219,7 +224,6 @@ class TellstickDevice(Entity):
def _send_repeated_command(self):
"""Send a tellstick command once and decrease the repeat count."""
- from tellcore.library import TelldusError
with TELLSTICK_LOCK:
if self._repeats_left > 0:
@@ -259,11 +263,6 @@ class TellstickDevice(Entity):
def _update_model_from_command(self, tellcore_command, tellcore_data):
"""Update the model, from a sent tellcore command and data."""
- from tellcore.constants import (
- TELLSTICK_TURNON,
- TELLSTICK_TURNOFF,
- TELLSTICK_DIM,
- )
if tellcore_command not in [TELLSTICK_TURNON, TELLSTICK_TURNOFF, TELLSTICK_DIM]:
_LOGGER.debug("Unhandled tellstick command: %d", tellcore_command)
@@ -289,12 +288,6 @@ class TellstickDevice(Entity):
def _update_from_tellcore(self):
"""Read the current state of the device from the tellcore library."""
- from tellcore.library import TelldusError
- from tellcore.constants import (
- TELLSTICK_TURNON,
- TELLSTICK_TURNOFF,
- TELLSTICK_DIM,
- )
with TELLSTICK_LOCK:
try:
diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py
index 98d162d6d81..1a55e67ac43 100644
--- a/homeassistant/components/tellstick/sensor.py
+++ b/homeassistant/components/tellstick/sensor.py
@@ -1,13 +1,15 @@
"""Support for Tellstick sensors."""
-import logging
from collections import namedtuple
+import logging
+from tellcore import telldus
+import tellcore.constants as tellcore_constants
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME, CONF_PROTOCOL
-from homeassistant.helpers.entity import Entity
+from homeassistant.const import CONF_ID, CONF_NAME, CONF_PROTOCOL, TEMP_CELSIUS
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -48,8 +50,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Tellstick sensors."""
- from tellcore import telldus
- import tellcore.constants as tellcore_constants
sensor_value_descriptions = {
tellcore_constants.TELLSTICK_TEMPERATURE: DatatypeDescription(
diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py
index 42790e618d9..606f18e5fe1 100644
--- a/homeassistant/components/template/fan.py
+++ b/homeassistant/components/template/fan.py
@@ -270,7 +270,7 @@ class TemplateFan(FanEntity):
# pylint: disable=arguments-differ
async def async_turn_on(self, speed: str = None) -> None:
"""Turn on the fan."""
- await self._on_script.async_run(context=self._context)
+ await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context)
self._state = STATE_ON
if speed is not None:
diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py
index 65e20f558a7..ea73d52fe4a 100644
--- a/homeassistant/components/tensorflow/image_processing.py
+++ b/homeassistant/components/tensorflow/image_processing.py
@@ -1,8 +1,11 @@
"""Support for performing TensorFlow classification on images."""
+import io
import logging
import os
import sys
+from PIL import Image, ImageDraw
+import numpy as np
import voluptuous as vol
from homeassistant.components.image_processing import (
@@ -88,6 +91,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
# Verify that the TensorFlow Object Detection API is pre-installed
# pylint: disable=unused-import,unused-variable
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
+ # These imports shouldn't be moved to the top, because they depend on code from the model_dir.
+ # (The model_dir is created during the manual setup process. See integration docs.)
import tensorflow as tf # noqa
from object_detection.utils import label_map_util # noqa
except ImportError:
@@ -236,9 +241,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
}
def _save_image(self, image, matches, paths):
- from PIL import Image, ImageDraw
- import io
-
img = Image.open(io.BytesIO(bytearray(image))).convert("RGB")
img_width, img_height = img.size
draw = ImageDraw.Draw(img)
@@ -280,7 +282,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
def process_image(self, image):
"""Process the image."""
- import numpy as np
try:
import cv2 # pylint: disable=import-error
@@ -289,9 +290,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
inp = img[:, :, [2, 1, 0]] # BGR->RGB
inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3)
except ImportError:
- from PIL import Image
- import io
-
img = Image.open(io.BytesIO(bytearray(image))).convert("RGB")
img.thumbnail((460, 460), Image.ANTIALIAS)
img_width, img_height = img.size
diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json
index e7d35829ffb..e0a8728b295 100644
--- a/homeassistant/components/tensorflow/manifest.json
+++ b/homeassistant/components/tensorflow/manifest.json
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/tensorflow",
"requirements": [
"tensorflow==1.13.2",
- "numpy==1.17.1",
+ "numpy==1.17.3",
"protobuf==3.6.1"
],
"dependencies": [],
diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py
index 4c90f0784af..a08112d66b3 100644
--- a/homeassistant/components/tesla/__init__.py
+++ b/homeassistant/components/tesla/__init__.py
@@ -3,6 +3,8 @@ from collections import defaultdict
import logging
import voluptuous as vol
+from teslajsonpy import Controller as teslaAPI, TeslaException
+
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
@@ -52,8 +54,6 @@ TESLA_COMPONENTS = [
def setup(hass, base_config):
"""Set up of Tesla component."""
- from teslajsonpy import Controller as teslaAPI, TeslaException
-
config = base_config.get(DOMAIN)
email = config.get(CONF_USERNAME)
diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json
index 4071178c7c3..87d76c16f05 100644
--- a/homeassistant/components/tesla/manifest.json
+++ b/homeassistant/components/tesla/manifest.json
@@ -2,11 +2,7 @@
"domain": "tesla",
"name": "Tesla",
"documentation": "https://www.home-assistant.io/integrations/tesla",
- "requirements": [
- "teslajsonpy==0.0.25"
- ],
+ "requirements": ["teslajsonpy==0.0.26"],
"dependencies": [],
- "codeowners": [
- "@zabuldon"
- ]
+ "codeowners": ["@zabuldon"]
}
diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py
index 19ac76018ff..985194f87b2 100644
--- a/homeassistant/components/tesla/switch.py
+++ b/homeassistant/components/tesla/switch.py
@@ -11,11 +11,12 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Tesla switch platform."""
- controller = hass.data[TESLA_DOMAIN]["devices"]["controller"]
+ controller = hass.data[TESLA_DOMAIN]["controller"]
devices = []
for device in hass.data[TESLA_DOMAIN]["devices"]["switch"]:
if device.bin_type == 0x8:
devices.append(ChargerSwitch(device, controller))
+ devices.append(UpdateSwitch(device, controller))
elif device.bin_type == 0x9:
devices.append(RangeSwitch(device, controller))
add_entities(devices, True)
@@ -72,10 +73,42 @@ class RangeSwitch(TeslaDevice, SwitchDevice):
@property
def is_on(self):
"""Get whether the switch is in on state."""
- return self._state == STATE_ON
+ return self._state
def update(self):
"""Update the state of the switch."""
_LOGGER.debug("Updating state for: %s", self._name)
self.tesla_device.update()
- self._state = STATE_ON if self.tesla_device.is_maxrange() else STATE_OFF
+ self._state = bool(self.tesla_device.is_maxrange())
+
+
+class UpdateSwitch(TeslaDevice, SwitchDevice):
+ """Representation of a Tesla update switch."""
+
+ def __init__(self, tesla_device, controller):
+ """Initialise of the switch."""
+ self._state = None
+ super().__init__(tesla_device, controller)
+ self._name = self._name.replace("charger", "update")
+ self.tesla_id = self.tesla_id.replace("charger", "update")
+
+ def turn_on(self, **kwargs):
+ """Send the on command."""
+ _LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id())
+ self.controller.set_updates(self.tesla_device.id(), True)
+
+ def turn_off(self, **kwargs):
+ """Send the off command."""
+ _LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id())
+ self.controller.set_updates(self.tesla_device.id(), False)
+
+ @property
+ def is_on(self):
+ """Get whether the switch is in on state."""
+ return self._state
+
+ def update(self):
+ """Update the state of the switch."""
+ car_id = self.tesla_device.id()
+ _LOGGER.debug("Updating state for: %s %s", self._name, car_id)
+ self._state = bool(self.controller.get_updates(car_id))
diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py
index 70a16287fcc..d5af021108a 100644
--- a/homeassistant/components/thermoworks_smoke/sensor.py
+++ b/homeassistant/components/thermoworks_smoke/sensor.py
@@ -9,18 +9,21 @@ https://home-assistant.io/components/sensor.thermoworks_smoke/
import logging
from requests import RequestException
+from requests.exceptions import HTTPError
+from stringcase import camelcase, snakecase
+import thermoworks_smoke
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- TEMP_FAHRENHEIT,
- CONF_EMAIL,
- CONF_PASSWORD,
- CONF_MONITORED_CONDITIONS,
- CONF_EXCLUDE,
ATTR_BATTERY_LEVEL,
+ CONF_EMAIL,
+ CONF_EXCLUDE,
+ CONF_MONITORED_CONDITIONS,
+ CONF_PASSWORD,
+ TEMP_FAHRENHEIT,
)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -65,8 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the thermoworks sensor."""
- import thermoworks_smoke
- from requests.exceptions import HTTPError
email = config[CONF_EMAIL]
password = config[CONF_PASSWORD]
@@ -144,7 +145,6 @@ class ThermoworksSmokeSensor(Entity):
def update(self):
"""Get the monitored data from firebase."""
- from stringcase import camelcase, snakecase
try:
values = self.mgr.data(self.serial)
diff --git a/homeassistant/components/thingspeak/__init__.py b/homeassistant/components/thingspeak/__init__.py
index 0893b3311bb..1870a317752 100644
--- a/homeassistant/components/thingspeak/__init__.py
+++ b/homeassistant/components/thingspeak/__init__.py
@@ -2,6 +2,7 @@
import logging
from requests.exceptions import RequestException
+import thingspeak
import voluptuous as vol
from homeassistant.const import (
@@ -36,7 +37,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Thingspeak environment."""
- import thingspeak
conf = config[DOMAIN]
api_key = conf.get(CONF_API_KEY)
diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py
index 0622b70f127..df56989714f 100644
--- a/homeassistant/components/tibber/__init__.py
+++ b/homeassistant/components/tibber/__init__.py
@@ -3,12 +3,13 @@ import asyncio
import logging
import aiohttp
+import tibber
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, CONF_NAME
+from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt as dt_util
DOMAIN = "tibber"
@@ -25,8 +26,6 @@ async def async_setup(hass, config):
"""Set up the Tibber component."""
conf = config.get(DOMAIN)
- import tibber
-
tibber_connection = tibber.Tibber(
conf[CONF_ACCESS_TOKEN],
websession=async_get_clientsession(hass),
diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py
index 013e5276f49..6c623f29f18 100644
--- a/homeassistant/components/tikteck/light.py
+++ b/homeassistant/components/tikteck/light.py
@@ -1,17 +1,18 @@
"""Support for Tikteck lights."""
import logging
+import tikteck
import voluptuous as vol
-from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_HS_COLOR,
+ PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
Light,
- PLATFORM_SCHEMA,
)
+from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
@@ -48,7 +49,6 @@ class TikteckLight(Light):
def __init__(self, device):
"""Initialize the light."""
- import tikteck
self._name = device["name"]
self._address = device["address"]
diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py
index e8ed5b06d27..924fa913d30 100644
--- a/homeassistant/components/tile/device_tracker.py
+++ b/homeassistant/components/tile/device_tracker.py
@@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Validate the configuration and return a Tile scanner."""
- from pytile import Client
+ from pytile import async_login
websession = aiohttp_client.async_get_clientsession(hass)
@@ -52,14 +52,16 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
)
config_data = await hass.async_add_job(load_json, config_file)
if config_data:
- client = Client(
+ client = await async_login(
config[CONF_USERNAME],
config[CONF_PASSWORD],
websession,
client_uuid=config_data["client_uuid"],
)
else:
- client = Client(config[CONF_USERNAME], config[CONF_PASSWORD], websession)
+ client = await async_login(
+ config[CONF_USERNAME], config[CONF_PASSWORD], websession
+ )
config_data = {"client_uuid": client.client_uuid}
await hass.async_add_job(save_json, config_file, config_data)
diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json
index 5e40c89369a..0dd0b70ef52 100644
--- a/homeassistant/components/tile/manifest.json
+++ b/homeassistant/components/tile/manifest.json
@@ -3,7 +3,7 @@
"name": "Tile",
"documentation": "https://www.home-assistant.io/integrations/tile",
"requirements": [
- "pytile==2.0.6"
+ "pytile==3.0.0"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py
new file mode 100644
index 00000000000..c765ed7da9c
--- /dev/null
+++ b/homeassistant/components/timer/reproduce_state.py
@@ -0,0 +1,70 @@
+"""Reproduce an Timer state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ ATTR_DURATION,
+ DOMAIN,
+ SERVICE_CANCEL,
+ SERVICE_PAUSE,
+ SERVICE_START,
+ STATUS_ACTIVE,
+ STATUS_IDLE,
+ STATUS_PAUSED,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+VALID_STATES = {STATUS_IDLE, STATUS_ACTIVE, STATUS_PAUSED}
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if state.state not in VALID_STATES:
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state and cur_state.attributes.get(
+ ATTR_DURATION
+ ) == state.attributes.get(ATTR_DURATION):
+ return
+
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+
+ if state.state == STATUS_ACTIVE:
+ service = SERVICE_START
+ if ATTR_DURATION in state.attributes:
+ service_data[ATTR_DURATION] = state.attributes[ATTR_DURATION]
+ elif state.state == STATUS_PAUSED:
+ service = SERVICE_PAUSE
+ elif state.state == STATUS_IDLE:
+ service = SERVICE_CANCEL
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Timer states."""
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json
index 0eddbe2a151..58e6f53986c 100644
--- a/homeassistant/components/toon/.translations/ru.json
+++ b/homeassistant/components/toon/.translations/ru.json
@@ -8,7 +8,7 @@
"unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
- "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435",
+ "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"step": {
diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py
index 075bffb9f26..7aa261564f3 100644
--- a/homeassistant/components/tplink/__init__.py
+++ b/homeassistant/components/tplink/__init__.py
@@ -3,20 +3,21 @@ import logging
import voluptuous as vol
-from homeassistant.const import CONF_HOST
from homeassistant import config_entries
+from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv
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,
+ CONF_STRIP,
SmartDevices,
+ async_discover_devices,
+ get_static_devices,
)
_LOGGER = logging.getLogger(__name__)
@@ -36,6 +37,9 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_SWITCH, default=[]): vol.All(
cv.ensure_list, [TPLINK_HOST_SCHEMA]
),
+ vol.Optional(CONF_STRIP, default=[]): vol.All(
+ cv.ensure_list, [TPLINK_HOST_SCHEMA]
+ ),
vol.Optional(CONF_DIMMER, default=[]): vol.All(
cv.ensure_list, [TPLINK_HOST_SCHEMA]
),
diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py
index 90895104170..548edc6822c 100644
--- a/homeassistant/components/tplink/common.py
+++ b/homeassistant/components/tplink/common.py
@@ -1,10 +1,17 @@
"""Common code for tplink."""
import asyncio
-import logging
from datetime import timedelta
+import logging
from typing import Any, Callable, List
-from pyHS100 import SmartBulb, SmartDevice, SmartPlug, SmartDeviceException
+from pyHS100 import (
+ Discover,
+ SmartBulb,
+ SmartDevice,
+ SmartDeviceException,
+ SmartPlug,
+ SmartStrip,
+)
from homeassistant.helpers.typing import HomeAssistantType
@@ -15,6 +22,7 @@ ATTR_CONFIG = "config"
CONF_DIMMER = "dimmer"
CONF_DISCOVERY = "discovery"
CONF_LIGHT = "light"
+CONF_STRIP = "strip"
CONF_SWITCH = "switch"
@@ -49,7 +57,6 @@ class SmartDevices:
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()
@@ -75,7 +82,10 @@ async def async_discover_devices(
if existing_devices.has_device_with_host(dev.host):
continue
- if isinstance(dev, SmartPlug):
+ if isinstance(dev, SmartStrip):
+ for plug in dev.plugs.values():
+ switches.append(plug)
+ elif isinstance(dev, SmartPlug):
try:
if dev.is_dimmable: # Dimmers act as lights
lights.append(dev)
@@ -100,7 +110,7 @@ def get_static_devices(config_data) -> SmartDevices:
lights = []
switches = []
- for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_DIMMER]:
+ for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER]:
for entry in config_data[type_]:
host = entry["host"]
@@ -108,6 +118,9 @@ def get_static_devices(config_data) -> SmartDevices:
lights.append(SmartBulb(host))
elif type_ == CONF_SWITCH:
switches.append(SmartPlug(host))
+ elif type_ == CONF_STRIP:
+ for plug in SmartStrip(host).plugs.values():
+ switches.append(plug)
# Dimmers need to be defined as smart plugs to work correctly.
elif type_ == CONF_DIMMER:
lights.append(SmartPlug(host))
diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py
index c4888ecee96..40583294bfd 100644
--- a/homeassistant/components/tplink/config_flow.py
+++ b/homeassistant/components/tplink/config_flow.py
@@ -1,9 +1,9 @@
"""Config flow for TP-Link."""
-from homeassistant.helpers import config_entry_flow
from homeassistant import config_entries
-from .const import DOMAIN
-from .common import async_get_discoverable_devices
+from homeassistant.helpers import config_entry_flow
+from .common import async_get_discoverable_devices
+from .const import DOMAIN
config_entry_flow.register_discovery_flow(
DOMAIN,
diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py
deleted file mode 100644
index e7f87074cb4..00000000000
--- a/homeassistant/components/tplink/device_tracker.py
+++ /dev/null
@@ -1,508 +0,0 @@
-"""Support for TP-Link routers."""
-import base64
-from datetime import datetime
-import hashlib
-import logging
-import re
-
-from aiohttp.hdrs import (
- ACCEPT,
- COOKIE,
- PRAGMA,
- REFERER,
- CONNECTION,
- KEEP_ALIVE,
- USER_AGENT,
- CONTENT_TYPE,
- CACHE_CONTROL,
- ACCEPT_ENCODING,
- ACCEPT_LANGUAGE,
-)
-import requests
-import voluptuous as vol
-
-from homeassistant.components.device_tracker import (
- DOMAIN,
- PLATFORM_SCHEMA,
- DeviceScanner,
-)
-from homeassistant.const import (
- CONF_HOST,
- CONF_PASSWORD,
- CONF_USERNAME,
- HTTP_HEADER_X_REQUESTED_WITH,
-)
-import homeassistant.helpers.config_validation as cv
-
-_LOGGER = logging.getLogger(__name__)
-
-HTTP_HEADER_NO_CACHE = "no-cache"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- }
-)
-
-
-def get_scanner(hass, config):
- """
- Validate the configuration and return a TP-Link scanner.
-
- The default way of integrating devices is to use a pypi
-
- package, The TplinkDeviceScanner has been refactored
-
- to depend on a pypi package, the other implementations
-
- 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,
- ]:
- scanner = cls(config[DOMAIN])
- if scanner.success_init:
- return scanner
-
- return None
-
-
-class TplinkDeviceScanner(DeviceScanner):
- """Queries the router for connected devices."""
-
- def __init__(self, config):
- """Initialize the scanner."""
- from tplink.tplink import TpLinkClient
-
- host = config[CONF_HOST]
- password = config[CONF_PASSWORD]
- username = config[CONF_USERNAME]
-
- self.success_init = False
- try:
- self.tplink_client = TpLinkClient(password, host=host, username=username)
-
- self.last_results = {}
-
- self.success_init = self._update_info()
- except requests.exceptions.RequestException:
- _LOGGER.debug("RequestException in %s", __class__.__name__)
-
- def scan_devices(self):
- """Scan for new devices and return a list with found device IDs."""
- self._update_info()
- return self.last_results.keys()
-
- def get_device_name(self, device):
- """Get the name of the device."""
- return self.last_results.get(device)
-
- def _update_info(self):
- """Ensure the information from the TP-Link router is up to date.
-
- Return boolean if scanning successful.
- """
- _LOGGER.info("Loading wireless clients...")
- result = self.tplink_client.get_connected_devices()
-
- if result:
- self.last_results = result
- return True
-
- return False
-
-
-class Tplink1DeviceScanner(DeviceScanner):
- """This class queries a wireless router running TP-Link firmware."""
-
- def __init__(self, config):
- """Initialize the scanner."""
- host = config[CONF_HOST]
- username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
-
- self.parse_macs = re.compile(
- "[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}-"
- + "[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}"
- )
-
- self.host = host
- self.username = username
- self.password = password
-
- self.last_results = {}
- self.success_init = False
- try:
- self.success_init = self._update_info()
- except requests.exceptions.RequestException:
- _LOGGER.debug("RequestException in %s", __class__.__name__)
-
- def scan_devices(self):
- """Scan for new devices and return a list with found device IDs."""
- self._update_info()
- return self.last_results
-
- def get_device_name(self, device):
- """Get firmware doesn't save the name of the wireless device."""
- return None
-
- def _update_info(self):
- """Ensure the information from the TP-Link router is up to date.
-
- Return boolean if scanning successful.
- """
- _LOGGER.info("Loading wireless clients...")
-
- url = f"http://{self.host}/userRpm/WlanStationRpm.htm"
- referer = f"http://{self.host}"
- page = requests.get(
- url,
- auth=(self.username, self.password),
- headers={REFERER: referer},
- timeout=4,
- )
-
- result = self.parse_macs.findall(page.text)
-
- if result:
- self.last_results = [mac.replace("-", ":") for mac in result]
- return True
-
- return False
-
-
-class Tplink2DeviceScanner(Tplink1DeviceScanner):
- """This class queries a router with newer version of TP-Link firmware."""
-
- def scan_devices(self):
- """Scan for new devices and return a list with found device IDs."""
- self._update_info()
- return self.last_results.keys()
-
- def get_device_name(self, device):
- """Get firmware doesn't save the name of the wireless device."""
- return self.last_results.get(device)
-
- def _update_info(self):
- """Ensure the information from the TP-Link router is up to date.
-
- Return boolean if scanning successful.
- """
- _LOGGER.info("Loading wireless clients...")
-
- url = f"http://{self.host}/data/map_access_wireless_client_grid.json"
- referer = f"http://{self.host}"
-
- # Router uses Authorization cookie instead of header
- # Let's create the cookie
- username_password = f"{self.username}:{self.password}"
- b64_encoded_username_password = base64.b64encode(
- username_password.encode("ascii")
- ).decode("ascii")
- cookie = f"Authorization=Basic {b64_encoded_username_password}"
-
- response = requests.post(
- url, headers={REFERER: referer, COOKIE: cookie}, timeout=4
- )
-
- try:
- result = response.json().get("data")
- except ValueError:
- _LOGGER.error(
- "Router didn't respond with JSON. " "Check if credentials are correct."
- )
- return False
-
- if result:
- self.last_results = {
- device["mac_addr"].replace("-", ":"): device["name"]
- for device in result
- }
- return True
-
- return False
-
-
-class Tplink3DeviceScanner(Tplink1DeviceScanner):
- """This class queries the Archer C9 router with version 150811 or high."""
-
- def __init__(self, config):
- """Initialize the scanner."""
- self.stok = ""
- self.sysauth = ""
- super().__init__(config)
-
- def scan_devices(self):
- """Scan for new devices and return a list with found device IDs."""
- self._update_info()
- self._log_out()
- return self.last_results.keys()
-
- def get_device_name(self, device):
- """Get the firmware doesn't save the name of the wireless device.
-
- We are forced to use the MAC address as name here.
- """
- return self.last_results.get(device)
-
- def _get_auth_tokens(self):
- """Retrieve auth tokens from the router."""
- _LOGGER.info("Retrieving auth tokens...")
-
- url = f"http://{self.host}/cgi-bin/luci/;stok=/login?form=login"
- referer = f"http://{self.host}/webpages/login.html"
-
- # If possible implement RSA encryption of password here.
- response = requests.post(
- url,
- params={
- "operation": "login",
- "username": self.username,
- "password": self.password,
- },
- headers={REFERER: referer},
- timeout=4,
- )
-
- try:
- self.stok = response.json().get("data").get("stok")
- _LOGGER.info(self.stok)
- regex_result = re.search("sysauth=(.*);", response.headers["set-cookie"])
- self.sysauth = regex_result.group(1)
- _LOGGER.info(self.sysauth)
- return True
- except (ValueError, KeyError):
- _LOGGER.error("Couldn't fetch auth tokens! Response was: %s", response.text)
- return False
-
- def _update_info(self):
- """Ensure the information from the TP-Link router is up to date.
-
- Return boolean if scanning successful.
- """
- if (self.stok == "") or (self.sysauth == ""):
- self._get_auth_tokens()
-
- _LOGGER.info("Loading wireless clients...")
-
- url = (
- "http://{}/cgi-bin/luci/;stok={}/admin/wireless?" "form=statistics"
- ).format(self.host, self.stok)
- referer = f"http://{self.host}/webpages/index.html"
-
- response = requests.post(
- url,
- params={"operation": "load"},
- headers={REFERER: referer},
- cookies={"sysauth": self.sysauth},
- timeout=5,
- )
-
- try:
- json_response = response.json()
-
- if json_response.get("success"):
- result = response.json().get("data")
- else:
- if json_response.get("errorcode") == "timeout":
- _LOGGER.info("Token timed out. Relogging on next scan")
- self.stok = ""
- self.sysauth = ""
- return False
- _LOGGER.error("An unknown error happened while fetching data")
- return False
- except ValueError:
- _LOGGER.error(
- "Router didn't respond with JSON. " "Check if credentials are correct"
- )
- return False
-
- if result:
- self.last_results = {
- device["mac"].replace("-", ":"): device["mac"] for device in result
- }
- return True
-
- return False
-
- def _log_out(self):
- _LOGGER.info("Logging out of router admin interface...")
-
- url = ("http://{}/cgi-bin/luci/;stok={}/admin/system?" "form=logout").format(
- self.host, self.stok
- )
- referer = f"http://{self.host}/webpages/index.html"
-
- requests.post(
- url,
- params={"operation": "write"},
- headers={REFERER: referer},
- cookies={"sysauth": self.sysauth},
- )
- self.stok = ""
- self.sysauth = ""
-
-
-class Tplink4DeviceScanner(Tplink1DeviceScanner):
- """This class queries an Archer C7 router with TP-Link firmware 150427."""
-
- def __init__(self, config):
- """Initialize the scanner."""
- self.credentials = ""
- self.token = ""
- super().__init__(config)
-
- def scan_devices(self):
- """Scan for new devices and return a list with found device IDs."""
- self._update_info()
- return self.last_results
-
- def get_device_name(self, device):
- """Get the name of the wireless device."""
- return None
-
- def _get_auth_tokens(self):
- """Retrieve auth tokens from the router."""
- _LOGGER.info("Retrieving auth tokens...")
- url = f"http://{self.host}/userRpm/LoginRpm.htm?Save=Save"
-
- # Generate md5 hash of password. The C7 appears to use the first 15
- # characters of the password only, so we truncate to remove additional
- # characters from being hashed.
- password = hashlib.md5(self.password.encode("utf")[:15]).hexdigest()
- credentials = f"{self.username}:{password}".encode("utf")
-
- # Encode the credentials to be sent as a cookie.
- self.credentials = base64.b64encode(credentials).decode("utf")
-
- # Create the authorization cookie.
- cookie = f"Authorization=Basic {self.credentials}"
-
- response = requests.get(url, headers={COOKIE: cookie})
-
- try:
- result = re.search(
- r"window.parent.location.href = "
- r'"https?:\/\/.*\/(.*)\/userRpm\/Index.htm";',
- response.text,
- )
- if not result:
- return False
- self.token = result.group(1)
- return True
- except ValueError:
- _LOGGER.error("Couldn't fetch auth tokens")
- return False
-
- def _update_info(self):
- """Ensure the information from the TP-Link router is up to date.
-
- Return boolean if scanning successful.
- """
- if (self.credentials == "") or (self.token == ""):
- self._get_auth_tokens()
-
- _LOGGER.info("Loading wireless clients...")
-
- mac_results = []
-
- # Check both the 2.4GHz and 5GHz client list URLs
- for clients_url in ("WlanStationRpm.htm", "WlanStationRpm_5g.htm"):
- url = f"http://{self.host}/{self.token}/userRpm/{clients_url}"
- referer = f"http://{self.host}"
- cookie = f"Authorization=Basic {self.credentials}"
-
- page = requests.get(url, headers={COOKIE: cookie, REFERER: referer})
- mac_results.extend(self.parse_macs.findall(page.text))
-
- if not mac_results:
- return False
-
- self.last_results = [mac.replace("-", ":") for mac in mac_results]
- return True
-
-
-class Tplink5DeviceScanner(Tplink1DeviceScanner):
- """This class queries a TP-Link EAP-225 AP with newer TP-Link FW."""
-
- def scan_devices(self):
- """Scan for new devices and return a list with found MAC IDs."""
- self._update_info()
- return self.last_results.keys()
-
- def get_device_name(self, device):
- """Get firmware doesn't save the name of the wireless device."""
- return None
-
- def _update_info(self):
- """Ensure the information from the TP-Link AP is up to date.
-
- Return boolean if scanning successful.
- """
- _LOGGER.info("Loading wireless clients...")
-
- base_url = f"http://{self.host}"
-
- header = {
- USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
- " rv:53.0) Gecko/20100101 Firefox/53.0",
- ACCEPT: "application/json, text/javascript, */*; q=0.01",
- ACCEPT_LANGUAGE: "Accept-Language: en-US,en;q=0.5",
- ACCEPT_ENCODING: "gzip, deflate",
- CONTENT_TYPE: "application/x-www-form-urlencoded; charset=UTF-8",
- HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest",
- REFERER: f"http://{self.host}/",
- CONNECTION: KEEP_ALIVE,
- PRAGMA: HTTP_HEADER_NO_CACHE,
- CACHE_CONTROL: HTTP_HEADER_NO_CACHE,
- }
-
- password_md5 = hashlib.md5(self.password.encode("utf")).hexdigest().upper()
-
- # Create a session to handle cookie easier
- session = requests.session()
- session.get(base_url, headers=header)
-
- login_data = {"username": self.username, "password": password_md5}
- session.post(base_url, login_data, headers=header)
-
- # A timestamp is required to be sent as get parameter
- timestamp = int(datetime.now().timestamp() * 1e3)
-
- client_list_url = f"{base_url}/data/monitor.client.client.json"
-
- get_params = {"operation": "load", "_": timestamp}
-
- response = session.get(client_list_url, headers=header, params=get_params)
- session.close()
- try:
- list_of_devices = response.json()
- except ValueError:
- _LOGGER.error(
- "AP didn't respond with JSON. " "Check if credentials are correct"
- )
- return False
-
- if list_of_devices:
- self.last_results = {
- device["MAC"].replace("-", ":"): device["DeviceName"]
- for device in list_of_devices["data"]
- }
- return True
-
- return False
diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json
index f299e02e2d3..c2a2197c844 100644
--- a/homeassistant/components/tplink/manifest.json
+++ b/homeassistant/components/tplink/manifest.json
@@ -4,8 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tplink",
"requirements": [
- "pyHS100==0.3.5",
- "tplink==0.2.1"
+ "pyHS100==0.3.5"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py
index ebeac984515..791d358c509 100644
--- a/homeassistant/components/tplink/switch.py
+++ b/homeassistant/components/tplink/switch.py
@@ -69,11 +69,12 @@ class SmartPlugSwitch(SwitchDevice):
self._mac = None
self._alias = None
self._model = None
+ self._device_id = None
@property
def unique_id(self):
"""Return a unique ID."""
- return self._mac
+ return self._device_id
@property
def name(self):
@@ -120,10 +121,26 @@ class SmartPlugSwitch(SwitchDevice):
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
+ if self.smartplug.context is None:
+ self._alias = self.smartplug.alias
+ self._device_id = self._mac
+ else:
+ self._alias = [
+ child
+ for child in self.smartplug.sys_info["children"]
+ if child["id"] == self.smartplug.context
+ ][0]["alias"]
+ self._device_id = self.smartplug.context
- self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON
+ if self.smartplug.context is None:
+ self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON
+ else:
+ self._state = [
+ child
+ for child in self.smartplug.sys_info["children"]
+ if child["id"] == self.smartplug.context
+ ][0]["state"] == 1
if self.smartplug.has_emeter:
emeter_readings = self.smartplug.get_emeter_realtime()
diff --git a/homeassistant/components/tplink_lte/__init__.py b/homeassistant/components/tplink_lte/__init__.py
index 215dd5c94b2..e495a14a38c 100644
--- a/homeassistant/components/tplink_lte/__init__.py
+++ b/homeassistant/components/tplink_lte/__init__.py
@@ -4,6 +4,7 @@ import logging
import aiohttp
import attr
+import tp_connected
import voluptuous as vol
from homeassistant.const import (
@@ -106,7 +107,6 @@ async def async_setup(hass, config):
async def _setup_lte(hass, lte_config, delay=0):
"""Set up a TP-Link LTE modem."""
- import tp_connected
host = lte_config[CONF_HOST]
password = lte_config[CONF_PASSWORD]
@@ -145,7 +145,6 @@ async def _login(hass, modem_data, password):
async def _retry_login(hass, modem_data, password):
"""Sleep and retry setup."""
- import tp_connected
_LOGGER.warning("Could not connect to %s. Will keep trying.", modem_data.host)
diff --git a/homeassistant/components/tplink_lte/notify.py b/homeassistant/components/tplink_lte/notify.py
index e677b42a511..478b3e998c0 100644
--- a/homeassistant/components/tplink_lte/notify.py
+++ b/homeassistant/components/tplink_lte/notify.py
@@ -2,6 +2,7 @@
import logging
import attr
+import tp_connected
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
from homeassistant.const import CONF_RECIPIENT
@@ -27,7 +28,6 @@ class TplinkNotifyService(BaseNotificationService):
async def async_send_message(self, message="", **kwargs):
"""Send a message to a user."""
- import tp_connected
modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config)
if not modem_data:
diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json
index 99844dc91ca..2e3dc8331be 100644
--- a/homeassistant/components/tradfri/.translations/ru.json
+++ b/homeassistant/components/tradfri/.translations/ru.json
@@ -1,11 +1,11 @@
{
"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_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443",
+ "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.",
"invalid_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0448\u043b\u044e\u0437.",
"timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430."
},
diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py
index bca91134bed..bdfabb4b00a 100644
--- a/homeassistant/components/tradfri/__init__.py
+++ b/homeassistant/components/tradfri/__init__.py
@@ -2,13 +2,25 @@
import logging
import voluptuous as vol
+from pytradfri import Gateway, RequestError
+from pytradfri.api.aiocoap_api import APIFactory
+import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
-import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json
-
+from . import config_flow # noqa pylint_disable=unused-import
from .const import (
+ DOMAIN,
+ CONFIG_FILE,
+ KEY_GATEWAY,
+ KEY_API,
+ CONF_ALLOW_TRADFRI_GROUPS,
+ DEFAULT_ALLOW_TRADFRI_GROUPS,
+ TRADFRI_DEVICE_TYPES,
+ ATTR_TRADFRI_MANUFACTURER,
+ ATTR_TRADFRI_GATEWAY,
+ ATTR_TRADFRI_GATEWAY_MODEL,
CONF_IMPORT_GROUPS,
CONF_IDENTITY,
CONF_HOST,
@@ -16,18 +28,8 @@ from .const import (
CONF_GATEWAY_ID,
)
-from . import config_flow # noqa pylint_disable=unused-import
-
_LOGGER = logging.getLogger(__name__)
-
-DOMAIN = "tradfri"
-CONFIG_FILE = ".tradfri_psk.conf"
-KEY_GATEWAY = "tradfri_gateway"
-KEY_API = "tradfri_api"
-CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups"
-DEFAULT_ALLOW_TRADFRI_GROUPS = False
-
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
@@ -91,8 +93,6 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Create a gateway."""
# host, identity, key, allow_tradfri_groups
- from pytradfri import Gateway, RequestError # pylint: disable=import-error
- from pytradfri.api.aiocoap_api import APIFactory
factory = APIFactory(
entry.data[CONF_HOST],
@@ -124,24 +124,16 @@ async def async_setup_entry(hass, entry):
config_entry_id=entry.entry_id,
connections=set(),
identifiers={(DOMAIN, entry.data[CONF_GATEWAY_ID])},
- manufacturer="IKEA",
- name="Gateway",
+ manufacturer=ATTR_TRADFRI_MANUFACTURER,
+ name=ATTR_TRADFRI_GATEWAY,
# They just have 1 gateway model. Type is not exposed yet.
- model="E1526",
+ model=ATTR_TRADFRI_GATEWAY_MODEL,
sw_version=gateway_info.firmware_version,
)
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, "cover")
- )
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, "light")
- )
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, "sensor")
- )
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, "switch")
- )
+ for device in TRADFRI_DEVICE_TYPES:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, device)
+ )
return True
diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py
new file mode 100644
index 00000000000..ba90fe05d1e
--- /dev/null
+++ b/homeassistant/components/tradfri/base_class.py
@@ -0,0 +1,113 @@
+"""Base class for IKEA TRADFRI."""
+import logging
+
+from pytradfri.error import PytradfriError
+
+from homeassistant.core import callback
+from homeassistant.helpers.entity import Entity
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TradfriBaseClass(Entity):
+ """Base class for IKEA TRADFRI.
+
+ All devices and groups should ultimately inherit from this class.
+ """
+
+ def __init__(self, device, api, gateway_id):
+ """Initialize a device."""
+ self._api = api
+ self._device = None
+ self._device_control = None
+ self._device_data = None
+ self._gateway_id = gateway_id
+ self._name = None
+ self._unique_id = None
+
+ self._refresh(device)
+
+ @callback
+ def _async_start_observe(self, exc=None):
+ """Start observation of device."""
+ if exc:
+ self.async_schedule_update_ha_state()
+ _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc)
+
+ try:
+ cmd = self._device.observe(
+ callback=self._observe_update,
+ err_callback=self._async_start_observe,
+ duration=0,
+ )
+ self.hass.async_create_task(self._api(cmd))
+ except PytradfriError as err:
+ _LOGGER.warning("Observation failed, trying again", exc_info=err)
+ self._async_start_observe()
+
+ async def async_added_to_hass(self):
+ """Start thread when added to hass."""
+ self._async_start_observe()
+
+ @property
+ def name(self):
+ """Return the display name of this device."""
+ return self._name
+
+ @property
+ def should_poll(self):
+ """No polling needed for tradfri device."""
+ return False
+
+ @property
+ def unique_id(self):
+ """Return unique ID for device."""
+ return self._unique_id
+
+ @callback
+ def _observe_update(self, device):
+ """Receive new state data for this device."""
+ self._refresh(device)
+ self.async_schedule_update_ha_state()
+
+ def _refresh(self, device):
+ """Refresh the device data."""
+ self._device = device
+ self._name = device.name
+
+
+class TradfriBaseDevice(TradfriBaseClass):
+ """Base class for a TRADFRI device.
+
+ All devices should inherit from this class.
+ """
+
+ def __init__(self, device, api, gateway_id):
+ """Initialize a device."""
+ super().__init__(device, api, gateway_id)
+ self._available = True
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._available
+
+ @property
+ def device_info(self):
+ """Return the device info."""
+ info = self._device.device_info
+
+ return {
+ "identifiers": {(DOMAIN, self._device.id)},
+ "manufacturer": info.manufacturer,
+ "model": info.model_number,
+ "name": self._name,
+ "sw_version": info.firmware_version,
+ "via_device": (DOMAIN, self._gateway_id),
+ }
+
+ def _refresh(self, device):
+ """Refresh the device data."""
+ super()._refresh(device)
+ self._available = device.reachable
diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py
index 6266766f394..bdb195cf53f 100644
--- a/homeassistant/components/tradfri/config_flow.py
+++ b/homeassistant/components/tradfri/config_flow.py
@@ -7,18 +7,15 @@ import async_timeout
import voluptuous as vol
from homeassistant import config_entries
-
from .const import (
CONF_IMPORT_GROUPS,
CONF_IDENTITY,
CONF_HOST,
CONF_KEY,
CONF_GATEWAY_ID,
+ KEY_SECURITY_CODE,
)
-KEY_SECURITY_CODE = "security_code"
-KEY_IMPORT_GROUPS = "import_groups"
-
class AuthError(Exception):
"""Exception if authentication occurs."""
@@ -83,7 +80,7 @@ class FlowHandler(config_entries.ConfigFlow):
"""Handle zeroconf discovery."""
host = user_input["host"]
- # pylint: disable=unsupported-assignment-operation
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["host"] = host
if any(host == flow["context"]["host"] for flow in self._async_in_progress()):
diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py
index d37b5d99f9f..038f0e91c76 100644
--- a/homeassistant/components/tradfri/const.py
+++ b/homeassistant/components/tradfri/const.py
@@ -1,7 +1,25 @@
"""Consts used by Tradfri."""
+from homeassistant.components.light import SUPPORT_TRANSITION, SUPPORT_BRIGHTNESS
from homeassistant.const import CONF_HOST # noqa pylint: disable=unused-import
-CONF_IMPORT_GROUPS = "import_groups"
+ATTR_DIMMER = "dimmer"
+ATTR_HUE = "hue"
+ATTR_SAT = "saturation"
+ATTR_TRADFRI_GATEWAY = "Gateway"
+ATTR_TRADFRI_GATEWAY_MODEL = "E1526"
+ATTR_TRADFRI_MANUFACTURER = "IKEA of Sweden"
+ATTR_TRANSITION_TIME = "transition_time"
+CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups"
CONF_IDENTITY = "identity"
-CONF_KEY = "key"
+CONF_IMPORT_GROUPS = "import_groups"
CONF_GATEWAY_ID = "gateway_id"
+CONF_KEY = "key"
+CONFIG_FILE = ".tradfri_psk.conf"
+DEFAULT_ALLOW_TRADFRI_GROUPS = False
+DOMAIN = "tradfri"
+KEY_API = "tradfri_api"
+KEY_GATEWAY = "tradfri_gateway"
+KEY_SECURITY_CODE = "security_code"
+SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
+SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION
+TRADFRI_DEVICE_TYPES = ["cover", "light", "sensor", "switch"]
diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py
index 3dea978044f..9b831dce0ec 100644
--- a/homeassistant/components/tradfri/cover.py
+++ b/homeassistant/components/tradfri/cover.py
@@ -1,7 +1,4 @@
"""Support for IKEA Tradfri covers."""
-import logging
-
-from pytradfri.error import PytradfriError
from homeassistant.components.cover import (
CoverDevice,
@@ -10,11 +7,8 @@ from homeassistant.components.cover import (
SUPPORT_CLOSE,
SUPPORT_SET_POSITION,
)
-from homeassistant.core import callback
-from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY
-from .const import CONF_GATEWAY_ID
-
-_LOGGER = logging.getLogger(__name__)
+from .base_class import TradfriBaseDevice
+from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -30,120 +24,51 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers)
-class TradfriCover(CoverDevice):
+class TradfriCover(TradfriBaseDevice, CoverDevice):
"""The platform class required by Home Assistant."""
- def __init__(self, cover, api, gateway_id):
+ def __init__(self, device, api, gateway_id):
"""Initialize a cover."""
- self._api = api
- self._unique_id = f"{gateway_id}-{cover.id}"
- self._cover = None
- self._cover_control = None
- self._cover_data = None
- self._name = None
- self._available = True
- self._gateway_id = gateway_id
+ super().__init__(device, api, gateway_id)
+ self._unique_id = f"{gateway_id}-{device.id}"
- self._refresh(cover)
+ self._refresh(device)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
- @property
- def unique_id(self):
- """Return unique ID for cover."""
- return self._unique_id
-
- @property
- def device_info(self):
- """Return the device info."""
- info = self._cover.device_info
-
- return {
- "identifiers": {(TRADFRI_DOMAIN, self._cover.id)},
- "name": self._name,
- "manufacturer": info.manufacturer,
- "model": info.model_number,
- "sw_version": info.firmware_version,
- "via_device": (TRADFRI_DOMAIN, self._gateway_id),
- }
-
- async def async_added_to_hass(self):
- """Start thread when added to hass."""
- self._async_start_observe()
-
- @property
- def available(self):
- """Return True if entity is available."""
- return self._available
-
- @property
- def should_poll(self):
- """No polling needed for tradfri cover."""
- return False
-
- @property
- def name(self):
- """Return the display name of this cover."""
- return self._name
-
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
- return 100 - self._cover_data.current_cover_position
+ return 100 - self._device_data.current_cover_position
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
- await self._api(self._cover_control.set_state(100 - kwargs[ATTR_POSITION]))
+ await self._api(self._device_control.set_state(100 - kwargs[ATTR_POSITION]))
async def async_open_cover(self, **kwargs):
"""Open the cover."""
- await self._api(self._cover_control.set_state(0))
+ await self._api(self._device_control.set_state(0))
async def async_close_cover(self, **kwargs):
"""Close cover."""
- await self._api(self._cover_control.set_state(100))
+ await self._api(self._device_control.set_state(100))
@property
def is_closed(self):
"""Return if the cover is closed or not."""
return self.current_cover_position == 0
- @callback
- def _async_start_observe(self, exc=None):
- """Start observation of cover."""
- if exc:
- self._available = False
- self.async_schedule_update_ha_state()
- _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc)
- try:
- cmd = self._cover.observe(
- callback=self._observe_update,
- err_callback=self._async_start_observe,
- duration=0,
- )
- self.hass.async_create_task(self._api(cmd))
- except PytradfriError as err:
- _LOGGER.warning("Observation failed, trying again", exc_info=err)
- self._async_start_observe()
-
- def _refresh(self, cover):
+ def _refresh(self, device):
"""Refresh the cover data."""
- self._cover = cover
+ super()._refresh(device)
+ self._device = device
# Caching of BlindControl and cover object
- self._available = cover.reachable
- self._cover_control = cover.blind_control
- self._cover_data = cover.blind_control.blinds[0]
- self._name = cover.name
-
- @callback
- def _observe_update(self, tradfri_device):
- """Receive new state data for this cover."""
- self._refresh(tradfri_device)
- self.async_schedule_update_ha_state()
+ self._device_control = device.blind_control
+ self._device_data = device.blind_control.blinds[0]
diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py
index 615899a98c8..9ee3c5d6a8c 100644
--- a/homeassistant/components/tradfri/light.py
+++ b/homeassistant/components/tradfri/light.py
@@ -1,36 +1,33 @@
"""Support for IKEA Tradfri lights."""
import logging
-from pytradfri.error import PytradfriError
-
import homeassistant.util.color as color_util
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_TRANSITION,
- PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
+ Light,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
- SUPPORT_TRANSITION,
- Light,
)
-from homeassistant.core import callback
-from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY
-from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS
+from .base_class import TradfriBaseDevice, TradfriBaseClass
+from .const import (
+ ATTR_DIMMER,
+ ATTR_HUE,
+ ATTR_SAT,
+ ATTR_TRANSITION_TIME,
+ SUPPORTED_LIGHT_FEATURES,
+ SUPPORTED_GROUP_FEATURES,
+ CONF_GATEWAY_ID,
+ CONF_IMPORT_GROUPS,
+ KEY_GATEWAY,
+ KEY_API,
+)
_LOGGER = logging.getLogger(__name__)
-ATTR_DIMMER = "dimmer"
-ATTR_HUE = "hue"
-ATTR_SAT = "saturation"
-ATTR_TRANSITION_TIME = "transition_time"
-PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
-TRADFRI_LIGHT_MANAGER = "Tradfri Light Manager"
-SUPPORTED_FEATURES = SUPPORT_TRANSITION
-SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Load Tradfri lights based on a config entry."""
@@ -51,50 +48,47 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(TradfriGroup(group, api, gateway_id) for group in groups)
-class TradfriGroup(Light):
- """The platform class required by hass."""
+class TradfriGroup(TradfriBaseClass, Light):
+ """The platform class for light groups required by hass."""
- def __init__(self, group, api, gateway_id):
+ def __init__(self, device, api, gateway_id):
"""Initialize a Group."""
- self._api = api
- self._unique_id = f"group-{gateway_id}-{group.id}"
- self._group = group
- self._name = group.name
+ super().__init__(device, api, gateway_id)
- self._refresh(group)
+ self._unique_id = f"group-{gateway_id}-{device.id}"
- async def async_added_to_hass(self):
- """Start thread when added to hass."""
- self._async_start_observe()
+ self._refresh(device)
@property
- def unique_id(self):
- """Return unique ID for this group."""
- return self._unique_id
+ def should_poll(self):
+ """Poll needed for tradfri groups."""
+ return True
+
+ async def async_update(self):
+ """Fetch new state data for the group.
+
+ This method is required for groups to update properly.
+ """
+ await self._api(self._device.update())
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORTED_GROUP_FEATURES
- @property
- def name(self):
- """Return the display name of this group."""
- return self._name
-
@property
def is_on(self):
"""Return true if group lights are on."""
- return self._group.state
+ return self._device.state
@property
def brightness(self):
"""Return the brightness of the group lights."""
- return self._group.dimmer
+ return self._device.dimmer
async def async_turn_off(self, **kwargs):
"""Instruct the group lights to turn off."""
- await self._api(self._group.set_state(0))
+ await self._api(self._device.set_state(0))
async def async_turn_on(self, **kwargs):
"""Instruct the group lights to turn on, or dim."""
@@ -106,136 +100,69 @@ class TradfriGroup(Light):
if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254
- await self._api(self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))
+ await self._api(self._device.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))
else:
- await self._api(self._group.set_state(1))
-
- @callback
- def _async_start_observe(self, exc=None):
- """Start observation of light."""
- if exc:
- _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc)
-
- try:
- cmd = self._group.observe(
- callback=self._observe_update,
- err_callback=self._async_start_observe,
- duration=0,
- )
- self.hass.async_create_task(self._api(cmd))
- except PytradfriError as err:
- _LOGGER.warning("Observation failed, trying again", exc_info=err)
- self._async_start_observe()
-
- def _refresh(self, group):
- """Refresh the light data."""
- self._group = group
- self._name = group.name
-
- @callback
- def _observe_update(self, tradfri_device):
- """Receive new state data for this light."""
- self._refresh(tradfri_device)
- self.async_schedule_update_ha_state()
-
- async def async_update(self):
- """Fetch new state data for the group."""
- await self._api(self._group.update())
+ await self._api(self._device.set_state(1))
-class TradfriLight(Light):
+class TradfriLight(TradfriBaseDevice, Light):
"""The platform class required by Home Assistant."""
- def __init__(self, light, api, gateway_id):
+ def __init__(self, device, api, gateway_id):
"""Initialize a Light."""
- self._api = api
- self._unique_id = f"light-{gateway_id}-{light.id}"
- self._light = None
- self._light_control = None
- self._light_data = None
- self._name = None
+ super().__init__(device, api, gateway_id)
+ self._unique_id = f"light-{gateway_id}-{device.id}"
self._hs_color = None
- self._features = SUPPORTED_FEATURES
- self._available = True
- self._gateway_id = gateway_id
- self._refresh(light)
+ # Calculate supported features
+ _features = SUPPORTED_LIGHT_FEATURES
+ if device.light_control.can_set_dimmer:
+ _features |= SUPPORT_BRIGHTNESS
+ if device.light_control.can_set_color:
+ _features |= SUPPORT_COLOR
+ if device.light_control.can_set_temp:
+ _features |= SUPPORT_COLOR_TEMP
+ self._features = _features
- @property
- def unique_id(self):
- """Return unique ID for light."""
- return self._unique_id
-
- @property
- def device_info(self):
- """Return the device info."""
- info = self._light.device_info
-
- return {
- "identifiers": {(TRADFRI_DOMAIN, self._light.id)},
- "name": self._name,
- "manufacturer": info.manufacturer,
- "model": info.model_number,
- "sw_version": info.firmware_version,
- "via_device": (TRADFRI_DOMAIN, self._gateway_id),
- }
+ self._refresh(device)
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
- return self._light_control.min_mireds
+ return self._device_control.min_mireds
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
- return self._light_control.max_mireds
-
- async def async_added_to_hass(self):
- """Start thread when added to hass."""
- self._async_start_observe()
-
- @property
- def available(self):
- """Return True if entity is available."""
- return self._available
-
- @property
- def should_poll(self):
- """No polling needed for tradfri light."""
- return False
+ return self._device_control.max_mireds
@property
def supported_features(self):
"""Flag supported features."""
return self._features
- @property
- def name(self):
- """Return the display name of this light."""
- return self._name
-
@property
def is_on(self):
"""Return true if light is on."""
- return self._light_data.state
+ return self._device_data.state
@property
def brightness(self):
"""Return the brightness of the light."""
- return self._light_data.dimmer
+ return self._device_data.dimmer
@property
def color_temp(self):
"""Return the color temp value in mireds."""
- return self._light_data.color_temp
+ return self._device_data.color_temp
@property
def hs_color(self):
"""HS color of the light."""
- if self._light_control.can_set_color:
- hsbxy = self._light_data.hsb_xy_color
- hue = hsbxy[0] / (self._light_control.max_hue / 360)
- sat = hsbxy[1] / (self._light_control.max_saturation / 100)
+ if self._device_control.can_set_color:
+ hsbxy = self._device_data.hsb_xy_color
+ hue = hsbxy[0] / (self._device_control.max_hue / 360)
+ sat = hsbxy[1] / (self._device_control.max_saturation / 100)
if hue is not None and sat is not None:
return hue, sat
@@ -248,9 +175,9 @@ class TradfriLight(Light):
transition_time = int(kwargs[ATTR_TRANSITION]) * 10
dimmer_data = {ATTR_DIMMER: 0, ATTR_TRANSITION_TIME: transition_time}
- await self._api(self._light_control.set_dimmer(**dimmer_data))
+ await self._api(self._device_control.set_dimmer(**dimmer_data))
else:
- await self._api(self._light_control.set_state(False))
+ await self._api(self._device_control.set_state(False))
async def async_turn_on(self, **kwargs):
"""Instruct the light to turn on."""
@@ -267,32 +194,32 @@ class TradfriLight(Light):
ATTR_DIMMER: brightness,
ATTR_TRANSITION_TIME: transition_time,
}
- dimmer_command = self._light_control.set_dimmer(**dimmer_data)
+ dimmer_command = self._device_control.set_dimmer(**dimmer_data)
transition_time = None
else:
- dimmer_command = self._light_control.set_state(True)
+ dimmer_command = self._device_control.set_state(True)
color_command = None
- if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color:
- hue = int(kwargs[ATTR_HS_COLOR][0] * (self._light_control.max_hue / 360))
+ if ATTR_HS_COLOR in kwargs and self._device_control.can_set_color:
+ hue = int(kwargs[ATTR_HS_COLOR][0] * (self._device_control.max_hue / 360))
sat = int(
- kwargs[ATTR_HS_COLOR][1] * (self._light_control.max_saturation / 100)
+ kwargs[ATTR_HS_COLOR][1] * (self._device_control.max_saturation / 100)
)
color_data = {
ATTR_HUE: hue,
ATTR_SAT: sat,
ATTR_TRANSITION_TIME: transition_time,
}
- color_command = self._light_control.set_hsb(**color_data)
+ color_command = self._device_control.set_hsb(**color_data)
transition_time = None
temp_command = None
if ATTR_COLOR_TEMP in kwargs and (
- self._light_control.can_set_temp or self._light_control.can_set_color
+ self._device_control.can_set_temp or self._device_control.can_set_color
):
temp = kwargs[ATTR_COLOR_TEMP]
# White Spectrum bulb
- if self._light_control.can_set_temp:
+ if self._device_control.can_set_temp:
if temp > self.max_mireds:
temp = self.max_mireds
elif temp < self.min_mireds:
@@ -301,21 +228,21 @@ class TradfriLight(Light):
ATTR_COLOR_TEMP: temp,
ATTR_TRANSITION_TIME: transition_time,
}
- temp_command = self._light_control.set_color_temp(**temp_data)
+ temp_command = self._device_control.set_color_temp(**temp_data)
transition_time = None
# Color bulb (CWS)
# color_temp needs to be set with hue/saturation
- elif self._light_control.can_set_color:
+ elif self._device_control.can_set_color:
temp_k = color_util.color_temperature_mired_to_kelvin(temp)
hs_color = color_util.color_temperature_to_hs(temp_k)
- hue = int(hs_color[0] * (self._light_control.max_hue / 360))
- sat = int(hs_color[1] * (self._light_control.max_saturation / 100))
+ hue = int(hs_color[0] * (self._device_control.max_hue / 360))
+ sat = int(hs_color[1] * (self._device_control.max_saturation / 100))
color_data = {
ATTR_HUE: hue,
ATTR_SAT: sat,
ATTR_TRANSITION_TIME: transition_time,
}
- color_command = self._light_control.set_hsb(**color_data)
+ color_command = self._device_control.set_hsb(**color_data)
transition_time = None
# HSB can always be set, but color temp + brightness is bulb dependant
@@ -325,7 +252,7 @@ class TradfriLight(Light):
else:
command = color_command
- if self._light_control.can_combine_commands:
+ if self._device_control.can_combine_commands:
await self._api(command + temp_command)
else:
if temp_command is not None:
@@ -333,46 +260,10 @@ class TradfriLight(Light):
if command is not None:
await self._api(command)
- @callback
- def _async_start_observe(self, exc=None):
- """Start observation of light."""
-
- if exc:
- self._available = False
- self.async_schedule_update_ha_state()
- _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc)
-
- try:
- cmd = self._light.observe(
- callback=self._observe_update,
- err_callback=self._async_start_observe,
- duration=0,
- )
- self.hass.async_create_task(self._api(cmd))
- except PytradfriError as err:
- _LOGGER.warning("Observation failed, trying again", exc_info=err)
- self._async_start_observe()
-
- def _refresh(self, light):
+ def _refresh(self, device):
"""Refresh the light data."""
- self._light = light
+ super()._refresh(device)
# Caching of LightControl and light object
- self._available = light.reachable
- self._light_control = light.light_control
- self._light_data = light.light_control.lights[0]
- self._name = light.name
- self._features = SUPPORTED_FEATURES
-
- if light.light_control.can_set_dimmer:
- self._features |= SUPPORT_BRIGHTNESS
- if light.light_control.can_set_color:
- self._features |= SUPPORT_COLOR
- if light.light_control.can_set_temp:
- self._features |= SUPPORT_COLOR_TEMP
-
- @callback
- def _observe_update(self, tradfri_device):
- """Receive new state data for this light."""
- self._refresh(tradfri_device)
- self.async_schedule_update_ha_state()
+ self._device_control = device.light_control
+ self._device_data = device.light_control.lights[0]
diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py
index 4877dbbb541..68a2c10291b 100644
--- a/homeassistant/components/tradfri/sensor.py
+++ b/homeassistant/components/tradfri/sensor.py
@@ -1,20 +1,13 @@
"""Support for IKEA Tradfri sensors."""
-import logging
-from datetime import timedelta
-from pytradfri.error import PytradfriError
-
-from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
-from . import KEY_API, KEY_GATEWAY
-
-_LOGGER = logging.getLogger(__name__)
-
-SCAN_INTERVAL = timedelta(minutes=5)
+from homeassistant.const import DEVICE_CLASS_BATTERY
+from .base_class import TradfriBaseDevice
+from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Tradfri config entry."""
+ gateway_id = config_entry.data[CONF_GATEWAY_ID]
api = hass.data[KEY_API][config_entry.entry_id]
gateway = hass.data[KEY_GATEWAY][config_entry.entry_id]
@@ -23,84 +16,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
devices = (
dev
for dev in all_devices
- if not dev.has_light_control and not dev.has_socket_control
+ if not dev.has_light_control
+ and not dev.has_socket_control
+ and not dev.has_blind_control
)
- async_add_entities(TradfriDevice(device, api) for device in devices)
+ if devices:
+ async_add_entities(TradfriSensor(device, api, gateway_id) for device in devices)
-class TradfriDevice(Entity):
+class TradfriSensor(TradfriBaseDevice):
"""The platform class required by Home Assistant."""
- def __init__(self, device, api):
+ def __init__(self, device, api, gateway_id):
"""Initialize the device."""
- self._api = api
- self._device = None
- self._name = None
-
- self._refresh(device)
-
- async def async_added_to_hass(self):
- """Start thread when added to hass."""
- self._async_start_observe()
+ super().__init__(device, api, gateway_id)
+ self._unique_id = f"{gateway_id}-{device.id}"
@property
- def should_poll(self):
- """No polling needed for tradfri."""
- return False
-
- @property
- def name(self):
- """Return the display name of this device."""
- return self._name
-
- @property
- def unit_of_measurement(self):
- """Return the unit_of_measurement of the device."""
- return "%"
-
- @property
- def device_state_attributes(self):
+ def device_class(self):
"""Return the devices' state attributes."""
- info = self._device.device_info
- attrs = {
- "manufacturer": info.manufacturer,
- "model_number": info.model_number,
- "serial": info.serial,
- "firmware_version": info.firmware_version,
- "power_source": info.power_source_str,
- "battery_level": info.battery_level,
- }
- return attrs
+ return DEVICE_CLASS_BATTERY
@property
def state(self):
"""Return the current state of the device."""
return self._device.device_info.battery_level
- @callback
- def _async_start_observe(self, exc=None):
- """Start observation of light."""
- if exc:
- _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc)
-
- try:
- cmd = self._device.observe(
- callback=self._observe_update,
- err_callback=self._async_start_observe,
- duration=0,
- )
- self.hass.async_create_task(self._api(cmd))
- except PytradfriError as err:
- _LOGGER.warning("Observation failed, trying again", exc_info=err)
- self._async_start_observe()
-
- def _refresh(self, device):
- """Refresh the device data."""
- self._device = device
- self._name = device.name
-
- def _observe_update(self, tradfri_device):
- """Receive new state data for this device."""
- self._refresh(tradfri_device)
-
- self.hass.async_create_task(self.async_update_ha_state())
+ @property
+ def unit_of_measurement(self):
+ """Return the unit_of_measurement of the device."""
+ return "%"
diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py
index 545c1ad93ce..e1c549a1805 100644
--- a/homeassistant/components/tradfri/switch.py
+++ b/homeassistant/components/tradfri/switch.py
@@ -1,16 +1,7 @@
"""Support for IKEA Tradfri switches."""
-import logging
-
-from pytradfri.error import PytradfriError
-
from homeassistant.components.switch import SwitchDevice
-from homeassistant.core import callback
-from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY
-from .const import CONF_GATEWAY_ID
-
-_LOGGER = logging.getLogger(__name__)
-
-TRADFRI_SWITCH_MANAGER = "Tradfri Switch Manager"
+from .base_class import TradfriBaseDevice
+from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -28,104 +19,31 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-class TradfriSwitch(SwitchDevice):
+class TradfriSwitch(TradfriBaseDevice, SwitchDevice):
"""The platform class required by Home Assistant."""
- def __init__(self, switch, api, gateway_id):
+ def __init__(self, device, api, gateway_id):
"""Initialize a switch."""
- self._api = api
- self._unique_id = f"{gateway_id}-{switch.id}"
- self._switch = None
- self._socket_control = None
- self._switch_data = None
- self._name = None
- self._available = True
- self._gateway_id = gateway_id
+ super().__init__(device, api, gateway_id)
+ self._unique_id = f"{gateway_id}-{device.id}"
- self._refresh(switch)
+ def _refresh(self, device):
+ """Refresh the switch data."""
+ super()._refresh(device)
- @property
- def unique_id(self):
- """Return unique ID for switch."""
- return self._unique_id
-
- @property
- def device_info(self):
- """Return the device info."""
- info = self._switch.device_info
-
- return {
- "identifiers": {(TRADFRI_DOMAIN, self._switch.id)},
- "name": self._name,
- "manufacturer": info.manufacturer,
- "model": info.model_number,
- "sw_version": info.firmware_version,
- "via_device": (TRADFRI_DOMAIN, self._gateway_id),
- }
-
- async def async_added_to_hass(self):
- """Start thread when added to hass."""
- self._async_start_observe()
-
- @property
- def available(self):
- """Return True if entity is available."""
- return self._available
-
- @property
- def should_poll(self):
- """No polling needed for tradfri switch."""
- return False
-
- @property
- def name(self):
- """Return the display name of this switch."""
- return self._name
+ # Caching of switch control and switch object
+ self._device_control = device.socket_control
+ self._device_data = device.socket_control.sockets[0]
@property
def is_on(self):
"""Return true if switch is on."""
- return self._switch_data.state
+ return self._device_data.state
async def async_turn_off(self, **kwargs):
"""Instruct the switch to turn off."""
- await self._api(self._socket_control.set_state(False))
+ await self._api(self._device_control.set_state(False))
async def async_turn_on(self, **kwargs):
"""Instruct the switch to turn on."""
- await self._api(self._socket_control.set_state(True))
-
- @callback
- def _async_start_observe(self, exc=None):
- """Start observation of switch."""
- if exc:
- self._available = False
- self.async_schedule_update_ha_state()
- _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc)
-
- try:
- cmd = self._switch.observe(
- callback=self._observe_update,
- err_callback=self._async_start_observe,
- duration=0,
- )
- self.hass.async_create_task(self._api(cmd))
- except PytradfriError as err:
- _LOGGER.warning("Observation failed, trying again", exc_info=err)
- self._async_start_observe()
-
- def _refresh(self, switch):
- """Refresh the switch data."""
- self._switch = switch
-
- # Caching of switchControl and switch object
- self._available = switch.reachable
- self._socket_control = switch.socket_control
- self._switch_data = switch.socket_control.sockets[0]
- self._name = switch.name
-
- @callback
- def _observe_update(self, tradfri_device):
- """Receive new state data for this switch."""
- self._refresh(tradfri_device)
- self.async_schedule_update_ha_state()
+ await self._api(self._device_control.set_state(True))
diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json
index ed0342b9430..1a2fa4a48c0 100644
--- a/homeassistant/components/transmission/.translations/de.json
+++ b/homeassistant/components/transmission/.translations/de.json
@@ -21,16 +21,19 @@
"password": "Passwort",
"port": "Port",
"username": "Benutzername"
- }
+ },
+ "title": "Transmission-Client einrichten"
}
- }
+ },
+ "title": "Transmission"
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Aktualisierungsfrequenz"
- }
+ },
+ "description": "Konfigurieren von Optionen f\u00fcr Transmission"
}
}
}
diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json
index 67461d1a3e8..aa8b99a4914 100644
--- a/homeassistant/components/transmission/.translations/en.json
+++ b/homeassistant/components/transmission/.translations/en.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "Host is already configured.",
"one_instance_allowed": "Only a single instance is necessary."
},
"error": {
"cannot_connect": "Unable to Connect to host",
+ "name_exists": "Name already exists",
"wrong_credentials": "Wrong username or password"
},
"step": {
@@ -33,7 +35,8 @@
"data": {
"scan_interval": "Update frequency"
},
- "description": "Configure options for Transmission"
+ "description": "Configure options for Transmission",
+ "title": "Configure options for Transmission"
}
}
}
diff --git a/homeassistant/components/transmission/.translations/ko.json b/homeassistant/components/transmission/.translations/ko.json
new file mode 100644
index 00000000000..a9b1b369f90
--- /dev/null
+++ b/homeassistant/components/transmission/.translations/ko.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "wrong_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "options": {
+ "data": {
+ "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4"
+ },
+ "title": "\uc635\uc158 \uc124\uc815"
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "name": "\uc774\ub984",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "port": "\ud3ec\ud2b8",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "title": "Transmission \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815"
+ }
+ },
+ "title": "Transmission"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4"
+ },
+ "description": "Transmission \uc635\uc158 \uc124\uc815"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/transmission/.translations/nl.json b/homeassistant/components/transmission/.translations/nl.json
new file mode 100644
index 00000000000..fdf3db99ed0
--- /dev/null
+++ b/homeassistant/components/transmission/.translations/nl.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig."
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken met host",
+ "wrong_credentials": "verkeerde gebruikersnaam of wachtwoord"
+ },
+ "step": {
+ "options": {
+ "data": {
+ "scan_interval": "Update frequentie"
+ },
+ "title": "Configureer opties"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Naam",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "username": "Gebruikersnaam"
+ },
+ "title": "Verzendclient instellen"
+ }
+ },
+ "title": "Transmission"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Update frequentie"
+ },
+ "description": "Configureer opties voor Transmission"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json
index f6ddce2a4a7..94044e692d9 100644
--- a/homeassistant/components/transmission/.translations/no.json
+++ b/homeassistant/components/transmission/.translations/no.json
@@ -22,7 +22,7 @@
"port": "Port",
"username": "Brukernavn"
},
- "title": "Oppsett av klient for Transmission"
+ "title": "Oppsett av Transmission-klient"
}
},
"title": "Transmission"
diff --git a/homeassistant/components/transmission/.translations/pt.json b/homeassistant/components/transmission/.translations/pt.json
new file mode 100644
index 00000000000..f681da4210f
--- /dev/null
+++ b/homeassistant/components/transmission/.translations/pt.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Servidor",
+ "port": "Porta"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json
index e7a438cae11..23f1ceaaa94 100644
--- a/homeassistant/components/transmission/.translations/ru.json
+++ b/homeassistant/components/transmission/.translations/ru.json
@@ -4,8 +4,8 @@
"one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443",
- "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c"
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.",
+ "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c."
},
"step": {
"options": {
diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py
index e6ddd87bdf5..6cfd6bf640a 100644
--- a/homeassistant/components/transmission/__init__.py
+++ b/homeassistant/components/transmission/__init__.py
@@ -19,11 +19,10 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.util import slugify
from .const import (
ATTR_TORRENT,
- DATA_TRANSMISSION,
- DATA_UPDATED,
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
@@ -37,74 +36,77 @@ _LOGGER = logging.getLogger(__name__)
SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string})
+TRANS_SCHEMA = vol.All(
+ vol.Schema(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(
+ CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
+ ): cv.time_period,
+ }
+ )
+)
+
CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_USERNAME): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(
- CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
- ): cv.time_period,
- }
- )
- },
- extra=vol.ALLOW_EXTRA,
+ {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}, extra=vol.ALLOW_EXTRA
)
async def async_setup(hass, config):
"""Import the Transmission Component from config."""
- if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config:
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
+ if DOMAIN in config:
+ for entry in config[DOMAIN]:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=entry
+ )
)
- )
return True
async def async_setup_entry(hass, config_entry):
"""Set up the Transmission Component."""
- if DOMAIN not in hass.data:
- hass.data[DOMAIN] = {}
-
- if not config_entry.options:
- await async_populate_options(hass, config_entry)
-
client = TransmissionClient(hass, config_entry)
- client_id = config_entry.entry_id
- hass.data[DOMAIN][client_id] = client
+ hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client
+
if not await client.async_setup():
return False
return True
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass, config_entry):
"""Unload Transmission Entry from config_entry."""
- hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT)
- if hass.data[DOMAIN][entry.entry_id].unsub_timer:
- hass.data[DOMAIN][entry.entry_id].unsub_timer()
+ client = hass.data[DOMAIN][config_entry.entry_id]
+ hass.services.async_remove(DOMAIN, client.service_name)
+ if client.unsub_timer:
+ client.unsub_timer()
for component in "sensor", "switch":
- await hass.config_entries.async_forward_entry_unload(entry, component)
+ await hass.config_entries.async_forward_entry_unload(config_entry, component)
- del hass.data[DOMAIN]
+ hass.data[DOMAIN].pop(config_entry.entry_id)
return True
-async def get_api(hass, host, port, username=None, password=None):
+async def get_api(hass, entry):
"""Get Transmission client."""
+ host = entry[CONF_HOST]
+ port = entry[CONF_PORT]
+ username = entry.get(CONF_USERNAME)
+ password = entry.get(CONF_PASSWORD)
+
try:
api = await hass.async_add_executor_job(
transmissionrpc.Client, host, port, username, password
)
+ _LOGGER.debug("Successfully connected to %s", host)
return api
except TransmissionError as error:
@@ -112,20 +114,13 @@ async def get_api(hass, host, port, username=None, password=None):
_LOGGER.error("Credentials for Transmission client are not valid")
raise AuthenticationError
if "111: Connection refused" in str(error):
- _LOGGER.error("Connecting to the Transmission client failed")
+ _LOGGER.error("Connecting to the Transmission client %s failed", host)
raise CannotConnect
_LOGGER.error(error)
raise UnknownError
-async def async_populate_options(hass, config_entry):
- """Populate default options for Transmission Client."""
- options = {CONF_SCAN_INTERVAL: config_entry.data["options"][CONF_SCAN_INTERVAL]}
-
- hass.config_entries.async_update_entry(config_entry, options=options)
-
-
class TransmissionClient:
"""Transmission Client Object."""
@@ -133,33 +128,35 @@ class TransmissionClient:
"""Initialize the Transmission RPC API."""
self.hass = hass
self.config_entry = config_entry
- self.scan_interval = self.config_entry.options[CONF_SCAN_INTERVAL]
- self.tm_data = None
+ self._tm_data = None
self.unsub_timer = None
+ @property
+ def service_name(self):
+ """Return the service name."""
+ return slugify(f"{SERVICE_ADD_TORRENT}_{self.config_entry.data[CONF_NAME]}")
+
+ @property
+ def api(self):
+ """Return the tm_data object."""
+ return self._tm_data
+
async def async_setup(self):
"""Set up the Transmission client."""
- config = {
- CONF_HOST: self.config_entry.data[CONF_HOST],
- CONF_PORT: self.config_entry.data[CONF_PORT],
- CONF_USERNAME: self.config_entry.data.get(CONF_USERNAME),
- CONF_PASSWORD: self.config_entry.data.get(CONF_PASSWORD),
- }
try:
- api = await get_api(self.hass, **config)
+ api = await get_api(self.hass, self.config_entry.data)
except CannotConnect:
raise ConfigEntryNotReady
except (AuthenticationError, UnknownError):
return False
- self.tm_data = self.hass.data[DOMAIN][DATA_TRANSMISSION] = TransmissionData(
- self.hass, self.config_entry, api
- )
+ self._tm_data = TransmissionData(self.hass, self.config_entry, api)
- await self.hass.async_add_executor_job(self.tm_data.init_torrent_list)
- await self.hass.async_add_executor_job(self.tm_data.update)
- self.set_scan_interval(self.scan_interval)
+ await self.hass.async_add_executor_job(self._tm_data.init_torrent_list)
+ await self.hass.async_add_executor_job(self._tm_data.update)
+ self.add_options()
+ self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL])
for platform in ["sensor", "switch"]:
self.hass.async_create_task(
@@ -181,19 +178,31 @@ class TransmissionClient:
)
self.hass.services.async_register(
- DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA
+ DOMAIN, self.service_name, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA
)
self.config_entry.add_update_listener(self.async_options_updated)
return True
+ def add_options(self):
+ """Add options for entry."""
+ if not self.config_entry.options:
+ scan_interval = self.config_entry.data.pop(
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
+ )
+ options = {CONF_SCAN_INTERVAL: scan_interval}
+
+ self.hass.config_entries.async_update_entry(
+ self.config_entry, options=options
+ )
+
def set_scan_interval(self, scan_interval):
"""Update scan interval."""
- def refresh(event_time):
+ async def refresh(event_time):
"""Get the latest data from Transmission."""
- self.tm_data.update()
+ self._tm_data.update()
if self.unsub_timer is not None:
self.unsub_timer()
@@ -215,6 +224,7 @@ class TransmissionData:
def __init__(self, hass, config, api):
"""Initialize the Transmission RPC API."""
self.hass = hass
+ self.config = config
self.data = None
self.torrents = None
self.session = None
@@ -223,6 +233,16 @@ class TransmissionData:
self.completed_torrents = []
self.started_torrents = []
+ @property
+ def host(self):
+ """Return the host name."""
+ return self.config.data[CONF_HOST]
+
+ @property
+ def signal_options_update(self):
+ """Option update signal per transmission entry."""
+ return f"tm-options-{self.host}"
+
def update(self):
"""Get the latest data from Transmission instance."""
try:
@@ -232,14 +252,13 @@ class TransmissionData:
self.check_completed_torrent()
self.check_started_torrent()
- _LOGGER.debug("Torrent Data Updated")
+ _LOGGER.debug("Torrent Data for %s Updated", self.host)
self.available = True
except TransmissionError:
self.available = False
- _LOGGER.error("Unable to connect to Transmission client")
-
- dispatcher_send(self.hass, DATA_UPDATED)
+ _LOGGER.error("Unable to connect to Transmission client %s", self.host)
+ dispatcher_send(self.hass, self.signal_options_update)
def init_torrent_list(self):
"""Initialize torrent lists."""
diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py
index 99376f4b6e0..d7b9efb15d8 100644
--- a/homeassistant/components/transmission/config_flow.py
+++ b/homeassistant/components/transmission/config_flow.py
@@ -29,32 +29,32 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return TransmissionOptionsFlowHandler(config_entry)
- def __init__(self):
- """Initialize the Transmission flow."""
- self.config = {}
- self.errors = {}
-
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
- if self.hass.config_entries.async_entries(DOMAIN):
- return self.async_abort(reason="one_instance_allowed")
+ errors = {}
if user_input is not None:
- self.config[CONF_NAME] = user_input.pop(CONF_NAME)
+ for entry in self.hass.config_entries.async_entries(DOMAIN):
+ if entry.data[CONF_HOST] == user_input[CONF_HOST]:
+ return self.async_abort(reason="already_configured")
+ if entry.data[CONF_NAME] == user_input[CONF_NAME]:
+ errors[CONF_NAME] = "name_exists"
+ break
+
try:
- await get_api(self.hass, **user_input)
- self.config.update(user_input)
- if "options" not in self.config:
- self.config["options"] = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}
- return self.async_create_entry(
- title=self.config[CONF_NAME], data=self.config
- )
+ await get_api(self.hass, user_input)
+
except AuthenticationError:
- self.errors[CONF_USERNAME] = "wrong_credentials"
- self.errors[CONF_PASSWORD] = "wrong_credentials"
+ errors[CONF_USERNAME] = "wrong_credentials"
+ errors[CONF_PASSWORD] = "wrong_credentials"
except (CannotConnect, UnknownError):
- self.errors["base"] = "cannot_connect"
+ errors["base"] = "cannot_connect"
+
+ if not errors:
+ return self.async_create_entry(
+ title=user_input[CONF_NAME], data=user_input
+ )
return self.async_show_form(
step_id="user",
@@ -67,15 +67,12 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
),
- errors=self.errors,
+ errors=errors,
)
async def async_step_import(self, import_config):
"""Import from Transmission client config."""
- self.config["options"] = {
- CONF_SCAN_INTERVAL: import_config.pop(CONF_SCAN_INTERVAL).seconds
- }
-
+ import_config[CONF_SCAN_INTERVAL] = import_config[CONF_SCAN_INTERVAL].seconds
return await self.async_step_user(user_input=import_config)
@@ -95,8 +92,7 @@ class TransmissionOptionsFlowHandler(config_entries.OptionsFlow):
vol.Optional(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(
- CONF_SCAN_INTERVAL,
- self.config_entry.data["options"][CONF_SCAN_INTERVAL],
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): int
}
diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py
index e4a8b1490c2..472bb32a391 100644
--- a/homeassistant/components/transmission/const.py
+++ b/homeassistant/components/transmission/const.py
@@ -21,4 +21,3 @@ ATTR_TORRENT = "torrent"
SERVICE_ADD_TORRENT = "add_torrent"
DATA_UPDATED = "transmission_data_updated"
-DATA_TRANSMISSION = "data_transmission"
diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py
index 30dfa4a3cbe..d9fd2b51144 100644
--- a/homeassistant/components/transmission/sensor.py
+++ b/homeassistant/components/transmission/sensor.py
@@ -6,7 +6,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SENSOR_TYPES
+from .const import DOMAIN, SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
@@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Transmission sensors."""
- transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION]
+ tm_client = hass.data[DOMAIN][config_entry.entry_id]
name = config_entry.data[CONF_NAME]
dev = []
@@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
dev.append(
TransmissionSensor(
sensor_type,
- transmission_api,
+ tm_client,
name,
SENSOR_TYPES[sensor_type][0],
SENSOR_TYPES[sensor_type][1],
@@ -41,17 +41,12 @@ class TransmissionSensor(Entity):
"""Representation of a Transmission sensor."""
def __init__(
- self,
- sensor_type,
- transmission_api,
- client_name,
- sensor_name,
- unit_of_measurement,
+ self, sensor_type, tm_client, client_name, sensor_name, unit_of_measurement
):
"""Initialize the sensor."""
self._name = sensor_name
self._state = None
- self._transmission_api = transmission_api
+ self._tm_client = tm_client
self._unit_of_measurement = unit_of_measurement
self._data = None
self.client_name = client_name
@@ -62,6 +57,11 @@ class TransmissionSensor(Entity):
"""Return the name of the sensor."""
return f"{self.client_name} {self._name}"
+ @property
+ def unique_id(self):
+ """Return the unique id of the entity."""
+ return f"{self._tm_client.api.host}-{self.name}"
+
@property
def state(self):
"""Return the state of the sensor."""
@@ -80,12 +80,14 @@ class TransmissionSensor(Entity):
@property
def available(self):
"""Could the device be accessed during the last update call."""
- return self._transmission_api.available
+ return self._tm_client.api.available
async def async_added_to_hass(self):
"""Handle entity which will be added."""
async_dispatcher_connect(
- self.hass, DATA_UPDATED, self._schedule_immediate_update
+ self.hass,
+ self._tm_client.api.signal_options_update,
+ self._schedule_immediate_update,
)
@callback
@@ -94,12 +96,12 @@ class TransmissionSensor(Entity):
def update(self):
"""Get the latest data from Transmission and updates the state."""
- self._data = self._transmission_api.data
+ self._data = self._tm_client.api.data
if self.type == "completed_torrents":
- self._state = self._transmission_api.get_completed_torrent_count()
+ self._state = self._tm_client.api.get_completed_torrent_count()
elif self.type == "started_torrents":
- self._state = self._transmission_api.get_started_torrent_count()
+ self._state = self._tm_client.api.get_started_torrent_count()
if self.type == "current_status":
if self._data:
diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json
index 7160cd109c4..45c16be36e2 100644
--- a/homeassistant/components/transmission/strings.json
+++ b/homeassistant/components/transmission/strings.json
@@ -7,30 +7,25 @@
"data": {
"name": "Name",
"host": "Host",
- "username": "User name",
+ "username": "Username",
"password": "Password",
"port": "Port"
}
- },
- "options": {
- "title": "Configure Options",
- "data": {
- "scan_interval": "Update frequency"
- }
}
},
"error": {
+ "name_exists": "Name already exists",
"wrong_credentials": "Wrong username or password",
"cannot_connect": "Unable to Connect to host"
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary."
+ "already_configured": "Host is already configured."
}
},
"options": {
"step": {
"init": {
- "description": "Configure options for Transmission",
+ "title": "Configure options for Transmission",
"data": {
"scan_interval": "Update frequency"
}
diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py
index 0bb43f715ac..4b93b3f06e2 100644
--- a/homeassistant/components/transmission/switch.py
+++ b/homeassistant/components/transmission/switch.py
@@ -6,7 +6,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import ToggleEntity
-from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SWITCH_TYPES
+from .const import DOMAIN, SWITCH_TYPES
_LOGGING = logging.getLogger(__name__)
@@ -19,12 +19,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Transmission switch."""
- transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION]
+ tm_client = hass.data[DOMAIN][config_entry.entry_id]
name = config_entry.data[CONF_NAME]
dev = []
for switch_type, switch_name in SWITCH_TYPES.items():
- dev.append(TransmissionSwitch(switch_type, switch_name, transmission_api, name))
+ dev.append(TransmissionSwitch(switch_type, switch_name, tm_client, name))
async_add_entities(dev, True)
@@ -32,12 +32,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class TransmissionSwitch(ToggleEntity):
"""Representation of a Transmission switch."""
- def __init__(self, switch_type, switch_name, transmission_api, name):
+ def __init__(self, switch_type, switch_name, tm_client, name):
"""Initialize the Transmission switch."""
self._name = switch_name
self.client_name = name
self.type = switch_type
- self._transmission_api = transmission_api
+ self._tm_client = tm_client
self._state = STATE_OFF
self._data = None
@@ -46,6 +46,11 @@ class TransmissionSwitch(ToggleEntity):
"""Return the name of the switch."""
return f"{self.client_name} {self._name}"
+ @property
+ def unique_id(self):
+ """Return the unique id of the entity."""
+ return f"{self._tm_client.api.host}-{self.name}"
+
@property
def state(self):
"""Return the state of the device."""
@@ -64,32 +69,34 @@ class TransmissionSwitch(ToggleEntity):
@property
def available(self):
"""Could the device be accessed during the last update call."""
- return self._transmission_api.available
+ return self._tm_client.api.available
def turn_on(self, **kwargs):
"""Turn the device on."""
if self.type == "on_off":
_LOGGING.debug("Starting all torrents")
- self._transmission_api.start_torrents()
+ self._tm_client.api.start_torrents()
elif self.type == "turtle_mode":
_LOGGING.debug("Turning Turtle Mode of Transmission on")
- self._transmission_api.set_alt_speed_enabled(True)
- self._transmission_api.update()
+ self._tm_client.api.set_alt_speed_enabled(True)
+ self._tm_client.api.update()
def turn_off(self, **kwargs):
"""Turn the device off."""
if self.type == "on_off":
_LOGGING.debug("Stoping all torrents")
- self._transmission_api.stop_torrents()
+ self._tm_client.api.stop_torrents()
if self.type == "turtle_mode":
_LOGGING.debug("Turning Turtle Mode of Transmission off")
- self._transmission_api.set_alt_speed_enabled(False)
- self._transmission_api.update()
+ self._tm_client.api.set_alt_speed_enabled(False)
+ self._tm_client.api.update()
async def async_added_to_hass(self):
"""Handle entity which will be added."""
async_dispatcher_connect(
- self.hass, DATA_UPDATED, self._schedule_immediate_update
+ self.hass,
+ self._tm_client.api.signal_options_update,
+ self._schedule_immediate_update,
)
@callback
@@ -100,12 +107,12 @@ class TransmissionSwitch(ToggleEntity):
"""Get the latest data from Transmission and updates the state."""
active = None
if self.type == "on_off":
- self._data = self._transmission_api.data
+ self._data = self._tm_client.api.data
if self._data:
active = self._data.activeTorrentCount > 0
elif self.type == "turtle_mode":
- active = self._transmission_api.get_alt_speed_enabled()
+ active = self._tm_client.api.get_alt_speed_enabled()
if active is None:
return
diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py
index 9d0610c139e..79df41ac489 100644
--- a/homeassistant/components/transport_nsw/sensor.py
+++ b/homeassistant/components/transport_nsw/sensor.py
@@ -3,6 +3,7 @@ from datetime import timedelta
import logging
import voluptuous as vol
+from TransportNSW import TransportNSW
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -120,8 +121,6 @@ class PublicTransportData:
def __init__(self, stop_id, route, destination, api_key):
"""Initialize the data object."""
- import TransportNSW
-
self._stop_id = stop_id
self._route = route
self._destination = destination
@@ -134,7 +133,7 @@ class PublicTransportData:
ATTR_DESTINATION: "n/a",
ATTR_MODE: None,
}
- self.tnsw = TransportNSW.TransportNSW()
+ self.tnsw = TransportNSW()
def update(self):
"""Get the next leave time."""
diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py
index 9154f891cc1..7c4a2dc4067 100644
--- a/homeassistant/components/trend/binary_sensor.py
+++ b/homeassistant/components/trend/binary_sensor.py
@@ -3,6 +3,7 @@ from collections import deque
import logging
import math
+import numpy as np
import voluptuous as vol
from homeassistant.components.binary_sensor import (
@@ -17,9 +18,9 @@ from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ENTITY_ID,
CONF_FRIENDLY_NAME,
- STATE_UNKNOWN,
- STATE_UNAVAILABLE,
CONF_SENSORS,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
@@ -207,8 +208,6 @@ class SensorTrend(BinarySensorDevice):
This need run inside executor.
"""
- import numpy as np
-
timestamps = np.array([t for t, _ in self.samples])
values = np.array([s for _, s in self.samples])
coeffs = np.polyfit(timestamps, values, 1)
diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json
index 1432d2d21a0..cf9333be7c3 100644
--- a/homeassistant/components/trend/manifest.json
+++ b/homeassistant/components/trend/manifest.json
@@ -3,7 +3,7 @@
"name": "Trend",
"documentation": "https://www.home-assistant.io/integrations/trend",
"requirements": [
- "numpy==1.17.1"
+ "numpy==1.17.3"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py
index 3e7900502d6..2ce0e18bee5 100644
--- a/homeassistant/components/tts/__init__.py
+++ b/homeassistant/components/tts/__init__.py
@@ -11,17 +11,18 @@ import re
from typing import Optional
from aiohttp import web
+import mutagen
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
+ DOMAIN as DOMAIN_MP,
MEDIA_TYPE_MUSIC,
SERVICE_PLAY_MEDIA,
)
-from homeassistant.components.media_player.const import DOMAIN as DOMAIN_MP
-from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, CONF_PLATFORM
+from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform
@@ -29,7 +30,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_prepare_setup_platform
-
# mypy: allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
@@ -433,7 +433,6 @@ class SpeechManager:
Async friendly.
"""
- import mutagen
data_bytes = io.BytesIO(data)
data_bytes.name = filename
diff --git a/homeassistant/components/twilio/.translations/ca.json b/homeassistant/components/twilio/.translations/ca.json
index 324ab0dd69a..bad78e51a36 100644
--- a/homeassistant/components/twilio/.translations/ca.json
+++ b/homeassistant/components/twilio/.translations/ca.json
@@ -5,7 +5,7 @@
"one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
},
"create_entry": {
- "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants."
+ "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants."
},
"step": {
"user": {
diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json
index b8d6f11f7ef..1c4e0653496 100644
--- a/homeassistant/components/twilio/.translations/ru.json
+++ b/homeassistant/components/twilio/.translations/ru.json
@@ -10,7 +10,7 @@
"step": {
"user": {
"description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Twilio?",
- "title": "Twilio Webhook"
+ "title": "Twilio"
}
},
"title": "Twilio"
diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py
index ea5629e7cab..15c6697b2f7 100644
--- a/homeassistant/components/twilio/__init__.py
+++ b/homeassistant/components/twilio/__init__.py
@@ -1,9 +1,12 @@
"""Support for Twilio."""
+from twilio.rest import Client
+from twilio.twiml import TwiML
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.helpers import config_entry_flow
+import homeassistant.helpers.config_validation as cv
+
from .const import DOMAIN
CONF_ACCOUNT_SID = "account_sid"
@@ -28,8 +31,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Set up the Twilio component."""
- from twilio.rest import Client
-
if DOMAIN not in config:
return True
@@ -42,8 +43,6 @@ async def async_setup(hass, config):
async def handle_webhook(hass, webhook_id, request):
"""Handle incoming webhook from Twilio for inbound messages and calls."""
- from twilio.twiml import TwiML
-
data = dict(await request.post())
data["webhook_id"] = webhook_id
hass.bus.async_fire(RECEIVED_DATA, dict(data))
diff --git a/homeassistant/components/twilio/config_flow.py b/homeassistant/components/twilio/config_flow.py
index dad8e0bf496..1539c1ffadc 100644
--- a/homeassistant/components/twilio/config_flow.py
+++ b/homeassistant/components/twilio/config_flow.py
@@ -3,7 +3,6 @@ from homeassistant.helpers import config_entry_flow
from .const import DOMAIN
-
config_entry_flow.register_webhook_flow(
DOMAIN,
"Twilio Webhook",
diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json
index 23fac51a347..8f4ed125fb6 100644
--- a/homeassistant/components/twilio/manifest.json
+++ b/homeassistant/components/twilio/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/twilio",
"requirements": [
- "twilio==6.19.1"
+ "twilio==6.32.0"
],
"dependencies": [
"webhook"
diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py
index 0672a3d3b9e..82705091814 100644
--- a/homeassistant/components/twilio_call/notify.py
+++ b/homeassistant/components/twilio_call/notify.py
@@ -4,14 +4,13 @@ import urllib
import voluptuous as vol
-from homeassistant.components.twilio import DATA_TWILIO
-import homeassistant.helpers.config_validation as cv
-
from homeassistant.components.notify import (
ATTR_TARGET,
PLATFORM_SCHEMA,
BaseNotificationService,
)
+from homeassistant.components.twilio import DATA_TWILIO
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/twilio_sms/notify.py b/homeassistant/components/twilio_sms/notify.py
index bd873f13468..da5e0e754b9 100644
--- a/homeassistant/components/twilio_sms/notify.py
+++ b/homeassistant/components/twilio_sms/notify.py
@@ -3,15 +3,14 @@ import logging
import voluptuous as vol
-from homeassistant.components.twilio import DATA_TWILIO
-import homeassistant.helpers.config_validation as cv
-
from homeassistant.components.notify import (
+ ATTR_DATA,
ATTR_TARGET,
PLATFORM_SCHEMA,
BaseNotificationService,
- ATTR_DATA,
)
+from homeassistant.components.twilio import DATA_TWILIO
+import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
r"^\+?[1-9]\d{1,14}$|"
r"^(?=.{1,11}$)[a-zA-Z0-9\s]*"
r"[a-zA-Z][a-zA-Z0-9\s]*$"
+ r"^(?:[a-zA-Z]+)\:?\+?[1-9]\d{1,14}$|"
),
)
}
diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json
index c40b7822073..0a100be0a11 100644
--- a/homeassistant/components/unifi/.translations/fr.json
+++ b/homeassistant/components/unifi/.translations/fr.json
@@ -33,6 +33,12 @@
"track_wired_clients": "Inclure les clients du r\u00e9seau filaire"
}
},
+ "init": {
+ "data": {
+ "one": "Vide",
+ "other": "Vide"
+ }
+ },
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau"
diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json
index 1fff9887906..295430b7284 100644
--- a/homeassistant/components/unifi/.translations/ko.json
+++ b/homeassistant/components/unifi/.translations/ko.json
@@ -32,6 +32,11 @@
"track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)",
"track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568"
}
+ },
+ "statistics_sensors": {
+ "data": {
+ "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc704\ud55c \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c \uc0dd\uc131\ud558\uae30"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json
index 518f0066534..36e21728f1d 100644
--- a/homeassistant/components/unifi/.translations/nl.json
+++ b/homeassistant/components/unifi/.translations/nl.json
@@ -38,6 +38,11 @@
"one": "Leeg",
"other": "Leeg"
}
+ },
+ "statistics_sensors": {
+ "data": {
+ "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json
index 113eaa000fe..a57bb33ee7a 100644
--- a/homeassistant/components/unifi/.translations/pt-BR.json
+++ b/homeassistant/components/unifi/.translations/pt-BR.json
@@ -32,6 +32,12 @@
"track_devices": "Rastrear dispositivos de rede (dispositivos Ubiquiti)",
"track_wired_clients": "Incluir clientes de rede com fio"
}
+ },
+ "init": {
+ "data": {
+ "one": "um",
+ "other": "uns"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/.translations/pt.json b/homeassistant/components/unifi/.translations/pt.json
index 6730a3d258e..c602a58660b 100644
--- a/homeassistant/components/unifi/.translations/pt.json
+++ b/homeassistant/components/unifi/.translations/pt.json
@@ -22,5 +22,28 @@
}
},
"title": "Controlador UniFi"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "detection_time": "Tempo em segundos desde a \u00faltima vez que foi visto at\u00e9 ser considerado afastado",
+ "track_clients": "Acompanhar clientes da rede",
+ "track_devices": "Acompanhar dispositivos de rede (dispositivos Ubiquiti)",
+ "track_wired_clients": "Incluir clientes da rede cablada"
+ }
+ },
+ "init": {
+ "data": {
+ "one": "Vazio",
+ "other": "Vazios"
+ }
+ },
+ "statistics_sensors": {
+ "data": {
+ "allow_bandwidth_sensors": "Criar sensores de uso de largura de banda para clientes da rede"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json
index d7451bd81a0..dbb6efd8343 100644
--- a/homeassistant/components/unifi/.translations/ru.json
+++ b/homeassistant/components/unifi/.translations/ru.json
@@ -2,11 +2,11 @@
"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.",
- "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c"
+ "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c."
},
"error": {
- "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435",
- "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430"
+ "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
+ "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430."
},
"step": {
"user": {
diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py
index 5b43289e403..4f3edf9ce79 100644
--- a/homeassistant/components/unifi/__init__.py
+++ b/homeassistant/components/unifi/__init__.py
@@ -77,12 +77,13 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN] = {}
controller = UniFiController(hass, config_entry)
- controller_id = get_controller_id_from_config_entry(config_entry)
- hass.data[DOMAIN][controller_id] = controller
if not await controller.async_setup():
return False
+ controller_id = get_controller_id_from_config_entry(config_entry)
+ hass.data[DOMAIN][controller_id] = controller
+
if controller.mac is None:
return True
diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py
index fdb75d09194..01b97a78366 100644
--- a/homeassistant/components/unifi/config_flow.py
+++ b/homeassistant/components/unifi/config_flow.py
@@ -12,6 +12,7 @@ from homeassistant.const import (
)
from .const import (
+ CONF_ALLOW_BANDWIDTH_SENSORS,
CONF_CONTROLLER,
CONF_DETECTION_TIME,
CONF_SITE_ID,
@@ -19,6 +20,7 @@ from .const import (
CONF_TRACK_DEVICES,
CONF_TRACK_WIRED_CLIENTS,
CONTROLLER_ID,
+ DEFAULT_ALLOW_BANDWIDTH_SENSORS,
DEFAULT_TRACK_CLIENTS,
DEFAULT_TRACK_DEVICES,
DEFAULT_TRACK_WIRED_CLIENTS,
@@ -148,12 +150,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.desc = next(iter(self.sites.values()))["desc"]
return await self.async_step_site(user_input={})
- if self.desc is not None:
- for site in self.sites.values():
- if self.desc == site["name"]:
- self.desc = site["desc"]
- return await self.async_step_site(user_input={})
-
sites = []
for site in self.sites.values():
sites.append(site["desc"])
@@ -171,6 +167,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry):
"""Initialize UniFi options flow."""
self.config_entry = config_entry
+ self.options = dict(config_entry.options)
async def async_step_init(self, user_input=None):
"""Manage the UniFi options."""
@@ -179,7 +176,8 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_device_tracker(self, user_input=None):
"""Manage the device tracker options."""
if user_input is not None:
- return self.async_create_entry(title="", data=user_input)
+ self.options.update(user_input)
+ return await self.async_step_statistics_sensors()
return self.async_show_form(
step_id="device_tracker",
@@ -212,3 +210,28 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
}
),
)
+
+ async def async_step_statistics_sensors(self, user_input=None):
+ """Manage the statistics sensors options."""
+ if user_input is not None:
+ self.options.update(user_input)
+ return await self._update_options()
+
+ return self.async_show_form(
+ step_id="statistics_sensors",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_ALLOW_BANDWIDTH_SENSORS,
+ default=self.config_entry.options.get(
+ CONF_ALLOW_BANDWIDTH_SENSORS,
+ DEFAULT_ALLOW_BANDWIDTH_SENSORS,
+ ),
+ ): bool
+ }
+ ),
+ )
+
+ async def _update_options(self):
+ """Update config entry options."""
+ return self.async_create_entry(title="", data=self.options)
diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py
index eac14735074..d82b7b49d45 100644
--- a/homeassistant/components/unifi/const.py
+++ b/homeassistant/components/unifi/const.py
@@ -12,6 +12,7 @@ CONF_SITE_ID = "site"
UNIFI_CONFIG = "unifi_config"
UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients"
+CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors"
CONF_BLOCK_CLIENT = "block_client"
CONF_DETECTION_TIME = "detection_time"
CONF_TRACK_CLIENTS = "track_clients"
@@ -23,6 +24,7 @@ CONF_DONT_TRACK_CLIENTS = "dont_track_clients"
CONF_DONT_TRACK_DEVICES = "dont_track_devices"
CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients"
+DEFAULT_ALLOW_BANDWIDTH_SENSORS = False
DEFAULT_BLOCK_CLIENTS = []
DEFAULT_TRACK_CLIENTS = True
DEFAULT_TRACK_DEVICES = True
diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
index ffea98b9050..3deb2e9040a 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -15,6 +15,7 @@ from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
+ CONF_ALLOW_BANDWIDTH_SENSORS,
CONF_BLOCK_CLIENT,
CONF_CONTROLLER,
CONF_DETECTION_TIME,
@@ -27,6 +28,7 @@ from .const import (
CONF_SITE_ID,
CONF_SSID_FILTER,
CONTROLLER_ID,
+ DEFAULT_ALLOW_BANDWIDTH_SENSORS,
DEFAULT_BLOCK_CLIENTS,
DEFAULT_TRACK_CLIENTS,
DEFAULT_TRACK_DEVICES,
@@ -40,6 +42,8 @@ from .const import (
)
from .errors import AuthenticationRequired, CannotConnect
+SUPPORTED_PLATFORMS = ["device_tracker", "sensor", "switch"]
+
class UniFiController:
"""Manages a single UniFi Controller."""
@@ -53,6 +57,7 @@ class UniFiController:
self.progress = None
self.wireless_clients = None
+ self.listeners = []
self._site_name = None
self._site_role = None
@@ -76,6 +81,13 @@ class UniFiController:
"""Return the site user role of this controller."""
return self._site_role
+ @property
+ def option_allow_bandwidth_sensors(self):
+ """Config entry option to allow bandwidth sensors."""
+ return self.config_entry.options.get(
+ CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS
+ )
+
@property
def option_block_clients(self):
"""Config entry option with list of clients to control network access."""
@@ -225,7 +237,7 @@ class UniFiController:
self.config_entry.add_update_listener(self.async_options_updated)
- for platform in ["device_tracker", "switch"]:
+ for platform in SUPPORTED_PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
self.config_entry, platform
@@ -247,13 +259,14 @@ class UniFiController:
def import_configuration(self):
"""Import configuration to config entry options."""
- unifi_config = {}
+ import_config = {}
+
for config in self.hass.data[UNIFI_CONFIG]:
if (
self.host == config[CONF_HOST]
and self.site_name == config[CONF_SITE_ID]
):
- unifi_config = config
+ import_config = config
break
old_options = dict(self.config_entry.options)
@@ -267,16 +280,17 @@ class UniFiController:
(CONF_DETECTION_TIME, CONF_DETECTION_TIME),
(CONF_SSID_FILTER, CONF_SSID_FILTER),
):
- if config in unifi_config:
- if config == option and unifi_config[
+ if config in import_config:
+ print(config)
+ if config == option and import_config[
config
] != self.config_entry.options.get(option):
- new_options[option] = unifi_config[config]
+ new_options[option] = import_config[config]
elif config != option and (
option not in self.config_entry.options
- or unifi_config[config] == self.config_entry.options.get(option)
+ or import_config[config] == self.config_entry.options.get(option)
):
- new_options[option] = not unifi_config[config]
+ new_options[option] = not import_config[config]
if new_options:
options = {**old_options, **new_options}
@@ -290,15 +304,15 @@ class UniFiController:
Will cancel any scheduled setup retry and will unload
the config entry.
"""
- # If the authentication was wrong.
- if self.api is None:
- return True
-
- for platform in ["device_tracker", "switch"]:
+ for platform in SUPPORTED_PLATFORMS:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, platform
)
+ for unsub_dispatcher in self.listeners:
+ unsub_dispatcher()
+ self.listeners = []
+
return True
diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py
index 48b19d7bada..b92211c4eae 100644
--- a/homeassistant/components/unifi/device_tracker.py
+++ b/homeassistant/components/unifi/device_tracker.py
@@ -67,7 +67,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Update the values of the controller."""
update_items(controller, async_add_entities, tracked)
- async_dispatcher_connect(hass, controller.signal_update, update_controller)
+ controller.listeners.append(
+ async_dispatcher_connect(hass, controller.signal_update, update_controller)
+ )
@callback
def update_disable_on_entities():
@@ -82,8 +84,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entity.registry_entry.entity_id, disabled_by=disabled_by
)
- async_dispatcher_connect(
- hass, controller.signal_options_update, update_disable_on_entities
+ controller.listeners.append(
+ async_dispatcher_connect(
+ hass, controller.signal_options_update, update_disable_on_entities
+ )
)
update_controller()
diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py
new file mode 100644
index 00000000000..e4f9b0df6c9
--- /dev/null
+++ b/homeassistant/components/unifi/sensor.py
@@ -0,0 +1,172 @@
+"""Support for bandwidth sensors with UniFi clients."""
+import logging
+
+from homeassistant.components.unifi.config_flow import get_controller_from_config_entry
+from homeassistant.core import callback
+from homeassistant.helpers import entity_registry
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY
+
+LOGGER = logging.getLogger(__name__)
+
+ATTR_RECEIVING = "receiving"
+ATTR_TRANSMITTING = "transmitting"
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Sensor platform doesn't support configuration through configuration.yaml."""
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up sensors for UniFi integration."""
+ controller = get_controller_from_config_entry(hass, config_entry)
+ sensors = {}
+
+ registry = await entity_registry.async_get_registry(hass)
+
+ @callback
+ def update_controller():
+ """Update the values of the controller."""
+ update_items(controller, async_add_entities, sensors)
+
+ controller.listeners.append(
+ async_dispatcher_connect(hass, controller.signal_update, update_controller)
+ )
+
+ @callback
+ def update_disable_on_entities():
+ """Update the values of the controller."""
+ for entity in sensors.values():
+
+ disabled_by = None
+ if not entity.entity_registry_enabled_default and entity.enabled:
+ disabled_by = DISABLED_CONFIG_ENTRY
+
+ registry.async_update_entity(
+ entity.registry_entry.entity_id, disabled_by=disabled_by
+ )
+
+ controller.listeners.append(
+ async_dispatcher_connect(
+ hass, controller.signal_options_update, update_disable_on_entities
+ )
+ )
+
+ update_controller()
+
+
+@callback
+def update_items(controller, async_add_entities, sensors):
+ """Update sensors from the controller."""
+ new_sensors = []
+
+ for client_id in controller.api.clients:
+ for direction, sensor_class in (
+ ("rx", UniFiRxBandwidthSensor),
+ ("tx", UniFiTxBandwidthSensor),
+ ):
+ item_id = f"{direction}-{client_id}"
+
+ if item_id in sensors:
+ sensor = sensors[item_id]
+ if sensor.enabled:
+ sensor.async_schedule_update_ha_state()
+ continue
+
+ sensors[item_id] = sensor_class(
+ controller.api.clients[client_id], controller
+ )
+ new_sensors.append(sensors[item_id])
+
+ if new_sensors:
+ async_add_entities(new_sensors)
+
+
+class UniFiBandwidthSensor(Entity):
+ """UniFi Bandwidth sensor base class."""
+
+ def __init__(self, client, controller):
+ """Set up client."""
+ self.client = client
+ self.controller = controller
+ self.is_wired = self.client.mac not in controller.wireless_clients
+
+ @property
+ def entity_registry_enabled_default(self):
+ """Return if the entity should be enabled when first added to the entity registry."""
+ if self.controller.option_allow_bandwidth_sensors:
+ return True
+ return False
+
+ async def async_added_to_hass(self):
+ """Client entity created."""
+ LOGGER.debug("New UniFi bandwidth sensor %s (%s)", self.name, self.client.mac)
+
+ async def async_update(self):
+ """Synchronize state with controller.
+
+ Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired.
+ """
+ LOGGER.debug(
+ "Updating UniFi bandwidth sensor %s (%s)", self.entity_id, self.client.mac
+ )
+ await self.controller.request_update()
+
+ if self.is_wired and self.client.mac in self.controller.wireless_clients:
+ self.is_wired = False
+
+ @property
+ def available(self) -> bool:
+ """Return if controller is available."""
+ return self.controller.available
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}}
+
+
+class UniFiRxBandwidthSensor(UniFiBandwidthSensor):
+ """Receiving bandwidth sensor."""
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ if self.is_wired:
+ return self.client.wired_rx_bytes / 1000000
+ return self.client.raw.get("rx_bytes", 0) / 1000000
+
+ @property
+ def name(self):
+ """Return the name of the client."""
+ name = self.client.name or self.client.hostname
+ return f"{name} RX"
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this bandwidth sensor."""
+ return f"rx-{self.client.mac}"
+
+
+class UniFiTxBandwidthSensor(UniFiBandwidthSensor):
+ """Transmitting bandwidth sensor."""
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ if self.is_wired:
+ return self.client.wired_tx_bytes / 1000000
+ return self.client.raw.get("tx_bytes", 0) / 1000000
+
+ @property
+ def name(self):
+ """Return the name of the client."""
+ name = self.client.name or self.client.hostname
+ return f"{name} TX"
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this bandwidth sensor."""
+ return f"tx-{self.client.mac}"
diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json
index c484bfbf09f..ce2f2345917 100644
--- a/homeassistant/components/unifi/strings.json
+++ b/homeassistant/components/unifi/strings.json
@@ -35,6 +35,11 @@
"track_devices": "Track network devices (Ubiquiti devices)",
"track_wired_clients": "Include wired network clients"
}
+ },
+ "statistics_sensors": {
+ "data": {
+ "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index f0183a7ecb3..82aa6f0384d 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -14,7 +14,6 @@ LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Component doesn't support configuration through configuration.yaml."""
- pass
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -54,7 +53,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Update the values of the controller."""
update_items(controller, async_add_entities, switches, switches_off)
- async_dispatcher_connect(hass, controller.signal_update, update_controller)
+ controller.listeners.append(
+ async_dispatcher_connect(hass, controller.signal_update, update_controller)
+ )
update_controller()
switches_off.clear()
@@ -231,8 +232,6 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity):
"""Return the device state attributes."""
attributes = {
"power": self.port.poe_power,
- "received": self.client.wired_rx_bytes / 1000000,
- "sent": self.client.wired_tx_bytes / 1000000,
"switch": self.client.sw_mac,
"port": self.client.sw_port,
"poe_mode": self.poe_mode,
diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py
index 3656ba48e74..c77b0fe3cdd 100644
--- a/homeassistant/components/upcloud/__init__.py
+++ b/homeassistant/components/upcloud/__init__.py
@@ -1,15 +1,16 @@
"""Support for UpCloud."""
-import logging
from datetime import timedelta
+import logging
+import upcloud_api
import voluptuous as vol
from homeassistant.const import (
- CONF_USERNAME,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
- STATE_ON,
+ CONF_USERNAME,
STATE_OFF,
+ STATE_ON,
STATE_PROBLEM,
)
from homeassistant.core import callback
@@ -60,8 +61,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the UpCloud component."""
- import upcloud_api
-
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py
index dd270a0bb75..22c11d0c38e 100644
--- a/homeassistant/components/updater/__init__.py
+++ b/homeassistant/components/updater/__init__.py
@@ -10,6 +10,7 @@ import uuid
import aiohttp
import async_timeout
+from distro import linux_distribution
import voluptuous as vol
from homeassistant.const import __version__ as current_version
@@ -145,9 +146,7 @@ async def get_newest_version(hass, huuid, include_components):
if include_components:
info_object["components"] = list(hass.config.components)
- import distro
-
- linux_dist = await hass.async_add_executor_job(distro.linux_distribution, False)
+ linux_dist = await hass.async_add_executor_job(linux_distribution, False)
info_object["distribution"] = linux_dist[0]
info_object["os_version"] = linux_dist[1]
diff --git a/homeassistant/components/upnp/.translations/fr.json b/homeassistant/components/upnp/.translations/fr.json
index a87ea9ec9c7..6864658b379 100644
--- a/homeassistant/components/upnp/.translations/fr.json
+++ b/homeassistant/components/upnp/.translations/fr.json
@@ -8,6 +8,10 @@
"no_sensors_or_port_mapping": "Activer au moins les capteurs ou la cartographie des ports",
"single_instance_allowed": "Une seule configuration UPnP / IGD est n\u00e9cessaire."
},
+ "error": {
+ "one": "Vide",
+ "other": "Vide"
+ },
"step": {
"confirm": {
"description": "Voulez-vous configurer UPnP / IGD?",
diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json
index cfbedd994af..8e173e4297f 100644
--- a/homeassistant/components/upnp/.translations/nn.json
+++ b/homeassistant/components/upnp/.translations/nn.json
@@ -8,8 +8,16 @@
"other": "Andre"
},
"step": {
+ "confirm": {
+ "title": "UPnP/IGD"
+ },
"init": {
"title": "UPnP / IGD"
+ },
+ "user": {
+ "data": {
+ "igd": "UPnP/IGD"
+ }
}
},
"title": "UPnP / IGD"
diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json
index 3351f0d5d8a..9599832799f 100644
--- a/homeassistant/components/upnp/.translations/ru.json
+++ b/homeassistant/components/upnp/.translations/ru.json
@@ -2,10 +2,10 @@
"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.",
- "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP",
- "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD",
+ "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP.",
+ "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD.",
"no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
- "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432",
+ "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432.",
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"step": {
diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py
index 5a5c7b38e7e..7f7f0f5b93a 100644
--- a/homeassistant/components/upnp/device.py
+++ b/homeassistant/components/upnp/device.py
@@ -3,6 +3,7 @@ import asyncio
from ipaddress import IPv4Address
import aiohttp
+from async_upnp_client.profiles.igd import IgdDevice
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType
@@ -29,9 +30,6 @@ class Device:
if local_ip:
local_ip = IPv4Address(local_ip)
- # discover devices
- from async_upnp_client.profiles.igd import IgdDevice
-
discovery_infos = await IgdDevice.async_search(source_ip=local_ip, timeout=10)
# add extra info and store devices
@@ -61,9 +59,6 @@ class Device:
factory = UpnpFactory(requester, disable_state_variable_validation=True)
upnp_device = await factory.async_create_device(ssdp_description)
- # wrap with async_upnp_client.IgdDevice
- from async_upnp_client.profiles.igd import IgdDevice
-
igd_device = IgdDevice(upnp_device, None)
return cls(igd_device)
diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py
index b721fa29cdd..40cb7ef2032 100644
--- a/homeassistant/components/upnp/sensor.py
+++ b/homeassistant/components/upnp/sensor.py
@@ -164,7 +164,6 @@ class PerSecondUPnPIGDSensor(UpnpSensor):
"""Get unit we are measuring in."""
raise NotImplementedError()
- @property
def _async_fetch_value(self):
"""Fetch a value from the IGD."""
raise NotImplementedError()
diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py
index bda6ad9041b..3f5175ad09d 100644
--- a/homeassistant/components/uscis/sensor.py
+++ b/homeassistant/components/uscis/sensor.py
@@ -1,7 +1,8 @@
"""Support for USCIS Case Status."""
-
import logging
from datetime import timedelta
+
+import uscisstatus
import voluptuous as vol
from homeassistant.helpers.entity import Entity
@@ -67,8 +68,6 @@ class UscisSensor(Entity):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Fetch data from the USCIS website and update state attributes."""
- import uscisstatus
-
try:
status = uscisstatus.get_case_status(self._case_id)
self._attributes = {self.CURRENT_STATUS: status["status"]}
diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py
index 9bc376916c6..55e56415b0d 100644
--- a/homeassistant/components/vacuum/__init__.py
+++ b/homeassistant/components/vacuum/__init__.py
@@ -6,7 +6,7 @@ import logging
import voluptuous as vol
from homeassistant.components import group
-from homeassistant.const import (
+from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API
ATTR_BATTERY_LEVEL,
ATTR_COMMAND,
SERVICE_TOGGLE,
@@ -68,8 +68,6 @@ VACUUM_SEND_COMMAND_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend(
STATE_CLEANING = "cleaning"
STATE_DOCKED = "docked"
-STATE_IDLE = STATE_IDLE
-STATE_PAUSED = STATE_PAUSED
STATE_RETURNING = "returning"
STATE_ERROR = "error"
diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py
new file mode 100644
index 00000000000..485ffef0c9f
--- /dev/null
+++ b/homeassistant/components/vacuum/reproduce_state.py
@@ -0,0 +1,101 @@
+"""Reproduce an Vacuum state."""
+import asyncio
+import logging
+from typing import Iterable, Optional
+
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_IDLE,
+ STATE_OFF,
+ STATE_ON,
+ STATE_PAUSED,
+)
+from homeassistant.core import Context, State
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ ATTR_FAN_SPEED,
+ DOMAIN,
+ SERVICE_PAUSE,
+ SERVICE_RETURN_TO_BASE,
+ SERVICE_SET_FAN_SPEED,
+ SERVICE_START,
+ SERVICE_STOP,
+ STATE_CLEANING,
+ STATE_DOCKED,
+ STATE_RETURNING,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+VALID_STATES_TOGGLE = {STATE_ON, STATE_OFF}
+VALID_STATES_STATE = {
+ STATE_CLEANING,
+ STATE_DOCKED,
+ STATE_IDLE,
+ STATE_RETURNING,
+ STATE_PAUSED,
+}
+
+
+async def _async_reproduce_state(
+ hass: HomeAssistantType, state: State, context: Optional[Context] = None
+) -> None:
+ """Reproduce a single state."""
+ cur_state = hass.states.get(state.entity_id)
+
+ if cur_state is None:
+ _LOGGER.warning("Unable to find entity %s", state.entity_id)
+ return
+
+ if state.state not in VALID_STATES_TOGGLE and state.state not in VALID_STATES_STATE:
+ _LOGGER.warning(
+ "Invalid state specified for %s: %s", state.entity_id, state.state
+ )
+ return
+
+ # Return if we are already at the right state.
+ if cur_state.state == state.state and cur_state.attributes.get(
+ ATTR_FAN_SPEED
+ ) == state.attributes.get(ATTR_FAN_SPEED):
+ return
+
+ service_data = {ATTR_ENTITY_ID: state.entity_id}
+
+ if cur_state.state != state.state:
+ # Wrong state
+ if state.state == STATE_ON:
+ service = SERVICE_TURN_ON
+ elif state.state == STATE_OFF:
+ service = SERVICE_TURN_OFF
+ elif state.state == STATE_CLEANING:
+ service = SERVICE_START
+ elif state.state == STATE_DOCKED or state.state == STATE_RETURNING:
+ service = SERVICE_RETURN_TO_BASE
+ elif state.state == STATE_IDLE:
+ service = SERVICE_STOP
+ elif state.state == STATE_PAUSED:
+ service = SERVICE_PAUSE
+
+ await hass.services.async_call(
+ DOMAIN, service, service_data, context=context, blocking=True
+ )
+
+ if cur_state.attributes.get(ATTR_FAN_SPEED) != state.attributes.get(ATTR_FAN_SPEED):
+ # Wrong fan speed
+ service_data["fan_speed"] = state.attributes[ATTR_FAN_SPEED]
+ await hass.services.async_call(
+ DOMAIN, SERVICE_SET_FAN_SPEED, service_data, context=context, blocking=True
+ )
+
+
+async def async_reproduce_states(
+ hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
+) -> None:
+ """Reproduce Vacuum states."""
+ # Reproduce states in parallel.
+ await asyncio.gather(
+ *(_async_reproduce_state(hass, state, context) for state in states)
+ )
diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py
index c107e4f8894..eb5edfe7fcf 100644
--- a/homeassistant/components/vallox/__init__.py
+++ b/homeassistant/components/vallox/__init__.py
@@ -252,7 +252,7 @@ class ValloxServiceHandler:
async def async_handle(self, service):
"""Dispatch a service call."""
method = SERVICE_TO_METHOD.get(service.service)
- params = {key: value for key, value in service.data.items()}
+ params = service.data.copy()
if not hasattr(self, method["method"]):
_LOGGER.error("Service not implemented: %s", method["method"])
diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py
index 0da730165fe..d13383a0832 100644
--- a/homeassistant/components/vasttrafik/sensor.py
+++ b/homeassistant/components/vasttrafik/sensor.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
+import vasttrafik
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -54,7 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the departure sensor."""
- import vasttrafik
planner = vasttrafik.JournyPlanner(config.get(CONF_KEY), config.get(CONF_SECRET))
sensors = []
@@ -62,7 +62,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for departure in config.get(CONF_DEPARTURES):
sensors.append(
VasttrafikDepartureSensor(
- vasttrafik,
planner,
departure.get(CONF_NAME),
departure.get(CONF_FROM),
@@ -77,9 +76,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class VasttrafikDepartureSensor(Entity):
"""Implementation of a Vasttrafik Departure Sensor."""
- def __init__(self, vasttrafik, planner, name, departure, heading, lines, delay):
+ def __init__(self, planner, name, departure, heading, lines, delay):
"""Initialize the sensor."""
- self._vasttrafik = vasttrafik
self._planner = planner
self._name = name or departure
self._departure = planner.location_name(departure)[0]
@@ -119,7 +117,7 @@ class VasttrafikDepartureSensor(Entity):
direction=self._heading["id"] if self._heading else None,
date=now() + self._delay,
)
- except self._vasttrafik.Error:
+ except vasttrafik.Error:
_LOGGER.debug("Unable to read departure board, updating token")
self._planner.update_token()
diff --git a/homeassistant/components/velbus/.translations/ru.json b/homeassistant/components/velbus/.translations/ru.json
index 3434c584221..10ae06ffa7c 100644
--- a/homeassistant/components/velbus/.translations/ru.json
+++ b/homeassistant/components/velbus/.translations/ru.json
@@ -10,7 +10,7 @@
"step": {
"user": {
"data": {
- "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Velbus",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"port": "\u0421\u0442\u0440\u043e\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
},
"title": "Velbus"
diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py
index 7be31d56c08..81afef97541 100644
--- a/homeassistant/components/venstar/climate.py
+++ b/homeassistant/components/venstar/climate.py
@@ -1,6 +1,7 @@
"""Support for Venstar WiFi Thermostats."""
import logging
+from venstarcolortouch import VenstarColorTouch
import voluptuous as vol
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
@@ -71,7 +72,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Venstar thermostat."""
- import venstarcolortouch
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
@@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
else:
proto = "http"
- client = venstarcolortouch.VenstarColorTouch(
+ client = VenstarColorTouch(
addr=host, timeout=timeout, user=username, password=password, proto=proto
)
diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py
index 5d9dd80061c..8fcc8a4a2fe 100644
--- a/homeassistant/components/vera/__init__.py
+++ b/homeassistant/components/vera/__init__.py
@@ -2,6 +2,7 @@
import logging
from collections import defaultdict
+import pyvera as veraApi
import voluptuous as vol
from requests.exceptions import RequestException
@@ -65,7 +66,6 @@ VERA_COMPONENTS = [
def setup(hass, base_config):
"""Set up for Vera devices."""
- import pyvera as veraApi
def stop_subscription(event):
"""Shutdown Vera subscriptions and subscription thread on exit."""
@@ -118,7 +118,6 @@ def setup(hass, base_config):
def map_vera_device(vera_device, remap):
"""Map vera classes to Home Assistant types."""
- import pyvera as veraApi
if isinstance(vera_device, veraApi.VeraDimmer):
return "light"
diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py
index c33187cd904..e409a123887 100644
--- a/homeassistant/components/vera/sensor.py
+++ b/homeassistant/components/vera/sensor.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+import pyvera as veraApi
+
from homeassistant.components.sensor import ENTITY_ID_FORMAT
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.helpers.entity import Entity
@@ -44,7 +46,6 @@ class VeraSensor(VeraDevice, Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
- import pyvera as veraApi
if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR:
return self._temperature_units
@@ -59,7 +60,6 @@ class VeraSensor(VeraDevice, Entity):
def update(self):
"""Update the state."""
- import pyvera as veraApi
if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR:
self.current_value = self.vera_device.temperature
diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py
index df95ed07ca5..f4313c7c1ac 100644
--- a/homeassistant/components/verisure/__init__.py
+++ b/homeassistant/components/verisure/__init__.py
@@ -3,6 +3,8 @@ import logging
import threading
from datetime import timedelta
+from jsonpath import jsonpath
+import verisure
import voluptuous as vol
from homeassistant.const import (
@@ -71,10 +73,8 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string})
def setup(hass, config):
"""Set up the Verisure component."""
- import verisure
-
global HUB
- HUB = VerisureHub(config[DOMAIN], verisure)
+ HUB = VerisureHub(config[DOMAIN])
HUB.update_overview = Throttle(config[DOMAIN][CONF_SCAN_INTERVAL])(
HUB.update_overview
)
@@ -109,13 +109,12 @@ def setup(hass, config):
class VerisureHub:
"""A Verisure hub wrapper class."""
- def __init__(self, domain_config, verisure):
+ def __init__(self, domain_config):
"""Initialize the Verisure hub."""
self.overview = {}
self.imageseries = {}
self.config = domain_config
- self._verisure = verisure
self._lock = threading.Lock()
@@ -125,15 +124,11 @@ class VerisureHub:
self.giid = domain_config.get(CONF_GIID)
- import jsonpath
-
- self.jsonpath = jsonpath.jsonpath
-
def login(self):
"""Login to Verisure."""
try:
self.session.login()
- except self._verisure.Error as ex:
+ except verisure.Error as ex:
_LOGGER.error("Could not log in to verisure, %s", ex)
return False
if self.giid:
@@ -144,7 +139,7 @@ class VerisureHub:
"""Logout from Verisure."""
try:
self.session.logout()
- except self._verisure.Error as ex:
+ except verisure.Error as ex:
_LOGGER.error("Could not log out from verisure, %s", ex)
return False
return True
@@ -153,7 +148,7 @@ class VerisureHub:
"""Set installation GIID."""
try:
self.session.set_giid(self.giid)
- except self._verisure.Error as ex:
+ except verisure.Error as ex:
_LOGGER.error("Could not set installation GIID, %s", ex)
return False
return True
@@ -162,7 +157,7 @@ class VerisureHub:
"""Update the overview."""
try:
self.overview = self.session.get_overview()
- except self._verisure.ResponseError as ex:
+ except verisure.ResponseError as ex:
_LOGGER.error("Could not read overview, %s", ex)
if ex.status_code == 503: # Service unavailable
_LOGGER.info("Trying to log in again")
@@ -182,7 +177,7 @@ class VerisureHub:
def get(self, jpath, *args):
"""Get values from the overview that matches the jsonpath."""
- res = self.jsonpath(self.overview, jpath % args)
+ res = jsonpath(self.overview, jpath % args)
return res if res else []
def get_first(self, jpath, *args):
@@ -192,5 +187,5 @@ class VerisureHub:
def get_image_info(self, jpath, *args):
"""Get values from the imageseries that matches the jsonpath."""
- res = self.jsonpath(self.imageseries, jpath % args)
+ res = jsonpath(self.imageseries, jpath % args)
return res if res else []
diff --git a/homeassistant/components/vesync/.translations/ru.json b/homeassistant/components/vesync/.translations/ru.json
index 38b86e9e29f..23cb6fdfac7 100644
--- a/homeassistant/components/vesync/.translations/ru.json
+++ b/homeassistant/components/vesync/.translations/ru.json
@@ -4,7 +4,7 @@
"already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"error": {
- "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c"
+ "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c."
},
"step": {
"user": {
diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py
index 7010f943707..0dcb83f758a 100644
--- a/homeassistant/components/vicare/climate.py
+++ b/homeassistant/components/vicare/climate.py
@@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
VICARE_MODE_DHW = "dhw"
VICARE_MODE_DHWANDHEATING = "dhwAndHeating"
+VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling"
VICARE_MODE_FORCEDREDUCED = "forcedReduced"
VICARE_MODE_FORCEDNORMAL = "forcedNormal"
VICARE_MODE_OFF = "standby"
@@ -46,6 +47,7 @@ SUPPORT_FLAGS_HEATING = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
VICARE_TO_HA_HVAC_HEATING = {
VICARE_MODE_DHW: HVAC_MODE_OFF,
VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO,
+ VICARE_MODE_DHWANDHEATINGCOOLING: HVAC_MODE_AUTO,
VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF,
VICARE_MODE_FORCEDNORMAL: HVAC_MODE_HEAT,
VICARE_MODE_OFF: HVAC_MODE_OFF,
@@ -200,9 +202,8 @@ class ViCareClimate(ClimateDevice):
"""Set new target temperatures."""
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is not None:
- self._api.setProgramTemperature(
- self._current_program, self._target_temperature
- )
+ self._api.setProgramTemperature(self._current_program, temp)
+ self._target_temperature = temp
@property
def preset_mode(self):
diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json
index 20b2ac347f6..ff498991127 100644
--- a/homeassistant/components/vivotek/manifest.json
+++ b/homeassistant/components/vivotek/manifest.json
@@ -6,5 +6,7 @@
"libpyvivotek==0.2.2"
],
"dependencies": [],
- "codeowners": []
+ "codeowners": [
+ "@HarlemSquirrel"
+ ]
}
diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py
index b844f94a187..f64fd2ca531 100644
--- a/homeassistant/components/vizio/media_player.py
+++ b/homeassistant/components/vizio/media_player.py
@@ -1,7 +1,10 @@
"""Vizio SmartCast Device support."""
from datetime import timedelta
import logging
+
import voluptuous as vol
+from pyvizio import Vizio
+
from homeassistant import util
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
from homeassistant.components.media_player.const import (
@@ -122,7 +125,6 @@ class VizioDevice(MediaPlayerDevice):
def __init__(self, host, token, name, volume_step, device_type):
"""Initialize Vizio device."""
- import pyvizio
self._name = name
self._state = None
@@ -132,7 +134,7 @@ class VizioDevice(MediaPlayerDevice):
self._available_inputs = None
self._device_type = device_type
self._supported_commands = SUPPORTED_COMMANDS[device_type]
- self._device = pyvizio.Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type)
+ self._device = Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type)
self._max_volume = float(self._device.get_max_volume())
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py
index aaef128f33d..30b316cb4e8 100644
--- a/homeassistant/components/vlc/media_player.py
+++ b/homeassistant/components/vlc/media_player.py
@@ -2,6 +2,7 @@
import logging
import voluptuous as vol
+import vlc
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
from homeassistant.components.media_player.const import (
@@ -17,6 +18,7 @@ from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYI
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
+
_LOGGER = logging.getLogger(__name__)
CONF_ARGUMENTS = "arguments"
@@ -51,8 +53,6 @@ class VlcDevice(MediaPlayerDevice):
def __init__(self, name, arguments):
"""Initialize the vlc device."""
- import vlc
-
self._instance = vlc.Instance(arguments)
self._vlc = self._instance.media_player_new()
self._name = name
@@ -65,8 +65,6 @@ class VlcDevice(MediaPlayerDevice):
def update(self):
"""Get the latest details from the device."""
- import vlc
-
status = self._vlc.get_state()
if status == vlc.State.Playing:
self._state = STATE_PLAYING
diff --git a/homeassistant/components/w800rf32/__init__.py b/homeassistant/components/w800rf32/__init__.py
index 9f15e0b2aa1..805cca47023 100644
--- a/homeassistant/components/w800rf32/__init__.py
+++ b/homeassistant/components/w800rf32/__init__.py
@@ -2,6 +2,7 @@
import logging
import voluptuous as vol
+import W800rf32 as w800
from homeassistant.const import (
CONF_DEVICE,
@@ -26,8 +27,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the w800rf32 component."""
- # Try to load the W800rf32 module.
- import W800rf32 as w800
# Declare the Handle event
def handle_receive(event):
diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py
index f1f1890f7aa..e08111da8ba 100644
--- a/homeassistant/components/w800rf32/binary_sensor.py
+++ b/homeassistant/components/w800rf32/binary_sensor.py
@@ -2,6 +2,7 @@
import logging
import voluptuous as vol
+import W800rf32 as w800
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA,
@@ -104,9 +105,8 @@ class W800rf32BinarySensor(BinarySensorDevice):
@callback
def binary_sensor_update(self, event):
"""Call for control updates from the w800rf32 gateway."""
- import W800rf32 as w800rf32mod
- if not isinstance(event, w800rf32mod.W800rf32Event):
+ if not isinstance(event, w800.W800rf32Event):
return
dev_id = event.device
diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py
index 421f6265c0c..b4aad4925b9 100644
--- a/homeassistant/components/wake_on_lan/__init__.py
+++ b/homeassistant/components/wake_on_lan/__init__.py
@@ -3,6 +3,7 @@ from functools import partial
import logging
import voluptuous as vol
+import wakeonlan
from homeassistant.const import CONF_MAC
import homeassistant.helpers.config_validation as cv
@@ -22,7 +23,6 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Set up the wake on LAN component."""
- import wakeonlan
async def send_magic_packet(call):
"""Send magic packet to wake up a device."""
diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py
index 453685b13f6..01f69679829 100644
--- a/homeassistant/components/wake_on_lan/switch.py
+++ b/homeassistant/components/wake_on_lan/switch.py
@@ -4,6 +4,7 @@ import platform
import subprocess as sp
import voluptuous as vol
+import wakeonlan
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
from homeassistant.const import CONF_HOST, CONF_NAME
@@ -48,8 +49,6 @@ class WOLSwitch(SwitchDevice):
def __init__(self, hass, name, host, mac_address, off_action, broadcast_address):
"""Initialize the WOL switch."""
- import wakeonlan
-
self._hass = hass
self._name = name
self._host = host
@@ -57,7 +56,6 @@ class WOLSwitch(SwitchDevice):
self._broadcast_address = broadcast_address
self._off_script = Script(hass, off_action) if off_action else None
self._state = False
- self._wol = wakeonlan
@property
def is_on(self):
@@ -72,11 +70,11 @@ class WOLSwitch(SwitchDevice):
def turn_on(self, **kwargs):
"""Turn the device on."""
if self._broadcast_address:
- self._wol.send_magic_packet(
+ wakeonlan.send_magic_packet(
self._mac_address, ip_address=self._broadcast_address
)
else:
- self._wol.send_magic_packet(self._mac_address)
+ wakeonlan.send_magic_packet(self._mac_address)
def turn_off(self, **kwargs):
"""Turn the device off if an off action is present."""
diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py
index dbfe6de1a60..b53723a29b6 100644
--- a/homeassistant/components/waqi/sensor.py
+++ b/homeassistant/components/waqi/sensor.py
@@ -5,6 +5,7 @@ from datetime import timedelta
import aiohttp
import voluptuous as vol
+from waqiasync import WaqiClient
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
@@ -60,13 +61,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the requested World Air Quality Index locations."""
- import waqiasync
token = config.get(CONF_TOKEN)
station_filter = config.get(CONF_STATIONS)
locations = config.get(CONF_LOCATIONS)
- client = waqiasync.WaqiClient(token, async_get_clientsession(hass), timeout=TIMEOUT)
+ client = WaqiClient(token, async_get_clientsession(hass), timeout=TIMEOUT)
dev = []
try:
for location_name in locations:
@@ -128,6 +128,16 @@ class WaqiSensor(Entity):
return self._data.get("aqi")
return None
+ @property
+ def available(self):
+ """Return sensor availability."""
+ return self._data is not None
+
+ @property
+ def unique_id(self):
+ """Return unique ID."""
+ return self.uid
+
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py
index acc1c22c734..b6eb22c89ae 100644
--- a/homeassistant/components/waterfurnace/__init__.py
+++ b/homeassistant/components/waterfurnace/__init__.py
@@ -5,6 +5,7 @@ import time
import threading
import voluptuous as vol
+from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
@@ -37,19 +38,18 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, base_config):
"""Set up waterfurnace platform."""
- import waterfurnace.waterfurnace as wf
config = base_config.get(DOMAIN)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
- wfconn = wf.WaterFurnace(username, password)
+ wfconn = WaterFurnace(username, password)
# NOTE(sdague): login will throw an exception if this doesn't
# work, which will abort the setup.
try:
wfconn.login()
- except wf.WFCredentialError:
+ except WFCredentialError:
_LOGGER.error("Invalid credentials for waterfurnace login.")
return False
@@ -83,7 +83,6 @@ class WaterFurnaceData(threading.Thread):
def _reconnect(self):
"""Reconnect on a failure."""
- import waterfurnace.waterfurnace as wf
self._fails += 1
if self._fails > MAX_FAILS:
@@ -105,7 +104,7 @@ class WaterFurnaceData(threading.Thread):
try:
self.client.login()
self.data = self.client.read()
- except wf.WFException:
+ except WFException:
_LOGGER.exception("Failed to reconnect attempt %s", self._fails)
else:
_LOGGER.debug("Reconnected to furnace")
@@ -113,7 +112,6 @@ class WaterFurnaceData(threading.Thread):
def run(self):
"""Thread run loop."""
- import waterfurnace.waterfurnace as wf
@callback
def register():
@@ -143,7 +141,7 @@ class WaterFurnaceData(threading.Thread):
try:
self.data = self.client.read()
- except wf.WFException:
+ except WFException:
# WFExceptions are things the WF library understands
# that pretty much can all be solved by logging in and
# back out again.
diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py
index aef2cc8ccce..adc05893fde 100644
--- a/homeassistant/components/watson_iot/__init__.py
+++ b/homeassistant/components/watson_iot/__init__.py
@@ -4,6 +4,8 @@ import queue
import threading
import time
+from ibmiotf import MissingMessageEncoderException
+from ibmiotf.gateway import Client
import voluptuous as vol
from homeassistant.const import (
@@ -67,7 +69,6 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Watson IoT Platform component."""
- from ibmiotf import gateway
conf = config[DOMAIN]
@@ -85,7 +86,7 @@ def setup(hass, config):
"auth-method": "token",
"auth-token": conf[CONF_TOKEN],
}
- watson_gateway = gateway.Client(client_args)
+ watson_gateway = Client(client_args)
def event_to_json(event):
"""Add an event to the outgoing list."""
@@ -190,7 +191,6 @@ class WatsonIOTThread(threading.Thread):
def write_to_watson(self, events):
"""Write preprocessed events to watson."""
- import ibmiotf
for event in events:
for retry in range(MAX_TRIES + 1):
@@ -208,7 +208,7 @@ class WatsonIOTThread(threading.Thread):
_LOGGER.error("Failed to publish message to Watson IoT")
continue
break
- except (ibmiotf.MissingMessageEncoderException, IOError):
+ except (MissingMessageEncoderException, IOError):
if retry < MAX_TRIES:
time.sleep(RETRY_DELAY)
else:
diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py
index 340c0adbc97..4392a20d801 100644
--- a/homeassistant/components/waze_travel_time/sensor.py
+++ b/homeassistant/components/waze_travel_time/sensor.py
@@ -3,21 +3,22 @@ from datetime import timedelta
import logging
import re
+import WazeRouteCalculator
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_ATTRIBUTION,
- CONF_NAME,
- CONF_REGION,
- EVENT_HOMEASSISTANT_START,
ATTR_LATITUDE,
ATTR_LONGITUDE,
- CONF_UNIT_SYSTEM_METRIC,
+ CONF_NAME,
+ CONF_REGION,
CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_UNIT_SYSTEM_METRIC,
+ EVENT_HOMEASSISTANT_START,
)
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import location
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -237,7 +238,6 @@ class WazeTravelTimeData:
vehicle_type,
):
"""Set up WazeRouteCalculator."""
- import WazeRouteCalculator
self._calc = WazeRouteCalculator
diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py
index a12e55c771a..5a41bfa9851 100644
--- a/homeassistant/components/webhook/__init__.py
+++ b/homeassistant/components/webhook/__init__.py
@@ -1,7 +1,7 @@
"""Webhooks for Home Assistant."""
import logging
-from aiohttp.web import Response
+from aiohttp.web import Response, Request
import voluptuous as vol
from homeassistant.core import callback
@@ -98,9 +98,11 @@ class WebhookView(HomeAssistantView):
url = URL_WEBHOOK_PATH
name = "api:webhook"
requires_auth = False
+ cors_allowed = True
- async def _handle(self, request, webhook_id):
+ async def _handle(self, request: Request, webhook_id):
"""Handle webhook call."""
+ _LOGGER.debug("Handling webhook %s payload for %s", request.method, webhook_id)
hass = request.app["hass"]
return await async_handle_webhook(hass, webhook_id, request)
diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py
index 716b20f4ca4..3971d39ee73 100644
--- a/homeassistant/components/websocket_api/auth.py
+++ b/homeassistant/components/websocket_api/auth.py
@@ -3,7 +3,6 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.auth.models import RefreshToken, User
-from homeassistant.auth.providers import legacy_api_password
from homeassistant.components.http.ban import process_wrong_login, process_success_login
from homeassistant.const import __version__
@@ -74,19 +73,6 @@ class AuthPhase:
if refresh_token is not None:
return await self._async_finish_auth(refresh_token.user, refresh_token)
- elif self._hass.auth.support_legacy and "api_password" in msg:
- self._logger.info(
- "Received api_password, it is going to deprecate, please use"
- " access_token instead. For instructions, see https://"
- "developers.home-assistant.io/docs/en/external_api_websocket"
- ".html#authentication-phase"
- )
- user = await legacy_api_password.async_validate_password(
- self._hass, msg["api_password"]
- )
- if user is not None:
- return await self._async_finish_auth(user, None)
-
self._send_message(auth_invalid_message("Invalid access token or password"))
await process_wrong_login(self._request)
raise Disconnect
diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py
index 08a0430ee2a..be1830aa07b 100644
--- a/homeassistant/components/websocket_api/http.py
+++ b/homeassistant/components/websocket_api/http.py
@@ -2,6 +2,7 @@
import asyncio
from contextlib import suppress
import logging
+from typing import Optional
from aiohttp import web, WSMsgType
import async_timeout
@@ -25,7 +26,7 @@ from .error import Disconnect
from .messages import error_message
-# mypy: allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
class WebsocketAPIView(HomeAssistantView):
@@ -47,7 +48,7 @@ class WebSocketHandler:
"""Initialize an active connection."""
self.hass = hass
self.request = request
- self.wsock = None
+ self.wsock: Optional[web.WebSocketResponse] = None
self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG)
self._handle_task = None
self._writer_task = None
@@ -115,7 +116,7 @@ class WebSocketHandler:
# Py3.7+
if hasattr(asyncio, "current_task"):
# pylint: disable=no-member
- self._handle_task = asyncio.current_task() # type: ignore
+ self._handle_task = asyncio.current_task()
else:
self._handle_task = asyncio.Task.current_task()
diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py
index 20a6a90860b..f8f1257aefc 100644
--- a/homeassistant/components/websocket_api/sensor.py
+++ b/homeassistant/components/websocket_api/sensor.py
@@ -10,7 +10,7 @@ from .const import (
)
-# mypy: allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py
index 9e479991d15..df2d8ed1f31 100644
--- a/homeassistant/components/wemo/__init__.py
+++ b/homeassistant/components/wemo/__init__.py
@@ -1,6 +1,7 @@
"""Support for WeMo device discovery."""
import logging
+import pywemo
import requests
import voluptuous as vol
@@ -87,7 +88,6 @@ def setup(hass, config):
async def async_setup_entry(hass, entry):
"""Set up a wemo config entry."""
- import pywemo
config = hass.data[DOMAIN]
diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py
index 4ef18f29021..bc300fde571 100644
--- a/homeassistant/components/wemo/binary_sensor.py
+++ b/homeassistant/components/wemo/binary_sensor.py
@@ -3,6 +3,7 @@ import asyncio
import logging
import async_timeout
+from pywemo import discovery
import requests
from homeassistant.components.binary_sensor import BinarySensorDevice
@@ -15,7 +16,6 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Register discovered WeMo binary sensors."""
- from pywemo import discovery
if discovery_info is not None:
location = discovery_info["ssdp_description"]
diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py
index dde5aa1cd89..91273fa033f 100644
--- a/homeassistant/components/wemo/fan.py
+++ b/homeassistant/components/wemo/fan.py
@@ -3,11 +3,12 @@ import asyncio
import logging
from datetime import timedelta
-import requests
import async_timeout
+from pywemo import discovery
+import requests
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
+import homeassistant.helpers.config_validation as cv
from homeassistant.components.fan import (
DOMAIN,
SUPPORT_SET_SPEED,
@@ -96,7 +97,6 @@ RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_i
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up discovered WeMo humidifiers."""
- from pywemo import discovery
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py
index be6aa6f47f7..dab96eb8c94 100644
--- a/homeassistant/components/wemo/light.py
+++ b/homeassistant/components/wemo/light.py
@@ -3,8 +3,9 @@ import asyncio
import logging
from datetime import timedelta
-import requests
import async_timeout
+from pywemo import discovery
+import requests
from homeassistant import util
from homeassistant.components.light import (
@@ -35,7 +36,6 @@ SUPPORT_WEMO = (
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up discovered WeMo switches."""
- from pywemo import discovery
if discovery_info is not None:
location = discovery_info["ssdp_description"]
diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py
index 1bc85506987..c1d07a06902 100644
--- a/homeassistant/components/wemo/switch.py
+++ b/homeassistant/components/wemo/switch.py
@@ -2,9 +2,10 @@
import asyncio
import logging
from datetime import datetime, timedelta
-import requests
import async_timeout
+from pywemo import discovery
+import requests
from homeassistant.components.switch import SwitchDevice
from homeassistant.exceptions import PlatformNotReady
@@ -32,7 +33,6 @@ WEMO_STANDBY = 8
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up discovered WeMo switches."""
- from pywemo import discovery
if discovery_info is not None:
location = discovery_info["ssdp_description"]
diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py
index 09cf40f193f..3c78d80ba92 100644
--- a/homeassistant/components/whois/sensor.py
+++ b/homeassistant/components/whois/sensor.py
@@ -119,7 +119,10 @@ class WhoisSensor(Entity):
attrs = {}
expiration_date = response["expiration_date"]
- attrs[ATTR_EXPIRES] = expiration_date.isoformat()
+ if isinstance(expiration_date, list):
+ attrs[ATTR_EXPIRES] = expiration_date[0].isoformat()
+ else:
+ attrs[ATTR_EXPIRES] = expiration_date.isoformat()
if "nameservers" in response:
attrs[ATTR_NAME_SERVERS] = " ".join(response["nameservers"])
diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py
index d0bb27c06e1..e2eb98938bb 100644
--- a/homeassistant/components/wink/__init__.py
+++ b/homeassistant/components/wink/__init__.py
@@ -5,6 +5,9 @@ import logging
import os
import time
+from aiohttp.web import Response
+import pywink
+from pubnubsubhandler import PubNubSubscriptionHandler
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
@@ -279,8 +282,6 @@ def _request_oauth_completion(hass, config):
def setup(hass, config):
"""Set up the Wink component."""
- import pywink
- from pubnubsubhandler import PubNubSubscriptionHandler
if hass.data.get(DOMAIN) is None:
hass.data[DOMAIN] = {
@@ -689,8 +690,6 @@ class WinkAuthCallbackView(HomeAssistantView):
@callback
def get(self, request):
"""Finish OAuth callback request."""
- from aiohttp import web
-
hass = request.app["hass"]
data = request.query
@@ -715,15 +714,13 @@ class WinkAuthCallbackView(HomeAssistantView):
hass.async_add_job(setup, hass, self.config)
- return web.Response(
+ return Response(
text=html_response.format(response_message), content_type="text/html"
)
error_msg = "No code returned from Wink API"
_LOGGER.error(error_msg)
- return web.Response(
- text=html_response.format(error_msg), content_type="text/html"
- )
+ return Response(text=html_response.format(error_msg), content_type="text/html")
class WinkDevice(Entity):
diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py
index 4708b6efee8..654252f5ffe 100644
--- a/homeassistant/components/wink/alarm_control_panel.py
+++ b/homeassistant/components/wink/alarm_control_panel.py
@@ -1,6 +1,8 @@
"""Support Wink alarm control panels."""
import logging
+import pywink
+
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
@@ -17,7 +19,6 @@ STATE_ALARM_PRIVACY = "Private"
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink platform."""
- import pywink
for camera in pywink.get_cameras():
# get_cameras returns multiple device types.
diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py
index e82a767fde8..6dd22a3f7b8 100644
--- a/homeassistant/components/wink/binary_sensor.py
+++ b/homeassistant/components/wink/binary_sensor.py
@@ -1,6 +1,8 @@
"""Support for Wink binary sensors."""
import logging
+import pywink
+
from homeassistant.components.binary_sensor import BinarySensorDevice
from . import DOMAIN, WinkDevice
@@ -26,7 +28,6 @@ SENSOR_TYPES = {
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink binary sensor platform."""
- import pywink
for sensor in pywink.get_sensors():
_id = sensor.object_id() + sensor.name()
diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py
index 38f25ef0a83..6323fa7bbfe 100644
--- a/homeassistant/components/wink/climate.py
+++ b/homeassistant/components/wink/climate.py
@@ -283,10 +283,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
target_temp_high = target_temp
if self.hvac_mode == HVAC_MODE_HEAT:
target_temp_low = target_temp
- if target_temp_low is not None:
- target_temp_low = target_temp_low
- if target_temp_high is not None:
- target_temp_high = target_temp_high
self.wink.set_temperature(target_temp_low, target_temp_high)
def set_hvac_mode(self, hvac_mode):
diff --git a/homeassistant/components/wink/cover.py b/homeassistant/components/wink/cover.py
index fa39909512a..1ce7f9b8875 100644
--- a/homeassistant/components/wink/cover.py
+++ b/homeassistant/components/wink/cover.py
@@ -1,4 +1,6 @@
"""Support for Wink covers."""
+import pywink
+
from homeassistant.components.cover import ATTR_POSITION, CoverDevice
from . import DOMAIN, WinkDevice
@@ -6,7 +8,6 @@ from . import DOMAIN, WinkDevice
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink cover platform."""
- import pywink
for shade in pywink.get_shades():
_id = shade.object_id() + shade.name()
diff --git a/homeassistant/components/wink/fan.py b/homeassistant/components/wink/fan.py
index 9f5f2f9b3a0..d1d4e30ada3 100644
--- a/homeassistant/components/wink/fan.py
+++ b/homeassistant/components/wink/fan.py
@@ -1,6 +1,8 @@
"""Support for Wink fans."""
import logging
+import pywink
+
from homeassistant.components.fan import (
SPEED_HIGH,
SPEED_LOW,
@@ -21,7 +23,6 @@ SUPPORTED_FEATURES = SUPPORT_DIRECTION + SUPPORT_SET_SPEED
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink platform."""
- import pywink
for fan in pywink.get_fans():
if fan.object_id() + fan.name() not in hass.data[DOMAIN]["unique_ids"]:
diff --git a/homeassistant/components/wink/light.py b/homeassistant/components/wink/light.py
index 76576f804fa..bd125e6a7c2 100644
--- a/homeassistant/components/wink/light.py
+++ b/homeassistant/components/wink/light.py
@@ -1,4 +1,6 @@
"""Support for Wink lights."""
+import pywink
+
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
@@ -18,7 +20,6 @@ from . import DOMAIN, WinkDevice
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink lights."""
- import pywink
for light in pywink.get_light_bulbs():
_id = light.object_id() + light.name()
diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py
index 5246fb49eed..37b27c0d500 100644
--- a/homeassistant/components/wink/lock.py
+++ b/homeassistant/components/wink/lock.py
@@ -1,6 +1,7 @@
"""Support for Wink locks."""
import logging
+import pywink
import voluptuous as vol
from homeassistant.components.lock import LockDevice
@@ -70,7 +71,6 @@ ADD_KEY_SCHEMA = vol.Schema(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink platform."""
- import pywink
for lock in pywink.get_locks():
_id = lock.object_id() + lock.name()
diff --git a/homeassistant/components/wink/scene.py b/homeassistant/components/wink/scene.py
index a00600ad784..ff083598b2e 100644
--- a/homeassistant/components/wink/scene.py
+++ b/homeassistant/components/wink/scene.py
@@ -1,6 +1,8 @@
"""Support for Wink scenes."""
import logging
+import pywink
+
from homeassistant.components.scene import Scene
from . import DOMAIN, WinkDevice
@@ -10,7 +12,6 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink platform."""
- import pywink
for scene in pywink.get_scenes():
_id = scene.object_id() + scene.name()
diff --git a/homeassistant/components/wink/sensor.py b/homeassistant/components/wink/sensor.py
index 030a1e5b9ec..2d0313ec211 100644
--- a/homeassistant/components/wink/sensor.py
+++ b/homeassistant/components/wink/sensor.py
@@ -1,6 +1,8 @@
"""Support for Wink sensors."""
import logging
+import pywink
+
from homeassistant.const import TEMP_CELSIUS
from . import DOMAIN, WinkDevice
@@ -12,7 +14,6 @@ SENSOR_TYPES = ["temperature", "humidity", "balance", "proximity"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink platform."""
- import pywink
for sensor in pywink.get_sensors():
_id = sensor.object_id() + sensor.name()
diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py
index 07d3ff4becc..cf2264e7eeb 100644
--- a/homeassistant/components/wink/switch.py
+++ b/homeassistant/components/wink/switch.py
@@ -1,6 +1,8 @@
"""Support for Wink switches."""
import logging
+import pywink
+
from homeassistant.helpers.entity import ToggleEntity
from . import DOMAIN, WinkDevice
@@ -10,7 +12,6 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink platform."""
- import pywink
for switch in pywink.get_switches():
_id = switch.object_id() + switch.name()
diff --git a/homeassistant/components/wink/water_heater.py b/homeassistant/components/wink/water_heater.py
index 4fceeeb313d..11330c7c9a5 100644
--- a/homeassistant/components/wink/water_heater.py
+++ b/homeassistant/components/wink/water_heater.py
@@ -1,6 +1,8 @@
"""Support for Wink water heaters."""
import logging
+import pywink
+
from homeassistant.components.water_heater import (
ATTR_TEMPERATURE,
STATE_ECO,
@@ -42,7 +44,6 @@ WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Wink water heater devices."""
- import pywink
for water_heater in pywink.get_water_heaters():
_id = water_heater.object_id() + water_heater.name()
diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json
index 15b6f4e3b01..dabf184d7ed 100644
--- a/homeassistant/components/withings/.translations/de.json
+++ b/homeassistant/components/withings/.translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_flows": "Withings muss konfiguriert werden, bevor die Integration authentifiziert werden kann. Bitte lies die Dokumentation."
+ },
"create_entry": {
"default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil."
},
diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py
index ecefa681b87..baed9300d46 100644
--- a/homeassistant/components/withings/__init__.py
+++ b/homeassistant/components/withings/__init__.py
@@ -92,8 +92,4 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Unload Withings config entry."""
- await hass.async_create_task(
- hass.config_entries.async_forward_entry_unload(entry, "sensor")
- )
-
- return True
+ return await hass.config_entries.async_forward_entry_unload(entry, "sensor")
diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py
index 67cf966c1bc..0293784fd3e 100644
--- a/homeassistant/components/withings/sensor.py
+++ b/homeassistant/components/withings/sensor.py
@@ -397,7 +397,7 @@ class WithingsHealthSensor(Entity):
]
if not measure_groups:
- _LOGGER.warning("No measure groups found, setting state to %s", None)
+ _LOGGER.debug("No measure groups found, setting state to %s", None)
self._state = None
return
@@ -417,7 +417,7 @@ class WithingsHealthSensor(Entity):
return
if not data.series:
- _LOGGER.warning("No sleep data, setting state to %s", None)
+ _LOGGER.debug("No sleep data, setting state to %s", None)
self._state = None
return
@@ -444,7 +444,7 @@ class WithingsHealthSensor(Entity):
return
if not data.series:
- _LOGGER.warning("Sleep data has no series, setting state to %s", None)
+ _LOGGER.debug("Sleep data has no series, setting state to %s", None)
self._state = None
return
diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py
index 19edf231624..3ca2afcc749 100644
--- a/homeassistant/components/workday/binary_sensor.py
+++ b/homeassistant/components/workday/binary_sensor.py
@@ -2,6 +2,7 @@
import logging
from datetime import datetime, timedelta
+import holidays
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -141,8 +142,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Workday sensor."""
- import holidays
-
sensor_name = config.get(CONF_NAME)
country = config.get(CONF_COUNTRY)
province = config.get(CONF_PROVINCE)
diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py
index ce044499c63..122d09feaa4 100644
--- a/homeassistant/components/wunderlist/__init__.py
+++ b/homeassistant/components/wunderlist/__init__.py
@@ -2,6 +2,7 @@
import logging
import voluptuous as vol
+from wunderpy2 import WunderApi
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME, CONF_ACCESS_TOKEN
@@ -59,9 +60,7 @@ class Wunderlist:
def __init__(self, access_token, client_id):
"""Create new instance of Wunderlist component."""
- import wunderpy2
-
- api = wunderpy2.WunderApi()
+ api = WunderApi()
self._client = api.get_client(access_token, client_id)
_LOGGER.debug("Instance created")
diff --git a/homeassistant/components/wwlln/.translations/pt.json b/homeassistant/components/wwlln/.translations/pt.json
new file mode 100644
index 00000000000..c7081cd694a
--- /dev/null
+++ b/homeassistant/components/wwlln/.translations/pt.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py
index 67dc12565d8..acac60e108a 100644
--- a/homeassistant/components/xiaomi_miio/fan.py
+++ b/homeassistant/components/xiaomi_miio/fan.py
@@ -43,7 +43,8 @@ MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2"
MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1"
MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1"
-MODEL_AIRHUMIDIFIER_CA = "zhimi.humidifier.ca1"
+MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1"
+MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1"
MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2"
@@ -68,7 +69,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
MODEL_AIRPURIFIER_SA2,
MODEL_AIRPURIFIER_2S,
MODEL_AIRHUMIDIFIER_V1,
- MODEL_AIRHUMIDIFIER_CA,
+ MODEL_AIRHUMIDIFIER_CA1,
+ MODEL_AIRHUMIDIFIER_CB1,
MODEL_AIRFRESH_VA2,
]
),
@@ -235,7 +237,7 @@ AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
ATTR_BUTTON_PRESSED: "button_pressed",
}
-AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = {
+AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = {
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON,
ATTR_MOTOR_SPEED: "motor_speed",
ATTR_DEPTH: "depth",
@@ -335,7 +337,7 @@ FEATURE_FLAGS_AIRHUMIDIFIER = (
| FEATURE_SET_TARGET_HUMIDITY
)
-FEATURE_FLAGS_AIRHUMIDIFIER_CA = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY
+FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY
FEATURE_FLAGS_AIRFRESH = (
FEATURE_SET_BUZZER
@@ -880,9 +882,9 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
super().__init__(name, device, model, unique_id)
- if self._model == MODEL_AIRHUMIDIFIER_CA:
- self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA
- self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA
+ if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
+ self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB
+ self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB
self._speed_list = [
mode.name for mode in OperationMode if mode is not OperationMode.Strong
]
diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json
index 4c01cce2d3c..b675e6e6746 100644
--- a/homeassistant/components/xiaomi_miio/manifest.json
+++ b/homeassistant/components/xiaomi_miio/manifest.json
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": [
"construct==2.9.45",
- "python-miio==0.4.5"
+ "python-miio==0.4.6"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py
index 3719113f7c9..5aa9dbfffd1 100644
--- a/homeassistant/components/xmpp/notify.py
+++ b/homeassistant/components/xmpp/notify.py
@@ -7,6 +7,14 @@ import random
import string
import requests
+import slixmpp
+from slixmpp.exceptions import IqError, IqTimeout, XMPPError
+from slixmpp.xmlstream.xmlstream import NotConnectedError
+from slixmpp.plugins.xep_0363.http_upload import (
+ FileTooBig,
+ FileUploadError,
+ UploadServiceNotFound,
+)
import voluptuous as vol
from homeassistant.const import (
@@ -118,14 +126,6 @@ async def async_send_message(
data=None,
):
"""Send a message over XMPP."""
- import slixmpp
- from slixmpp.exceptions import IqError, IqTimeout, XMPPError
- from slixmpp.xmlstream.xmlstream import NotConnectedError
- from slixmpp.plugins.xep_0363.http_upload import (
- FileTooBig,
- FileUploadError,
- UploadServiceNotFound,
- )
class SendNotificationBot(slixmpp.ClientXMPP):
"""Service for sending Jabber (XMPP) messages."""
diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py
index e699ab74e68..eabb1ef34f1 100644
--- a/homeassistant/components/yamaha/media_player.py
+++ b/homeassistant/components/yamaha/media_player.py
@@ -2,6 +2,7 @@
import logging
import requests
+import rxv
import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
@@ -82,7 +83,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Yamaha platform."""
- import rxv
# Keep track of configured receivers so that we don't end up
# discovering a receiver dynamically that we have static config
@@ -336,8 +336,6 @@ class YamahaDevice(MediaPlayerDevice):
self._call_playback_function(self.receiver.next, "next track")
def _call_playback_function(self, function, function_text):
- import rxv
-
try:
function()
except rxv.exceptions.ResponseException:
diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py
index 38e606a0962..18b80cc4085 100644
--- a/homeassistant/components/yamaha_musiccast/media_player.py
+++ b/homeassistant/components/yamaha_musiccast/media_player.py
@@ -1,6 +1,8 @@
"""Support for Yamaha MusicCast Receivers."""
import logging
+import socket
+import pymusiccast
import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
@@ -61,8 +63,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Yamaha MusicCast platform."""
- import socket
- import pymusiccast
known_hosts = hass.data.get(KNOWN_HOSTS_KEY)
if known_hosts is None:
diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json
index 91267b43480..44dcf5b100c 100644
--- a/homeassistant/components/yandex_transport/manifest.json
+++ b/homeassistant/components/yandex_transport/manifest.json
@@ -3,7 +3,7 @@
"name": "Yandex Transport",
"documentation": "https://www.home-assistant.io/integrations/yandex_transport",
"requirements": [
- "ya_ma==0.3.7"
+ "ya_ma==0.3.8"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py
index 26311a4c72e..4bf634a61f4 100644
--- a/homeassistant/components/yandex_transport/sensor.py
+++ b/homeassistant/components/yandex_transport/sensor.py
@@ -79,18 +79,21 @@ class DiscoverMoscowYandexTransport(Entity):
transport_list = stop_metadata["Transport"]
for transport in transport_list:
route = transport["name"]
- if self._routes and route not in self._routes:
- # skip unnecessary route info
- continue
- if "Events" in transport["BriefSchedule"]:
- for event in transport["BriefSchedule"]["Events"]:
- if "Estimated" in event:
- posix_time_next = int(event["Estimated"]["value"])
- if closer_time is None or closer_time > posix_time_next:
- closer_time = posix_time_next
- if route not in attrs:
- attrs[route] = []
- attrs[route].append(event["Estimated"]["text"])
+ for thread in transport["threads"]:
+ if self._routes and route not in self._routes:
+ # skip unnecessary route info
+ continue
+ if "Events" not in thread["BriefSchedule"]:
+ continue
+ for event in thread["BriefSchedule"]["Events"]:
+ if "Estimated" not in event:
+ continue
+ posix_time_next = int(event["Estimated"]["value"])
+ if closer_time is None or closer_time > posix_time_next:
+ closer_time = posix_time_next
+ if route not in attrs:
+ attrs[route] = []
+ attrs[route].append(event["Estimated"]["text"])
attrs[STOP_NAME] = stop_name
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
if closer_time is None:
diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py
index ab63e6fb319..772fb00977b 100644
--- a/homeassistant/components/yeelight/light.py
+++ b/homeassistant/components/yeelight/light.py
@@ -2,8 +2,16 @@
import logging
import voluptuous as vol
-from yeelight import RGBTransition, SleepTransition, Flow, BulbException
+import yeelight
+from yeelight import (
+ RGBTransition,
+ SleepTransition,
+ Flow,
+ BulbException,
+ transitions as yee_transitions,
+)
from yeelight.enums import PowerMode, LightType, BulbType, SceneClass
+
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.service import extract_entity_ids
import homeassistant.helpers.config_validation as cv
@@ -190,8 +198,6 @@ SERVICE_SCHEMA_SET_AUTO_DELAY_OFF = YEELIGHT_SERVICE_SCHEMA.extend(
def _transitions_config_parser(transitions):
"""Parse transitions config into initialized objects."""
- import yeelight
-
transition_objects = []
for transition_config in transitions:
transition, params = list(transition_config.items())[0]
@@ -652,39 +658,23 @@ class YeelightGenericLight(Light):
def set_effect(self, effect) -> None:
"""Activate effect."""
if effect:
- from yeelight.transitions import (
- disco,
- temp,
- strobe,
- pulse,
- strobe_color,
- alarm,
- police,
- police2,
- christmas,
- rgb,
- randomloop,
- lsd,
- slowdown,
- )
-
if effect == EFFECT_STOP:
self._bulb.stop_flow(light_type=self.light_type)
return
effects_map = {
- EFFECT_DISCO: disco,
- EFFECT_TEMP: temp,
- EFFECT_STROBE: strobe,
- EFFECT_STROBE_COLOR: strobe_color,
- EFFECT_ALARM: alarm,
- EFFECT_POLICE: police,
- EFFECT_POLICE2: police2,
- EFFECT_CHRISTMAS: christmas,
- EFFECT_RGB: rgb,
- EFFECT_RANDOM_LOOP: randomloop,
- EFFECT_LSD: lsd,
- EFFECT_SLOWDOWN: slowdown,
+ EFFECT_DISCO: yee_transitions.disco,
+ EFFECT_TEMP: yee_transitions.temp,
+ EFFECT_STROBE: yee_transitions.strobe,
+ EFFECT_STROBE_COLOR: yee_transitions.strobe_color,
+ EFFECT_ALARM: yee_transitions.alarm,
+ EFFECT_POLICE: yee_transitions.police,
+ EFFECT_POLICE2: yee_transitions.police2,
+ EFFECT_CHRISTMAS: yee_transitions.christmas,
+ EFFECT_RGB: yee_transitions.rgb,
+ EFFECT_RANDOM_LOOP: yee_transitions.randomloop,
+ EFFECT_LSD: yee_transitions.lsd,
+ EFFECT_SLOWDOWN: yee_transitions.slowdown,
}
if effect in self.custom_effects_names:
@@ -692,13 +682,15 @@ class YeelightGenericLight(Light):
elif effect in effects_map:
flow = Flow(count=0, transitions=effects_map[effect]())
elif effect == EFFECT_FAST_RANDOM_LOOP:
- flow = Flow(count=0, transitions=randomloop(duration=250))
+ flow = Flow(
+ count=0, transitions=yee_transitions.randomloop(duration=250)
+ )
elif effect == EFFECT_WHATSAPP:
- flow = Flow(count=2, transitions=pulse(37, 211, 102))
+ flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102))
elif effect == EFFECT_FACEBOOK:
- flow = Flow(count=2, transitions=pulse(59, 89, 152))
+ flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152))
elif effect == EFFECT_TWITTER:
- flow = Flow(count=2, transitions=pulse(0, 172, 237))
+ flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237))
try:
self._bulb.start_flow(flow, light_type=self.light_type)
diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py
index fa836f2776f..3424014e8f4 100644
--- a/homeassistant/components/yeelightsunflower/light.py
+++ b/homeassistant/components/yeelightsunflower/light.py
@@ -1,6 +1,7 @@
"""Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi)."""
import logging
+import yeelightsunflower
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -24,7 +25,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Yeelight Sunflower Light platform."""
- import yeelightsunflower
host = config.get(CONF_HOST)
hub = yeelightsunflower.Hub(host)
diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py
index 3d8c63621be..f562f519ab5 100644
--- a/homeassistant/components/yr/sensor.py
+++ b/homeassistant/components/yr/sensor.py
@@ -7,6 +7,7 @@ from xml.parsers.expat import ExpatError
import aiohttp
import async_timeout
+import xmltodict
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -155,7 +156,6 @@ class YrData:
async def fetching_data(self, *_):
"""Get the latest data from yr.no."""
- import xmltodict
def try_again(err: str):
"""Retry in 15 to 20 minutes."""
diff --git a/homeassistant/components/yweather/sensor.py b/homeassistant/components/yweather/sensor.py
index 4dc23699872..c7f752a8836 100644
--- a/homeassistant/components/yweather/sensor.py
+++ b/homeassistant/components/yweather/sensor.py
@@ -1,17 +1,23 @@
"""Support for the Yahoo! Weather service."""
-import logging
from datetime import timedelta
+import logging
import voluptuous as vol
+from yahooweather import ( # pylint: disable=import-error
+ UNIT_C,
+ UNIT_F,
+ YahooWeather,
+ get_woeid,
+)
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- TEMP_CELSIUS,
+ ATTR_ATTRIBUTION,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
- ATTR_ATTRIBUTION,
+ TEMP_CELSIUS,
)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
@@ -20,6 +26,7 @@ _LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Weather details provided by Yahoo! Inc."
CONF_FORECAST = "forecast"
+
CONF_WOEID = "woeid"
DEFAULT_NAME = "Yweather"
@@ -52,8 +59,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Yahoo! weather sensor."""
- from yahooweather import get_woeid, UNIT_C, UNIT_F
-
unit = hass.config.units.temperature_unit
woeid = config.get(CONF_WOEID)
forecast = config.get(CONF_FORECAST)
@@ -181,8 +186,6 @@ class YahooWeatherData:
def __init__(self, woeid, temp_unit):
"""Initialize the data object."""
- from yahooweather import YahooWeather
-
self._yahoo = YahooWeather(woeid, temp_unit)
@property
diff --git a/homeassistant/components/yweather/weather.py b/homeassistant/components/yweather/weather.py
index 6779fd1896d..202124aa340 100644
--- a/homeassistant/components/yweather/weather.py
+++ b/homeassistant/components/yweather/weather.py
@@ -3,6 +3,12 @@ from datetime import timedelta
import logging
import voluptuous as vol
+from yahooweather import ( # pylint: disable=import-error
+ UNIT_C,
+ UNIT_F,
+ YahooWeather,
+ get_woeid,
+)
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
@@ -21,7 +27,6 @@ DATA_CONDITION = "yahoo_condition"
ATTRIBUTION = "Weather details provided by Yahoo! Inc."
-
CONF_WOEID = "woeid"
DEFAULT_NAME = "Yweather"
@@ -46,7 +51,6 @@ CONDITION_CLASSES = {
"exceptional": [0, 1, 2, 19, 22],
}
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_WOEID): cv.string,
@@ -57,8 +61,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Yahoo! weather platform."""
- from yahooweather import get_woeid, UNIT_C, UNIT_F
-
unit = hass.config.units.temperature_unit
woeid = config.get(CONF_WOEID)
name = config.get(CONF_NAME)
@@ -181,8 +183,6 @@ class YahooWeatherData:
def __init__(self, woeid, temp_unit):
"""Initialize the data object."""
- from yahooweather import YahooWeather
-
self._yahoo = YahooWeather(woeid, temp_unit)
@property
diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py
index a75cbba5f42..d890b193d72 100644
--- a/homeassistant/components/zengge/light.py
+++ b/homeassistant/components/zengge/light.py
@@ -1,6 +1,7 @@
"""Support for Zengge lights."""
import logging
+from zengge import zengge
import voluptuous as vol
from homeassistant.const import CONF_DEVICES, CONF_NAME
@@ -47,12 +48,11 @@ class ZenggeLight(Light):
def __init__(self, device):
"""Initialize the light."""
- import zengge
self._name = device["name"]
self._address = device["address"]
self.is_valid = True
- self._bulb = zengge.zengge(self._address)
+ self._bulb = zengge(self._address)
self._white = 0
self._brightness = 0
self._hs_color = (0, 0)
diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py
index af107a6ae0d..2f9fb7b4580 100644
--- a/homeassistant/components/zeroconf/__init__.py
+++ b/homeassistant/components/zeroconf/__init__.py
@@ -11,7 +11,11 @@ import voluptuous as vol
from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf
from homeassistant import util
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP, __version__
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_STOP,
+ EVENT_HOMEASSISTANT_START,
+ __version__,
+)
from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT
_LOGGER = logging.getLogger(__name__)
@@ -33,6 +37,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up Zeroconf and make Home Assistant discoverable."""
+ zeroconf = Zeroconf()
zeroconf_name = f"{hass.config.location_name}.{ZEROCONF_TYPE}"
params = {
@@ -58,9 +63,15 @@ def setup(hass, config):
properties=params,
)
- zeroconf = Zeroconf()
+ def zeroconf_hass_start(_event):
+ """Expose Home Assistant on zeroconf when it starts.
- zeroconf.register_service(info)
+ Wait till started or otherwise HTTP is not up and running.
+ """
+ _LOGGER.info("Starting Zeroconf broadcast")
+ zeroconf.register_service(info)
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start)
def service_update(zeroconf, service_type, name, state_change):
"""Service state changed."""
diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py
index 703e3bf25a0..4b8bdf5fa2e 100644
--- a/homeassistant/components/zestimate/sensor.py
+++ b/homeassistant/components/zestimate/sensor.py
@@ -3,6 +3,7 @@ from datetime import timedelta
import logging
import requests
+import xmltodict
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -101,7 +102,6 @@ class ZestimateDataSensor(Entity):
def update(self):
"""Get the latest data and update the states."""
- import xmltodict
try:
response = requests.get(_RESOURCE, params=self.params, timeout=5)
diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json
index 9ffd5211a1f..3329eafa1c6 100644
--- a/homeassistant/components/zha/.translations/de.json
+++ b/homeassistant/components/zha/.translations/de.json
@@ -18,11 +18,50 @@
"title": "ZHA"
},
"device_automation": {
+ "action_type": {
+ "squawk": "Kreischen",
+ "warn": "Warnen"
+ },
"trigger_subtype": {
+ "both_buttons": "Beide Tasten",
+ "button_1": "Erste Taste",
+ "button_2": "Zweite Taste",
+ "button_3": "Dritte Taste",
+ "button_4": "Vierte Taste",
+ "button_5": "F\u00fcnfte Taste",
+ "button_6": "Sechste Taste",
"close": "Schlie\u00dfen",
+ "dim_down": "Dimmer runter",
+ "dim_up": "Dimmer hoch",
+ "face_1": "mit Fl\u00e4che 1 aktiviert",
+ "face_2": "mit Fl\u00e4che 2 aktiviert",
+ "face_3": "mit Fl\u00e4che 3 aktiviert",
+ "face_4": "mit Fl\u00e4che 4 aktiviert",
+ "face_5": "mit Fl\u00e4che 5 aktiviert",
+ "face_6": "mit Fl\u00e4che 6 aktiviert",
+ "face_any": "Mit einer beliebigen/festgelegten Fl\u00e4che(n) aktiviert",
"left": "Links",
"open": "Offen",
- "right": "Rechts"
+ "right": "Rechts",
+ "turn_off": "Ausschalten",
+ "turn_on": "Einschalten"
+ },
+ "trigger_type": {
+ "device_dropped": "Ger\u00e4t ist gefallen",
+ "device_flipped": "Ger\u00e4t umgedreht \"{subtype}\"",
+ "device_knocked": "Ger\u00e4t klopfte \"{subtype}\"",
+ "device_rotated": "Ger\u00e4t wurde gedreht \"{subtype}\"",
+ "device_shaken": "Ger\u00e4t ersch\u00fcttert",
+ "device_slid": "Ger\u00e4t gerutscht \"{subtype}\"",
+ "device_tilted": "Ger\u00e4t gekippt",
+ "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt",
+ "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt",
+ "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen",
+ "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt",
+ "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt",
+ "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt",
+ "remote_button_short_release": "\"{subtype}\" Taste losgelassen",
+ "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json
index f8b78af5721..9b1ba025d7c 100644
--- a/homeassistant/components/zha/.translations/fr.json
+++ b/homeassistant/components/zha/.translations/fr.json
@@ -31,6 +31,15 @@
"button_5": "Cinqui\u00e8me bouton",
"button_6": "Sixi\u00e8me bouton",
"close": "Fermer",
+ "dim_down": "Assombrir",
+ "dim_up": "\u00c9claircir",
+ "face_1": "avec face 1 activ\u00e9e",
+ "face_2": "avec face 2 activ\u00e9e",
+ "face_3": "avec face 3 activ\u00e9e",
+ "face_4": "avec face 4 activ\u00e9e",
+ "face_5": "avec face 5 activ\u00e9e",
+ "face_6": "avec face 6 activ\u00e9e",
+ "face_any": "Avec n'importe quelle face / face sp\u00e9cifi\u00e9e(s) activ\u00e9e",
"left": "Gauche",
"open": "Ouvert",
"right": "Droite",
@@ -38,10 +47,21 @@
"turn_on": "Allumer"
},
"trigger_type": {
+ "device_dropped": "Appareil tomb\u00e9",
+ "device_flipped": "Appareil retourn\u00e9 \"{subtype}\"",
+ "device_knocked": "Appareil frapp\u00e9 \"{subtype}\"",
+ "device_rotated": "Appareil tourn\u00e9 \"{subtype}\"",
"device_shaken": "Appareil secou\u00e9",
+ "device_slid": "Appareil gliss\u00e9 \"{subtype}\"",
"device_tilted": "Dispositif inclin\u00e9",
+ "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9",
+ "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement",
+ "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long",
+ "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clics",
+ "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clics",
+ "remote_button_short_press": "bouton \" {subtype} \" enfonc\u00e9",
"remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9",
- "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics"
+ "remote_button_triple_press": "Bouton \"{subtype}\" \u00e0 trois clics"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json
index 44f45f43570..3a62f5d7ebe 100644
--- a/homeassistant/components/zha/.translations/ko.json
+++ b/homeassistant/components/zha/.translations/ko.json
@@ -16,5 +16,52 @@
}
},
"title": "ZHA"
+ },
+ "device_automation": {
+ "action_type": {
+ "squawk": "\ube44\uc0c1",
+ "warn": "\uacbd\uace0"
+ },
+ "trigger_subtype": {
+ "both_buttons": "\ub450 \uac1c",
+ "button_1": "\uccab \ubc88\uc9f8",
+ "button_2": "\ub450 \ubc88\uc9f8",
+ "button_3": "\uc138 \ubc88\uc9f8",
+ "button_4": "\ub124 \ubc88\uc9f8",
+ "button_5": "\ub2e4\uc12f \ubc88\uc9f8",
+ "button_6": "\uc5ec\uc12f \ubc88\uc9f8",
+ "close": "\ub2eb\uae30",
+ "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30",
+ "dim_up": "\ubc1d\uac8c \ud558\uae30",
+ "face_1": "\uba74 1\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c",
+ "face_2": "\uba74 2\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c",
+ "face_3": "\uba74 3\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c",
+ "face_4": "\uba74 4\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c",
+ "face_5": "\uba74 5\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c",
+ "face_6": "\uba74 6\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c",
+ "face_any": "\uc784\uc758\uc758 \uba74 \ub610\ub294 \ud2b9\uc815 \uba74\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c",
+ "left": "\uc67c\ucabd",
+ "open": "\uc5f4\uae30",
+ "right": "\uc624\ub978\ucabd",
+ "turn_off": "\ub044\uae30",
+ "turn_on": "\ucf1c\uae30"
+ },
+ "trigger_type": {
+ "device_dropped": "\uae30\uae30\ub97c \ub5a8\uad7c",
+ "device_flipped": "\"{subtype}\" \uae30\uae30\ub97c \ub4a4\uc9d1\uc74c",
+ "device_knocked": "\"{subtype}\" \uae30\uae30\ub97c \ub450\ub4dc\ub9bc",
+ "device_rotated": "\"{subtype}\" \uae30\uae30\ub97c \ud68c\uc804",
+ "device_shaken": "\uae30\uae30\ub97c \ud754\ub4e6",
+ "device_slid": "\"{subtype}\" \uae30\uae30\ub97c \uc2ac\ub77c\uc774\ub4dc",
+ "device_tilted": "\uae30\uae30\ub97c \uae30\uc6b8\uc784",
+ "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984",
+ "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984",
+ "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc",
+ "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984",
+ "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984",
+ "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984",
+ "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc",
+ "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json
index 5e5c666b1a4..fc7ae970503 100644
--- a/homeassistant/components/zha/.translations/nl.json
+++ b/homeassistant/components/zha/.translations/nl.json
@@ -18,7 +18,28 @@
"title": "ZHA"
},
"device_automation": {
+ "action_type": {
+ "squawk": "Schreeuw",
+ "warn": "Waarschuwen"
+ },
"trigger_subtype": {
+ "both_buttons": "Beide knoppen",
+ "button_1": "Eerste knop",
+ "button_2": "Tweede knop",
+ "button_3": "Derde knop",
+ "button_4": "Vierde knop",
+ "button_5": "Vijfde knop",
+ "button_6": "Zesde knop",
+ "close": "Sluiten",
+ "dim_down": "Dim omlaag",
+ "dim_up": "Dim omhoog",
+ "face_1": "met gezicht 1 geactiveerd",
+ "face_2": "met gezicht 2 geactiveerd",
+ "face_3": "met gezicht 3 geactiveerd",
+ "face_4": "met gezicht 4 geactiveerd",
+ "face_5": "met gezicht 5 geactiveerd",
+ "face_6": "met gezicht 6 geactiveerd",
+ "face_any": "Met elk/opgegeven gezicht (en) geactiveerd",
"left": "Links",
"open": "Open",
"right": "Rechts",
diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json
index 18c4c3c9ff2..a70f5ad1c33 100644
--- a/homeassistant/components/zha/.translations/no.json
+++ b/homeassistant/components/zha/.translations/no.json
@@ -19,8 +19,8 @@
},
"device_automation": {
"action_type": {
- "squawk": "Squawk",
- "warn": "Advarer"
+ "squawk": "Varsle",
+ "warn": "Advar"
},
"trigger_subtype": {
"both_buttons": "Begge knapper",
@@ -39,7 +39,7 @@
"face_4": "med ansikt 4 aktivert",
"face_5": "med ansikt 5 aktivert",
"face_6": "med ansikt 6 aktivert",
- "face_any": "Med alle/angitte ansikt (er) aktivert",
+ "face_any": "Med alle/angitte ansikt(er) aktivert",
"left": "Venstre",
"open": "\u00c5pen",
"right": "H\u00f8yre",
@@ -47,7 +47,7 @@
"turn_on": "Sl\u00e5 p\u00e5"
},
"trigger_type": {
- "device_dropped": "Enheten ble brutt",
+ "device_dropped": "Enheten ble sluppet",
"device_flipped": "Enheten snudd \"{subtype}\"",
"device_knocked": "Enheten sl\u00e5tt \"{subtype}\"",
"device_rotated": "Enheten roterte \"{subtype}\"",
@@ -55,13 +55,13 @@
"device_slid": "Enheten skled \"{subtype}\"",
"device_tilted": "Enheten skr\u00e5stilt",
"remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket",
- "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket",
+ "remote_button_long_press": "\"{subtype}\"-knappen ble holdt inne",
"remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk",
- "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket",
- "remote_button_quintuple_press": "\"{subtype}\"-knappen ble femdobbelt klikket",
+ "remote_button_quadruple_press": "\"{subtype}\"-knappen ble trykket fire ganger",
+ "remote_button_quintuple_press": "\"{subtype}\"-knappen ble trykket fem ganger",
"remote_button_short_press": "\"{subtype}\"-knappen ble trykket",
"remote_button_short_release": "\"{subtype}\"-knappen sluppet",
- "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket"
+ "remote_button_triple_press": "\"{subtype}\"-knappen ble trippelklikket"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json
index 0e1b7028dbb..4189ea6d9be 100644
--- a/homeassistant/components/zha/.translations/pl.json
+++ b/homeassistant/components/zha/.translations/pl.json
@@ -30,9 +30,9 @@
"button_4": "czwarty przycisk",
"button_5": "pi\u0105ty przycisk",
"button_6": "sz\u00f3sty przycisk",
- "close": "zamkni\u0119cie",
- "dim_down": "zmniejszenie jasno\u015bci",
- "dim_up": "zwi\u0119kszenie jasno\u015bci",
+ "close": "nast\u0105pi zamkni\u0119cie",
+ "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci",
+ "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci",
"face_1": "z aktywowan\u0105 twarz\u0105 1",
"face_2": "z aktywowan\u0105 twarz\u0105 2",
"face_3": "z aktywowan\u0105 twarz\u0105 3",
@@ -43,25 +43,25 @@
"left": "w lewo",
"open": "otwarcie",
"right": "w prawo",
- "turn_off": "wy\u0142\u0105czenie",
- "turn_on": "w\u0142\u0105czenie"
+ "turn_off": "nast\u0105pi wy\u0142\u0105czenie",
+ "turn_on": "nast\u0105pi w\u0142\u0105czenie"
},
"trigger_type": {
- "device_dropped": "upadek urz\u0105dzenia",
- "device_flipped": "odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"",
- "device_knocked": "pukni\u0119cie urz\u0105dzenia \"{subtype}\"",
- "device_rotated": "obr\u00f3cenie urz\u0105dzenia \"{subtype}\"",
- "device_shaken": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem",
- "device_slid": "przesuni\u0119cie urz\u0105dzenia \"{subtype}\"",
- "device_tilted": "przechylenie urz\u0105dzenia",
- "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty",
- "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
- "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
+ "device_dropped": "nast\u0105pi upadek urz\u0105dzenia",
+ "device_flipped": "nast\u0105pi odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"",
+ "device_knocked": "nast\u0105pi pukni\u0119cie w urz\u0105dzenie \"{subtype}\"",
+ "device_rotated": "nast\u0105pi obr\u00f3cenie urz\u0105dzenia \"{subtype}\"",
+ "device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem",
+ "device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"",
+ "device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia",
+ "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty",
+ "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
+ "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
"remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty",
- "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty",
- "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty",
- "remote_button_short_release": "przycisk \"{subtype}\" zwolniony",
- "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty"
+ "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty",
+ "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty",
+ "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony",
+ "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/.translations/pt.json b/homeassistant/components/zha/.translations/pt.json
index 8606a04e197..0c86dc95d09 100644
--- a/homeassistant/components/zha/.translations/pt.json
+++ b/homeassistant/components/zha/.translations/pt.json
@@ -16,5 +16,13 @@
}
},
"title": "ZHA"
+ },
+ "device_automation": {
+ "action_type": {
+ "warn": "Avisar"
+ },
+ "trigger_subtype": {
+ "left": "Esquerda"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json
index 2f6f42311c3..1779ed613fc 100644
--- a/homeassistant/components/zha/.translations/ru.json
+++ b/homeassistant/components/zha/.translations/ru.json
@@ -4,7 +4,7 @@
"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": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
"step": {
"user": {
@@ -19,7 +19,42 @@
},
"device_automation": {
"action_type": {
- "warn": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435"
+ "squawk": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u0438\u0440\u0435\u043d\u0443",
+ "warn": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435"
+ },
+ "trigger_subtype": {
+ "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438",
+ "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_5": "\u041f\u044f\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_6": "\u0428\u0435\u0441\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
+ "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f",
+ "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f",
+ "left": "\u041d\u0430\u043b\u0435\u0432\u043e",
+ "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
+ "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e",
+ "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f"
+ },
+ "trigger_type": {
+ "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u043b\u0438",
+ "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"",
+ "device_knocked": "\u041f\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\"",
+ "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"",
+ "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438",
+ "device_slid": "\u0421\u0434\u0432\u0438\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \"{subtype}\"",
+ "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u043a\u043b\u043e\u043d\u0438\u043b\u0438",
+ "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430",
+ "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430",
+ "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f",
+ "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430",
+ "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437",
+ "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430",
+ "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430",
+ "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py
index ff9f27d4843..6f24db442dd 100644
--- a/homeassistant/components/zha/api.py
+++ b/homeassistant/components/zha/api.py
@@ -4,6 +4,8 @@ import asyncio
import logging
import voluptuous as vol
+from zigpy.types.named import EUI64
+import zigpy.zdo.types as zdo_types
from homeassistant.components import websocket_api
from homeassistant.core import callback
@@ -44,7 +46,7 @@ from .core.const import (
WARNING_DEVICE_STROBE_HIGH,
WARNING_DEVICE_STROBE_YES,
)
-from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters
+from .core.helpers import async_is_bindable_target, get_matched_clusters
_LOGGER = logging.getLogger(__name__)
@@ -76,16 +78,16 @@ IEEE_SERVICE = "ieee_based_service"
SERVICE_SCHEMAS = {
SERVICE_PERMIT: vol.Schema(
{
- vol.Optional(ATTR_IEEE_ADDRESS, default=None): convert_ieee,
+ vol.Optional(ATTR_IEEE_ADDRESS, default=None): EUI64.convert,
vol.Optional(ATTR_DURATION, default=60): vol.All(
vol.Coerce(int), vol.Range(0, 254)
),
}
),
- IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): convert_ieee}),
+ IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): EUI64.convert}),
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema(
{
- vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Required(ATTR_IEEE): EUI64.convert,
vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
@@ -96,7 +98,7 @@ SERVICE_SCHEMAS = {
),
SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema(
{
- vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Required(ATTR_IEEE): EUI64.convert,
vol.Optional(
ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED
): cv.positive_int,
@@ -110,7 +112,7 @@ SERVICE_SCHEMAS = {
),
SERVICE_WARNING_DEVICE_WARN: vol.Schema(
{
- vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Required(ATTR_IEEE): EUI64.convert,
vol.Optional(
ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY
): cv.positive_int,
@@ -131,7 +133,7 @@ SERVICE_SCHEMAS = {
),
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema(
{
- vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Required(ATTR_IEEE): EUI64.convert,
vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
@@ -149,7 +151,7 @@ SERVICE_SCHEMAS = {
@websocket_api.websocket_command(
{
vol.Required("type"): "zha/devices/permit",
- vol.Optional(ATTR_IEEE, default=None): convert_ieee,
+ vol.Optional(ATTR_IEEE, default=None): EUI64.convert,
vol.Optional(ATTR_DURATION, default=60): vol.All(
vol.Coerce(int), vol.Range(0, 254)
),
@@ -200,7 +202,7 @@ async def websocket_get_devices(hass, connection, msg):
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
- {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): convert_ieee}
+ {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): EUI64.convert}
)
async def websocket_get_device(hass, connection, msg):
"""Get ZHA devices."""
@@ -252,7 +254,7 @@ def async_get_device_info(hass, device, ha_device_registry=None):
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/reconfigure",
- vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Required(ATTR_IEEE): EUI64.convert,
}
)
async def websocket_reconfigure_node(hass, connection, msg):
@@ -267,7 +269,7 @@ async def websocket_reconfigure_node(hass, connection, msg):
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
- {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): convert_ieee}
+ {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): EUI64.convert}
)
async def websocket_device_clusters(hass, connection, msg):
"""Return a list of device clusters."""
@@ -305,7 +307,7 @@ async def websocket_device_clusters(hass, connection, msg):
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/clusters/attributes",
- vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Required(ATTR_IEEE): EUI64.convert,
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_CLUSTER_ID): int,
vol.Required(ATTR_CLUSTER_TYPE): str,
@@ -346,7 +348,7 @@ async def websocket_device_cluster_attributes(hass, connection, msg):
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/clusters/commands",
- vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Required(ATTR_IEEE): EUI64.convert,
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_CLUSTER_ID): int,
vol.Required(ATTR_CLUSTER_TYPE): str,
@@ -400,7 +402,7 @@ async def websocket_device_cluster_commands(hass, connection, msg):
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/clusters/attributes/value",
- vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Required(ATTR_IEEE): EUI64.convert,
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_CLUSTER_ID): int,
vol.Required(ATTR_CLUSTER_TYPE): str,
@@ -444,7 +446,7 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg):
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
- {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): convert_ieee}
+ {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): EUI64.convert}
)
async def websocket_get_bindable_devices(hass, connection, msg):
"""Directly bind devices."""
@@ -472,8 +474,8 @@ async def websocket_get_bindable_devices(hass, connection, msg):
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/bind",
- vol.Required(ATTR_SOURCE_IEEE): convert_ieee,
- vol.Required(ATTR_TARGET_IEEE): convert_ieee,
+ vol.Required(ATTR_SOURCE_IEEE): EUI64.convert,
+ vol.Required(ATTR_TARGET_IEEE): EUI64.convert,
}
)
async def websocket_bind_devices(hass, connection, msg):
@@ -494,8 +496,8 @@ async def websocket_bind_devices(hass, connection, msg):
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/unbind",
- vol.Required(ATTR_SOURCE_IEEE): convert_ieee,
- vol.Required(ATTR_TARGET_IEEE): convert_ieee,
+ vol.Required(ATTR_SOURCE_IEEE): EUI64.convert,
+ vol.Required(ATTR_TARGET_IEEE): EUI64.convert,
}
)
async def websocket_unbind_devices(hass, connection, msg):
@@ -513,7 +515,6 @@ async def websocket_unbind_devices(hass, connection, msg):
async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation):
"""Create or remove a direct zigbee binding between 2 devices."""
- from zigpy.zdo import types as zdo_types
source_device = zha_gateway.get_device(source_ieee)
target_device = zha_gateway.get_device(target_ieee)
diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py
index 37b0bec207b..66a31ff8f21 100644
--- a/homeassistant/components/zha/core/channels/__init__.py
+++ b/homeassistant/components/zha/core/channels/__init__.py
@@ -11,6 +11,8 @@ from functools import wraps
import logging
from random import uniform
+import zigpy.exceptions
+
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -48,8 +50,6 @@ def decorate_command(channel, command):
@wraps(command)
async def wrapper(*args, **kwds):
- from zigpy.exceptions import DeliveryError
-
try:
result = await command(*args, **kwds)
channel.debug(
@@ -61,7 +61,7 @@ def decorate_command(channel, command):
)
return result
- except (DeliveryError, Timeout) as ex:
+ except (zigpy.exceptions.DeliveryError, Timeout) as ex:
channel.debug("command failed: %s exception: %s", command.__name__, str(ex))
return ex
@@ -143,12 +143,10 @@ class ZigbeeChannel(LogMixin):
This also swallows DeliveryError exceptions that are thrown when
devices are unreachable.
"""
- from zigpy.exceptions import DeliveryError
-
try:
res = await self.cluster.bind()
self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0])
- except (DeliveryError, Timeout) as ex:
+ except (zigpy.exceptions.DeliveryError, Timeout) as ex:
self.debug(
"Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex)
)
@@ -167,8 +165,6 @@ class ZigbeeChannel(LogMixin):
This also swallows DeliveryError exceptions that are thrown when
devices are unreachable.
"""
- from zigpy.exceptions import DeliveryError
-
attr_name = self.cluster.attributes.get(attr, [attr])[0]
kwargs = {}
@@ -189,7 +185,7 @@ class ZigbeeChannel(LogMixin):
reportable_change,
res,
)
- except (DeliveryError, Timeout) as ex:
+ except (zigpy.exceptions.DeliveryError, Timeout) as ex:
self.debug(
"failed to set reporting for '%s' attr on '%s' cluster: %s",
attr_name,
diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py
index e9e2c3b7ea6..b3be8037ff6 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -10,6 +10,10 @@ from enum import Enum
import logging
import time
+import zigpy.exceptions
+import zigpy.quirks
+from zigpy.profiles import zha, zll
+
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
@@ -87,9 +91,7 @@ class ZHADevice(LogMixin):
self._unsub = async_dispatcher_connect(
self.hass, self._available_signal, self.async_initialize
)
- from zigpy.quirks import CustomDevice
-
- self.quirk_applied = isinstance(self._zigpy_device, CustomDevice)
+ self.quirk_applied = isinstance(self._zigpy_device, zigpy.quirks.CustomDevice)
self.quirk_class = "{}.{}".format(
self._zigpy_device.__class__.__module__,
self._zigpy_device.__class__.__name__,
@@ -348,7 +350,6 @@ class ZHADevice(LogMixin):
zdo_task = None
for channel in channels:
if channel.name == CHANNEL_ZDO:
- # pylint: disable=E1111
if zdo_task is None: # We only want to do this once
zdo_task = self._async_create_task(
semaphore, channel, task_name, *args
@@ -373,8 +374,7 @@ class ZHADevice(LogMixin):
@callback
def async_unsub_dispatcher(self):
"""Unsubscribe the dispatcher."""
- if self._unsub:
- self._unsub()
+ self._unsub()
@callback
def async_update_last_seen(self, last_seen):
@@ -396,7 +396,6 @@ class ZHADevice(LogMixin):
@callback
def async_get_std_clusters(self):
"""Get ZHA and ZLL clusters for this device."""
- from zigpy.profiles import zha, zll
return {
ep_id: {
@@ -450,8 +449,6 @@ class ZHADevice(LogMixin):
if cluster is None:
return None
- from zigpy.exceptions import DeliveryError
-
try:
response = await cluster.write_attributes(
{attribute: value}, manufacturer=manufacturer
@@ -465,7 +462,7 @@ class ZHADevice(LogMixin):
response,
)
return response
- except DeliveryError as exc:
+ except zigpy.exceptions.DeliveryError as exc:
self.debug(
"failed to set attribute: %s %s %s %s %s",
f"{ATTR_VALUE}: {value}",
diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py
index 622adead803..e23862a7d3e 100644
--- a/homeassistant/components/zha/core/discovery.py
+++ b/homeassistant/components/zha/core/discovery.py
@@ -7,6 +7,9 @@ https://home-assistant.io/integrations/zha/
import logging
+import zigpy.profiles
+from zigpy.zcl.clusters.general import OnOff, PowerConfiguration
+
from homeassistant import const as ha_const
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.sensor import DOMAIN as SENSOR
@@ -52,8 +55,6 @@ def async_process_endpoint(
is_new_join,
):
"""Process an endpoint on a zigpy device."""
- import zigpy.profiles
-
if endpoint_id == 0: # ZDO
_async_create_cluster_channel(
endpoint, zha_device, is_new_join, channel_class=ZDOChannel
@@ -179,8 +180,6 @@ def _async_handle_single_cluster_matches(
hass, endpoint, zha_device, profile_clusters, device_key, is_new_join
):
"""Dispatch single cluster matches to HA components."""
- from zigpy.zcl.clusters.general import OnOff, PowerConfiguration
-
cluster_matches = []
cluster_match_results = []
matched_power_configuration = False
diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py
index a64e8cf7fd9..77702c8f3de 100644
--- a/homeassistant/components/zha/core/gateway.py
+++ b/homeassistant/components/zha/core/gateway.py
@@ -108,9 +108,9 @@ class ZHAGateway:
baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE)
radio_type = self._config_entry.data.get(CONF_RADIO_TYPE)
- radio_details = RADIO_TYPES[radio_type][ZHA_GW_RADIO]()
- radio = radio_details[ZHA_GW_RADIO]
- self.radio_description = RADIO_TYPES[radio_type][ZHA_GW_RADIO_DESCRIPTION]
+ radio_details = RADIO_TYPES[radio_type]
+ radio = radio_details[ZHA_GW_RADIO]()
+ self.radio_description = radio_details[ZHA_GW_RADIO_DESCRIPTION]
await radio.connect(usb_path, baudrate)
if CONF_DATABASE in self._config:
diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py
index 88a472716cc..d3f06090dae 100644
--- a/homeassistant/components/zha/core/helpers.py
+++ b/homeassistant/components/zha/core/helpers.py
@@ -8,6 +8,16 @@ import asyncio
import collections
import logging
+import bellows.ezsp
+import bellows.zigbee.application
+import zigpy.types
+import zigpy_deconz.api
+import zigpy_deconz.zigbee.application
+import zigpy_xbee.api
+import zigpy_xbee.zigbee.application
+import zigpy_zigate.api
+import zigpy_zigate.zigbee.application
+
from homeassistant.core import callback
from .const import (
@@ -49,25 +59,17 @@ async def safe_read(
async def check_zigpy_connection(usb_path, radio_type, database_path):
"""Test zigpy radio connection."""
if radio_type == RadioType.ezsp.name:
- import bellows.ezsp
- from bellows.zigbee.application import ControllerApplication
-
radio = bellows.ezsp.EZSP()
+ ControllerApplication = bellows.zigbee.application.ControllerApplication
elif radio_type == RadioType.xbee.name:
- import zigpy_xbee.api
- from zigpy_xbee.zigbee.application import ControllerApplication
-
radio = zigpy_xbee.api.XBee()
+ ControllerApplication = zigpy_xbee.zigbee.application.ControllerApplication
elif radio_type == RadioType.deconz.name:
- import zigpy_deconz.api
- from zigpy_deconz.zigbee.application import ControllerApplication
-
radio = zigpy_deconz.api.Deconz()
+ ControllerApplication = zigpy_deconz.zigbee.application.ControllerApplication
elif radio_type == RadioType.zigate.name:
- import zigpy_zigate.api
- from zigpy_zigate.zigbee.application import ControllerApplication
-
radio = zigpy_zigate.api.ZiGate()
+ ControllerApplication = zigpy_zigate.zigbee.application.ControllerApplication
try:
await radio.connect(usb_path, DEFAULT_BAUDRATE)
controller = ControllerApplication(radio, database_path)
@@ -78,15 +80,6 @@ async def check_zigpy_connection(usb_path, radio_type, database_path):
return True
-def convert_ieee(ieee_str):
- """Convert given ieee string to EUI64."""
- from zigpy.types import EUI64, uint8_t
-
- if ieee_str is None:
- return None
- return EUI64([uint8_t(p, base=16) for p in ieee_str.split(":")])
-
-
def get_attr_id_by_name(cluster, attr_name):
"""Get the attribute id for a cluster attribute by its name."""
return next(
@@ -145,7 +138,7 @@ async def async_get_zha_device(hass, device_id):
registry_device = device_registry.async_get(device_id)
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ieee_address = list(list(registry_device.identifiers)[0])[1]
- ieee = convert_ieee(ieee_address)
+ ieee = zigpy.types.EUI64.convert(ieee_address)
return zha_gateway.devices[ieee]
diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py
index 43ddc888d2f..571e77d4fae 100644
--- a/homeassistant/components/zha/core/registries.py
+++ b/homeassistant/components/zha/core/registries.py
@@ -6,6 +6,18 @@ https://home-assistant.io/integrations/zha/
"""
import collections
+import bellows.ezsp
+import bellows.zigbee.application
+import zigpy.profiles.zha
+import zigpy.profiles.zll
+import zigpy.zcl as zcl
+import zigpy_deconz.api
+import zigpy_deconz.zigbee.application
+import zigpy_xbee.api
+import zigpy_xbee.zigbee.application
+import zigpy_zigate.api
+import zigpy_zigate.zigbee.application
+
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
from homeassistant.components.fan import DOMAIN as FAN
@@ -14,6 +26,8 @@ from homeassistant.components.lock import DOMAIN as LOCK
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
+# importing channels updates registries
+from . import channels # noqa pylint: disable=wrong-import-position,unused-import
from .const import (
CONTROLLER,
SENSOR_ACCELERATION,
@@ -63,9 +77,6 @@ COMPONENT_CLUSTERS = {
ZIGBEE_CHANNEL_REGISTRY = DictRegistry()
-# importing channels updates registries
-from . import channels # noqa pylint: disable=wrong-import-position,unused-import
-
def establish_device_mappings():
"""Establish mappings between ZCL objects and HA ZHA objects.
@@ -73,56 +84,27 @@ def establish_device_mappings():
These cannot be module level, as importing bellows must be done in a
in a function.
"""
- from zigpy import zcl
- from zigpy.profiles import zha, zll
-
- def get_ezsp_radio():
- import bellows.ezsp
- from bellows.zigbee.application import ControllerApplication
-
- return {ZHA_GW_RADIO: bellows.ezsp.EZSP(), CONTROLLER: ControllerApplication}
-
RADIO_TYPES[RadioType.ezsp.name] = {
- ZHA_GW_RADIO: get_ezsp_radio,
+ ZHA_GW_RADIO: bellows.ezsp.EZSP,
+ CONTROLLER: bellows.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "EZSP",
}
- def get_deconz_radio():
- import zigpy_deconz.api
- from zigpy_deconz.zigbee.application import ControllerApplication
-
- return {
- ZHA_GW_RADIO: zigpy_deconz.api.Deconz(),
- CONTROLLER: ControllerApplication,
- }
-
RADIO_TYPES[RadioType.deconz.name] = {
- ZHA_GW_RADIO: get_deconz_radio,
+ ZHA_GW_RADIO: zigpy_deconz.api.Deconz,
+ CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "Deconz",
}
- def get_xbee_radio():
- import zigpy_xbee.api
- from zigpy_xbee.zigbee.application import ControllerApplication
-
- return {ZHA_GW_RADIO: zigpy_xbee.api.XBee(), CONTROLLER: ControllerApplication}
-
RADIO_TYPES[RadioType.xbee.name] = {
- ZHA_GW_RADIO: get_xbee_radio,
+ ZHA_GW_RADIO: zigpy_xbee.api.XBee,
+ CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "XBee",
}
- def get_zigate_radio():
- import zigpy_zigate.api
- from zigpy_zigate.zigbee.application import ControllerApplication
-
- return {
- ZHA_GW_RADIO: zigpy_zigate.api.ZiGate(),
- CONTROLLER: ControllerApplication,
- }
-
RADIO_TYPES[RadioType.zigate.name] = {
- ZHA_GW_RADIO: get_zigate_radio,
+ ZHA_GW_RADIO: zigpy_zigate.api.ZiGate,
+ CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "ZiGate",
}
@@ -137,33 +119,33 @@ def establish_device_mappings():
}
)
- DEVICE_CLASS[zha.PROFILE_ID].update(
+ DEVICE_CLASS[zigpy.profiles.zha.PROFILE_ID].update(
{
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER,
- zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT,
- zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT,
- zha.DeviceType.DIMMABLE_BALLAST: LIGHT,
- zha.DeviceType.DIMMABLE_LIGHT: LIGHT,
- zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT,
- zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT,
- zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT,
- zha.DeviceType.ON_OFF_BALLAST: SWITCH,
- zha.DeviceType.ON_OFF_LIGHT: LIGHT,
- zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH,
- zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH,
- zha.DeviceType.SMART_PLUG: SWITCH,
+ zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT,
+ zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT,
+ zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT,
+ zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT,
+ zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT,
+ zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT,
+ zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT,
+ zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH,
+ zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT,
+ zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH,
+ zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH,
+ zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH,
}
)
- DEVICE_CLASS[zll.PROFILE_ID].update(
+ DEVICE_CLASS[zigpy.profiles.zll.PROFILE_ID].update(
{
- zll.DeviceType.COLOR_LIGHT: LIGHT,
- zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT,
- zll.DeviceType.DIMMABLE_LIGHT: LIGHT,
- zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT,
- zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT,
- zll.DeviceType.ON_OFF_LIGHT: LIGHT,
- zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH,
+ zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT,
+ zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT,
+ zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT,
+ zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT,
+ zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT,
+ zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT,
+ zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH,
}
)
@@ -207,19 +189,21 @@ def establish_device_mappings():
}
)
- zhap = zha.PROFILE_ID
- REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_CONTROLLER)
- REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_DIMMER_SWITCH)
- REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_SCENE_CONTROLLER)
- REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.DIMMER_SWITCH)
- REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_CONTROLLER)
- REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_SCENE_CONTROLLER)
- REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.REMOTE_CONTROL)
- REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.SCENE_SELECTOR)
+ zha = zigpy.profiles.zha
+ REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_CONTROLLER)
+ REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_DIMMER_SWITCH)
+ REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_SCENE_CONTROLLER)
+ REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.DIMMER_SWITCH)
+ REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.NON_COLOR_CONTROLLER)
+ REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(
+ zha.DeviceType.NON_COLOR_SCENE_CONTROLLER
+ )
+ REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.REMOTE_CONTROL)
+ REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.SCENE_SELECTOR)
- zllp = zll.PROFILE_ID
- REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_CONTROLLER)
- REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_SCENE_CONTROLLER)
- REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROL_BRIDGE)
- REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROLLER)
- REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.SCENE_CONTROLLER)
+ zll = zigpy.profiles.zll
+ REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_CONTROLLER)
+ REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_SCENE_CONTROLLER)
+ REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROL_BRIDGE)
+ REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROLLER)
+ REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.SCENE_CONTROLLER)
diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py
index 460676a75a0..60cfa0eec00 100644
--- a/homeassistant/components/zha/device_action.py
+++ b/homeassistant/components/zha/device_action.py
@@ -5,7 +5,7 @@ import voluptuous as vol
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE
from homeassistant.core import Context, HomeAssistant
-from homeassistant.helpers import config_validation as cv, service
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN
@@ -78,13 +78,10 @@ async def _execute_service_based_action(
service_name = SERVICE_NAMES[action_type]
zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID])
- service_action = {
- service.CONF_SERVICE: "{}.{}".format(DOMAIN, service_name),
- ATTR_DATA: {ATTR_IEEE: str(zha_device.ieee)},
- }
+ service_data = {ATTR_IEEE: str(zha_device.ieee)}
- await service.async_call_from_config(
- hass, service_action, blocking=True, variables=variables, context=context
+ await hass.services.async_call(
+ DOMAIN, service_name, service_data, blocking=True, context=context
)
diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py
index c1ea3c2b761..cdd62b11d1e 100644
--- a/homeassistant/components/zha/device_trigger.py
+++ b/homeassistant/components/zha/device_trigger.py
@@ -21,24 +21,36 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
)
+async def async_validate_trigger_config(hass, config):
+ """Validate config."""
+ config = TRIGGER_SCHEMA(config)
+
+ if "zha" in hass.config.components:
+ trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
+ zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID])
+ if (
+ zha_device.device_automation_triggers is None
+ or trigger not in zha_device.device_automation_triggers
+ ):
+ raise InvalidDeviceAutomationConfig
+
+ return config
+
+
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID])
- if (
- zha_device.device_automation_triggers is None
- or trigger not in zha_device.device_automation_triggers
- ):
- raise InvalidDeviceAutomationConfig
-
trigger = zha_device.device_automation_triggers[trigger]
event_config = {
+ event.CONF_PLATFORM: "event",
event.CONF_EVENT_TYPE: ZHA_EVENT,
event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger},
}
+ event_config = event.TRIGGER_SCHEMA(event_config)
return await event.async_attach_trigger(
hass, event_config, action, automation_info, platform_type="device"
)
diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py
index 00c3942358e..c11cd405a99 100644
--- a/homeassistant/components/zha/entity.py
+++ b/homeassistant/components/zha/entity.py
@@ -40,7 +40,7 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity):
self._unique_id = unique_id
if not skip_entity_id:
ieee = zha_device.ieee
- ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]])
+ ieeetail = "".join([f"{o:02x}" for o in ieee[:4]])
self.entity_id = "{}.{}_{}_{}_{}{}".format(
self._domain,
slugify(zha_device.manufacturer),
diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py
index 1f119ef6657..43ad2291cb7 100644
--- a/homeassistant/components/zha/fan.py
+++ b/homeassistant/components/zha/fan.py
@@ -43,7 +43,7 @@ SPEED_LIST = [
SPEED_SMART,
]
-VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)}
+VALUE_TO_SPEED = dict(enumerate(SPEED_LIST))
SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py
index afc4618343c..a2151b4bdcb 100644
--- a/homeassistant/components/zha/lock.py
+++ b/homeassistant/components/zha/lock.py
@@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED]
-VALUE_TO_STATE = {i: state for i, state in enumerate(STATE_LIST)}
+VALUE_TO_STATE = dict(enumerate(STATE_LIST))
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json
index 9790fbffd06..9821ec2025b 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -6,10 +6,10 @@
"requirements": [
"bellows-homeassistant==0.10.0",
"zha-quirks==0.0.26",
- "zigpy-deconz==0.5.0",
- "zigpy-homeassistant==0.9.0",
- "zigpy-xbee-homeassistant==0.5.0",
- "zigpy-zigate==0.4.1"
+ "zigpy-deconz==0.6.0",
+ "zigpy-homeassistant==0.10.0",
+ "zigpy-xbee-homeassistant==0.6.0",
+ "zigpy-zigate==0.5.0"
],
"dependencies": [],
"codeowners": ["@dmulcahey", "@adminiuga"]
diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py
index 31cbc0c65b6..e74726a70f9 100644
--- a/homeassistant/components/zigbee/__init__.py
+++ b/homeassistant/components/zigbee/__init__.py
@@ -2,6 +2,11 @@
import logging
from binascii import hexlify, unhexlify
+import xbee_helper.const as xb_const
+from xbee_helper import ZigBee
+from xbee_helper.device import convert_adc
+from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure
+from serial import Serial, SerialException
import voluptuous as vol
from homeassistant.const import (
@@ -75,12 +80,6 @@ def setup(hass, config):
global ZIGBEE_EXCEPTION
global ZIGBEE_TX_FAILURE
- import xbee_helper.const as xb_const
- from xbee_helper import ZigBee
- from xbee_helper.device import convert_adc
- from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure
- from serial import Serial, SerialException
-
GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW
GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH
ADC_PERCENTAGE = xb_const.ADC_PERCENTAGE
diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py
index d23fb5a4757..39633754772 100644
--- a/homeassistant/components/zone/config_flow.py
+++ b/homeassistant/components/zone/config_flow.py
@@ -20,7 +20,7 @@ from homeassistant.util import slugify
from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE
-# mypy: allow-untyped-defs
+# mypy: allow-untyped-defs, no-check-untyped-defs
@callback
diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json
index ed2e20f3527..4243f583082 100644
--- a/homeassistant/components/zwave/.translations/ru.json
+++ b/homeassistant/components/zwave/.translations/ru.json
@@ -2,7 +2,7 @@
"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.",
- "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave"
+ "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave."
},
"error": {
"option_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Z-Wave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
@@ -13,7 +13,7 @@
"network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)",
"usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
},
- "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430",
+ "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.",
"title": "Z-Wave"
}
},
diff --git a/homeassistant/config.py b/homeassistant/config.py
index 97c996d9e59..9f49889791e 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -66,9 +66,11 @@ VERSION_FILE = ".HA_VERSION"
CONFIG_DIR_NAME = ".homeassistant"
DATA_CUSTOMIZE = "hass_customize"
-FILE_MIGRATION = (("ios.conf", ".ios.conf"),)
+GROUP_CONFIG_PATH = "groups.yaml"
+AUTOMATION_CONFIG_PATH = "automations.yaml"
+SCRIPT_CONFIG_PATH = "scripts.yaml"
-DEFAULT_CONFIG = """
+DEFAULT_CONFIG = f"""
# Configure a default setup of Home Assistant (frontend, api, etc)
default_config:
@@ -80,9 +82,9 @@ default_config:
tts:
- platform: google_translate
-group: !include groups.yaml
-automation: !include automations.yaml
-script: !include scripts.yaml
+group: !include {GROUP_CONFIG_PATH}
+automation: !include {AUTOMATION_CONFIG_PATH}
+script: !include {SCRIPT_CONFIG_PATH}
"""
DEFAULT_SECRETS = """
# Use this file to store secrets like usernames and passwords.
@@ -253,12 +255,6 @@ async def async_create_default_config(
def _write_default_config(config_dir: str) -> Optional[str]:
"""Write the default config."""
- from homeassistant.components.config.group import CONFIG_PATH as GROUP_CONFIG_PATH
- from homeassistant.components.config.automation import (
- CONFIG_PATH as AUTOMATION_CONFIG_PATH,
- )
- from homeassistant.components.config.script import CONFIG_PATH as SCRIPT_CONFIG_PATH
-
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
secret_path = os.path.join(config_dir, SECRET_YAML)
version_path = os.path.join(config_dir, VERSION_FILE)
@@ -407,12 +403,6 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
with open(version_path, "wt") as outp:
outp.write(__version__)
- _LOGGER.debug("Migrating old system configuration files to new locations")
- for oldf, newf in FILE_MIGRATION:
- if os.path.isfile(hass.config.path(oldf)):
- _LOGGER.info("Migrating %s to %s", oldf, newf)
- os.rename(hass.config.path(oldf), hass.config.path(newf))
-
@callback
def async_log_exception(
@@ -468,12 +458,7 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str:
return message
-async def async_process_ha_core_config(
- hass: HomeAssistant,
- config: Dict,
- api_password: Optional[str] = None,
- trusted_networks: Optional[Any] = None,
-) -> None:
+async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> None:
"""Process the [homeassistant] section from the configuration.
This method is a coroutine.
@@ -486,14 +471,6 @@ async def async_process_ha_core_config(
if auth_conf is None:
auth_conf = [{"type": "homeassistant"}]
- if api_password:
- auth_conf.append(
- {"type": "legacy_api_password", "api_password": api_password}
- )
- if trusted_networks:
- auth_conf.append(
- {"type": "trusted_networks", "trusted_networks": trusted_networks}
- )
mfa_conf = config.get(
CONF_AUTH_MFA_MODULES,
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 8a40cff1bd5..aee15d6c0ce 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component, async_process_deps_reqs
from homeassistant.util.decorator import Registry
from homeassistant.helpers import entity_registry
-# mypy: allow-untyped-defs
+# mypy: allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
_UNDEF = object()
@@ -337,7 +337,7 @@ class ConfigEntry:
return False
if result:
# pylint: disable=protected-access
- hass.config_entries._async_schedule_save() # type: ignore
+ hass.config_entries._async_schedule_save()
return result
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
@@ -676,7 +676,7 @@ async def _old_conf_migrator(old_config):
class ConfigFlow(data_entry_flow.FlowHandler):
"""Base class for config flows with some helpers."""
- def __init_subclass__(cls, domain=None, **kwargs):
+ def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None:
"""Initialize a subclass, register if possible."""
super().__init_subclass__(**kwargs) # type: ignore
if domain is not None:
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 8c7299e2962..f6f1a4f2de2 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,10 +1,10 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 100
-PATCH_VERSION = "3"
+MINOR_VERSION = 101
+PATCH_VERSION = "0"
__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION)
__version__ = "{}.{}".format(__short_version__, PATCH_VERSION)
-REQUIRED_PYTHON_VER = (3, 6, 0)
+REQUIRED_PYTHON_VER = (3, 6, 1)
# Format for platform files
PLATFORM_FORMAT = "{platform}.{domain}"
@@ -124,6 +124,7 @@ CONF_RECIPIENT = "recipient"
CONF_REGION = "region"
CONF_RESOURCE = "resource"
CONF_RESOURCES = "resources"
+CONF_RESOURCE_TEMPLATE = "resource_template"
CONF_RGB = "rgb"
CONF_ROOM = "room"
CONF_SCAN_INTERVAL = "scan_interval"
@@ -451,7 +452,6 @@ HTTP_SERVICE_UNAVAILABLE = 503
HTTP_BASIC_AUTHENTICATION = "basic"
HTTP_DIGEST_AUTHENTICATION = "digest"
-HTTP_HEADER_HA_AUTH = "X-HA-access"
HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With"
CONTENT_TYPE_JSON = "application/json"
diff --git a/homeassistant/core.py b/homeassistant/core.py
index feb4445d36d..ec11b14edaa 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -77,7 +77,8 @@ from homeassistant.util.unit_system import ( # NOQA
# Typing imports that create a circular dependency
# pylint: disable=using-constant-test
if TYPE_CHECKING:
- from homeassistant.config_entries import ConfigEntries # noqa
+ from homeassistant.config_entries import ConfigEntries
+ from homeassistant.components.http import HomeAssistantHTTP
# pylint: disable=invalid-name
T = TypeVar("T")
@@ -162,6 +163,9 @@ class CoreState(enum.Enum):
class HomeAssistant:
"""Root object of the Home Assistant home automation."""
+ http: "HomeAssistantHTTP" = None # type: ignore
+ config_entries: "ConfigEntries" = None # type: ignore
+
def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None:
"""Initialize new Home Assistant object."""
self.loop: asyncio.events.AbstractEventLoop = (loop or asyncio.get_event_loop())
@@ -186,7 +190,6 @@ class HomeAssistant:
self.data: dict = {}
self.state = CoreState.not_running
self.exit_code = 0
- self.config_entries: Optional[ConfigEntries] = None
# If not None, use to signal end-of-loop
self._stopped: Optional[asyncio.Event] = None
diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py
index 0bc27498f76..c06c69d9213 100644
--- a/homeassistant/data_entry_flow.py
+++ b/homeassistant/data_entry_flow.py
@@ -168,7 +168,7 @@ class FlowHandler:
"""Handle the configuration flow of a component."""
# Set by flow manager
- flow_id: Optional[str] = None
+ flow_id: str = None # type: ignore
hass: Optional[HomeAssistant] = None
handler: Optional[Hashable] = None
cur_step: Optional[Dict[str, str]] = None
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 21f57934e95..4668528fedb 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -6,12 +6,15 @@ To update, run python3 -m script.hassfest
# fmt: off
FLOWS = [
+ "abode",
"adguard",
+ "airly",
"ambiclimate",
"ambient_station",
"axis",
"cast",
"cert_expiry",
+ "coolmaster",
"daikin",
"deconz",
"dialogflow",
@@ -20,6 +23,7 @@ FLOWS = [
"esphome",
"geofency",
"geonetnz_quakes",
+ "glances",
"gpslogger",
"hangouts",
"heos",
@@ -42,8 +46,10 @@ FLOWS = [
"met",
"mobile_app",
"mqtt",
+ "neato",
"nest",
"notion",
+ "opentherm_gw",
"openuv",
"owntracks",
"plaato",
@@ -55,6 +61,7 @@ FLOWS = [
"smartthings",
"smhi",
"solaredge",
+ "solarlog",
"soma",
"somfy",
"sonos",
diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py
index 922878fb324..7a1512957a2 100644
--- a/homeassistant/helpers/config_entry_flow.py
+++ b/homeassistant/helpers/config_entry_flow.py
@@ -3,7 +3,7 @@ from typing import Callable, Awaitable, Union
from homeassistant import config_entries
from .typing import HomeAssistantType
-# mypy: allow-untyped-defs
+# mypy: allow-untyped-defs, no-check-untyped-defs
DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]]
@@ -38,7 +38,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
if user_input is None:
return self.async_show_form(step_id="confirm")
- if (
+ if ( # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context
and self.context.get("source") != config_entries.SOURCE_DISCOVERY
):
@@ -124,7 +124,10 @@ class WebhookFlowHandler(config_entries.ConfigFlow):
webhook_id = self.hass.components.webhook.async_generate_id()
- if self.hass.components.cloud.async_active_subscription():
+ if (
+ "cloud" in self.hass.config.components
+ and self.hass.components.cloud.async_active_subscription()
+ ):
webhook_url = await self.hass.components.cloud.async_create_cloudhook(
webhook_id
)
diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py
new file mode 100644
index 00000000000..7fb954378ee
--- /dev/null
+++ b/homeassistant/helpers/config_entry_oauth2_flow.py
@@ -0,0 +1,420 @@
+"""Config Flow using OAuth2.
+
+This module exists of the following parts:
+ - OAuth2 config flow which supports multiple OAuth2 implementations
+ - OAuth2 implementation that works with local provided client ID/secret
+
+"""
+import asyncio
+from abc import ABCMeta, ABC, abstractmethod
+import logging
+from typing import Optional, Any, Dict, cast
+import time
+
+import async_timeout
+from aiohttp import web, client
+import jwt
+import voluptuous as vol
+from yarl import URL
+
+from homeassistant.auth.util import generate_secret
+from homeassistant.core import HomeAssistant, callback
+from homeassistant import config_entries
+from homeassistant.components.http import HomeAssistantView
+
+from .aiohttp_client import async_get_clientsession
+
+
+DATA_JWT_SECRET = "oauth2_jwt_secret"
+DATA_VIEW_REGISTERED = "oauth2_view_reg"
+DATA_IMPLEMENTATIONS = "oauth2_impl"
+AUTH_CALLBACK_PATH = "/auth/external/callback"
+
+
+class AbstractOAuth2Implementation(ABC):
+ """Base class to abstract OAuth2 authentication."""
+
+ @property
+ @abstractmethod
+ def name(self) -> str:
+ """Name of the implementation."""
+
+ @property
+ @abstractmethod
+ def domain(self) -> str:
+ """Domain that is providing the implementation."""
+
+ @abstractmethod
+ async def async_generate_authorize_url(self, flow_id: str) -> str:
+ """Generate a url for the user to authorize.
+
+ This step is called when a config flow is initialized. It should redirect the
+ user to the vendor website where they can authorize Home Assistant.
+
+ The implementation is responsible to get notified when the user is authorized
+ and pass this to the specified config flow. Do as little work as possible once
+ notified. You can do the work inside async_resolve_external_data. This will
+ give the best UX.
+
+ Pass external data in with:
+
+ ```python
+ await hass.config_entries.flow.async_configure(
+ flow_id=flow_id, user_input=external_data
+ )
+ ```
+ """
+
+ @abstractmethod
+ async def async_resolve_external_data(self, external_data: Any) -> dict:
+ """Resolve external data to tokens.
+
+ Turn the data that the implementation passed to the config flow as external
+ step data into tokens. These tokens will be stored as 'token' in the
+ config entry data.
+ """
+
+ async def async_refresh_token(self, token: dict) -> dict:
+ """Refresh a token and update expires info."""
+ new_token = await self._async_refresh_token(token)
+ new_token["expires_at"] = time.time() + new_token["expires_in"]
+ return new_token
+
+ @abstractmethod
+ async def _async_refresh_token(self, token: dict) -> dict:
+ """Refresh a token."""
+
+
+class LocalOAuth2Implementation(AbstractOAuth2Implementation):
+ """Local OAuth2 implementation."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ domain: str,
+ client_id: str,
+ client_secret: str,
+ authorize_url: str,
+ token_url: str,
+ ):
+ """Initialize local auth implementation."""
+ self.hass = hass
+ self._domain = domain
+ self.client_id = client_id
+ self.client_secret = client_secret
+ self.authorize_url = authorize_url
+ self.token_url = token_url
+
+ @property
+ def name(self) -> str:
+ """Name of the implementation."""
+ return "Configuration.yaml"
+
+ @property
+ def domain(self) -> str:
+ """Domain providing the implementation."""
+ return self._domain
+
+ @property
+ def redirect_uri(self) -> str:
+ """Return the redirect uri."""
+ return f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" # type: ignore
+
+ async def async_generate_authorize_url(self, flow_id: str) -> str:
+ """Generate a url for the user to authorize."""
+ return str(
+ URL(self.authorize_url).with_query(
+ {
+ "response_type": "code",
+ "client_id": self.client_id,
+ "redirect_uri": self.redirect_uri,
+ "state": _encode_jwt(self.hass, {"flow_id": flow_id}),
+ }
+ )
+ )
+
+ async def async_resolve_external_data(self, external_data: Any) -> dict:
+ """Resolve the authorization code to tokens."""
+ return await self._token_request(
+ {
+ "grant_type": "authorization_code",
+ "code": external_data,
+ "redirect_uri": self.redirect_uri,
+ }
+ )
+
+ async def _async_refresh_token(self, token: dict) -> dict:
+ """Refresh tokens."""
+ new_token = await self._token_request(
+ {
+ "grant_type": "refresh_token",
+ "client_id": self.client_id,
+ "refresh_token": token["refresh_token"],
+ }
+ )
+ return {**token, **new_token}
+
+ async def _token_request(self, data: dict) -> dict:
+ """Make a token request."""
+ session = async_get_clientsession(self.hass)
+
+ data["client_id"] = self.client_id
+
+ if self.client_secret is not None:
+ data["client_secret"] = self.client_secret
+
+ resp = await session.post(self.token_url, data=data)
+ resp.raise_for_status()
+ return cast(dict, await resp.json())
+
+
+class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
+ """Handle a config flow."""
+
+ DOMAIN = ""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN
+
+ def __init__(self) -> None:
+ """Instantiate config flow."""
+ if self.DOMAIN == "":
+ raise TypeError(
+ f"Can't instantiate class {self.__class__.__name__} without DOMAIN being set"
+ )
+
+ self.external_data: Any = None
+ self.flow_impl: AbstractOAuth2Implementation = None # type: ignore
+
+ @property
+ @abstractmethod
+ def logger(self) -> logging.Logger:
+ """Return logger."""
+
+ @property
+ def extra_authorize_data(self) -> dict:
+ """Extra data that needs to be appended to the authorize url."""
+ return {}
+
+ async def async_step_pick_implementation(self, user_input: dict = None) -> dict:
+ """Handle a flow start."""
+ assert self.hass
+ implementations = await async_get_implementations(self.hass, self.DOMAIN)
+
+ if user_input is not None:
+ self.flow_impl = implementations[user_input["implementation"]]
+ return await self.async_step_auth()
+
+ if not implementations:
+ return self.async_abort(reason="missing_configuration")
+
+ if len(implementations) == 1:
+ # Pick first implementation as we have only one.
+ self.flow_impl = list(implementations.values())[0]
+ return await self.async_step_auth()
+
+ return self.async_show_form(
+ step_id="pick_implementation",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ "implementation", default=list(implementations.keys())[0]
+ ): vol.In({key: impl.name for key, impl in implementations.items()})
+ }
+ ),
+ )
+
+ async def async_step_auth(self, user_input: dict = None) -> dict:
+ """Create an entry for auth."""
+ # Flow has been triggered by external data
+ if user_input:
+ self.external_data = user_input
+ return self.async_external_step_done(next_step_id="creation")
+
+ try:
+ with async_timeout.timeout(10):
+ url = await self.flow_impl.async_generate_authorize_url(self.flow_id)
+ except asyncio.TimeoutError:
+ return self.async_abort(reason="authorize_url_timeout")
+
+ url = str(URL(url).update_query(self.extra_authorize_data))
+
+ return self.async_external_step(step_id="auth", url=url)
+
+ async def async_step_creation(self, user_input: dict = None) -> dict:
+ """Create config entry from external data."""
+ token = await self.flow_impl.async_resolve_external_data(self.external_data)
+ token["expires_at"] = time.time() + token["expires_in"]
+
+ self.logger.info("Successfully authenticated")
+
+ return await self.async_oauth_create_entry(
+ {"auth_implementation": self.flow_impl.domain, "token": token}
+ )
+
+ async def async_oauth_create_entry(self, data: dict) -> dict:
+ """Create an entry for the flow.
+
+ Ok to override if you want to fetch extra info or even add another step.
+ """
+ return self.async_create_entry(title=self.flow_impl.name, data=data)
+
+ async_step_user = async_step_pick_implementation
+ async_step_ssdp = async_step_pick_implementation
+ async_step_zeroconf = async_step_pick_implementation
+ async_step_homekit = async_step_pick_implementation
+
+ @classmethod
+ def async_register_implementation(
+ cls, hass: HomeAssistant, local_impl: LocalOAuth2Implementation
+ ) -> None:
+ """Register a local implementation."""
+ async_register_implementation(hass, cls.DOMAIN, local_impl)
+
+
+@callback
+def async_register_implementation(
+ hass: HomeAssistant, domain: str, implementation: AbstractOAuth2Implementation
+) -> None:
+ """Register an OAuth2 flow implementation for an integration."""
+ if isinstance(implementation, LocalOAuth2Implementation) and not hass.data.get(
+ DATA_VIEW_REGISTERED, False
+ ):
+ hass.http.register_view(OAuth2AuthorizeCallbackView()) # type: ignore
+ hass.data[DATA_VIEW_REGISTERED] = True
+
+ implementations = hass.data.setdefault(DATA_IMPLEMENTATIONS, {})
+ implementations.setdefault(domain, {})[implementation.domain] = implementation
+
+
+async def async_get_implementations(
+ hass: HomeAssistant, domain: str
+) -> Dict[str, AbstractOAuth2Implementation]:
+ """Return OAuth2 implementations for specified domain."""
+ return cast(
+ Dict[str, AbstractOAuth2Implementation],
+ hass.data.setdefault(DATA_IMPLEMENTATIONS, {}).get(domain, {}),
+ )
+
+
+async def async_get_config_entry_implementation(
+ hass: HomeAssistant, config_entry: config_entries.ConfigEntry
+) -> AbstractOAuth2Implementation:
+ """Return the implementation for this config entry."""
+ implementations = await async_get_implementations(hass, config_entry.domain)
+ implementation = implementations.get(config_entry.data["auth_implementation"])
+
+ if implementation is None:
+ raise ValueError("Implementation not available")
+
+ return implementation
+
+
+class OAuth2AuthorizeCallbackView(HomeAssistantView):
+ """OAuth2 Authorization Callback View."""
+
+ requires_auth = False
+ url = AUTH_CALLBACK_PATH
+ name = "auth:external:callback"
+
+ async def get(self, request: web.Request) -> web.Response:
+ """Receive authorization code."""
+ if "code" not in request.query or "state" not in request.query:
+ return web.Response(
+ text=f"Missing code or state parameter in {request.url}"
+ )
+
+ hass = request.app["hass"]
+
+ state = _decode_jwt(hass, request.query["state"])
+
+ if state is None:
+ return web.Response(text=f"Invalid state")
+
+ await hass.config_entries.flow.async_configure(
+ flow_id=state["flow_id"], user_input=request.query["code"]
+ )
+
+ return web.Response(
+ headers={"content-type": "text/html"},
+ text="",
+ )
+
+
+class OAuth2Session:
+ """Session to make requests authenticated with OAuth2."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ config_entry: config_entries.ConfigEntry,
+ implementation: AbstractOAuth2Implementation,
+ ):
+ """Initialize an OAuth2 session."""
+ self.hass = hass
+ self.config_entry = config_entry
+ self.implementation = implementation
+
+ async def async_ensure_token_valid(self) -> None:
+ """Ensure that the current token is valid."""
+ token = self.config_entry.data["token"]
+
+ if token["expires_at"] > time.time():
+ return
+
+ new_token = await self.implementation.async_refresh_token(token)
+
+ self.hass.config_entries.async_update_entry( # type: ignore
+ self.config_entry, data={**self.config_entry.data, "token": new_token}
+ )
+
+ async def async_request(
+ self, method: str, url: str, **kwargs: Any
+ ) -> client.ClientResponse:
+ """Make a request."""
+ await self.async_ensure_token_valid()
+ return await async_oauth2_request(
+ self.hass, self.config_entry.data["token"], method, url, **kwargs
+ )
+
+
+async def async_oauth2_request(
+ hass: HomeAssistant, token: dict, method: str, url: str, **kwargs: Any
+) -> client.ClientResponse:
+ """Make an OAuth2 authenticated request.
+
+ This method will not refresh tokens. Use OAuth2 session for that.
+ """
+ session = async_get_clientsession(hass)
+
+ return await session.request(
+ method,
+ url,
+ **kwargs,
+ headers={
+ **(kwargs.get("headers") or {}),
+ "authorization": f"Bearer {token['access_token']}",
+ },
+ )
+
+
+@callback
+def _encode_jwt(hass: HomeAssistant, data: dict) -> str:
+ """JWT encode data."""
+ secret = hass.data.get(DATA_JWT_SECRET)
+
+ if secret is None:
+ secret = hass.data[DATA_JWT_SECRET] = generate_secret()
+
+ return jwt.encode(data, secret, algorithm="HS256").decode()
+
+
+@callback
+def _decode_jwt(hass: HomeAssistant, encoded: str) -> Optional[dict]:
+ """JWT encode data."""
+ secret = cast(str, hass.data.get(DATA_JWT_SECRET))
+
+ try:
+ return jwt.decode(encoded, secret, algorithms=["HS256"])
+ except jwt.InvalidTokenError:
+ return None
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 2d1bb89d23a..7ca5a7e86f9 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -386,6 +386,7 @@ def remove_falsy(value: List[T]) -> List[T]:
def service(value):
"""Validate service."""
# Services use same format as entities so we can use same helper.
+ value = string(value).lower()
if valid_entity_id(value):
return value
raise vol.Invalid("Service {} does not match format .".format(value))
@@ -600,7 +601,8 @@ def deprecated(
if module is not None:
module_name = module.__name__
else:
- # Unclear when it is None, but it happens, so let's guard.
+ # If Python is unable to access the sources files, the call stack frame
+ # will be missing information, so let's guard.
# https://github.com/home-assistant/home-assistant/issues/24982
module_name = __name__
@@ -884,6 +886,8 @@ DEVICE_ACTION_BASE_SCHEMA = vol.Schema(
DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
+_SCRIPT_SCENE_SCHEMA = vol.Schema({vol.Required("scene"): entity_domain("scene")})
+
SCRIPT_SCHEMA = vol.All(
ensure_list,
[
@@ -894,6 +898,7 @@ SCRIPT_SCHEMA = vol.All(
EVENT_SCHEMA,
CONDITION_SCHEMA,
DEVICE_ACTION_SCHEMA,
+ _SCRIPT_SCENE_SCHEMA,
)
],
)
diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py
index 881534b5bed..2a4fafde75b 100644
--- a/homeassistant/helpers/deprecation.py
+++ b/homeassistant/helpers/deprecation.py
@@ -54,7 +54,15 @@ def get_deprecated(
and a warning is issued to the user.
"""
if old_name in config:
- module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ # type: ignore
+ module = inspect.getmodule(inspect.stack()[1][0])
+ if module is not None:
+ module_name = module.__name__
+ else:
+ # If Python is unable to access the sources files, the call stack frame
+ # will be missing information, so let's guard.
+ # https://github.com/home-assistant/home-assistant/issues/24982
+ module_name = __name__
+
logger = logging.getLogger(module_name)
logger.warning(
"'%s' is deprecated. Please rename '%s' to '%s' in your "
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index 836ad954ae0..0d2182f88e1 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -148,7 +148,8 @@ class Entity:
def state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return the state attributes.
- Implemented by component base class.
+ Implemented by component base class. Convention for attribute names
+ is lowercase snake_case.
"""
return None
@@ -156,7 +157,8 @@ class Entity:
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return device specific state attributes.
- Implemented by platform classes.
+ Implemented by platform classes. Convention for attribute names
+ is lowercase snake_case.
"""
return None
@@ -551,6 +553,19 @@ class Entity:
"""Return the representation."""
return "".format(self.name, self.state)
+ # call an requests
+ async def async_request_call(self, coro):
+ """Process request batched."""
+
+ if self.parallel_updates:
+ await self.parallel_updates.acquire()
+
+ try:
+ await coro
+ finally:
+ if self.parallel_updates:
+ self.parallel_updates.release()
+
class ToggleEntity(Entity):
"""An abstract class for entities that can be turned on and off."""
diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py
index b7707b844d4..e819da9873a 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -1,13 +1,13 @@
"""Helpers for listening to events."""
from datetime import datetime, timedelta
import functools as ft
-from typing import Callable
+from typing import Any, Callable, Iterable, Optional, Union
import attr
from homeassistant.loader import bind_hass
from homeassistant.helpers.sun import get_astral_event_next
-from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE
+from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event
from homeassistant.const import (
ATTR_NOW,
EVENT_STATE_CHANGED,
@@ -240,7 +240,9 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim
@callback
@bind_hass
-def async_call_later(hass, delay, action):
+def async_call_later(
+ hass: HomeAssistant, delay: float, action: Callable[..., None]
+) -> CALLBACK_TYPE:
"""Add a listener that is called in ."""
return async_track_point_in_utc_time(
hass, action, dt_util.utcnow() + timedelta(seconds=delay)
@@ -252,7 +254,9 @@ call_later = threaded_listener_factory(async_call_later)
@callback
@bind_hass
-def async_track_time_interval(hass, action, interval):
+def async_track_time_interval(
+ hass: HomeAssistant, action: Callable[..., None], interval: timedelta
+) -> CALLBACK_TYPE:
"""Add a listener that fires repetitively at every timedelta interval."""
remove = None
@@ -284,14 +288,14 @@ class SunListener:
"""Helper class to help listen to sun events."""
hass = attr.ib(type=HomeAssistant)
- action = attr.ib(type=Callable)
- event = attr.ib(type=str)
- offset = attr.ib(type=timedelta)
- _unsub_sun: CALLBACK_TYPE = attr.ib(default=None)
- _unsub_config: CALLBACK_TYPE = attr.ib(default=None)
+ action: Callable[..., None] = attr.ib()
+ event: str = attr.ib()
+ offset: Optional[timedelta] = attr.ib()
+ _unsub_sun: Optional[CALLBACK_TYPE] = attr.ib(default=None)
+ _unsub_config: Optional[CALLBACK_TYPE] = attr.ib(default=None)
@callback
- def async_attach(self):
+ def async_attach(self) -> None:
"""Attach a sun listener."""
assert self._unsub_config is None
@@ -302,7 +306,7 @@ class SunListener:
self._listen_next_sun_event()
@callback
- def async_detach(self):
+ def async_detach(self) -> None:
"""Detach the sun listener."""
assert self._unsub_sun is not None
assert self._unsub_config is not None
@@ -313,7 +317,7 @@ class SunListener:
self._unsub_config = None
@callback
- def _listen_next_sun_event(self):
+ def _listen_next_sun_event(self) -> None:
"""Set up the sun event listener."""
assert self._unsub_sun is None
@@ -324,14 +328,14 @@ class SunListener:
)
@callback
- def _handle_sun_event(self, _now):
+ def _handle_sun_event(self, _now: Any) -> None:
"""Handle solar event."""
self._unsub_sun = None
self._listen_next_sun_event()
self.hass.async_run_job(self.action)
@callback
- def _handle_config_event(self, _event):
+ def _handle_config_event(self, _event: Any) -> None:
"""Handle core config update."""
assert self._unsub_sun is not None
self._unsub_sun()
@@ -341,7 +345,9 @@ class SunListener:
@callback
@bind_hass
-def async_track_sunrise(hass, action, offset=None):
+def async_track_sunrise(
+ hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None
+) -> CALLBACK_TYPE:
"""Add a listener that will fire a specified offset from sunrise daily."""
listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset)
listener.async_attach()
@@ -353,7 +359,9 @@ track_sunrise = threaded_listener_factory(async_track_sunrise)
@callback
@bind_hass
-def async_track_sunset(hass, action, offset=None):
+def async_track_sunset(
+ hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None
+) -> CALLBACK_TYPE:
"""Add a listener that will fire a specified offset from sunset daily."""
listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset)
listener.async_attach()
@@ -366,8 +374,13 @@ track_sunset = threaded_listener_factory(async_track_sunset)
@callback
@bind_hass
def async_track_utc_time_change(
- hass, action, hour=None, minute=None, second=None, local=False
-):
+ hass: HomeAssistant,
+ action: Callable[..., None],
+ hour: Optional[Any] = None,
+ minute: Optional[Any] = None,
+ second: Optional[Any] = None,
+ local: bool = False,
+) -> CALLBACK_TYPE:
"""Add a listener that will fire if time matches a pattern."""
# We do not have to wrap the function with time pattern matching logic
# if no pattern given
@@ -386,7 +399,7 @@ def async_track_utc_time_change(
next_time = None
- def calculate_next(now):
+ def calculate_next(now: datetime) -> None:
"""Calculate and set the next time the trigger should fire."""
nonlocal next_time
@@ -397,10 +410,10 @@ def async_track_utc_time_change(
# Make sure rolling back the clock doesn't prevent the timer from
# triggering.
- last_now = None
+ last_now: Optional[datetime] = None
@callback
- def pattern_time_change_listener(event):
+ def pattern_time_change_listener(event: Event) -> None:
"""Listen for matching time_changed events."""
nonlocal next_time, last_now
@@ -427,7 +440,13 @@ track_utc_time_change = threaded_listener_factory(async_track_utc_time_change)
@callback
@bind_hass
-def async_track_time_change(hass, action, hour=None, minute=None, second=None):
+def async_track_time_change(
+ hass: HomeAssistant,
+ action: Callable[..., None],
+ hour: Optional[Any] = None,
+ minute: Optional[Any] = None,
+ second: Optional[Any] = None,
+) -> CALLBACK_TYPE:
"""Add a listener that will fire if UTC time matches a pattern."""
return async_track_utc_time_change(hass, action, hour, minute, second, local=True)
@@ -435,7 +454,9 @@ def async_track_time_change(hass, action, hour=None, minute=None, second=None):
track_time_change = threaded_listener_factory(async_track_time_change)
-def _process_state_match(parameter):
+def _process_state_match(
+ parameter: Union[None, str, Iterable[str]]
+) -> Callable[[str], bool]:
"""Convert parameter to function that matches input against parameter."""
if parameter is None or parameter == MATCH_ALL:
return lambda _: True
@@ -443,5 +464,5 @@ def _process_state_match(parameter):
if isinstance(parameter, str) or not hasattr(parameter, "__iter__"):
return lambda state: state == parameter
- parameter = tuple(parameter)
- return lambda state: state in parameter
+ parameter_tuple = tuple(parameter)
+ return lambda state: state in parameter_tuple
diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py
index fdf52c99075..5d47f34b002 100644
--- a/homeassistant/helpers/restore_state.py
+++ b/homeassistant/helpers/restore_state.py
@@ -164,23 +164,20 @@ class RestoreStateData:
@callback
def async_setup_dump(self, *args: Any) -> None:
"""Set up the restore state listeners."""
+
+ def _async_dump_states(*_: Any) -> None:
+ self.hass.async_create_task(self.async_dump_states())
+
# Dump the initial states now. This helps minimize the risk of having
# old states loaded by overwritting the last states once home assistant
# has started and the old states have been read.
- self.hass.async_create_task(self.async_dump_states())
+ _async_dump_states()
# Dump states periodically
- async_track_time_interval(
- self.hass,
- lambda *_: self.hass.async_create_task(self.async_dump_states()),
- STATE_DUMP_INTERVAL,
- )
+ async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL)
# Dump states when stopping hass
- self.hass.bus.async_listen_once(
- EVENT_HOMEASSISTANT_STOP,
- lambda *_: self.hass.async_create_task(self.async_dump_states()),
- )
+ self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states)
@callback
def async_restore_entity_added(self, entity_id: str) -> None:
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index 05b28102726..1e65c24eaaf 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -9,12 +9,15 @@ from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any
import voluptuous as vol
import homeassistant.components.device_automation as device_automation
+import homeassistant.components.scene as scene
from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE
from homeassistant.const import (
+ ATTR_ENTITY_ID,
CONF_CONDITION,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_TIMEOUT,
+ SERVICE_TURN_ON,
)
from homeassistant import exceptions
from homeassistant.helpers import (
@@ -46,6 +49,7 @@ CONF_EVENT_DATA_TEMPLATE = "event_data_template"
CONF_DELAY = "delay"
CONF_WAIT_TEMPLATE = "wait_template"
CONF_CONTINUE = "continue_on_timeout"
+CONF_SCENE = "scene"
ACTION_DELAY = "delay"
@@ -54,6 +58,7 @@ ACTION_CHECK_CONDITION = "condition"
ACTION_FIRE_EVENT = "event"
ACTION_CALL_SERVICE = "call_service"
ACTION_DEVICE_AUTOMATION = "device"
+ACTION_ACTIVATE_SCENE = "scene"
def _determine_action(action):
@@ -73,6 +78,9 @@ def _determine_action(action):
if CONF_DEVICE_ID in action:
return ACTION_DEVICE_AUTOMATION
+ if CONF_SCENE in action:
+ return ACTION_ACTIVATE_SCENE
+
return ACTION_CALL_SERVICE
@@ -147,6 +155,7 @@ class Script:
ACTION_FIRE_EVENT: self._async_fire_event,
ACTION_CALL_SERVICE: self._async_call_service,
ACTION_DEVICE_AUTOMATION: self._async_device_automation,
+ ACTION_ACTIVATE_SCENE: self._async_activate_scene,
}
@property
@@ -362,6 +371,21 @@ class Script:
self.hass, action, variables, context
)
+ async def _async_activate_scene(self, action, variables, context):
+ """Activate the scene specified in the action.
+
+ This method is a coroutine.
+ """
+ self.last_action = action.get(CONF_ALIAS, "activate scene")
+ self._log("Executing step %s" % self.last_action)
+ await self.hass.services.async_call(
+ scene.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: action[CONF_SCENE]},
+ blocking=True,
+ context=context,
+ )
+
async def _async_fire_event(self, action, variables, context):
"""Fire an event."""
self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT])
diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py
index 2f49a566a32..4cb7fb85bff 100644
--- a/homeassistant/helpers/state.py
+++ b/homeassistant/helpers/state.py
@@ -11,11 +11,9 @@ from homeassistant.loader import bind_hass, async_get_integration, IntegrationNo
import homeassistant.util.dt as dt_util
from homeassistant.components.notify import ATTR_MESSAGE, SERVICE_NOTIFY
from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON
-from homeassistant.components.mysensors.switch import ATTR_IR_CODE, SERVICE_SEND_IR_CODE
from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION
from homeassistant.const import (
ATTR_ENTITY_ID,
- ATTR_OPTION,
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_DISARM,
@@ -41,7 +39,6 @@ from homeassistant.const import (
STATE_OPEN,
STATE_UNKNOWN,
STATE_UNLOCKED,
- SERVICE_SELECT_OPTION,
)
from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN
from .typing import HomeAssistantType
@@ -54,8 +51,6 @@ GROUP_DOMAIN = "group"
# Each item is a service with a list of required attributes.
SERVICE_ATTRIBUTES = {
SERVICE_NOTIFY: [ATTR_MESSAGE],
- SERVICE_SEND_IR_CODE: [ATTR_IR_CODE],
- SERVICE_SELECT_OPTION: [ATTR_OPTION],
SERVICE_SET_COVER_POSITION: [ATTR_POSITION],
SERVICE_SET_COVER_TILT_POSITION: [ATTR_TILT_POSITION],
}
diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py
index cd99a47cf57..bd18eebfb25 100644
--- a/homeassistant/helpers/storage.py
+++ b/homeassistant/helpers/storage.py
@@ -6,13 +6,14 @@ import os
from typing import Dict, List, Optional, Callable, Union, Any, Type
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE
from homeassistant.loader import bind_hass
from homeassistant.util import json as json_util
from homeassistant.helpers.event import async_call_later
# mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any
+# mypy: no-check-untyped-defs
STORAGE_DIR = ".storage"
_LOGGER = logging.getLogger(__name__)
@@ -71,8 +72,8 @@ class Store:
self.hass = hass
self._private = private
self._data: Optional[Dict[str, Any]] = None
- self._unsub_delay_listener = None
- self._unsub_stop_listener = None
+ self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None
+ self._unsub_stop_listener: Optional[CALLBACK_TYPE] = None
self._write_lock = asyncio.Lock()
self._load_task: Optional[asyncio.Future] = None
self._encoder = encoder
@@ -136,9 +137,7 @@ class Store:
await self._async_handle_write_data()
@callback
- def async_delay_save(
- self, data_func: Callable[[], Dict], delay: Optional[int] = None
- ) -> None:
+ def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> None:
"""Save data with an optional delay."""
self._data = {"version": self.version, "key": self.key, "data_func": data_func}
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 9af1998e894..1d9ca691451 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -884,6 +884,16 @@ def ordinal(value):
)
+def from_json(value):
+ """Convert a JSON string to an object."""
+ return json.loads(value)
+
+
+def to_json(value):
+ """Convert an object to a JSON string."""
+ return json.dumps(value)
+
+
@contextfilter
def random_every_time(context, values):
"""Choose a random value.
@@ -916,6 +926,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.filters["timestamp_custom"] = timestamp_custom
self.filters["timestamp_local"] = timestamp_local
self.filters["timestamp_utc"] = timestamp_utc
+ self.filters["to_json"] = to_json
+ self.filters["from_json"] = from_json
self.filters["is_defined"] = fail_when_undefined
self.filters["max"] = max
self.filters["min"] = min
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 19acae64b16..85bb00ce6eb 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -6,22 +6,22 @@ astral==1.10.1
async_timeout==3.0.1
attrs==19.2.0
bcrypt==3.1.7
-certifi>=2019.6.16
+certifi>=2019.9.11
contextvars==2.4;python_version<"3.7"
-cryptography==2.7
+cryptography==2.8
distro==1.4.0
hass-nabucasa==0.22
-home-assistant-frontend==20191002.2
+home-assistant-frontend==20191025.1
importlib-metadata==0.23
jinja2>=2.10.1
netdisco==2.6.0
pip>=8.0.3
-python-slugify==3.0.4
-pytz>=2019.02
+python-slugify==3.0.6
+pytz>=2019.03
pyyaml==5.1.2
requests==2.22.0
ruamel.yaml==0.15.100
-sqlalchemy==1.3.8
+sqlalchemy==1.3.10
voluptuous-serialize==2.3.0
voluptuous==0.11.7
zeroconf==0.23.0
@@ -33,6 +33,3 @@ enum34==1000000000.0.0
# This is a old unmaintained library and is replaced with pycryptodome
pycrypto==1000000000.0.0
-
-# Contains code to modify Home Assistant to work around our rules
-python-systemair-savecair==1000000000.0.0
diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py
index 6ca422b595b..0c5623a50ad 100644
--- a/homeassistant/scripts/keyring.py
+++ b/homeassistant/scripts/keyring.py
@@ -8,7 +8,7 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE
# mypy: allow-untyped-defs
-REQUIREMENTS = ["keyring==17.1.1", "keyrings.alt==3.1.1"]
+REQUIREMENTS = ["keyring==19.2.0", "keyrings.alt==3.1.1"]
def run(args):
diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py
index 89d1dcfc4c1..640e5c5540a 100644
--- a/homeassistant/util/color.py
+++ b/homeassistant/util/color.py
@@ -167,8 +167,8 @@ COLORS = {
class XYPoint:
"""Represents a CIE 1931 XY coordinate pair."""
- x = attr.ib(type=float)
- y = attr.ib(type=float)
+ x = attr.ib(type=float) # pylint: disable=invalid-name
+ y = attr.ib(type=float) # pylint: disable=invalid-name
@attr.s()
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index a948c4407ae..1abb4294398 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -220,7 +220,7 @@ def get_age(date: dt.datetime) -> str:
def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> List[int]:
"""Parse the time expression part and return a list of times to match."""
if parameter is None or parameter == MATCH_ALL:
- res = [x for x in range(min_value, max_value + 1)]
+ res = list(range(min_value, max_value + 1))
elif isinstance(parameter, str) and parameter.startswith("/"):
parameter = int(parameter[1:])
res = [x for x in range(min_value, max_value + 1) if x % parameter == 0]
diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py
index f81c40a52bb..7c61a8ab1e9 100644
--- a/homeassistant/util/location.py
+++ b/homeassistant/util/location.py
@@ -6,7 +6,7 @@ detect_location_info and elevation are mocked by default during tests.
import asyncio
import collections
import math
-from typing import Any, Optional, Tuple, Dict, cast
+from typing import Any, Optional, Tuple, Dict
import aiohttp
@@ -159,7 +159,7 @@ def vincenty(
if miles:
s *= MILES_PER_KILOMETER # kilometers to miles
- return round(cast(float, s), 6)
+ return round(s, 6)
async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]:
diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py
index 99e606d2866..de04f23d9dd 100644
--- a/homeassistant/util/logging.py
+++ b/homeassistant/util/logging.py
@@ -130,7 +130,15 @@ def catch_log_exception(
"""Decorate a callback to catch and log exceptions."""
def log_exception(*args: Any) -> None:
- module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ # type: ignore
+ module = inspect.getmodule(inspect.stack()[1][0])
+ if module is not None:
+ module_name = module.__name__
+ else:
+ # If Python is unable to access the sources files, the call stack frame
+ # will be missing information, so let's guard.
+ # https://github.com/home-assistant/home-assistant/issues/24982
+ module_name = __name__
+
# Do not print the wrapper in the traceback
frames = len(inspect.trace()) - 1
exc_msg = traceback.format_exc(-frames)
@@ -178,9 +186,15 @@ def catch_log_coro_exception(
try:
return await target
except Exception: # pylint: disable=broad-except
- module_name = inspect.getmodule( # type: ignore
- inspect.trace()[1][0]
- ).__name__
+ module = inspect.getmodule(inspect.stack()[1][0])
+ if module is not None:
+ module_name = module.__name__
+ else:
+ # If Python is unable to access the sources files, the frame
+ # will be missing information, so let's guard.
+ # https://github.com/home-assistant/home-assistant/issues/24982
+ module_name = __name__
+
# Do not print the wrapper in the traceback
frames = len(inspect.trace()) - 1
exc_msg = traceback.format_exc(-frames)
diff --git a/mypyrc b/mypyrc
deleted file mode 100644
index 08413ecd23c..00000000000
--- a/mypyrc
+++ /dev/null
@@ -1,38 +0,0 @@
-homeassistant/*.py
-homeassistant/auth/
-homeassistant/components/*.py
-homeassistant/components/automation/
-homeassistant/components/binary_sensor/
-homeassistant/components/calendar/
-homeassistant/components/camera/
-homeassistant/components/cover/
-homeassistant/components/device_automation/
-homeassistant/components/frontend/
-homeassistant/components/geo_location/
-homeassistant/components/group/
-homeassistant/components/history/
-homeassistant/components/http/
-homeassistant/components/image_processing/
-homeassistant/components/integration/
-homeassistant/components/light/
-homeassistant/components/lock/
-homeassistant/components/mailbox/
-homeassistant/components/media_player/
-homeassistant/components/notify/
-homeassistant/components/persistent_notification/
-homeassistant/components/proximity/
-homeassistant/components/remote/
-homeassistant/components/scene/
-homeassistant/components/sensor/
-homeassistant/components/sun/
-homeassistant/components/switch/
-homeassistant/components/systemmonitor/
-homeassistant/components/tts/
-homeassistant/components/vacuum/
-homeassistant/components/water_heater/
-homeassistant/components/weather/
-homeassistant/components/websocket_api/
-homeassistant/components/zone/
-homeassistant/helpers/
-homeassistant/scripts/
-homeassistant/util/
diff --git a/pylintrc b/pylintrc
index bb4f1fe96d0..4aced384b63 100644
--- a/pylintrc
+++ b/pylintrc
@@ -1,8 +1,11 @@
[MASTER]
ignore=tests
+# Use a conservative default here; 2 should speed up most setups and not hurt
+# any too bad. Override on command line as appropriate.
+jobs=2
[BASIC]
-good-names=i,j,k,ex,Run,_,fp
+good-names=id,i,j,k,ex,Run,_,fp
[MESSAGES CONTROL]
# Reasons disabled:
@@ -18,8 +21,8 @@ good-names=i,j,k,ex,Run,_,fp
# too-few-* - same as too-many-*
# abstract-method - with intro of async there are always methods missing
# inconsistent-return-statements - doesn't handle raise
-# not-an-iterable - https://github.com/PyCQA/pylint/issues/2311
# unnecessary-pass - readability for functions which only contain pass
+# import-outside-toplevel - TODO
disable=
format,
abstract-class-little-used,
@@ -27,9 +30,9 @@ disable=
cyclic-import,
duplicate-code,
global-statement,
+ import-outside-toplevel,
inconsistent-return-statements,
locally-disabled,
- not-an-iterable,
not-context-manager,
redefined-variable-type,
too-few-public-methods,
diff --git a/requirements_all.txt b/requirements_all.txt
index 448504eba5b..df9cafe7aa0 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -4,15 +4,15 @@ astral==1.10.1
async_timeout==3.0.1
attrs==19.2.0
bcrypt==3.1.7
-certifi>=2019.6.16
+certifi>=2019.9.11
contextvars==2.4;python_version<"3.7"
importlib-metadata==0.23
jinja2>=2.10.1
PyJWT==1.7.1
-cryptography==2.7
+cryptography==2.8
pip>=8.0.3
-python-slugify==3.0.4
-pytz>=2019.02
+python-slugify==3.0.6
+pytz>=2019.03
pyyaml==5.1.2
requests==2.22.0
ruamel.yaml==0.15.100
@@ -38,16 +38,16 @@ Adafruit-SHT31==1.0.2
HAP-python==2.6.0
# homeassistant.components.mastodon
-Mastodon.py==1.4.6
+Mastodon.py==1.5.0
# homeassistant.components.orangepi_gpio
-OPi.GPIO==0.3.6
+OPi.GPIO==0.4.0
# homeassistant.components.essent
PyEssent==0.13
# homeassistant.components.github
-PyGithub==1.43.5
+PyGithub==1.43.8
# homeassistant.components.isy994
PyISY==1.1.2
@@ -56,7 +56,7 @@ PyISY==1.1.2
PyMVGLive==1.1.4
# homeassistant.components.arduino
-PyMata==2.14
+PyMata==2.20
# homeassistant.components.mobile_app
# homeassistant.components.owntracks
@@ -66,7 +66,7 @@ PyNaCl==1.3.0
PyQRCode==1.2.1
# homeassistant.components.rmvtransport
-PyRMVtransport==0.1.3
+PyRMVtransport==0.2.9
# homeassistant.components.switchbot
# PySwitchbot==0.6.2
@@ -82,10 +82,10 @@ PyXiaomiGateway==0.12.4
# homeassistant.components.mcp23017
# homeassistant.components.rpi_gpio
-# RPi.GPIO==0.6.5
+# RPi.GPIO==0.7.0
# homeassistant.components.remember_the_milk
-RtmAPI==0.7.0
+RtmAPI==0.7.2
# homeassistant.components.travisci
TravisPy==0.3.5
@@ -103,7 +103,7 @@ WazeRouteCalculator==0.10
YesssSMS==0.4.1
# homeassistant.components.abode
-abodepy==0.15.0
+abodepy==0.16.6
# homeassistant.components.mcp23017
adafruit-blinka==1.2.1
@@ -112,10 +112,10 @@ adafruit-blinka==1.2.1
adafruit-circuitpython-mcp230xx==1.1.2
# homeassistant.components.androidtv
-adb-shell==0.0.4
+adb-shell==0.0.7
# homeassistant.components.adguard
-adguardhome==0.2.1
+adguardhome==0.3.0
# homeassistant.components.frontier_silicon
afsapi==0.0.4
@@ -139,7 +139,7 @@ aiobotocore==0.10.2
aiodns==2.0.0
# homeassistant.components.esphome
-aioesphomeapi==2.2.0
+aioesphomeapi==2.4.2
# homeassistant.components.freebox
aiofreepybox==0.0.8
@@ -184,6 +184,9 @@ aiounifi==11
# homeassistant.components.wwlln
aiowwlln==2.0.2
+# homeassistant.components.airly
+airly==0.0.2
+
# homeassistant.components.aladdin_connect
aladdin_connect==0.3
@@ -191,7 +194,7 @@ aladdin_connect==0.3
alarmdecoder==1.13.2
# homeassistant.components.alpha_vantage
-alpha_vantage==2.1.0
+alpha_vantage==2.1.1
# homeassistant.components.ambiclimate
ambiclimate==0.2.1
@@ -200,7 +203,7 @@ ambiclimate==0.2.1
amcrest==1.5.3
# homeassistant.components.androidtv
-androidtv==0.0.30
+androidtv==0.0.32
# homeassistant.components.anel_pwrctrl
anel_pwrctrl-homeassistant==0.0.1.dev2
@@ -214,6 +217,9 @@ apcaccess==0.0.13
# homeassistant.components.apns
apns2==0.3.0
+# homeassistant.components.apprise
+apprise==0.8.1
+
# homeassistant.components.aprs
aprslib==0.6.46
@@ -264,7 +270,7 @@ batinfo==0.4.2
# beacontools[scan]==1.2.3
# homeassistant.components.scrape
-beautifulsoup4==4.8.0
+beautifulsoup4==4.8.1
# homeassistant.components.beewi_smartclim
beewi_smartclim==0.0.7
@@ -279,7 +285,7 @@ bimmer_connected==0.6.0
bizkaibus==0.1.1
# homeassistant.components.blink
-blinkpy==0.14.1
+blinkpy==0.14.2
# homeassistant.components.blinksticklight
blinkstick==1.1.8
@@ -308,7 +314,7 @@ boto3==1.9.233
braviarc-homeassistant==0.3.7.dev0
# homeassistant.components.broadlink
-broadlink==0.11.1
+broadlink==0.12.0
# homeassistant.components.brottsplatskartan
brottsplatskartan==0.0.1
@@ -340,6 +346,9 @@ ciscosparkapi==0.4.2
# homeassistant.components.cppm_tracker
clearpasspy==1.0.2
+# homeassistant.components.sinch
+clx-sdk-xms==1.0.0
+
# homeassistant.components.co2signal
co2signal==0.4.2
@@ -399,7 +408,7 @@ directpy==0.5
discogs_client==2.2.1
# homeassistant.components.discord
-discord.py==1.2.3
+discord.py==1.2.4
# homeassistant.components.updater
distro==1.4.0
@@ -447,7 +456,7 @@ enocean==0.50
enturclient==0.2.0
# homeassistant.components.environment_canada
-env_canada==0.0.25
+env_canada==0.0.27
# homeassistant.components.envirophat
# envirophat==0.0.6
@@ -471,7 +480,7 @@ eternalegypt==0.0.10
# evdev==0.6.1
# homeassistant.components.evohome
-evohome-async==0.3.3b4
+evohome-async==0.3.4b1
# homeassistant.components.dlib_face_detect
# homeassistant.components.dlib_face_identify
@@ -510,7 +519,7 @@ freesms==0.1.2
# homeassistant.components.fritz
# homeassistant.components.fritzbox_callmonitor
# homeassistant.components.fritzbox_netmonitor
-# fritzconnection==0.6.5
+# fritzconnection==0.8.4
# homeassistant.components.fritzdect
fritzhome==1.0.4
@@ -525,7 +534,7 @@ gearbest_parser==1.0.7
geizhals==0.0.9
# homeassistant.components.geniushub
-geniushub-client==0.6.26
+geniushub-client==0.6.28
# homeassistant.components.geo_json_events
# homeassistant.components.nsw_rural_fire_service_feed
@@ -613,7 +622,7 @@ hass-nabucasa==0.22
hbmqtt==0.9.5
# homeassistant.components.jewish_calendar
-hdate==0.9.0
+hdate==0.9.1
# homeassistant.components.heatmiser
heatmiserV3==0.9.1
@@ -624,9 +633,6 @@ herepy==0.6.3.1
# homeassistant.components.hikvisioncam
hikvision==0.4
-# homeassistant.components.hipchat
-hipnotify==1.0.8
-
# homeassistant.components.harman_kardon_avr
hkavr==0.0.5
@@ -640,7 +646,7 @@ hole==0.5.0
holidays==0.9.11
# homeassistant.components.frontend
-home-assistant-frontend==20191002.2
+home-assistant-frontend==20191025.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.4
@@ -649,7 +655,7 @@ homeassistant-pyozw==0.1.4
homekit[IP]==0.15.0
# homeassistant.components.homematicip_cloud
-homematicip==0.10.12
+homematicip==0.10.13
# homeassistant.components.horizon
horimote==0.4.1
@@ -715,7 +721,7 @@ kaiterra-async-client==0.0.2
keba-kecontact==0.2.0
# homeassistant.scripts.keyring
-keyring==17.1.1
+keyring==19.2.0
# homeassistant.scripts.keyring
keyrings.alt==3.1.1
@@ -844,7 +850,7 @@ n26==0.2.7
nad_receiver==0.0.11
# homeassistant.components.keenetic_ndms2
-ndms2_client==0.0.9
+ndms2_client==0.0.10
# homeassistant.components.ness_alarm
nessclient==0.9.15
@@ -878,7 +884,7 @@ nuheat==0.3.0
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==1.17.1
+numpy==1.17.3
# homeassistant.components.oasa_telematics
oasatelematics==0.3
@@ -890,7 +896,7 @@ oauth2client==4.0.0
oemthermostat==1.1
# homeassistant.components.onkyo
-onkyo-eiscp==1.2.4
+onkyo-eiscp==1.2.7
# homeassistant.components.onvif
onvif-zeep-async==0.2.0
@@ -911,7 +917,10 @@ opensensemap-api==0.1.5
openwebifpy==3.1.1
# homeassistant.components.luci
-openwrt-luci-rpc==1.1.1
+openwrt-luci-rpc==1.1.2
+
+# homeassistant.components.oru
+oru==0.1.9
# homeassistant.components.orvibo
orvibo==1.1.1
@@ -953,7 +962,7 @@ pilight==0.1.1
# homeassistant.components.image_processing
# homeassistant.components.proxy
# homeassistant.components.qrcode
-pillow==6.1.0
+pillow==6.2.0
# homeassistant.components.dominos
pizzapi==0.0.3
@@ -962,7 +971,10 @@ pizzapi==0.0.3
plexapi==3.0.6
# homeassistant.components.plex
-plexauth==0.0.4
+plexauth==0.0.5
+
+# homeassistant.components.plex
+plexwebsocket==0.0.3
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -1078,7 +1090,7 @@ pyalarmdotcom==0.3.2
pyarlo==0.2.3
# homeassistant.components.netatmo
-pyatmo==2.2.1
+pyatmo==2.3.2
# homeassistant.components.atome
pyatome==0.1.1
@@ -1096,7 +1108,7 @@ pyblackbird==0.5
# pybluez==0.22
# homeassistant.components.neato
-pybotvac==0.0.15
+pybotvac==0.0.17
# homeassistant.components.nissan_leaf
pycarwings2==2.9
@@ -1120,7 +1132,7 @@ pycomfoconnect==0.3
pycoolmasternet==0.0.4
# homeassistant.components.microsoft
-pycsspeechtts==1.0.2
+pycsspeechtts==1.0.3
# homeassistant.components.cups
# pycups==1.9.73
@@ -1132,7 +1144,7 @@ pydaikin==1.6.1
pydanfossair==0.1.0
# homeassistant.components.deconz
-pydeconz==63
+pydeconz==64
# homeassistant.components.delijn
pydelijn==0.5.1
@@ -1165,7 +1177,7 @@ pyeight==0.1.1
pyemby==1.6
# homeassistant.components.envisalink
-pyenvisalink==3.8
+pyenvisalink==4.0
# homeassistant.components.ephember
pyephember==0.2.0
@@ -1202,7 +1214,7 @@ pyfttt==0.3
# homeassistant.components.bluetooth_le_tracker
# homeassistant.components.skybeacon
-pygatt[GATTTOOL]==4.0.1
+pygatt[GATTTOOL]==4.0.5
# homeassistant.components.gogogate2
pygogogate2==0.1.1
@@ -1220,20 +1232,17 @@ pyhaversion==3.1.0
pyheos==0.6.0
# homeassistant.components.hikvision
-pyhik==0.2.3
+pyhik==0.2.4
# homeassistant.components.hive
-pyhiveapi==0.2.19.2
+pyhiveapi==0.2.19.3
# homeassistant.components.homematic
-pyhomematic==0.1.60
+pyhomematic==0.1.61
# homeassistant.components.homeworks
pyhomeworks==0.0.6
-# homeassistant.components.hydroquebec
-pyhydroquebec==2.2.2
-
# homeassistant.components.ialarm
pyialarm==0.3
@@ -1298,7 +1307,7 @@ pymailgunner==1.4
pymediaroom==0.6.4
# homeassistant.components.somfy
-pymfy==0.5.2
+pymfy==0.6.0
# homeassistant.components.xiaomi_tv
pymitv==1.4.3
@@ -1312,6 +1321,9 @@ pymodbus==1.5.2
# homeassistant.components.monoprice
pymonoprice==0.3
+# homeassistant.components.msteams
+pymsteams==0.1.12
+
# homeassistant.components.yamaha_musiccast
pymusiccast==0.1.6
@@ -1364,7 +1376,7 @@ pyoppleio==1.0.5
pyota==2.0.5
# homeassistant.components.opentherm_gw
-pyotgw==0.4b4
+pyotgw==0.5b0
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@@ -1390,7 +1402,7 @@ pypjlink2==1.2.0
pypoint==1.1.1
# homeassistant.components.ps4
-pyps4-homeassistant==0.8.7
+pyps4-2ndscreen==1.0.1
# homeassistant.components.qwikswitch
pyqwikswitch==0.93
@@ -1411,7 +1423,7 @@ pyrepetier==3.0.5
pysabnzbd==1.1.0
# homeassistant.components.saj
-pysaj==0.0.9
+pysaj==0.0.12
# homeassistant.components.sony_projector
pysdcp==1
@@ -1450,7 +1462,7 @@ pysnmp==4.4.11
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.23
+pysonos==0.0.24
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -1458,9 +1470,6 @@ pyspcwebgw==0.4.0
# homeassistant.components.stiebel_eltron
pystiebeleltron==0.0.1.dev2
-# homeassistant.components.stride
-pystride==0.1.7
-
# homeassistant.components.suez_water
pysuez==0.1.17
@@ -1468,7 +1477,7 @@ pysuez==0.1.17
pysupla==0.0.3
# homeassistant.components.syncthru
-pysyncthru==0.4.3
+pysyncthru==0.5.0
# homeassistant.components.tautulli
pytautulli==0.5.0
@@ -1528,7 +1537,7 @@ python-juicenet==0.0.5
# python-lirc==1.2.3
# homeassistant.components.xiaomi_miio
-python-miio==0.4.5
+python-miio==0.4.6
# homeassistant.components.mpd
python-mpd2==1.0.0
@@ -1594,7 +1603,7 @@ python_opendata_transport==0.1.4
pythonegardia==1.0.40
# homeassistant.components.tile
-pytile==2.0.6
+pytile==3.0.0
# homeassistant.components.touchline
pytouchline==0.7
@@ -1817,7 +1826,7 @@ spotipy-homeassistant==2.4.4.dev1
# homeassistant.components.recorder
# homeassistant.components.sql
-sqlalchemy==1.3.8
+sqlalchemy==1.3.10
# homeassistant.components.starlingbank
starlingbank==3.1
@@ -1839,6 +1848,9 @@ stringcase==1.2.0
# homeassistant.components.ecovacs
sucks==0.9.4
+# homeassistant.components.solarlog
+sunwatcher==0.2.1
+
# homeassistant.components.swiss_hydrological_data
swisshydrodata==0.0.3
@@ -1873,7 +1885,7 @@ temperusb==1.5.3
# tensorflow==1.13.2
# homeassistant.components.tesla
-teslajsonpy==0.0.25
+teslajsonpy==0.0.26
# homeassistant.components.thermoworks_smoke
thermoworks_smoke==0.1.8
@@ -1896,9 +1908,6 @@ total_connect_client==0.28
# homeassistant.components.tplink_lte
tp-connected==0.0.4
-# homeassistant.components.tplink
-tplink==0.2.1
-
# homeassistant.components.transmission
transmissionrpc==0.11
@@ -1909,7 +1918,7 @@ tuyaha==0.0.4
twentemilieu==0.1.0
# homeassistant.components.twilio
-twilio==6.19.1
+twilio==6.32.0
# homeassistant.components.upcloud
upcloud-api==0.4.3
@@ -1999,7 +2008,7 @@ xmltodict==0.12.0
xs1-api-client==2.3.5
# homeassistant.components.yandex_transport
-ya_ma==0.3.7
+ya_ma==0.3.8
# homeassistant.components.yweather
yahooweather==0.10
@@ -2014,7 +2023,7 @@ yeelight==0.5.0
yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2019.09.28
+youtube_dl==2019.10.22
# homeassistant.components.zengge
zengge==0.2
@@ -2032,16 +2041,16 @@ zhong_hong_hvac==1.0.9
ziggo-mediabox-xl==1.1.0
# homeassistant.components.zha
-zigpy-deconz==0.5.0
+zigpy-deconz==0.6.0
# homeassistant.components.zha
-zigpy-homeassistant==0.9.0
+zigpy-homeassistant==0.10.0
# homeassistant.components.zha
-zigpy-xbee-homeassistant==0.5.0
+zigpy-xbee-homeassistant==0.6.0
# homeassistant.components.zha
-zigpy-zigate==0.4.1
+zigpy-zigate==0.5.0
# homeassistant.components.zoneminder
zm-py==0.3.3
diff --git a/requirements_test.txt b/requirements_test.txt
index 9da375b33c8..7af2ec0dde3 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -6,17 +6,17 @@
asynctest==0.13.0
black==19.3b0
codecov==2.0.15
-flake8-docstrings==1.3.1
+flake8-docstrings==1.5.0
flake8==3.7.8
mock-open==1.3.1
-mypy==0.730
+mypy==0.740
pre-commit==1.18.3
pydocstyle==4.0.1
-pylint==2.3.1
-astroid==2.2.5
+pylint==2.4.3
+astroid==2.3.2
pytest-aiohttp==0.3.0
-pytest-cov==2.7.1
+pytest-cov==2.8.1
pytest-sugar==0.9.2
pytest-timeout==1.3.3
-pytest==5.2.0
+pytest==5.2.1
requests_mock==1.7.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 22a69139b7a..36f423860d0 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -7,19 +7,19 @@
asynctest==0.13.0
black==19.3b0
codecov==2.0.15
-flake8-docstrings==1.3.1
+flake8-docstrings==1.5.0
flake8==3.7.8
mock-open==1.3.1
-mypy==0.730
+mypy==0.740
pre-commit==1.18.3
pydocstyle==4.0.1
-pylint==2.3.1
-astroid==2.2.5
+pylint==2.4.3
+astroid==2.3.2
pytest-aiohttp==0.3.0
-pytest-cov==2.7.1
+pytest-cov==2.8.1
pytest-sugar==0.9.2
pytest-timeout==1.3.3
-pytest==5.2.0
+pytest==5.2.1
requests_mock==1.7.0
@@ -30,17 +30,29 @@ HAP-python==2.6.0
# homeassistant.components.owntracks
PyNaCl==1.3.0
+# homeassistant.auth.mfa_modules.totp
+PyQRCode==1.2.1
+
# homeassistant.components.rmvtransport
-PyRMVtransport==0.1.3
+PyRMVtransport==0.2.9
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
+# homeassistant.components.remember_the_milk
+RtmAPI==0.7.2
+
# homeassistant.components.yessssms
YesssSMS==0.4.1
+# homeassistant.components.abode
+abodepy==0.16.6
+
+# homeassistant.components.androidtv
+adb-shell==0.0.7
+
# homeassistant.components.adguard
-adguardhome==0.2.1
+adguardhome==0.3.0
# homeassistant.components.geonetnz_quakes
aio_geojson_geonetnz_quakes==0.10
@@ -48,6 +60,9 @@ aio_geojson_geonetnz_quakes==0.10
# homeassistant.components.ambient_station
aioambient==0.3.2
+# homeassistant.components.asuswrt
+aioasuswrt==1.1.21
+
# homeassistant.components.automatic
aioautomatic==0.6.5
@@ -55,7 +70,7 @@ aioautomatic==0.6.5
aiobotocore==0.10.2
# homeassistant.components.esphome
-aioesphomeapi==2.2.0
+aioesphomeapi==2.4.2
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -76,18 +91,31 @@ aiounifi==11
# homeassistant.components.wwlln
aiowwlln==2.0.2
+# homeassistant.components.airly
+airly==0.0.2
+
# homeassistant.components.ambiclimate
ambiclimate==0.2.1
# homeassistant.components.androidtv
-androidtv==0.0.30
+androidtv==0.0.32
# homeassistant.components.apns
apns2==0.3.0
+# homeassistant.components.apprise
+apprise==0.8.1
+
# homeassistant.components.aprs
aprslib==0.6.46
+# homeassistant.components.arcam_fmj
+arcam-fmj==0.4.3
+
+# homeassistant.components.dlna_dmr
+# homeassistant.components.upnp
+async-upnp-client==0.14.11
+
# homeassistant.components.stream
av==6.1.2
@@ -97,17 +125,46 @@ axis==25
# homeassistant.components.zha
bellows-homeassistant==0.10.0
+# homeassistant.components.bom
+bomradarloop==0.1.3
+
+# homeassistant.components.broadlink
+broadlink==0.12.0
+
+# homeassistant.components.buienradar
+buienradar==1.0.1
+
# homeassistant.components.caldav
caldav==0.6.1
# homeassistant.components.coinmarketcap
coinmarketcap==5.0.3
+# homeassistant.scripts.check_config
+colorlog==4.0.2
+
+# homeassistant.components.eddystone_temperature
+# homeassistant.components.eq3btsmart
+# homeassistant.components.xiaomi_miio
+construct==2.9.45
+
+# homeassistant.scripts.credstash
+# credstash==1.15.0
+
+# homeassistant.components.datadog
+datadog==0.15.0
+
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
# homeassistant.components.ohmconnect
defusedxml==0.6.0
+# homeassistant.components.directv
+directpy==0.5
+
+# homeassistant.components.updater
+distro==1.4.0
+
# homeassistant.components.dsmr
dsmr_parser==0.12
@@ -117,9 +174,6 @@ eebrightbox==0.0.4
# homeassistant.components.emulated_roku
emulated_roku==0.1.8
-# homeassistant.components.enocean
-enocean==0.50
-
# homeassistant.components.season
ephem==3.7.6.0
@@ -154,9 +208,15 @@ georss_qld_bushfire_alert_client==0.3
# homeassistant.components.nmap_tracker
getmac==0.8.1
+# homeassistant.components.glances
+glances_api==0.2.0
+
# homeassistant.components.google
google-api-python-client==1.6.4
+# homeassistant.components.google_pubsub
+google-cloud-pubsub==0.39.1
+
# homeassistant.components.ffmpeg
ha-ffmpeg==2.0
@@ -170,7 +230,7 @@ hass-nabucasa==0.22
hbmqtt==0.9.5
# homeassistant.components.jewish_calendar
-hdate==0.9.0
+hdate==0.9.1
# homeassistant.components.here_travel_time
herepy==0.6.3.1
@@ -182,13 +242,16 @@ hole==0.5.0
holidays==0.9.11
# homeassistant.components.frontend
-home-assistant-frontend==20191002.2
+home-assistant-frontend==20191025.1
+
+# homeassistant.components.zwave
+homeassistant-pyozw==0.1.4
# homeassistant.components.homekit_controller
homekit[IP]==0.15.0
# homeassistant.components.homematicip_cloud
-homematicip==0.10.12
+homematicip==0.10.13
# homeassistant.components.google
# homeassistant.components.remember_the_milk
@@ -206,12 +269,21 @@ influxdb==5.2.3
# homeassistant.components.verisure
jsonpath==0.75
+# homeassistant.scripts.keyring
+keyring==19.2.0
+
+# homeassistant.scripts.keyring
+keyrings.alt==3.1.1
+
# homeassistant.components.dyson
libpurecool==0.5.0
# homeassistant.components.soundtouch
libsoundtouch==0.7.2
+# homeassistant.components.logi_circle
+logi_circle==0.2.2
+
# homeassistant.components.luftdaten
luftdaten==0.6.3
@@ -224,15 +296,27 @@ mficlient==0.3.0
# homeassistant.components.minio
minio==4.0.9
+# homeassistant.components.tts
+mutagen==1.42.0
+
+# homeassistant.components.ness_alarm
+nessclient==0.9.15
+
# homeassistant.components.discovery
# homeassistant.components.ssdp
netdisco==2.6.0
+# homeassistant.components.nsw_fuel_station
+nsw-fuel-api-client==1.0.10
+
+# homeassistant.components.nuheat
+nuheat==0.3.0
+
# homeassistant.components.iqvia
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==1.17.1
+numpy==1.17.3
# homeassistant.components.google
oauth2client==4.0.0
@@ -253,18 +337,27 @@ pilight==0.1.1
# homeassistant.components.image_processing
# homeassistant.components.proxy
# homeassistant.components.qrcode
-pillow==6.1.0
+pillow==6.2.0
# homeassistant.components.plex
plexapi==3.0.6
# homeassistant.components.plex
-plexauth==0.0.4
+plexauth==0.0.5
+
+# homeassistant.components.plex
+plexwebsocket==0.0.3
# homeassistant.components.mhz19
# homeassistant.components.serial_pm
pmsensor==0.4
+# homeassistant.components.reddit
+praw==6.3.1
+
+# homeassistant.components.islamic_prayer_times
+prayer_times_calculator==0.0.3
+
# homeassistant.components.prometheus
prometheus_client==0.7.1
@@ -277,6 +370,9 @@ pushbullet.py==0.11.0
# homeassistant.components.canary
py-canary==0.5.0
+# homeassistant.components.melissa
+py-melissa-climate==2.0.0
+
# homeassistant.components.seventeentrack
py17track==2.2.2
@@ -287,35 +383,86 @@ pyHS100==0.3.5
# homeassistant.components.norway_air
pyMetno==0.4.6
+# homeassistant.components.rfxtrx
+pyRFXtrx==0.23
+
+# homeassistant.components.nextbus
+py_nextbusnext==0.1.4
+
+# homeassistant.components.arlo
+pyarlo==0.2.3
+
# homeassistant.components.blackbird
pyblackbird==0.5
+# homeassistant.components.neato
+pybotvac==0.0.17
+
# homeassistant.components.cast
pychromecast==4.0.1
+# homeassistant.components.coolmaster
+pycoolmasternet==0.0.4
+
+# homeassistant.components.daikin
+pydaikin==1.6.1
+
# homeassistant.components.deconz
-pydeconz==63
+pydeconz==64
# homeassistant.components.zwave
pydispatcher==2.0.5
+# homeassistant.components.everlights
+pyeverlights==0.1.0
+
+# homeassistant.components.fido
+pyfido==2.1.1
+
+# homeassistant.components.fritzbox
+pyfritzhome==0.4.0
+
+# homeassistant.components.ifttt
+pyfttt==0.3
+
+# homeassistant.components.version
+pyhaversion==3.1.0
+
# homeassistant.components.heos
pyheos==0.6.0
# homeassistant.components.homematic
-pyhomematic==0.1.60
+pyhomematic==0.1.61
+
+# homeassistant.components.ipma
+pyipma==1.2.1
# homeassistant.components.iqvia
pyiqvia==0.2.1
+# homeassistant.components.kira
+pykira==0.1.1
+
+# homeassistant.components.webostv
+pylgtv==0.1.9
+
# homeassistant.components.linky
pylinky==0.4.0
# homeassistant.components.litejet
pylitejet==0.1
+# homeassistant.components.mailgun
+pymailgunner==1.4
+
# homeassistant.components.somfy
-pymfy==0.5.2
+pymfy==0.6.0
+
+# homeassistant.components.mochad
+pymochad==0.2.0
+
+# homeassistant.components.modbus
+pymodbus==1.5.2
# homeassistant.components.monoprice
pymonoprice==0.3
@@ -329,13 +476,19 @@ pynx584==0.4
# homeassistant.components.openuv
pyopenuv==1.0.9
+# homeassistant.components.opentherm_gw
+pyotgw==0.5b0
+
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
# homeassistant.components.otp
pyotp==2.3.0
+# homeassistant.components.point
+pypoint==1.1.1
+
# homeassistant.components.ps4
-pyps4-homeassistant==0.8.7
+pyps4-2ndscreen==1.0.1
# homeassistant.components.qwikswitch
pyqwikswitch==0.93
@@ -353,7 +506,7 @@ pysmartthings==0.6.9
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.23
+pysonos==0.0.24
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -367,6 +520,9 @@ python-forecastio==1.4.0
# homeassistant.components.izone
python-izone==1.1.1
+# homeassistant.components.xiaomi_miio
+python-miio==0.4.6
+
# homeassistant.components.nest
python-nest==4.1.0
@@ -376,6 +532,9 @@ python-velbus==2.0.27
# homeassistant.components.awair
python_awair==0.0.4
+# homeassistant.components.traccar
+pytraccar==0.9.0
+
# homeassistant.components.tradfri
pytradfri[async]==6.3.1
@@ -400,6 +559,9 @@ ring_doorbell==0.2.3
# homeassistant.components.yamaha
rxv==0.6.0
+# homeassistant.components.samsungtv
+samsungctl[websocket]==0.7.1
+
# homeassistant.components.simplisafe
simplisafe-python==5.0.1
@@ -417,11 +579,22 @@ somecomfort==0.5.2
# homeassistant.components.recorder
# homeassistant.components.sql
-sqlalchemy==1.3.8
+sqlalchemy==1.3.10
# homeassistant.components.statsd
statsd==3.2.1
+# homeassistant.components.solaredge
+# homeassistant.components.thermoworks_smoke
+# homeassistant.components.traccar
+stringcase==1.2.0
+
+# homeassistant.components.solarlog
+sunwatcher==0.2.1
+
+# homeassistant.components.tellduslive
+tellduslive==0.10.10
+
# homeassistant.components.toon
toonapilib==3.2.4
@@ -431,6 +604,9 @@ transmissionrpc==0.11
# homeassistant.components.twentemilieu
twentemilieu==0.1.0
+# homeassistant.components.twilio
+twilio==6.32.0
+
# homeassistant.components.uvc
uvcclient==0.11.0
@@ -445,11 +621,42 @@ vultr==0.1.2
# homeassistant.components.wake_on_lan
wakeonlan==1.1.6
+# homeassistant.components.folder_watcher
+watchdog==0.8.3
+
+# homeassistant.components.webostv
+websockets==6.0
+
# homeassistant.components.withings
withings-api==2.0.0b8
+# homeassistant.components.bluesound
+# homeassistant.components.startca
+# homeassistant.components.ted5000
+# homeassistant.components.yr
+# homeassistant.components.zestimate
+xmltodict==0.12.0
+
+# homeassistant.components.yandex_transport
+ya_ma==0.3.8
+
+# homeassistant.components.yweather
+yahooweather==0.10
+
# homeassistant.components.zeroconf
zeroconf==0.23.0
# homeassistant.components.zha
-zigpy-homeassistant==0.9.0
+zha-quirks==0.0.26
+
+# homeassistant.components.zha
+zigpy-deconz==0.6.0
+
+# homeassistant.components.zha
+zigpy-homeassistant==0.10.0
+
+# homeassistant.components.zha
+zigpy-xbee-homeassistant==0.6.0
+
+# homeassistant.components.zha
+zigpy-zigate==0.5.0
diff --git a/script/bootstrap b/script/bootstrap
index ed6cd55be36..211f1355b7d 100755
--- a/script/bootstrap
+++ b/script/bootstrap
@@ -6,5 +6,5 @@ set -e
cd "$(dirname "$0")/.."
-echo "Installing test dependencies..."
-python3 -m pip install tox colorlog pre-commit
+echo "Installing development dependencies..."
+python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt)
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index b1ad5240d68..930ffa11b5f 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
"""Generate an updated requirements_all.txt."""
+import difflib
import importlib
import os
-import pathlib
+from pathlib import Path
import pkgutil
import re
import sys
@@ -41,157 +42,8 @@ COMMENT_REQUIREMENTS = (
"VL53L1X2",
)
-TEST_REQUIREMENTS = (
- "adguardhome",
- "aio_geojson_geonetnz_quakes",
- "aioambient",
- "aioautomatic",
- "aiobotocore",
- "aioesphomeapi",
- "aiohttp_cors",
- "aiohue",
- "aionotion",
- "aioswitcher",
- "aiounifi",
- "aiowwlln",
- "ambiclimate",
- "androidtv",
- "apns2",
- "aprslib",
- "av",
- "axis",
- "bellows-homeassistant",
- "caldav",
- "coinmarketcap",
- "defusedxml",
- "dsmr_parser",
- "eebrightbox",
- "emulated_roku",
- "enocean",
- "ephem",
- "evohomeclient",
- "feedparser-homeassistant",
- "foobot_async",
- "geojson_client",
- "geopy",
- "georss_generic_client",
- "georss_ign_sismologia_client",
- "georss_qld_bushfire_alert_client",
- "getmac",
- "google-api-python-client",
- "gTTS-token",
- "ha-ffmpeg",
- "hangups",
- "HAP-python",
- "hass-nabucasa",
- "haversine",
- "hbmqtt",
- "hdate",
- "herepy",
- "hole",
- "holidays",
- "home-assistant-frontend",
- "homekit[IP]",
- "homematicip",
- "httplib2",
- "huawei-lte-api",
- "iaqualink",
- "influxdb",
- "jsonpath",
- "libpurecool",
- "libsoundtouch",
- "luftdaten",
- "mbddns",
- "mficlient",
- "minio",
- "netdisco",
- "numpy",
- "oauth2client",
- "paho-mqtt",
- "pexpect",
- "pilight",
- "pillow",
- "plexapi",
- "plexauth",
- "pmsensor",
- "prometheus_client",
- "ptvsd",
- "pushbullet.py",
- "py-canary",
- "py17track",
- "pyblackbird",
- "pychromecast",
- "pydeconz",
- "pydispatcher",
- "pyheos",
- "pyhomematic",
- "pyHS100",
- "pyiqvia",
- "pylinky",
- "pylitejet",
- "pyMetno",
- "pymfy",
- "pymonoprice",
- "PyNaCl",
- "pynws",
- "pynx584",
- "pyopenuv",
- "pyotp",
- "pyps4-homeassistant",
- "pyqwikswitch",
- "PyRMVtransport",
- "pysma",
- "pysmartapp",
- "pysmartthings",
- "pysoma",
- "pysonos",
- "pyspcwebgw",
- "python_awair",
- "python-ecobee-api",
- "python-forecastio",
- "python-izone",
- "python-nest",
- "python-velbus",
- "pythonwhois",
- "pytradfri[async]",
- "PyTransportNSW",
- "pyunifi",
- "pyupnp-async",
- "pyvesync",
- "pywebpush",
- "regenmaschine",
- "restrictedpython",
- "rflink",
- "ring_doorbell",
- "ruamel.yaml",
- "rxv",
- "simplisafe-python",
- "sleepyq",
- "smhi-pkg",
- "solaredge",
- "somecomfort",
- "sqlalchemy",
- "srpenergy",
- "statsd",
- "toonapilib",
- "transmissionrpc",
- "twentemilieu",
- "uvcclient",
- "vsure",
- "vultr",
- "wakeonlan",
- "warrant",
- "nokia",
- "YesssSMS",
- "zeroconf",
- "zigpy-homeassistant",
- "withings-api",
-)
-
IGNORE_PIN = ("colorlog>2.1,<3", "keyring>=9.3,<10.0", "urllib3")
-IGNORE_REQ = ("colorama<=1",) # Windows only requirement in check_config
-
URL_PIN = (
"https://developers.home-assistant.io/docs/"
"creating_platform_code_review.html#1-requirements"
@@ -209,12 +61,31 @@ enum34==1000000000.0.0
# This is a old unmaintained library and is replaced with pycryptodome
pycrypto==1000000000.0.0
-
-# Contains code to modify Home Assistant to work around our rules
-python-systemair-savecair==1000000000.0.0
"""
+def has_tests(module: str):
+ """Test if a module has tests.
+
+ Module format: homeassistant.components.hue
+ Test if exists: tests/components/hue
+ """
+ path = Path(module.replace(".", "/").replace("homeassistant", "tests"))
+ if not path.exists():
+ return False
+
+ if not path.is_dir():
+ return True
+
+ # Dev environments might have stale directories around
+ # from removed tests. Check for that.
+ content = [f.name for f in path.glob("*")]
+
+ # Directories need to contain more than `__pycache__`
+ # to exist in Git and so be seen by CI.
+ return content != ["__pycache__"]
+
+
def explore_module(package, explore_children):
"""Explore the modules."""
module = importlib.import_module(package)
@@ -235,8 +106,9 @@ def explore_module(package, explore_children):
def core_requirements():
"""Gather core requirements out of setup.py."""
- with open("setup.py") as inp:
- reqs_raw = re.search(r"REQUIRES = \[(.*?)\]", inp.read(), re.S).group(1)
+ reqs_raw = re.search(
+ r"REQUIRES = \[(.*?)\]", Path("setup.py").read_text(), re.S
+ ).group(1)
return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)]
@@ -246,7 +118,7 @@ def gather_recursive_requirements(domain, seen=None):
seen = set()
seen.add(domain)
- integration = Integration(pathlib.Path(f"homeassistant/components/{domain}"))
+ integration = Integration(Path(f"homeassistant/components/{domain}"))
integration.load_manifest()
reqs = set(integration.manifest["requirements"])
for dep_domain in integration.manifest["dependencies"]:
@@ -281,7 +153,7 @@ def gather_modules():
def gather_requirements_from_manifests(errors, reqs):
"""Gather all of the requirements from manifests."""
- integrations = Integration.load_dir(pathlib.Path("homeassistant/components"))
+ integrations = Integration.load_dir(Path("homeassistant/components"))
for domain in sorted(integrations):
integration = integrations[domain]
@@ -317,8 +189,6 @@ def gather_requirements_from_modules(errors, reqs):
def process_requirements(errors, module_requirements, package, reqs):
"""Process all of the requirements."""
for req in module_requirements:
- if req in IGNORE_REQ:
- continue
if "://" in req:
errors.append(f"{package}[Only pypi dependencies are allowed: {req}]")
if req.partition("==")[1] == "" and req not in IGNORE_PIN:
@@ -357,15 +227,18 @@ def requirements_test_output(reqs):
output = []
output.append("# Home Assistant test")
output.append("\n")
- with open("requirements_test.txt") as test_file:
- output.append(test_file.read())
+ output.append(Path("requirements_test.txt").read_text())
output.append("\n")
+
filtered = {
- key: value
- for key, value in reqs.items()
+ requirement: modules
+ for requirement, modules in reqs.items()
if any(
- re.search(r"(^|#){}($|[=><])".format(re.escape(ign)), key) is not None
- for ign in TEST_REQUIREMENTS
+ # Always install requirements that are not part of integrations
+ not mdl.startswith("homeassistant.components.") or
+ # Install tests for integrations that have tests
+ has_tests(mdl)
+ for mdl in modules
)
}
output.append(generate_requirements_list(filtered))
@@ -375,48 +248,28 @@ def requirements_test_output(reqs):
def gather_constraints():
"""Construct output for constraint file."""
- return "\n".join(
- sorted(
- core_requirements() + list(gather_recursive_requirements("default_config"))
+ return (
+ "\n".join(
+ sorted(
+ core_requirements()
+ + list(gather_recursive_requirements("default_config"))
+ )
+ + [""]
)
- + [""]
+ + CONSTRAINT_BASE
)
-def write_requirements_file(data):
- """Write the modules to the requirements_all.txt."""
- with open("requirements_all.txt", "w+", newline="\n") as req_file:
- req_file.write(data)
-
-
-def write_test_requirements_file(data):
- """Write the modules to the requirements_test_all.txt."""
- with open("requirements_test_all.txt", "w+", newline="\n") as req_file:
- req_file.write(data)
-
-
-def write_constraints_file(data):
- """Write constraints to a file."""
- with open(CONSTRAINT_PATH, "w+", newline="\n") as req_file:
- req_file.write(data + CONSTRAINT_BASE)
-
-
-def validate_requirements_file(data):
- """Validate if requirements_all.txt is up to date."""
- with open("requirements_all.txt", "r") as req_file:
- return data == req_file.read()
-
-
-def validate_requirements_test_file(data):
- """Validate if requirements_test_all.txt is up to date."""
- with open("requirements_test_all.txt", "r") as req_file:
- return data == req_file.read()
-
-
-def validate_constraints_file(data):
- """Validate if constraints is up to date."""
- with open(CONSTRAINT_PATH, "r") as req_file:
- return data + CONSTRAINT_BASE == req_file.read()
+def diff_file(filename, content):
+ """Diff a file."""
+ return list(
+ difflib.context_diff(
+ [line + "\n" for line in Path(filename).read_text().split("\n")],
+ [line + "\n" for line in content.split("\n")],
+ filename,
+ "generated",
+ )
+ )
def main(validate):
@@ -430,33 +283,38 @@ def main(validate):
if data is None:
return 1
- constraints = gather_constraints()
-
reqs_file = requirements_all_output(data)
reqs_test_file = requirements_test_output(data)
+ constraints = gather_constraints()
+
+ files = (
+ ("requirements_all.txt", reqs_file),
+ ("requirements_test_all.txt", reqs_test_file),
+ ("homeassistant/package_constraints.txt", constraints),
+ )
if validate:
errors = []
- if not validate_requirements_file(reqs_file):
- errors.append("requirements_all.txt is not up to date")
- if not validate_requirements_test_file(reqs_test_file):
- errors.append("requirements_test_all.txt is not up to date")
-
- if not validate_constraints_file(constraints):
- errors.append("home-assistant/package_constraints.txt is not up to date")
+ for filename, content in files:
+ diff = diff_file(filename, content)
+ if diff:
+ errors.append("".join(diff))
if errors:
- print("******* ERROR")
- print("\n".join(errors))
- print("Please run script/gen_requirements_all.py")
+ print("ERROR - FOUND THE FOLLOWING DIFFERENCES")
+ print()
+ print()
+ print("\n\n".join(errors))
+ print()
+ print("Please run python3 -m script.gen_requirements_all")
return 1
return 0
- write_requirements_file(reqs_file)
- write_test_requirements_file(reqs_test_file)
- write_constraints_file(constraints)
+ for filename, content in files:
+ Path(filename).write_text(content)
+
return 0
diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py
index ab87799d6b2..bb119c0e42e 100644
--- a/script/scaffold/docs.py
+++ b/script/scaffold/docs.py
@@ -38,6 +38,7 @@ that can occur in the state will cause the right service to be called.
f"""
Device trigger base has been added to the {info.domain} integration:
- {info.integration_dir / "device_trigger.py"}
+ - {info.integration_dir / "strings.json"} (translations)
- {info.tests_dir / "test_device_trigger.py"}
You will now need to update the code to make sure that relevant triggers
@@ -50,6 +51,7 @@ are exposed.
f"""
Device condition base has been added to the {info.domain} integration:
- {info.integration_dir / "device_condition.py"}
+ - {info.integration_dir / "strings.json"} (translations)
- {info.tests_dir / "test_device_condition.py"}
You will now need to update the code to make sure that relevant condtions
@@ -62,6 +64,7 @@ are exposed.
f"""
Device action base has been added to the {info.domain} integration:
- {info.integration_dir / "device_action.py"}
+ - {info.integration_dir / "strings.json"} (translations)
- {info.tests_dir / "test_device_action.py"}
You will now need to update the code to make sure that relevant services
diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py
index 6bccf6529fe..e16316fd76b 100644
--- a/script/scaffold/generate.py
+++ b/script/scaffold/generate.py
@@ -68,6 +68,39 @@ def _custom_tasks(template, info) -> None:
info.update_manifest(**changes)
+ if template == "device_trigger":
+ info.update_strings(
+ device_automation={
+ **info.strings().get("device_automation", {}),
+ "trigger_type": {
+ "turned_on": "{entity_name} turned on",
+ "turned_off": "{entity_name} turned off",
+ },
+ }
+ )
+
+ if template == "device_condition":
+ info.update_strings(
+ device_automation={
+ **info.strings().get("device_automation", {}),
+ "condtion_type": {
+ "is_on": "{entity_name} is on",
+ "is_off": "{entity_name} is off",
+ },
+ }
+ )
+
+ if template == "device_action":
+ info.update_strings(
+ device_automation={
+ **info.strings().get("device_automation", {}),
+ "action_type": {
+ "turn_on": "Turn on {entity_name}",
+ "turn_off": "Turn off {entity_name}",
+ },
+ }
+ )
+
if template == "config_flow":
info.update_manifest(config_flow=True)
info.update_strings(
diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py
index f8a00bf1ec8..b65c8257531 100644
--- a/script/scaffold/templates/device_action/tests/test_device_action.py
+++ b/script/scaffold/templates/device_action/tests/test_device_action.py
@@ -8,6 +8,7 @@ from homeassistant.helpers import device_registry
from tests.common import (
MockConfigEntry,
+ assert_lists_same,
async_mock_service,
mock_device_registry,
mock_registry,
@@ -28,7 +29,7 @@ def entity_reg(hass):
async def test_get_actions(hass, device_reg, entity_reg):
- """Test we get the expected actions from a switch."""
+ """Test we get the expected actions from a NEW_DOMAIN."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
@@ -51,7 +52,7 @@ async def test_get_actions(hass, device_reg, entity_reg):
},
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
- assert actions == expected_actions
+ assert_lists_same(actions, expected_actions)
async def test_action(hass):
diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py
index d19fa8817a0..fa123cff8e0 100644
--- a/script/scaffold/templates/device_condition/integration/device_condition.py
+++ b/script/scaffold/templates/device_condition/integration/device_condition.py
@@ -1,31 +1,37 @@
"""Provides device automations for NEW_NAME."""
-from typing import List
+from typing import Dict, List
import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
+ CONF_CONDITION,
CONF_DOMAIN,
CONF_TYPE,
- CONF_PLATFORM,
CONF_DEVICE_ID,
CONF_ENTITY_ID,
+ STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import condition, entity_registry
+from homeassistant.helpers import condition, config_validation as cv, entity_registry
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
from . import DOMAIN
# TODO specify your supported condition types.
-CONDITION_TYPES = {"is_on"}
+CONDITION_TYPES = {"is_on", "is_off"}
CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend(
- {vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES)}
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES),
+ }
)
-async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str]:
+async def async_get_conditions(
+ hass: HomeAssistant, device_id: str
+) -> List[Dict[str, str]]:
"""List device conditions for NEW_NAME devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = []
@@ -39,13 +45,22 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str]
# TODO add your own conditions.
conditions.append(
{
- CONF_PLATFORM: "device",
+ CONF_CONDITION: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "is_on",
}
)
+ conditions.append(
+ {
+ CONF_CONDITION: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: "is_off",
+ }
+ )
return conditions
@@ -56,9 +71,13 @@ def async_condition_from_config(
"""Create a function to test a device condition."""
if config_validation:
config = CONDITION_SCHEMA(config)
+ if config[CONF_TYPE] == "is_on":
+ state = STATE_ON
+ else:
+ state = STATE_OFF
- def test_is_on(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
- """Test if an entity is on."""
- return condition.state(hass, config[ATTR_ENTITY_ID], STATE_ON)
+ def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
+ """Test if an entity is a certain state."""
+ return condition.state(hass, config[ATTR_ENTITY_ID], state)
- return test_is_on
+ return test_is_state
diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py
index d9cef083510..1ae4df5f1b7 100644
--- a/script/scaffold/templates/device_condition/tests/test_device_condition.py
+++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py
@@ -1,7 +1,7 @@
"""The tests for NEW_NAME device conditions."""
import pytest
-from homeassistant.components.switch import DOMAIN
+from homeassistant.components.NEW_DOMAIN import DOMAIN
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation
@@ -9,6 +9,7 @@ from homeassistant.helpers import device_registry
from tests.common import (
MockConfigEntry,
+ assert_lists_same,
async_mock_service,
mock_device_registry,
mock_registry,
@@ -35,7 +36,7 @@ def calls(hass):
async def test_get_conditions(hass, device_reg, entity_reg):
- """Test we get the expected conditions from a switch."""
+ """Test we get the expected conditions from a NEW_DOMAIN."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
@@ -60,7 +61,7 @@ async def test_get_conditions(hass, device_reg, entity_reg):
},
]
conditions = await async_get_device_automations(hass, "condition", device_entry.id)
- assert conditions == expected_conditions
+ assert_lists_same(conditions, expected_conditions)
async def test_if_state(hass, calls):
diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py
index f7e9fc091f8..e0741734d5f 100644
--- a/script/scaffold/templates/device_trigger/integration/device_trigger.py
+++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py
@@ -12,7 +12,7 @@ from homeassistant.const import (
STATE_OFF,
)
from homeassistant.core import HomeAssistant, CALLBACK_TYPE
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import config_validation as cv, entity_registry
from homeassistant.helpers.typing import ConfigType
from homeassistant.components.automation import state, AutomationActionType
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
@@ -22,7 +22,10 @@ from . import DOMAIN
TRIGGER_TYPES = {"turned_on", "turned_off"}
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
- {vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES)}
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ }
)
@@ -87,14 +90,13 @@ async def async_attach_trigger(
from_state = STATE_ON
to_state = STATE_OFF
- return state.async_attach_trigger(
- hass,
- {
- CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- state.CONF_FROM: from_state,
- state.CONF_TO: to_state,
- },
- action,
- automation_info,
- platform_type="device",
+ state_config = {
+ state.CONF_PLATFORM: "state",
+ CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ state.CONF_FROM: from_state,
+ state.CONF_TO: to_state,
+ }
+ state_config = state.TRIGGER_SCHEMA(state_config)
+ return await state.async_attach_trigger(
+ hass, state_config, action, automation_info, platform_type="device"
)
diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py
index c22197bb136..99e1f8937af 100644
--- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py
+++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py
@@ -1,7 +1,7 @@
"""The tests for NEW_NAME device triggers."""
import pytest
-from homeassistant.components.switch import DOMAIN
+from homeassistant.components.NEW_DOMAIN import DOMAIN
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation
@@ -9,6 +9,7 @@ from homeassistant.helpers import device_registry
from tests.common import (
MockConfigEntry,
+ assert_lists_same,
async_mock_service,
mock_device_registry,
mock_registry,
@@ -35,7 +36,7 @@ def calls(hass):
async def test_get_triggers(hass, device_reg, entity_reg):
- """Test we get the expected triggers from a switch."""
+ """Test we get the expected triggers from a NEW_DOMAIN."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
@@ -60,7 +61,7 @@ async def test_get_triggers(hass, device_reg, entity_reg):
},
]
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
- assert triggers == expected_triggers
+ assert_lists_same(triggers, expected_triggers)
async def test_if_fires_on_state_change(hass, calls):
diff --git a/setup.cfg b/setup.cfg
index 4c9c892b93f..6d0e5378b44 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -57,17 +57,21 @@ combine_as_imports = true
[mypy]
python_version = 3.6
+ignore_errors = true
+follow_imports = silent
+ignore_missing_imports = true
+warn_incomplete_stub = true
+warn_redundant_casts = true
+warn_unused_configs = true
+
+[mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.monkey_patch,homeassistant.requirements,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*]
+ignore_errors = false
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_calls = true
disallow_untyped_defs = true
-follow_imports = silent
-ignore_missing_imports = true
no_implicit_optional = true
strict_equality = true
-warn_incomplete_stub = true
-warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
-warn_unused_configs = true
warn_unused_ignores = true
diff --git a/setup.py b/setup.py
index 23a8a808f43..d2c4934713b 100755
--- a/setup.py
+++ b/setup.py
@@ -36,16 +36,16 @@ REQUIRES = [
"async_timeout==3.0.1",
"attrs==19.2.0",
"bcrypt==3.1.7",
- "certifi>=2019.6.16",
+ "certifi>=2019.9.11",
'contextvars==2.4;python_version<"3.7"',
"importlib-metadata==0.23",
"jinja2>=2.10.1",
"PyJWT==1.7.1",
# PyJWT has loose dependency. We want the latest one.
- "cryptography==2.7",
+ "cryptography==2.8",
"pip>=8.0.3",
- "python-slugify==3.0.4",
- "pytz>=2019.02",
+ "python-slugify==3.0.6",
+ "pytz>=2019.03",
"pyyaml==5.1.2",
"requests==2.22.0",
"ruamel.yaml==0.15.100",
diff --git a/tests/common.py b/tests/common.py
index 0684e6daafc..f40019c5d24 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -27,6 +27,7 @@ from homeassistant.auth import (
)
from homeassistant.auth.permissions import system_policies
from homeassistant.components import mqtt, recorder
+from homeassistant.components.mqtt.models import Message
from homeassistant.config import async_process_component_config
from homeassistant.const import (
ATTR_DISCOVERED,
@@ -230,7 +231,6 @@ def get_test_instance_port():
return _TEST_INSTANCE_PORT
-@ha.callback
def async_mock_service(hass, domain, service, schema=None):
"""Set up a fake service & return a calls log list to this service."""
calls = []
@@ -272,7 +272,7 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False):
"""Fire the MQTT message."""
if isinstance(payload, str):
payload = payload.encode("utf-8")
- msg = mqtt.Message(topic, payload, qos, retain)
+ msg = Message(topic, payload, qos, retain)
hass.data["mqtt"]._mqtt_handle_message(msg)
@@ -1015,14 +1015,23 @@ def mock_entity_platform(hass, platform_path, module):
hue.light.
"""
domain, platform_name = platform_path.split(".")
- integration_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {})
+ mock_platform(hass, f"{platform_name}.{domain}", module)
+
+
+def mock_platform(hass, platform_path, module=None):
+ """Mock a platform.
+
+ platform_path is in form hue.config_flow.
+ """
+ domain, platform_name = platform_path.split(".")
+ integration_cache = hass.data.setdefault(loader.DATA_INTEGRATIONS, {})
module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {})
- if platform_name not in integration_cache:
- mock_integration(hass, MockModule(platform_name))
+ if domain not in integration_cache:
+ mock_integration(hass, MockModule(domain))
_LOGGER.info("Adding mock integration platform: %s", platform_path)
- module_cache["{}.{}".format(platform_name, domain)] = module
+ module_cache[platform_path] = module or Mock()
def async_capture_events(hass, event_name):
diff --git a/tests/components/abode/__init__.py b/tests/components/abode/__init__.py
new file mode 100644
index 00000000000..a34320c21de
--- /dev/null
+++ b/tests/components/abode/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Abode component."""
diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py
new file mode 100644
index 00000000000..c3f5d170767
--- /dev/null
+++ b/tests/components/abode/test_config_flow.py
@@ -0,0 +1,120 @@
+"""Tests for the Abode config flow."""
+from unittest.mock import patch
+
+from abodepy.exceptions import AbodeAuthenticationException
+
+from homeassistant import data_entry_flow
+from homeassistant.components.abode import config_flow
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from tests.common import MockConfigEntry
+
+CONF_POLLING = "polling"
+
+
+async def test_show_form(hass):
+ """Test that the form is served with no input."""
+ flow = config_flow.AbodeFlowHandler()
+ 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_one_config_allowed(hass):
+ """Test that only one Abode configuration is allowed."""
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
+
+ MockConfigEntry(
+ domain="abode",
+ data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
+ ).add_to_hass(hass)
+
+ step_user_result = await flow.async_step_user()
+
+ assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert step_user_result["reason"] == "single_instance_allowed"
+
+ conf = {
+ CONF_USERNAME: "user@email.com",
+ CONF_PASSWORD: "password",
+ CONF_POLLING: False,
+ }
+
+ import_config_result = await flow.async_step_import(conf)
+
+ assert import_config_result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert import_config_result["reason"] == "single_instance_allowed"
+
+
+async def test_invalid_credentials(hass):
+ """Test that invalid credentials throws an error."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
+
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
+
+ with patch(
+ "homeassistant.components.abode.config_flow.Abode",
+ side_effect=AbodeAuthenticationException((400, "auth error")),
+ ):
+ result = await flow.async_step_user(user_input=conf)
+ assert result["errors"] == {"base": "invalid_credentials"}
+
+
+async def test_connection_error(hass):
+ """Test other than invalid credentials throws an error."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
+
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
+
+ with patch(
+ "homeassistant.components.abode.config_flow.Abode",
+ side_effect=AbodeAuthenticationException((500, "connection error")),
+ ):
+ result = await flow.async_step_user(user_input=conf)
+ assert result["errors"] == {"base": "connection_error"}
+
+
+async def test_step_import(hass):
+ """Test that the import step works."""
+ conf = {
+ CONF_USERNAME: "user@email.com",
+ CONF_PASSWORD: "password",
+ CONF_POLLING: False,
+ }
+
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
+
+ with patch("homeassistant.components.abode.config_flow.Abode"):
+ result = await flow.async_step_import(import_config=conf)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ result = await flow.async_step_user(user_input=result["data"])
+ assert result["title"] == "user@email.com"
+ assert result["data"] == {
+ CONF_USERNAME: "user@email.com",
+ CONF_PASSWORD: "password",
+ CONF_POLLING: False,
+ }
+
+
+async def test_step_user(hass):
+ """Test that the user step works."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
+
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
+
+ with patch("homeassistant.components.abode.config_flow.Abode"):
+ result = await flow.async_step_user(user_input=conf)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "user@email.com"
+ assert result["data"] == {
+ CONF_USERNAME: "user@email.com",
+ CONF_PASSWORD: "password",
+ CONF_POLLING: False,
+ }
diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py
index ea5e5ad2276..dbda1e99a48 100644
--- a/tests/components/adguard/test_config_flow.py
+++ b/tests/components/adguard/test_config_flow.py
@@ -3,9 +3,9 @@ from unittest.mock import patch
import aiohttp
-from homeassistant import data_entry_flow, config_entries
+from homeassistant import config_entries, data_entry_flow
from homeassistant.components.adguard import config_flow
-from homeassistant.components.adguard.const import DOMAIN
+from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -65,7 +65,7 @@ async def test_full_flow_implementation(hass, aioclient_mock):
FIXTURE_USER_INPUT[CONF_HOST],
FIXTURE_USER_INPUT[CONF_PORT],
),
- json={"version": "1.0"},
+ json={"version": "v0.99.0"},
headers={"Content-Type": "application/json"},
)
@@ -133,8 +133,19 @@ async def test_hassio_update_instance_not_running(hass):
assert result["reason"] == "existing_instance_updated"
-async def test_hassio_update_instance_running(hass):
+async def test_hassio_update_instance_running(hass, aioclient_mock):
"""Test we only allow a single config flow."""
+ aioclient_mock.get(
+ "http://mock-adguard-updated:3000/control/status",
+ json={"version": "v0.99.0"},
+ headers={"Content-Type": "application/json"},
+ )
+ aioclient_mock.get(
+ "http://mock-adguard:3000/control/status",
+ json={"version": "v0.99.0"},
+ headers={"Content-Type": "application/json"},
+ )
+
entry = MockConfigEntry(
domain="adguard",
data={
@@ -187,7 +198,7 @@ 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"},
+ json={"version": "v0.99.0"},
headers={"Content-Type": "application/json"},
)
@@ -228,3 +239,54 @@ async def test_hassio_connection_error(hass, aioclient_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "hassio_confirm"
assert result["errors"] == {"base": "connection_error"}
+
+
+async def test_outdated_adguard_version(hass, aioclient_mock):
+ """Test we show abort when connecting with unsupported AdGuard version."""
+ 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": "v0.98.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_ABORT
+ assert result["reason"] == "adguard_home_outdated"
+ assert result["description_placeholders"] == {
+ "current_version": "v0.98.0",
+ "minimal_version": MIN_ADGUARD_HOME_VERSION,
+ }
+
+
+async def test_outdated_adguard_addon_version(hass, aioclient_mock):
+ """Test we show abort when connecting with unsupported AdGuard add-on version."""
+ aioclient_mock.get(
+ "http://mock-adguard:3000/control/status",
+ json={"version": "v0.98.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"},
+ )
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "adguard_home_addon_outdated"
+ assert result["description_placeholders"] == {
+ "current_version": "v0.98.0",
+ "minimal_version": MIN_ADGUARD_HOME_VERSION,
+ }
diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py
new file mode 100644
index 00000000000..f31dfb7712d
--- /dev/null
+++ b/tests/components/airly/__init__.py
@@ -0,0 +1 @@
+"""Tests for Airly."""
diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py
new file mode 100644
index 00000000000..8b615b34c2a
--- /dev/null
+++ b/tests/components/airly/test_config_flow.py
@@ -0,0 +1,93 @@
+"""Define tests for the Airly config flow."""
+import json
+
+from airly.exceptions import AirlyError
+from asynctest import patch
+
+from homeassistant import data_entry_flow
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.components.airly import config_flow
+from homeassistant.components.airly.const import DOMAIN
+
+from tests.common import load_fixture, MockConfigEntry
+
+CONFIG = {
+ CONF_NAME: "abcd",
+ CONF_API_KEY: "foo",
+ CONF_LATITUDE: 123,
+ CONF_LONGITUDE: 456,
+}
+
+
+async def test_show_form(hass):
+ """Test that the form is served with no input."""
+ flow = config_flow.AirlyFlowHandler()
+ 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_invalid_api_key(hass):
+ """Test that errors are shown when API key is invalid."""
+ with patch(
+ "airly._private._RequestsHandler.get",
+ side_effect=AirlyError(403, {"message": "Invalid authentication credentials"}),
+ ):
+ flow = config_flow.AirlyFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_user(user_input=CONFIG)
+
+ assert result["errors"] == {"base": "auth"}
+
+
+async def test_invalid_location(hass):
+ """Test that errors are shown when location is invalid."""
+ with patch(
+ "airly._private._RequestsHandler.get",
+ return_value=json.loads(load_fixture("airly_no_station.json")),
+ ):
+ flow = config_flow.AirlyFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_user(user_input=CONFIG)
+
+ assert result["errors"] == {"base": "wrong_location"}
+
+
+async def test_duplicate_error(hass):
+ """Test that errors are shown when duplicates are added."""
+
+ with patch(
+ "airly._private._RequestsHandler.get",
+ return_value=json.loads(load_fixture("airly_valid_station.json")),
+ ):
+ MockConfigEntry(domain=DOMAIN, data=CONFIG).add_to_hass(hass)
+ flow = config_flow.AirlyFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_user(user_input=CONFIG)
+
+ assert result["errors"] == {CONF_NAME: "name_exists"}
+
+
+async def test_create_entry(hass):
+ """Test that the user step works."""
+
+ with patch(
+ "airly._private._RequestsHandler.get",
+ return_value=json.loads(load_fixture("airly_valid_station.json")),
+ ):
+ flow = config_flow.AirlyFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_user(user_input=CONFIG)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == CONFIG[CONF_NAME]
+ assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE]
+ assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE]
+ assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py
new file mode 100644
index 00000000000..c2dfcbd78b9
--- /dev/null
+++ b/tests/components/alarm_control_panel/test_device_action.py
@@ -0,0 +1,274 @@
+"""The tests for Alarm control panel device actions."""
+import pytest
+
+from homeassistant.components.alarm_control_panel import DOMAIN
+from homeassistant.const import (
+ CONF_PLATFORM,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+ STATE_UNKNOWN,
+)
+from homeassistant.setup import async_setup_component
+import homeassistant.components.automation as automation
+from homeassistant.helpers import device_registry
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ mock_device_registry,
+ mock_registry,
+ async_get_device_automations,
+ async_get_device_automation_capabilities,
+)
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+async def test_get_actions(hass, device_reg, entity_reg):
+ """Test we get the expected actions from a alarm_control_panel."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_actions = [
+ {
+ "domain": DOMAIN,
+ "type": "arm_away",
+ "device_id": device_entry.id,
+ "entity_id": "alarm_control_panel.test_5678",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "arm_home",
+ "device_id": device_entry.id,
+ "entity_id": "alarm_control_panel.test_5678",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "arm_night",
+ "device_id": device_entry.id,
+ "entity_id": "alarm_control_panel.test_5678",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "disarm",
+ "device_id": device_entry.id,
+ "entity_id": "alarm_control_panel.test_5678",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "trigger",
+ "device_id": device_entry.id,
+ "entity_id": "alarm_control_panel.test_5678",
+ },
+ ]
+ actions = await async_get_device_automations(hass, "action", device_entry.id)
+ assert_lists_same(actions, expected_actions)
+
+
+async def test_get_action_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a sensor trigger."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES["no_arm_code"].unique_id,
+ device_id=device_entry.id,
+ )
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ expected_capabilities = {
+ "arm_away": {"extra_fields": []},
+ "arm_home": {"extra_fields": []},
+ "arm_night": {"extra_fields": []},
+ "disarm": {
+ "extra_fields": [{"name": "code", "optional": True, "type": "string"}]
+ },
+ "trigger": {"extra_fields": []},
+ }
+ actions = await async_get_device_automations(hass, "action", device_entry.id)
+ assert len(actions) == 5
+ for action in actions:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "action", action
+ )
+ assert capabilities == expected_capabilities[action["type"]]
+
+
+async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a sensor trigger."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES["arm_code"].unique_id,
+ device_id=device_entry.id,
+ )
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ expected_capabilities = {
+ "arm_away": {
+ "extra_fields": [{"name": "code", "optional": True, "type": "string"}]
+ },
+ "arm_home": {
+ "extra_fields": [{"name": "code", "optional": True, "type": "string"}]
+ },
+ "arm_night": {
+ "extra_fields": [{"name": "code", "optional": True, "type": "string"}]
+ },
+ "disarm": {
+ "extra_fields": [{"name": "code", "optional": True, "type": "string"}]
+ },
+ "trigger": {"extra_fields": []},
+ }
+ actions = await async_get_device_automations(hass, "action", device_entry.id)
+ assert len(actions) == 5
+ for action in actions:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "action", action
+ )
+ assert capabilities == expected_capabilities[action["type"]]
+
+
+async def test_action(hass):
+ """Test for turn_on and turn_off actions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_event_arm_away",
+ },
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": "alarm_control_panel.alarm_no_arm_code",
+ "type": "arm_away",
+ },
+ },
+ {
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_event_arm_home",
+ },
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": "alarm_control_panel.alarm_no_arm_code",
+ "type": "arm_home",
+ },
+ },
+ {
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_event_arm_night",
+ },
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": "alarm_control_panel.alarm_no_arm_code",
+ "type": "arm_night",
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event_disarm"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": "alarm_control_panel.alarm_no_arm_code",
+ "type": "disarm",
+ "code": "1234",
+ },
+ },
+ {
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_event_trigger",
+ },
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": "alarm_control_panel.alarm_no_arm_code",
+ "type": "trigger",
+ },
+ },
+ ]
+ },
+ )
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+ assert (
+ hass.states.get("alarm_control_panel.alarm_no_arm_code").state == STATE_UNKNOWN
+ )
+
+ hass.bus.async_fire("test_event_arm_away")
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("alarm_control_panel.alarm_no_arm_code").state
+ == STATE_ALARM_ARMED_AWAY
+ )
+
+ hass.bus.async_fire("test_event_arm_home")
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("alarm_control_panel.alarm_no_arm_code").state
+ == STATE_ALARM_ARMED_HOME
+ )
+
+ hass.bus.async_fire("test_event_arm_night")
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("alarm_control_panel.alarm_no_arm_code").state
+ == STATE_ALARM_ARMED_NIGHT
+ )
+
+ hass.bus.async_fire("test_event_disarm")
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("alarm_control_panel.alarm_no_arm_code").state
+ == STATE_ALARM_DISARMED
+ )
+
+ hass.bus.async_fire("test_event_trigger")
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("alarm_control_panel.alarm_no_arm_code").state
+ == STATE_ALARM_TRIGGERED
+ )
diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py
index 48406a11aef..0fa1961ad61 100644
--- a/tests/components/alexa/__init__.py
+++ b/tests/components/alexa/__init__.py
@@ -13,7 +13,7 @@ TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token"
class MockConfig(config.AbstractConfig):
"""Mock Alexa config."""
- entity_config = {}
+ entity_config = {"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}}
@property
def supports_auth(self):
@@ -67,13 +67,22 @@ def get_new_request(namespace, name, endpoint=None):
async def assert_request_calls_service(
- namespace, name, endpoint, service, hass, response_type="Response", payload=None
+ namespace,
+ name,
+ endpoint,
+ service,
+ hass,
+ response_type="Response",
+ payload=None,
+ instance=None,
):
"""Assert an API request calls a hass service."""
context = Context()
request = get_new_request(namespace, name, endpoint)
if payload:
request["directive"]["payload"] = payload
+ if instance:
+ request["directive"]["header"]["instance"] = instance
domain, service_name = service.split(".")
calls = async_mock_service(hass, domain, service_name)
diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py
index d53f145e6ff..be4a2ba4806 100644
--- a/tests/components/alexa/test_capabilities.py
+++ b/tests/components/alexa/test_capabilities.py
@@ -8,6 +8,11 @@ from homeassistant.const import (
STATE_UNLOCKED,
STATE_UNKNOWN,
STATE_UNAVAILABLE,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
)
from homeassistant.components.climate import const as climate
from homeassistant.components.alexa import smart_home
@@ -300,7 +305,7 @@ async def test_report_colored_temp_light_state(hass):
async def test_report_fan_speed_state(hass):
- """Test PercentageController reports fan speed correctly."""
+ """Test PercentageController, PowerLevelController, RangeController reports fan speed correctly."""
hass.states.async_set(
"fan.off",
"off",
@@ -328,15 +333,82 @@ async def test_report_fan_speed_state(hass):
properties = await reported_properties(hass, "fan.off")
properties.assert_equal("Alexa.PercentageController", "percentage", 0)
+ properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 0)
+ properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
properties = await reported_properties(hass, "fan.low_speed")
properties.assert_equal("Alexa.PercentageController", "percentage", 33)
+ properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 33)
+ properties.assert_equal("Alexa.RangeController", "rangeValue", 1)
properties = await reported_properties(hass, "fan.medium_speed")
properties.assert_equal("Alexa.PercentageController", "percentage", 66)
+ properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 66)
+ properties.assert_equal("Alexa.RangeController", "rangeValue", 2)
properties = await reported_properties(hass, "fan.high_speed")
properties.assert_equal("Alexa.PercentageController", "percentage", 100)
+ properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 100)
+ properties.assert_equal("Alexa.RangeController", "rangeValue", 3)
+
+
+async def test_report_fan_oscillating(hass):
+ """Test ToggleController reports fan oscillating correctly."""
+ hass.states.async_set(
+ "fan.off",
+ "off",
+ {"friendly_name": "Off fan", "speed": "off", "supported_features": 3},
+ )
+ hass.states.async_set(
+ "fan.low_speed",
+ "on",
+ {
+ "friendly_name": "Low speed fan",
+ "speed": "low",
+ "oscillating": True,
+ "supported_features": 3,
+ },
+ )
+
+ properties = await reported_properties(hass, "fan.off")
+ properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF")
+
+ properties = await reported_properties(hass, "fan.low_speed")
+ properties.assert_equal("Alexa.ToggleController", "toggleState", "ON")
+
+
+async def test_report_fan_direction(hass):
+ """Test ModeController reports fan direction correctly."""
+ hass.states.async_set(
+ "fan.off", "off", {"friendly_name": "Off fan", "supported_features": 4}
+ )
+ hass.states.async_set(
+ "fan.reverse",
+ "on",
+ {
+ "friendly_name": "Fan Reverse",
+ "direction": "reverse",
+ "supported_features": 4,
+ },
+ )
+ hass.states.async_set(
+ "fan.forward",
+ "on",
+ {
+ "friendly_name": "Fan Forward",
+ "direction": "forward",
+ "supported_features": 4,
+ },
+ )
+
+ properties = await reported_properties(hass, "fan.off")
+ properties.assert_not_has_property("Alexa.ModeController", "mode")
+
+ properties = await reported_properties(hass, "fan.reverse")
+ properties.assert_equal("Alexa.ModeController", "mode", "reverse")
+
+ properties = await reported_properties(hass, "fan.forward")
+ properties.assert_equal("Alexa.ModeController", "mode", "forward")
async def test_report_cover_percentage_state(hass):
@@ -527,3 +599,33 @@ async def test_temperature_sensor_climate(hass):
properties.assert_equal(
"Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"}
)
+
+
+async def test_report_alarm_control_panel_state(hass):
+ """Test SecurityPanelController implements armState property."""
+ hass.states.async_set("alarm_control_panel.armed_away", STATE_ALARM_ARMED_AWAY, {})
+ hass.states.async_set(
+ "alarm_control_panel.armed_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS, {}
+ )
+ hass.states.async_set("alarm_control_panel.armed_home", STATE_ALARM_ARMED_HOME, {})
+ hass.states.async_set(
+ "alarm_control_panel.armed_night", STATE_ALARM_ARMED_NIGHT, {}
+ )
+ hass.states.async_set("alarm_control_panel.disarmed", STATE_ALARM_DISARMED, {})
+
+ properties = await reported_properties(hass, "alarm_control_panel.armed_away")
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
+
+ properties = await reported_properties(
+ hass, "alarm_control_panel.armed_custom_bypass"
+ )
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY")
+
+ properties = await reported_properties(hass, "alarm_control_panel.armed_home")
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY")
+
+ properties = await reported_properties(hass, "alarm_control_panel.armed_night")
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT")
+
+ properties = await reported_properties(hass, "alarm_control_panel.disarmed")
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED")
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index e5e5b8ab7ae..c50c0748147 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -4,6 +4,19 @@ import pytest
from homeassistant.core import Context, callback
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.components.alexa import smart_home, messages
+from homeassistant.components.media_player.const import (
+ SUPPORT_NEXT_TRACK,
+ SUPPORT_PAUSE,
+ SUPPORT_PLAY,
+ SUPPORT_PLAY_MEDIA,
+ SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_SELECT_SOURCE,
+ SUPPORT_STOP,
+ SUPPORT_TURN_OFF,
+ SUPPORT_TURN_ON,
+ SUPPORT_VOLUME_MUTE,
+ SUPPORT_VOLUME_SET,
+)
from homeassistant.helpers import entityfilter
from tests.common import async_mock_service
@@ -310,10 +323,14 @@ async def test_fan(hass):
assert appliance["endpointId"] == "fan#test_1"
assert appliance["displayCategories"][0] == "FAN"
assert appliance["friendlyName"] == "Test fan 1"
- assert_endpoint_capabilities(
+ capabilities = assert_endpoint_capabilities(
appliance, "Alexa.PowerController", "Alexa.EndpointHealth"
)
+ power_capability = get_capability(capabilities, "Alexa.PowerController")
+ assert "capabilityResources" not in power_capability
+ assert "configuration" not in power_capability
+
async def test_variable_fan(hass):
"""Test fan discovery.
@@ -336,13 +353,33 @@ async def test_variable_fan(hass):
assert appliance["displayCategories"][0] == "FAN"
assert appliance["friendlyName"] == "Test fan 2"
- assert_endpoint_capabilities(
+ capabilities = assert_endpoint_capabilities(
appliance,
"Alexa.PercentageController",
"Alexa.PowerController",
+ "Alexa.PowerLevelController",
+ "Alexa.RangeController",
"Alexa.EndpointHealth",
)
+ range_capability = get_capability(capabilities, "Alexa.RangeController")
+ assert range_capability is not None
+ assert range_capability["instance"] == "fan.speed"
+
+ properties = range_capability["properties"]
+ assert properties["nonControllable"] is False
+ assert {"name": "rangeValue"} in properties["supported"]
+
+ capability_resources = range_capability["capabilityResources"]
+ assert capability_resources is not None
+ assert {
+ "@type": "asset",
+ "value": {"assetId": "Alexa.Setting.FanSpeed"},
+ } in capability_resources["friendlyNames"]
+
+ configuration = range_capability["configuration"]
+ assert configuration is not None
+
call, _ = await assert_request_calls_service(
"Alexa.PercentageController",
"SetPercentage",
@@ -364,6 +401,272 @@ async def test_variable_fan(hass):
"speed",
)
+ call, _ = await assert_request_calls_service(
+ "Alexa.PowerLevelController",
+ "SetPowerLevel",
+ "fan#test_2",
+ "fan.set_speed",
+ hass,
+ payload={"powerLevel": "50"},
+ )
+ assert call.data["speed"] == "medium"
+
+ await assert_percentage_changes(
+ hass,
+ [("high", "-5"), ("medium", "-50"), ("low", "-80")],
+ "Alexa.PowerLevelController",
+ "AdjustPowerLevel",
+ "fan#test_2",
+ "powerLevelDelta",
+ "fan.set_speed",
+ "speed",
+ )
+
+
+async def test_oscillating_fan(hass):
+ """Test oscillating fan discovery."""
+ device = (
+ "fan.test_3",
+ "off",
+ {"friendly_name": "Test fan 3", "supported_features": 3},
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance["endpointId"] == "fan#test_3"
+ assert appliance["displayCategories"][0] == "FAN"
+ assert appliance["friendlyName"] == "Test fan 3"
+ capabilities = assert_endpoint_capabilities(
+ appliance,
+ "Alexa.PercentageController",
+ "Alexa.PowerController",
+ "Alexa.PowerLevelController",
+ "Alexa.RangeController",
+ "Alexa.ToggleController",
+ "Alexa.EndpointHealth",
+ )
+
+ toggle_capability = get_capability(capabilities, "Alexa.ToggleController")
+ assert toggle_capability is not None
+ assert toggle_capability["instance"] == "fan.oscillating"
+
+ properties = toggle_capability["properties"]
+ assert properties["nonControllable"] is False
+ assert {"name": "toggleState"} in properties["supported"]
+
+ capability_resources = toggle_capability["capabilityResources"]
+ assert capability_resources is not None
+ assert {
+ "@type": "asset",
+ "value": {"assetId": "Alexa.Setting.Oscillate"},
+ } in capability_resources["friendlyNames"]
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ToggleController",
+ "TurnOn",
+ "fan#test_3",
+ "fan.oscillate",
+ hass,
+ payload={},
+ instance="fan.oscillating",
+ )
+ assert call.data["oscillating"]
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ToggleController",
+ "TurnOff",
+ "fan#test_3",
+ "fan.oscillate",
+ hass,
+ payload={},
+ instance="fan.oscillating",
+ )
+ assert not call.data["oscillating"]
+
+
+async def test_direction_fan(hass):
+ """Test direction fan discovery."""
+ device = (
+ "fan.test_4",
+ "on",
+ {
+ "friendly_name": "Test fan 4",
+ "supported_features": 5,
+ "direction": "forward",
+ },
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance["endpointId"] == "fan#test_4"
+ assert appliance["displayCategories"][0] == "FAN"
+ assert appliance["friendlyName"] == "Test fan 4"
+ capabilities = assert_endpoint_capabilities(
+ appliance,
+ "Alexa.PercentageController",
+ "Alexa.PowerController",
+ "Alexa.PowerLevelController",
+ "Alexa.RangeController",
+ "Alexa.ModeController",
+ "Alexa.EndpointHealth",
+ )
+
+ mode_capability = get_capability(capabilities, "Alexa.ModeController")
+ assert mode_capability is not None
+ assert mode_capability["instance"] == "fan.direction"
+
+ properties = mode_capability["properties"]
+ assert properties["nonControllable"] is False
+ assert {"name": "mode"} in properties["supported"]
+
+ capability_resources = mode_capability["capabilityResources"]
+ assert capability_resources is not None
+ assert {
+ "@type": "asset",
+ "value": {"assetId": "Alexa.Setting.Direction"},
+ } in capability_resources["friendlyNames"]
+
+ configuration = mode_capability["configuration"]
+ assert configuration is not None
+ assert configuration["ordered"] is False
+
+ supported_modes = configuration["supportedModes"]
+ assert supported_modes is not None
+ assert {
+ "value": "direction.forward",
+ "modeResources": {
+ "friendlyNames": [
+ {"@type": "text", "value": {"text": "forward", "locale": "en-US"}}
+ ]
+ },
+ } in supported_modes
+ assert {
+ "value": "direction.reverse",
+ "modeResources": {
+ "friendlyNames": [
+ {"@type": "text", "value": {"text": "reverse", "locale": "en-US"}}
+ ]
+ },
+ } in supported_modes
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ModeController",
+ "SetMode",
+ "fan#test_4",
+ "fan.set_direction",
+ hass,
+ payload={"mode": "direction.reverse"},
+ instance="fan.direction",
+ )
+ assert call.data["direction"] == "reverse"
+
+ # Test for AdjustMode instance=None Error coverage
+ with pytest.raises(AssertionError):
+ call, _ = await assert_request_calls_service(
+ "Alexa.ModeController",
+ "AdjustMode",
+ "fan#test_4",
+ "fan.set_direction",
+ hass,
+ payload={},
+ instance=None,
+ )
+ assert call.data
+
+
+async def test_fan_range(hass):
+ """Test fan discovery with range controller.
+
+ This one has variable speed.
+ """
+ device = (
+ "fan.test_5",
+ "off",
+ {
+ "friendly_name": "Test fan 5",
+ "supported_features": 1,
+ "speed_list": ["low", "medium", "high"],
+ "speed": "medium",
+ },
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance["endpointId"] == "fan#test_5"
+ assert appliance["displayCategories"][0] == "FAN"
+ assert appliance["friendlyName"] == "Test fan 5"
+
+ capabilities = assert_endpoint_capabilities(
+ appliance,
+ "Alexa.PercentageController",
+ "Alexa.PowerController",
+ "Alexa.PowerLevelController",
+ "Alexa.RangeController",
+ "Alexa.EndpointHealth",
+ )
+
+ range_capability = get_capability(capabilities, "Alexa.RangeController")
+ assert range_capability is not None
+ assert range_capability["instance"] == "fan.speed"
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.RangeController",
+ "SetRangeValue",
+ "fan#test_5",
+ "fan.set_speed",
+ hass,
+ payload={"rangeValue": "1"},
+ instance="fan.speed",
+ )
+ assert call.data["speed"] == "low"
+
+ await assert_range_changes(
+ hass,
+ [("low", "-1"), ("high", "1"), ("medium", "0")],
+ "Alexa.RangeController",
+ "AdjustRangeValue",
+ "fan#test_5",
+ False,
+ "fan.set_speed",
+ "speed",
+ instance="fan.speed",
+ )
+
+
+async def test_fan_range_off(hass):
+ """Test fan range controller 0 turns_off fan."""
+ device = (
+ "fan.test_6",
+ "off",
+ {
+ "friendly_name": "Test fan 6",
+ "supported_features": 1,
+ "speed_list": ["low", "medium", "high"],
+ "speed": "high",
+ },
+ )
+ await discovery_test(device, hass)
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.RangeController",
+ "SetRangeValue",
+ "fan#test_6",
+ "fan.turn_off",
+ hass,
+ payload={"rangeValue": "0"},
+ instance="fan.speed",
+ )
+ assert call.data["speed"] == "off"
+
+ await assert_range_changes(
+ hass,
+ [("off", "-3")],
+ "Alexa.RangeController",
+ "AdjustRangeValue",
+ "fan#test_6",
+ False,
+ "fan.turn_off",
+ "speed",
+ instance="fan.speed",
+ )
+
async def test_lock(hass):
"""Test lock discovery."""
@@ -381,12 +684,20 @@ async def test_lock(hass):
"Alexa.LockController", "Lock", "lock#test", "lock.lock", hass
)
- # always return LOCKED for now
properties = msg["context"]["properties"][0]
assert properties["name"] == "lockState"
assert properties["namespace"] == "Alexa.LockController"
assert properties["value"] == "LOCKED"
+ _, msg = await assert_request_calls_service(
+ "Alexa.LockController", "Unlock", "lock#test", "lock.unlock", hass
+ )
+
+ properties = msg["context"]["properties"][0]
+ assert properties["name"] == "lockState"
+ assert properties["namespace"] == "Alexa.LockController"
+ assert properties["value"] == "UNLOCKED"
+
async def test_media_player(hass):
"""Test media player discovery."""
@@ -395,7 +706,17 @@ async def test_media_player(hass):
"off",
{
"friendly_name": "Test media player",
- "supported_features": 0x59BD,
+ "supported_features": SUPPORT_NEXT_TRACK
+ | SUPPORT_PAUSE
+ | SUPPORT_PLAY
+ | SUPPORT_PLAY_MEDIA
+ | SUPPORT_PREVIOUS_TRACK
+ | SUPPORT_SELECT_SOURCE
+ | SUPPORT_STOP
+ | SUPPORT_TURN_OFF
+ | SUPPORT_TURN_ON
+ | SUPPORT_VOLUME_MUTE
+ | SUPPORT_VOLUME_SET,
"volume_level": 0.75,
},
)
@@ -413,6 +734,7 @@ async def test_media_player(hass):
"Alexa.StepSpeaker",
"Alexa.PlaybackController",
"Alexa.EndpointHealth",
+ "Alexa.ChannelController",
)
await assert_power_controller_works(
@@ -526,7 +848,7 @@ async def test_media_player(hass):
"media_player#test",
"media_player.volume_up",
hass,
- payload={"volumeSteps": 20},
+ payload={"volumeSteps": 1, "volumeStepsDefault": False},
)
call, _ = await assert_request_calls_service(
@@ -535,7 +857,69 @@ async def test_media_player(hass):
"media_player#test",
"media_player.volume_down",
hass,
- payload={"volumeSteps": -20},
+ payload={"volumeSteps": -1, "volumeStepsDefault": False},
+ )
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.StepSpeaker",
+ "AdjustVolume",
+ "media_player#test",
+ "media_player.volume_up",
+ hass,
+ payload={"volumeSteps": 10, "volumeStepsDefault": True},
+ )
+ call, _ = await assert_request_calls_service(
+ "Alexa.ChannelController",
+ "ChangeChannel",
+ "media_player#test",
+ "media_player.play_media",
+ hass,
+ payload={"channel": {"number": 24}},
+ )
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ChannelController",
+ "ChangeChannel",
+ "media_player#test",
+ "media_player.play_media",
+ hass,
+ payload={"channel": {"callSign": "ABC"}},
+ )
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ChannelController",
+ "ChangeChannel",
+ "media_player#test",
+ "media_player.play_media",
+ hass,
+ payload={"channel": {"affiliateCallSign": "ABC"}},
+ )
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ChannelController",
+ "ChangeChannel",
+ "media_player#test",
+ "media_player.play_media",
+ hass,
+ payload={"channel": {"uri": "ABC"}},
+ )
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ChannelController",
+ "SkipChannels",
+ "media_player#test",
+ "media_player.media_next_track",
+ hass,
+ payload={"channelCount": 1},
+ )
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.ChannelController",
+ "SkipChannels",
+ "media_player#test",
+ "media_player.media_previous_track",
+ hass,
+ payload={"channelCount": -1},
)
@@ -564,6 +948,7 @@ async def test_media_player_power(hass):
"Alexa.StepSpeaker",
"Alexa.PlaybackController",
"Alexa.EndpointHealth",
+ "Alexa.ChannelController",
)
await assert_request_calls_service(
@@ -699,6 +1084,33 @@ async def assert_percentage_changes(
assert call.data[changed_parameter] == result_volume
+async def assert_range_changes(
+ hass,
+ adjustments,
+ namespace,
+ name,
+ endpoint,
+ delta_default,
+ service,
+ changed_parameter,
+ instance,
+):
+ """Assert an API request making range changes works.
+
+ AdjustRangeValue are examples of such requests.
+ """
+ for result_range, adjustment in adjustments:
+ payload = {
+ "rangeValueDelta": adjustment,
+ "rangeValueDeltaDefault": delta_default,
+ }
+
+ call, _ = await assert_request_calls_service(
+ namespace, name, endpoint, service, hass, payload=payload, instance=instance
+ )
+ assert call.data[changed_parameter] == result_range
+
+
async def test_temp_sensor(hass):
"""Test temperature sensor discovery."""
device = (
@@ -784,6 +1196,28 @@ async def test_motion_sensor(hass):
properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED")
+async def test_doorbell_sensor(hass):
+ """Test doorbell sensor discovery."""
+ device = (
+ "binary_sensor.test_doorbell",
+ "off",
+ {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"},
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance["endpointId"] == "binary_sensor#test_doorbell"
+ assert appliance["displayCategories"][0] == "DOORBELL"
+ assert appliance["friendlyName"] == "Test Doorbell Sensor"
+
+ capabilities = assert_endpoint_capabilities(
+ appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth"
+ )
+
+ doorbell_capability = get_capability(capabilities, "Alexa.DoorbellEventSource")
+ assert doorbell_capability is not None
+ assert doorbell_capability["proactivelyReported"] is True
+
+
async def test_unknown_sensor(hass):
"""Test sensors of unknown quantities are not discovered."""
device = (
@@ -1284,3 +1718,165 @@ async def test_endpoint_bad_health(hass):
properties.assert_equal(
"Alexa.EndpointHealth", "connectivity", {"value": "UNREACHABLE"}
)
+
+
+async def test_alarm_control_panel_disarmed(hass):
+ """Test alarm_control_panel discovery."""
+ device = (
+ "alarm_control_panel.test_1",
+ "disarmed",
+ {
+ "friendly_name": "Test Alarm Control Panel 1",
+ "code_arm_required": False,
+ "code_format": "number",
+ "code": "1234",
+ },
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance["endpointId"] == "alarm_control_panel#test_1"
+ assert appliance["displayCategories"][0] == "SECURITY_PANEL"
+ assert appliance["friendlyName"] == "Test Alarm Control Panel 1"
+ capabilities = assert_endpoint_capabilities(
+ appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth"
+ )
+ security_panel_capability = get_capability(
+ capabilities, "Alexa.SecurityPanelController"
+ )
+ assert security_panel_capability is not None
+ configuration = security_panel_capability["configuration"]
+ assert {"type": "FOUR_DIGIT_PIN"} in configuration["supportedAuthorizationTypes"]
+
+ properties = await reported_properties(hass, "alarm_control_panel#test_1")
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED")
+
+ call, msg = await assert_request_calls_service(
+ "Alexa.SecurityPanelController",
+ "Arm",
+ "alarm_control_panel#test_1",
+ "alarm_control_panel.alarm_arm_home",
+ hass,
+ response_type="Arm.Response",
+ payload={"armState": "ARMED_STAY"},
+ )
+ properties = ReportedProperties(msg["context"]["properties"])
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY")
+
+ call, msg = await assert_request_calls_service(
+ "Alexa.SecurityPanelController",
+ "Arm",
+ "alarm_control_panel#test_1",
+ "alarm_control_panel.alarm_arm_away",
+ hass,
+ response_type="Arm.Response",
+ payload={"armState": "ARMED_AWAY"},
+ )
+ properties = ReportedProperties(msg["context"]["properties"])
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
+
+ call, msg = await assert_request_calls_service(
+ "Alexa.SecurityPanelController",
+ "Arm",
+ "alarm_control_panel#test_1",
+ "alarm_control_panel.alarm_arm_night",
+ hass,
+ response_type="Arm.Response",
+ payload={"armState": "ARMED_NIGHT"},
+ )
+ properties = ReportedProperties(msg["context"]["properties"])
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT")
+
+
+async def test_alarm_control_panel_armed(hass):
+ """Test alarm_control_panel discovery."""
+ device = (
+ "alarm_control_panel.test_2",
+ "armed_away",
+ {
+ "friendly_name": "Test Alarm Control Panel 2",
+ "code_arm_required": False,
+ "code_format": "FORMAT_NUMBER",
+ "code": "1234",
+ },
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance["endpointId"] == "alarm_control_panel#test_2"
+ assert appliance["displayCategories"][0] == "SECURITY_PANEL"
+ assert appliance["friendlyName"] == "Test Alarm Control Panel 2"
+ assert_endpoint_capabilities(
+ appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth"
+ )
+
+ properties = await reported_properties(hass, "alarm_control_panel#test_2")
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
+
+ call, msg = await assert_request_calls_service(
+ "Alexa.SecurityPanelController",
+ "Disarm",
+ "alarm_control_panel#test_2",
+ "alarm_control_panel.alarm_disarm",
+ hass,
+ payload={"authorization": {"type": "FOUR_DIGIT_PIN", "value": "1234"}},
+ )
+ assert call.data["code"] == "1234"
+ properties = ReportedProperties(msg["context"]["properties"])
+ properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED")
+
+ msg = await assert_request_fails(
+ "Alexa.SecurityPanelController",
+ "Arm",
+ "alarm_control_panel#test_2",
+ "alarm_control_panel.alarm_arm_home",
+ hass,
+ payload={"armState": "ARMED_STAY"},
+ )
+ assert msg["event"]["payload"]["type"] == "AUTHORIZATION_REQUIRED"
+
+
+async def test_alarm_control_panel_code_arm_required(hass):
+ """Test alarm_control_panel with code_arm_required discovery."""
+ device = (
+ "alarm_control_panel.test_3",
+ "disarmed",
+ {"friendly_name": "Test Alarm Control Panel 3", "code_arm_required": True},
+ )
+ await discovery_test(device, hass, expected_endpoints=0)
+
+
+async def test_range_unsupported_domain(hass):
+ """Test rangeController with unsupported domain."""
+ device = ("switch.test", "on", {"friendly_name": "Test switch"})
+ await discovery_test(device, hass)
+
+ context = Context()
+ request = get_new_request("Alexa.RangeController", "SetRangeValue", "switch#test")
+ request["directive"]["payload"] = {"rangeValue": "1"}
+ request["directive"]["header"]["instance"] = "switch.speed"
+
+ msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context)
+
+ assert "event" in msg
+ msg = msg["event"]
+ assert msg["header"]["name"] == "ErrorResponse"
+ assert msg["header"]["namespace"] == "Alexa"
+ assert msg["payload"]["type"] == "INVALID_DIRECTIVE"
+
+
+async def test_mode_unsupported_domain(hass):
+ """Test modeController with unsupported domain."""
+ device = ("switch.test", "on", {"friendly_name": "Test switch"})
+ await discovery_test(device, hass)
+
+ context = Context()
+ request = get_new_request("Alexa.ModeController", "SetMode", "switch#test")
+ request["directive"]["payload"] = {"mode": "testMode"}
+ request["directive"]["header"]["instance"] = "switch.direction"
+
+ msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context)
+
+ assert "event" in msg
+ msg = msg["event"]
+ assert msg["header"]["name"] == "ErrorResponse"
+ assert msg["header"]["namespace"] == "Alexa"
+ assert msg["payload"]["type"] == "INVALID_DIRECTIVE"
diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py
index c05eed2a89b..2c58d1ed45e 100644
--- a/tests/components/alexa/test_state_report.py
+++ b/tests/components/alexa/test_state_report.py
@@ -37,6 +37,54 @@ async def test_report_state(hass, aioclient_mock):
assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_contact"
+async def test_report_state_instance(hass, aioclient_mock):
+ """Test proactive state reports with instance."""
+ aioclient_mock.post(TEST_URL, text="", status=202)
+
+ hass.states.async_set(
+ "fan.test_fan",
+ "off",
+ {
+ "friendly_name": "Test fan",
+ "supported_features": 3,
+ "speed": "off",
+ "oscillating": False,
+ },
+ )
+
+ await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG)
+
+ hass.states.async_set(
+ "fan.test_fan",
+ "on",
+ {
+ "friendly_name": "Test fan",
+ "supported_features": 3,
+ "speed": "high",
+ "oscillating": True,
+ },
+ )
+
+ # To trigger event listener
+ await hass.async_block_till_done()
+
+ assert len(aioclient_mock.mock_calls) == 1
+ call = aioclient_mock.mock_calls
+
+ call_json = call[0][2]
+ assert call_json["event"]["header"]["namespace"] == "Alexa"
+ assert call_json["event"]["header"]["name"] == "ChangeReport"
+
+ change_reports = call_json["event"]["payload"]["change"]["properties"]
+ for report in change_reports:
+ if report["name"] == "toggleState":
+ assert report["value"] == "ON"
+ assert report["instance"] == "fan.oscillating"
+ assert report["namespace"] == "Alexa.ToggleController"
+
+ assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan"
+
+
async def test_send_add_or_update_message(hass, aioclient_mock):
"""Test sending an AddOrUpdateReport message."""
aioclient_mock.post(TEST_URL, text="")
@@ -89,3 +137,34 @@ async def test_send_delete_message(hass, aioclient_mock):
call_json["event"]["payload"]["endpoints"][0]["endpointId"]
== "binary_sensor#test_contact"
)
+
+
+async def test_doorbell_event(hass, aioclient_mock):
+ """Test doorbell press reports."""
+ aioclient_mock.post(TEST_URL, text="", status=202)
+
+ hass.states.async_set(
+ "binary_sensor.test_doorbell",
+ "off",
+ {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"},
+ )
+
+ await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG)
+
+ hass.states.async_set(
+ "binary_sensor.test_doorbell",
+ "on",
+ {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"},
+ )
+
+ # To trigger event listener
+ await hass.async_block_till_done()
+
+ assert len(aioclient_mock.mock_calls) == 1
+ call = aioclient_mock.mock_calls
+
+ call_json = call[0][2]
+ assert call_json["event"]["header"]["namespace"] == "Alexa.DoorbellEventSource"
+ assert call_json["event"]["header"]["name"] == "DoorbellPress"
+ assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION"
+ assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell"
diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py
index 73aa5225989..5fc6bc754fa 100644
--- a/tests/components/androidtv/patchers.py
+++ b/tests/components/androidtv/patchers.py
@@ -1,7 +1,7 @@
"""Define patches used for androidtv tests."""
from socket import error as socket_error
-from unittest.mock import patch
+from unittest.mock import mock_open, patch
class AdbDeviceFake:
@@ -128,3 +128,15 @@ def patch_shell(response=None, error=False):
PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake)
+PATCH_ANDROIDTV_OPEN = patch("androidtv.adb_manager.open", mock_open())
+PATCH_KEYGEN = patch("homeassistant.components.androidtv.media_player.keygen")
+PATCH_SIGNER = patch("androidtv.adb_manager.PythonRSASigner")
+
+
+def isfile(filepath):
+ """Mock `os.path.isfile`."""
+ return filepath.endswith("adbkey")
+
+
+PATCH_ISFILE = patch("os.path.isfile", isfile)
+PATCH_ACCESS = patch("os.access", return_value=True)
diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py
index feffc70d841..85f562a3500 100644
--- a/tests/components/androidtv/test_media_player.py
+++ b/tests/components/androidtv/test_media_player.py
@@ -5,6 +5,7 @@ from homeassistant.setup import async_setup_component
from homeassistant.components.androidtv.media_player import (
ANDROIDTV_DOMAIN,
CONF_ADB_SERVER_IP,
+ CONF_ADBKEY,
)
from homeassistant.components.media_player.const import DOMAIN
from homeassistant.const import (
@@ -61,14 +62,8 @@ CONFIG_FIRETV_ADB_SERVER = {
}
-async def _test_reconnect(hass, caplog, config):
- """Test that the error and reconnection attempts are logged correctly.
-
- "Handles device/service unavailable. Log a warning once when
- unavailable, log once when reconnected."
-
- https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html
- """
+def _setup(hass, config):
+ """Perform common setup tasks for the tests."""
if CONF_ADB_SERVER_IP not in config[DOMAIN]:
patch_key = "python"
else:
@@ -79,10 +74,26 @@ async def _test_reconnect(hass, caplog, config):
else:
entity_id = "media_player.fire_tv"
+ return patch_key, entity_id
+
+
+async def _test_reconnect(hass, caplog, config):
+ """Test that the error and reconnection attempts are logged correctly.
+
+ "Handles device/service unavailable. Log a warning once when
+ unavailable, log once when reconnected."
+
+ https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html
+ """
+ patch_key, entity_id = _setup(hass, config)
+
with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[
patch_key
- ], patchers.patch_shell("")[patch_key]:
+ ], patchers.patch_shell("")[
+ patch_key
+ ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
assert await async_setup_component(hass, DOMAIN, config)
+
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
assert state is not None
@@ -93,7 +104,7 @@ async def _test_reconnect(hass, caplog, config):
with patchers.patch_connect(False)[patch_key], patchers.patch_shell(error=True)[
patch_key
- ]:
+ ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
for _ in range(5):
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
@@ -105,7 +116,9 @@ async def _test_reconnect(hass, caplog, config):
assert caplog.record_tuples[1][1] == logging.WARNING
caplog.set_level(logging.DEBUG)
- with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[patch_key]:
+ with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[
+ patch_key
+ ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
# Update 1 will reconnect
await hass.helpers.entity_component.async_update_entity(entity_id)
@@ -143,19 +156,13 @@ async def _test_adb_shell_returns_none(hass, config):
The state should be `None` and the device should be unavailable.
"""
- if CONF_ADB_SERVER_IP not in config[DOMAIN]:
- patch_key = "python"
- else:
- patch_key = "server"
-
- if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv":
- entity_id = "media_player.android_tv"
- else:
- entity_id = "media_player.fire_tv"
+ patch_key, entity_id = _setup(hass, config)
with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[
patch_key
- ], patchers.patch_shell("")[patch_key]:
+ ], patchers.patch_shell("")[
+ patch_key
+ ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
assert await async_setup_component(hass, DOMAIN, config)
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
@@ -164,7 +171,7 @@ async def _test_adb_shell_returns_none(hass, config):
with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[
patch_key
- ]:
+ ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
assert state is not None
@@ -251,3 +258,21 @@ async def test_adb_shell_returns_none_firetv_adb_server(hass):
"""
assert await _test_adb_shell_returns_none(hass, CONFIG_FIRETV_ADB_SERVER)
+
+
+async def test_setup_with_adbkey(hass):
+ """Test that setup succeeds when using an ADB key."""
+ config = CONFIG_ANDROIDTV_PYTHON_ADB.copy()
+ config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey")
+ patch_key, entity_id = _setup(hass, config)
+
+ with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[
+ patch_key
+ ], patchers.patch_shell("")[
+ patch_key
+ ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, patchers.PATCH_ISFILE, patchers.PATCH_ACCESS:
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == STATE_OFF
diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py
index a4202f74d39..78f597c58ad 100644
--- a/tests/components/apns/test_notify.py
+++ b/tests/components/apns/test_notify.py
@@ -239,7 +239,7 @@ class TestApns(unittest.TestCase):
assert "tracking123" == test_device_1.tracking_device_id
assert "tracking456" == test_device_2.tracking_device_id
- @patch("apns2.client.APNsClient")
+ @patch("homeassistant.components.apns.notify.APNsClient")
def test_send(self, mock_client):
"""Test updating an existing device."""
send = mock_client.return_value.send_notification
@@ -274,7 +274,7 @@ class TestApns(unittest.TestCase):
assert "test.mp3" == payload.sound
assert "testing" == payload.category
- @patch("apns2.client.APNsClient")
+ @patch("homeassistant.components.apns.notify.APNsClient")
def test_send_when_disabled(self, mock_client):
"""Test updating an existing device."""
send = mock_client.return_value.send_notification
@@ -299,7 +299,7 @@ class TestApns(unittest.TestCase):
assert not send.called
- @patch("apns2.client.APNsClient")
+ @patch("homeassistant.components.apns.notify.APNsClient")
def test_send_with_state(self, mock_client):
"""Test updating an existing device."""
send = mock_client.return_value.send_notification
@@ -334,7 +334,7 @@ class TestApns(unittest.TestCase):
assert "5678" == target
assert "Hello" == payload.alert
- @patch("apns2.client.APNsClient")
+ @patch("homeassistant.components.apns.notify.APNsClient")
@patch("homeassistant.components.apns.notify._write_device")
def test_disable_when_unregistered(self, mock_write, mock_client):
"""Test disabling a device when it is unregistered."""
diff --git a/tests/components/apprise/__init__.py b/tests/components/apprise/__init__.py
new file mode 100644
index 00000000000..ffebc35b4e1
--- /dev/null
+++ b/tests/components/apprise/__init__.py
@@ -0,0 +1 @@
+"""Tests for the apprise component."""
diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py
new file mode 100644
index 00000000000..237f99de676
--- /dev/null
+++ b/tests/components/apprise/test_notify.py
@@ -0,0 +1,148 @@
+"""The tests for the apprise notification platform."""
+from unittest.mock import patch
+from unittest.mock import MagicMock
+
+from homeassistant.setup import async_setup_component
+
+BASE_COMPONENT = "notify"
+
+
+async def test_apprise_config_load_fail01(hass):
+ """Test apprise configuration failures 1."""
+
+ config = {
+ BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"}
+ }
+
+ with patch("apprise.AppriseConfig.add", return_value=False):
+ assert await async_setup_component(hass, BASE_COMPONENT, config)
+ await hass.async_block_till_done()
+
+ # Test that our service failed to load
+ assert not hass.services.has_service(BASE_COMPONENT, "test")
+
+
+async def test_apprise_config_load_fail02(hass):
+ """Test apprise configuration failures 2."""
+
+ config = {
+ BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"}
+ }
+
+ with patch("apprise.Apprise.add", return_value=False):
+ with patch("apprise.AppriseConfig.add", return_value=True):
+ assert await async_setup_component(hass, BASE_COMPONENT, config)
+ await hass.async_block_till_done()
+
+ # Test that our service failed to load
+ assert not hass.services.has_service(BASE_COMPONENT, "test")
+
+
+async def test_apprise_config_load_okay(hass, tmp_path):
+ """Test apprise configuration failures."""
+
+ # Test cases where our URL is invalid
+ d = tmp_path / "apprise-config"
+ d.mkdir()
+ f = d / "apprise"
+ f.write_text("mailto://user:pass@example.com/")
+
+ config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}}
+
+ assert await async_setup_component(hass, BASE_COMPONENT, config)
+ await hass.async_block_till_done()
+
+ # Valid configuration was loaded; our service is good
+ assert hass.services.has_service(BASE_COMPONENT, "test")
+
+
+async def test_apprise_url_load_fail(hass):
+ """Test apprise url failure."""
+
+ config = {
+ BASE_COMPONENT: {
+ "name": "test",
+ "platform": "apprise",
+ "url": "mailto://user:pass@example.com",
+ }
+ }
+ with patch("apprise.Apprise.add", return_value=False):
+ assert await async_setup_component(hass, BASE_COMPONENT, config)
+ await hass.async_block_till_done()
+
+ # Test that our service failed to load
+ assert not hass.services.has_service(BASE_COMPONENT, "test")
+
+
+async def test_apprise_notification(hass):
+ """Test apprise notification."""
+
+ config = {
+ BASE_COMPONENT: {
+ "name": "test",
+ "platform": "apprise",
+ "url": "mailto://user:pass@example.com",
+ }
+ }
+
+ # Our Message
+ data = {"title": "Test Title", "message": "Test Message"}
+
+ with patch("apprise.Apprise") as mock_apprise:
+ obj = MagicMock()
+ obj.add.return_value = True
+ obj.notify.return_value = True
+ mock_apprise.return_value = obj
+ assert await async_setup_component(hass, BASE_COMPONENT, config)
+ await hass.async_block_till_done()
+
+ # Test the existance of our service
+ assert hass.services.has_service(BASE_COMPONENT, "test")
+
+ # Test the call to our underlining notify() call
+ await hass.services.async_call(BASE_COMPONENT, "test", data)
+ await hass.async_block_till_done()
+
+ # Validate calls were made under the hood correctly
+ obj.add.assert_called_once_with([config[BASE_COMPONENT]["url"]])
+ obj.notify.assert_called_once_with(
+ **{"body": data["message"], "title": data["title"], "tag": None}
+ )
+
+
+async def test_apprise_notification_with_target(hass, tmp_path):
+ """Test apprise notification with a target."""
+
+ # Test cases where our URL is invalid
+ d = tmp_path / "apprise-config"
+ d.mkdir()
+ f = d / "apprise"
+
+ # Write 2 config entries each assigned to different tags
+ f.write_text("devops=mailto://user:pass@example.com/\r\n")
+ f.write_text("system,alert=syslog://\r\n")
+
+ config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}}
+
+ # Our Message, only notify the services tagged with "devops"
+ data = {"title": "Test Title", "message": "Test Message", "target": ["devops"]}
+
+ with patch("apprise.Apprise") as mock_apprise:
+ apprise_obj = MagicMock()
+ apprise_obj.add.return_value = True
+ apprise_obj.notify.return_value = True
+ mock_apprise.return_value = apprise_obj
+ assert await async_setup_component(hass, BASE_COMPONENT, config)
+ await hass.async_block_till_done()
+
+ # Test the existance of our service
+ assert hass.services.has_service(BASE_COMPONENT, "test")
+
+ # Test the call to our underlining notify() call
+ await hass.services.async_call(BASE_COMPONENT, "test", data)
+ await hass.async_block_till_done()
+
+ # Validate calls were made under the hood correctly
+ apprise_obj.notify.assert_called_once_with(
+ **{"body": data["message"], "title": data["title"], "tag": data["target"]}
+ )
diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py
index e79d8a67845..5114e18889b 100644
--- a/tests/components/auth/__init__.py
+++ b/tests/components/auth/__init__.py
@@ -30,7 +30,7 @@ async def async_setup_auth(
hass, provider_configs, module_configs
)
ensure_auth_manager_loaded(hass.auth)
- await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}})
+ await async_setup_component(hass, "auth", {})
if setup_api:
await async_setup_component(hass, "api", {})
return await aiohttp_client(hass.http.app)
diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py
index 80527c2636b..de91613b74b 100644
--- a/tests/components/auth/test_init.py
+++ b/tests/components/auth/test_init.py
@@ -103,7 +103,7 @@ def test_auth_code_store_expiration():
async def test_ws_current_user(hass, hass_ws_client, hass_access_token):
"""Test the current user command with homeassistant creds."""
- assert await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}})
+ assert await async_setup_component(hass, "auth", {})
refresh_token = await hass.auth.async_validate_access_token(hass_access_token)
user = refresh_token.user
diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py
index 6acb40cec88..a0573ce7c1b 100644
--- a/tests/components/automation/test_init.py
+++ b/tests/components/automation/test_init.py
@@ -842,6 +842,25 @@ async def test_automation_with_error_in_script(hass, caplog):
assert "Service not found" in caplog.text
+async def test_automation_with_error_in_script_2(hass, caplog):
+ """Test automation with an error in script."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "alias": "hello",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {"service": None, "entity_id": "hello.world"},
+ }
+ },
+ )
+
+ hass.bus.async_fire("test_event")
+ await hass.async_block_till_done()
+ assert "string value is None" in caplog.text
+
+
async def test_automation_restore_last_triggered_with_initial_state(hass):
"""Ensure last_triggered is restored, even when initial state is set."""
time = dt_util.utcnow()
diff --git a/tests/components/automation/test_reproduce_state.py b/tests/components/automation/test_reproduce_state.py
new file mode 100644
index 00000000000..4f3fd735fc5
--- /dev/null
+++ b/tests/components/automation/test_reproduce_state.py
@@ -0,0 +1,50 @@
+"""Test reproduce state for Automation."""
+from homeassistant.core import State
+
+from tests.common import async_mock_service
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Automation states."""
+ hass.states.async_set("automation.entity_off", "off", {})
+ hass.states.async_set("automation.entity_on", "on", {})
+
+ turn_on_calls = async_mock_service(hass, "automation", "turn_on")
+ turn_off_calls = async_mock_service(hass, "automation", "turn_off")
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [State("automation.entity_off", "off"), State("automation.entity_on", "on")],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("automation.entity_off", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("automation.entity_on", "off"),
+ State("automation.entity_off", "on"),
+ # Should not raise
+ State("automation.non_existing", "on"),
+ ],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 1
+ assert turn_on_calls[0].domain == "automation"
+ assert turn_on_calls[0].data == {"entity_id": "automation.entity_off"}
+
+ assert len(turn_off_calls) == 1
+ assert turn_off_calls[0].domain == "automation"
+ assert turn_off_calls[0].data == {"entity_id": "automation.entity_on"}
diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py
index 5ec3f933e9e..5aec416961d 100644
--- a/tests/components/axis/test_config_flow.py
+++ b/tests/components/axis/test_config_flow.py
@@ -186,6 +186,7 @@ async def test_zeroconf_flow(hass):
data={
config_flow.CONF_HOST: "1.2.3.4",
config_flow.CONF_PORT: 80,
+ "hostname": "name",
"properties": {"macaddress": "00408C12345"},
},
context={"source": "zeroconf"},
@@ -319,6 +320,7 @@ async def test_zeroconf_flow_bad_config_file(hass):
config_flow.DOMAIN,
data={
config_flow.CONF_HOST: "1.2.3.4",
+ "hostname": "name",
"properties": {"macaddress": "00408C12345"},
},
context={"source": "zeroconf"},
diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py
index b5502d8fe3d..34cf4030a50 100644
--- a/tests/components/binary_sensor/test_device_condition.py
+++ b/tests/components/binary_sensor/test_device_condition.py
@@ -1,5 +1,7 @@
"""The test for binary_sensor device automation."""
+from datetime import timedelta
import pytest
+from unittest.mock import patch
from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES
from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS
@@ -7,6 +9,7 @@ from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation
from homeassistant.helpers import device_registry
+import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
@@ -14,6 +17,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
async_get_device_automations,
+ async_get_device_automation_capabilities,
)
@@ -71,6 +75,28 @@ async def test_get_conditions(hass, device_reg, entity_reg):
assert conditions == expected_conditions
+async def test_get_condition_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a binary_sensor condition."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_capabilities = {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ for condition in conditions:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "condition", condition
+ )
+ assert capabilities == expected_capabilities
+
+
async def test_if_state(hass, calls):
"""Test for turn_on and turn_off conditions."""
platform = getattr(hass.components, f"test.{DOMAIN}")
@@ -131,7 +157,6 @@ async def test_if_state(hass, calls):
assert len(calls) == 0
hass.bus.async_fire("test_event1")
- hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data["some"] == "is_on event - test_event1"
@@ -142,3 +167,73 @@ async def test_if_state(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 2
assert calls[1].data["some"] == "is_off event - test_event2"
+
+
+async def test_if_fires_on_for_condition(hass, calls):
+ """Test for firing if condition is on with delay."""
+ point1 = dt_util.utcnow()
+ point2 = point1 + timedelta(seconds=10)
+ point3 = point2 + timedelta(seconds=10)
+
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ sensor1 = platform.ENTITIES["battery"]
+
+ with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow:
+ mock_utcnow.return_value = point1
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": sensor1.entity_id,
+ "type": "is_not_bat_low",
+ "for": {"seconds": 5},
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ ("platform", "event.event_type")
+ )
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(sensor1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ # Time travel 10 secs into the future
+ mock_utcnow.return_value = point2
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(sensor1.entity_id, STATE_OFF)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ # Time travel 20 secs into the future
+ mock_utcnow.return_value = point3
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "is_off event - test_event1"
diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py
index c41d5dcef41..34309fdbcf3 100644
--- a/tests/components/blackbird/test_media_player.py
+++ b/tests/components/blackbird/test_media_player.py
@@ -180,7 +180,10 @@ class TestBlackbirdMediaPlayer(unittest.TestCase):
self.hass = tests.common.get_test_home_assistant()
self.hass.start()
# Note, source dictionary is unsorted!
- with mock.patch("pyblackbird.get_blackbird", new=lambda *a: self.blackbird):
+ with mock.patch(
+ "homeassistant.components.blackbird.media_player.get_blackbird",
+ new=lambda *a: self.blackbird,
+ ):
setup_platform(
self.hass,
{
diff --git a/tests/components/buienradar/__init__.py b/tests/components/buienradar/__init__.py
new file mode 100644
index 00000000000..15cdd8646d2
--- /dev/null
+++ b/tests/components/buienradar/__init__.py
@@ -0,0 +1 @@
+"""Tests for the buienradar component."""
diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py
new file mode 100644
index 00000000000..c1569e4576b
--- /dev/null
+++ b/tests/components/buienradar/test_sensor.py
@@ -0,0 +1,26 @@
+"""The tests for the Buienradar sensor platform."""
+from homeassistant.setup import async_setup_component
+from homeassistant.components import sensor
+
+
+CONDITIONS = ["stationname", "temperature"]
+BASE_CONFIG = {
+ "sensor": [
+ {
+ "platform": "buienradar",
+ "name": "volkel",
+ "latitude": 51.65,
+ "longitude": 5.7,
+ "monitored_conditions": CONDITIONS,
+ }
+ ]
+}
+
+
+async def test_smoke_test_setup_component(hass):
+ """Smoke test for successfully set-up with default config."""
+ assert await async_setup_component(hass, sensor.DOMAIN, BASE_CONFIG)
+
+ for cond in CONDITIONS:
+ state = hass.states.get(f"sensor.volkel_{cond}")
+ assert state.state == "unknown"
diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py
new file mode 100644
index 00000000000..1a8c94e1712
--- /dev/null
+++ b/tests/components/buienradar/test_weather.py
@@ -0,0 +1,25 @@
+"""The tests for the buienradar weather component."""
+from homeassistant.components import weather
+from homeassistant.setup import async_setup_component
+
+
+# Example config snippet from documentation.
+BASE_CONFIG = {
+ "weather": [
+ {
+ "platform": "buienradar",
+ "name": "volkel",
+ "latitude": 51.65,
+ "longitude": 5.7,
+ "forecast": True,
+ }
+ ]
+}
+
+
+async def test_smoke_test_setup_component(hass):
+ """Smoke test for successfully set-up with default config."""
+ assert await async_setup_component(hass, weather.DOMAIN, BASE_CONFIG)
+
+ state = hass.states.get("weather.volkel")
+ assert state.state == "unknown"
diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py
index 209ab780265..c0be635988a 100644
--- a/tests/components/caldav/test_calendar.py
+++ b/tests/components/caldav/test_calendar.py
@@ -111,6 +111,19 @@ LOCATION:San Francisco
DESCRIPTION:Sunny day
END:VEVENT
END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Global Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:8
+DTSTART:20171127T190000
+DTEND:20171127T200000
+SUMMARY:This is a floating Event
+LOCATION:Hamburg
+DESCRIPTION:What a day
+END:VEVENT
+END:VCALENDAR
""",
]
@@ -292,6 +305,29 @@ async def test_ongoing_event_different_tz(mock_now, hass, calendar):
}
+@patch("homeassistant.util.dt.now", return_value=_local_datetime(19, 10))
+async def test_ongoing_floating_event_returned(mock_now, hass, calendar):
+ """Test that floating events without timezones work."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ print(dt.DEFAULT_TIME_ZONE)
+ print(state)
+ assert state.name == calendar.name
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is a floating Event",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2017-11-27 19:00:00",
+ "end_time": "2017-11-27 20:00:00",
+ "location": "Hamburg",
+ "description": "What a day",
+ }
+
+
@patch("homeassistant.util.dt.now", return_value=_local_datetime(8, 30))
async def test_ongoing_event_with_offset(mock_now, hass, calendar):
"""Test that the offset is taken into account."""
diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py
index f44e65512e3..3754551c230 100644
--- a/tests/components/cert_expiry/test_config_flow.py
+++ b/tests/components/cert_expiry/test_config_flow.py
@@ -1,11 +1,12 @@
"""Tests for the Cert Expiry config flow."""
import pytest
+import ssl
import socket
from unittest.mock import patch
from homeassistant import data_entry_flow
from homeassistant.components.cert_expiry import config_flow
-from homeassistant.components.cert_expiry.const import DEFAULT_PORT
+from homeassistant.components.cert_expiry.const import DEFAULT_NAME, DEFAULT_PORT
from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST
from tests.common import MockConfigEntry, mock_coro
@@ -45,7 +46,7 @@ async def test_user(hass, test_connect):
{CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == "cert_expiry_test_1_2_3"
+ assert result["title"] == NAME
assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_PORT] == PORT
@@ -57,21 +58,21 @@ async def test_import(hass, test_connect):
# import with only host
result = await flow.async_step_import({CONF_HOST: HOST})
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == "ssl_certificate_expiry"
+ assert result["title"] == DEFAULT_NAME
assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_PORT] == DEFAULT_PORT
# import with host and name
result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME})
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == "cert_expiry_test_1_2_3"
+ assert result["title"] == NAME
assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_PORT] == DEFAULT_PORT
# improt with host and port
result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT})
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == "ssl_certificate_expiry"
+ assert result["title"] == DEFAULT_NAME
assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_PORT] == PORT
@@ -80,7 +81,7 @@ async def test_import(hass, test_connect):
{CONF_HOST: HOST, CONF_PORT: PORT, CONF_NAME: NAME}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == "cert_expiry_test_1_2_3"
+ assert result["title"] == NAME
assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_PORT] == PORT
@@ -112,7 +113,7 @@ async def test_abort_if_already_setup(hass, test_connect):
{CONF_HOST: HOST, CONF_NAME: NAME, CONF_PORT: 888}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == "cert_expiry_test_1_2_3"
+ assert result["title"] == NAME
assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_PORT] == 888
@@ -131,7 +132,22 @@ async def test_abort_on_socket_failed(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_HOST: "connection_timeout"}
- with patch("socket.create_connection", side_effect=OSError()):
+ with patch(
+ "socket.create_connection",
+ side_effect=ssl.CertificateError(f"{HOST} doesn't match somethingelse.com"),
+ ):
result = await flow.async_step_user({CONF_HOST: HOST})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_HOST: "certificate_fetch_failed"}
+ assert result["errors"] == {CONF_HOST: "wrong_host"}
+
+ with patch(
+ "socket.create_connection", side_effect=ssl.CertificateError("different error")
+ ):
+ result = await flow.async_step_user({CONF_HOST: HOST})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_HOST: "certificate_error"}
+
+ with patch("socket.create_connection", side_effect=ssl.SSLError()):
+ result = await flow.async_step_user({CONF_HOST: HOST})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_HOST: "certificate_error"}
diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py
index a0196cae32a..45ea4e43ee4 100644
--- a/tests/components/cloud/__init__.py
+++ b/tests/components/cloud/__init__.py
@@ -25,4 +25,4 @@ def mock_cloud_prefs(hass, prefs={}):
}
prefs_to_set.update(prefs)
hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set
- return prefs_to_set
+ return hass.data[cloud.DOMAIN].client._prefs
diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py
index b7ac5f4cffd..054b38daffc 100644
--- a/tests/components/cloud/test_client.py
+++ b/tests/components/cloud/test_client.py
@@ -61,7 +61,7 @@ async def test_handler_alexa(hass):
async def test_handler_alexa_disabled(hass, mock_cloud_fixture):
"""Test handler Alexa when user has disabled it."""
- mock_cloud_fixture[PREF_ENABLE_ALEXA] = False
+ mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False
cloud = hass.data["cloud"]
resp = await cloud.client.async_alexa_message(
@@ -125,7 +125,7 @@ async def test_handler_google_actions(hass):
async def test_handler_google_actions_disabled(hass, mock_cloud_fixture):
"""Test handler Google Actions when user has disabled it."""
- mock_cloud_fixture[PREF_ENABLE_GOOGLE] = False
+ mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False
with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()):
assert await async_setup_component(hass, "cloud", {})
diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py
index 8e03fb82b2c..314db3a9e88 100644
--- a/tests/components/cloud/test_http_api.py
+++ b/tests/components/cloud/test_http_api.py
@@ -10,13 +10,7 @@ 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,
- RequireRelink,
-)
+from homeassistant.components.cloud.const import DOMAIN, RequireRelink
from homeassistant.components.google_assistant.helpers import GoogleEntity
from homeassistant.components.alexa.entities import LightCapabilities
from homeassistant.components.alexa import errors as alexa_errors
@@ -474,9 +468,9 @@ async def test_websocket_update_preferences(
hass, hass_ws_client, aioclient_mock, setup_api, mock_cloud_login
):
"""Test updating preference."""
- assert setup_api[PREF_ENABLE_GOOGLE]
- assert setup_api[PREF_ENABLE_ALEXA]
- assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] is None
+ assert setup_api.google_enabled
+ assert setup_api.alexa_enabled
+ assert setup_api.google_secure_devices_pin is None
client = await hass_ws_client(hass)
await client.send_json(
{
@@ -490,9 +484,9 @@ async def test_websocket_update_preferences(
response = await client.receive_json()
assert response["success"]
- assert not setup_api[PREF_ENABLE_GOOGLE]
- assert not setup_api[PREF_ENABLE_ALEXA]
- assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] == "1234"
+ assert not setup_api.google_enabled
+ assert not setup_api.alexa_enabled
+ assert setup_api.google_secure_devices_pin == "1234"
async def test_websocket_update_preferences_require_relink(
diff --git a/tests/components/conftest.py b/tests/components/conftest.py
index 8c2515939f3..4f1f3e64e02 100644
--- a/tests/components/conftest.py
+++ b/tests/components/conftest.py
@@ -40,7 +40,9 @@ def hass_ws_client(aiohttp_client, hass_access_token):
assert auth_resp["type"] == TYPE_AUTH_REQUIRED
if access_token is None:
- await websocket.send_json({"type": TYPE_AUTH, "api_password": "bla"})
+ await websocket.send_json(
+ {"type": TYPE_AUTH, "access_token": "incorrect"}
+ )
else:
await websocket.send_json(
{"type": TYPE_AUTH, "access_token": access_token}
diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py
index d4142f2ce5f..a9116ac0d98 100644
--- a/tests/components/conversation/test_init.py
+++ b/tests/components/conversation/test_init.py
@@ -263,54 +263,27 @@ async def test_http_api_wrong_data(hass, hass_client):
assert resp.status == 400
-def test_create_matcher():
- """Test the create matcher method."""
- # Basic sentence
- pattern = conversation.create_matcher("Hello world")
- assert pattern.match("Hello world") is not None
+async def test_custom_agent(hass, hass_client):
+ """Test a custom conversation agent."""
- # Match a part
- pattern = conversation.create_matcher("Hello {name}")
- match = pattern.match("hello world")
- assert match is not None
- assert match.groupdict()["name"] == "world"
- no_match = pattern.match("Hello world, how are you?")
- assert no_match is None
+ class MyAgent(conversation.AbstractConversationAgent):
+ """Test Agent."""
- # Optional and matching part
- pattern = conversation.create_matcher("Turn on [the] {name}")
- match = pattern.match("turn on the kitchen lights")
- assert match is not None
- assert match.groupdict()["name"] == "kitchen lights"
- match = pattern.match("turn on kitchen lights")
- assert match is not None
- assert match.groupdict()["name"] == "kitchen lights"
- match = pattern.match("turn off kitchen lights")
- assert match is None
+ async def async_process(self, text):
+ """Process some text."""
+ response = intent.IntentResponse()
+ response.async_set_speech("Test response")
+ return response
- # Two different optional parts, 1 matching part
- pattern = conversation.create_matcher("Turn on [the] [a] {name}")
- match = pattern.match("turn on the kitchen lights")
- assert match is not None
- assert match.groupdict()["name"] == "kitchen lights"
- match = pattern.match("turn on kitchen lights")
- assert match is not None
- assert match.groupdict()["name"] == "kitchen lights"
- match = pattern.match("turn on a kitchen light")
- assert match is not None
- assert match.groupdict()["name"] == "kitchen light"
+ conversation.async_set_agent(hass, MyAgent())
- # Strip plural
- pattern = conversation.create_matcher("Turn {name}[s] on")
- match = pattern.match("turn kitchen lights on")
- assert match is not None
- assert match.groupdict()["name"] == "kitchen light"
+ assert await async_setup_component(hass, "conversation", {})
- # Optional 2 words
- pattern = conversation.create_matcher("Turn [the great] {name} on")
- match = pattern.match("turn the great kitchen lights on")
- assert match is not None
- assert match.groupdict()["name"] == "kitchen lights"
- match = pattern.match("turn kitchen lights on")
- assert match is not None
- assert match.groupdict()["name"] == "kitchen lights"
+ client = await hass_client()
+
+ resp = await client.post("/api/conversation/process", json={"text": "Test Text"})
+ assert resp.status == 200
+ assert await resp.json() == {
+ "card": {},
+ "speech": {"plain": {"extra_data": None, "speech": "Test response"}},
+ }
diff --git a/tests/components/conversation/test_util.py b/tests/components/conversation/test_util.py
new file mode 100644
index 00000000000..2fa4527e9b1
--- /dev/null
+++ b/tests/components/conversation/test_util.py
@@ -0,0 +1,55 @@
+"""Test the conversation utils."""
+from homeassistant.components.conversation.util import create_matcher
+
+
+def test_create_matcher():
+ """Test the create matcher method."""
+ # Basic sentence
+ pattern = create_matcher("Hello world")
+ assert pattern.match("Hello world") is not None
+
+ # Match a part
+ pattern = create_matcher("Hello {name}")
+ match = pattern.match("hello world")
+ assert match is not None
+ assert match.groupdict()["name"] == "world"
+ no_match = pattern.match("Hello world, how are you?")
+ assert no_match is None
+
+ # Optional and matching part
+ pattern = create_matcher("Turn on [the] {name}")
+ match = pattern.match("turn on the kitchen lights")
+ assert match is not None
+ assert match.groupdict()["name"] == "kitchen lights"
+ match = pattern.match("turn on kitchen lights")
+ assert match is not None
+ assert match.groupdict()["name"] == "kitchen lights"
+ match = pattern.match("turn off kitchen lights")
+ assert match is None
+
+ # Two different optional parts, 1 matching part
+ pattern = create_matcher("Turn on [the] [a] {name}")
+ match = pattern.match("turn on the kitchen lights")
+ assert match is not None
+ assert match.groupdict()["name"] == "kitchen lights"
+ match = pattern.match("turn on kitchen lights")
+ assert match is not None
+ assert match.groupdict()["name"] == "kitchen lights"
+ match = pattern.match("turn on a kitchen light")
+ assert match is not None
+ assert match.groupdict()["name"] == "kitchen light"
+
+ # Strip plural
+ pattern = create_matcher("Turn {name}[s] on")
+ match = pattern.match("turn kitchen lights on")
+ assert match is not None
+ assert match.groupdict()["name"] == "kitchen light"
+
+ # Optional 2 words
+ pattern = create_matcher("Turn [the great] {name} on")
+ match = pattern.match("turn the great kitchen lights on")
+ assert match is not None
+ assert match.groupdict()["name"] == "kitchen lights"
+ match = pattern.match("turn kitchen lights on")
+ assert match is not None
+ assert match.groupdict()["name"] == "kitchen lights"
diff --git a/tests/components/coolmaster/__init__.py b/tests/components/coolmaster/__init__.py
new file mode 100644
index 00000000000..a7e1bf08c99
--- /dev/null
+++ b/tests/components/coolmaster/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Coolmaster component."""
diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py
new file mode 100644
index 00000000000..d49858fcf05
--- /dev/null
+++ b/tests/components/coolmaster/test_config_flow.py
@@ -0,0 +1,104 @@
+"""Test the Coolmaster config flow."""
+from unittest.mock import patch
+
+from homeassistant import config_entries, setup
+from homeassistant.components.coolmaster.const import DOMAIN, AVAILABLE_MODES
+
+# from homeassistant.components.coolmaster.config_flow import validate_connection
+
+from tests.common import mock_coro
+
+
+def _flow_data():
+ options = {"host": "1.1.1.1"}
+ for mode in AVAILABLE_MODES:
+ options[mode] = True
+ return options
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] is None
+
+ with patch(
+ "homeassistant.components.coolmaster.config_flow.validate_connection",
+ return_value=mock_coro(True),
+ ), patch(
+ "homeassistant.components.coolmaster.async_setup", return_value=mock_coro(True)
+ ) as mock_setup, patch(
+ "homeassistant.components.coolmaster.async_setup_entry",
+ return_value=mock_coro(True),
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], _flow_data()
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "1.1.1.1"
+ assert result2["data"] == {
+ "host": "1.1.1.1",
+ "port": 10102,
+ "supported_modes": AVAILABLE_MODES,
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_timeout(hass):
+ """Test we handle a connection timeout."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.coolmaster.config_flow.validate_connection",
+ side_effect=TimeoutError(),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], _flow_data()
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "connection_error"}
+
+
+async def test_form_connection_refused(hass):
+ """Test we handle a connection error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.coolmaster.config_flow.validate_connection",
+ side_effect=ConnectionRefusedError(),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], _flow_data()
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "connection_error"}
+
+
+async def test_form_no_units(hass):
+ """Test we handle no units found."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.coolmaster.config_flow.validate_connection",
+ return_value=mock_coro(False),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], _flow_data()
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "no_units"}
diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py
index 664f4d014b7..8ce90e164b6 100644
--- a/tests/components/counter/test_init.py
+++ b/tests/components/counter/test_init.py
@@ -174,14 +174,22 @@ def test_initial_state_overrules_restore_state(hass):
@asyncio.coroutine
def test_restore_state_overrules_initial_state(hass):
"""Ensure states are restored on startup."""
+
+ attr = {"initial": 6, "minimum": 1, "maximum": 8, "step": 2}
+
mock_restore_cache(
- hass, (State("counter.test1", "11"), State("counter.test2", "-22"))
+ hass,
+ (
+ State("counter.test1", "11"),
+ State("counter.test2", "-22"),
+ State("counter.test3", "5", attr),
+ ),
)
hass.state = CoreState.starting
yield from async_setup_component(
- hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}}}
+ hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}, "test3": {}}}
)
state = hass.states.get("counter.test1")
@@ -192,6 +200,14 @@ def test_restore_state_overrules_initial_state(hass):
assert state
assert int(state.state) == -22
+ state = hass.states.get("counter.test3")
+ assert state
+ assert int(state.state) == 5
+ assert state.attributes.get("initial") == 6
+ assert state.attributes.get("minimum") == 1
+ assert state.attributes.get("maximum") == 8
+ assert state.attributes.get("step") == 2
+
@asyncio.coroutine
def test_no_initial_state_and_no_restore_state(hass):
@@ -379,11 +395,45 @@ async def test_configure(hass, hass_admin_user):
assert state.state == "5"
assert 3 == state.attributes.get("step")
+ # update value
+ await hass.services.async_call(
+ "counter",
+ "configure",
+ {"entity_id": state.entity_id, "value": 6},
+ True,
+ Context(user_id=hass_admin_user.id),
+ )
+
+ state = hass.states.get("counter.test")
+ assert state is not None
+ assert state.state == "6"
+
+ # update initial
+ await hass.services.async_call(
+ "counter",
+ "configure",
+ {"entity_id": state.entity_id, "initial": 5},
+ True,
+ Context(user_id=hass_admin_user.id),
+ )
+
+ state = hass.states.get("counter.test")
+ assert state is not None
+ assert state.state == "6"
+ assert 5 == state.attributes.get("initial")
+
# update all
await hass.services.async_call(
"counter",
"configure",
- {"entity_id": state.entity_id, "step": 5, "minimum": 0, "maximum": 9},
+ {
+ "entity_id": state.entity_id,
+ "step": 5,
+ "minimum": 0,
+ "maximum": 9,
+ "value": 5,
+ "initial": 6,
+ },
True,
Context(user_id=hass_admin_user.id),
)
@@ -394,3 +444,4 @@ async def test_configure(hass, hass_admin_user):
assert 5 == state.attributes.get("step")
assert 0 == state.attributes.get("minimum")
assert 9 == state.attributes.get("maximum")
+ assert 6 == state.attributes.get("initial")
diff --git a/tests/components/counter/test_reproduce_state.py b/tests/components/counter/test_reproduce_state.py
new file mode 100644
index 00000000000..aa2c5ddbd9a
--- /dev/null
+++ b/tests/components/counter/test_reproduce_state.py
@@ -0,0 +1,71 @@
+"""Test reproduce state for Counter."""
+from homeassistant.core import State
+
+from tests.common import async_mock_service
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Counter states."""
+ hass.states.async_set("counter.entity", "5", {})
+ hass.states.async_set(
+ "counter.entity_attr",
+ "8",
+ {"initial": 12, "minimum": 5, "maximum": 15, "step": 3},
+ )
+
+ configure_calls = async_mock_service(hass, "counter", "configure")
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("counter.entity", "5"),
+ State(
+ "counter.entity_attr",
+ "8",
+ {"initial": 12, "minimum": 5, "maximum": 15, "step": 3},
+ ),
+ ],
+ blocking=True,
+ )
+
+ assert len(configure_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("counter.entity", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(configure_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("counter.entity", "2"),
+ State(
+ "counter.entity_attr",
+ "7",
+ {"initial": 10, "minimum": 3, "maximum": 21, "step": 5},
+ ),
+ # Should not raise
+ State("counter.non_existing", "6"),
+ ],
+ blocking=True,
+ )
+
+ valid_calls = [
+ {"entity_id": "counter.entity", "value": "2"},
+ {
+ "entity_id": "counter.entity_attr",
+ "value": "7",
+ "initial": 10,
+ "minimum": 3,
+ "maximum": 21,
+ "step": 5,
+ },
+ ]
+ assert len(configure_calls) == 2
+ for call in configure_calls:
+ assert call.domain == "counter"
+ assert call.data in valid_calls
+ valid_calls.remove(call.data)
diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py
new file mode 100644
index 00000000000..494368f76ff
--- /dev/null
+++ b/tests/components/cover/test_device_condition.py
@@ -0,0 +1,190 @@
+"""The tests for Cover device conditions."""
+import pytest
+
+from homeassistant.components.cover import DOMAIN
+from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING
+from homeassistant.setup import async_setup_component
+import homeassistant.components.automation as automation
+from homeassistant.helpers import device_registry
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+ async_get_device_automations,
+)
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock serivce."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_conditions(hass, device_reg, entity_reg):
+ """Test we get the expected conditions from a cover."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_open",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_closed",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_opening",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_closing",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ ]
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ assert_lists_same(conditions, expected_conditions)
+
+
+async def test_if_state(hass, calls):
+ """Test for turn_on and turn_off conditions."""
+ hass.states.async_set("cover.entity", STATE_OPEN)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "cover.entity",
+ "type": "is_open",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_open - {{ trigger.platform }} - {{ trigger.event.event_type }}"
+ },
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event2"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "cover.entity",
+ "type": "is_closed",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_closed - {{ trigger.platform }} - {{ trigger.event.event_type }}"
+ },
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event3"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "cover.entity",
+ "type": "is_opening",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_opening - {{ trigger.platform }} - {{ trigger.event.event_type }}"
+ },
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event4"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "cover.entity",
+ "type": "is_closing",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_closing - {{ trigger.platform }} - {{ trigger.event.event_type }}"
+ },
+ },
+ },
+ ]
+ },
+ )
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "is_open - event - test_event1"
+
+ hass.states.async_set("cover.entity", STATE_CLOSED)
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "is_closed - event - test_event2"
+
+ hass.states.async_set("cover.entity", STATE_OPENING)
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event3")
+ await hass.async_block_till_done()
+ assert len(calls) == 3
+ assert calls[2].data["some"] == "is_opening - event - test_event3"
+
+ hass.states.async_set("cover.entity", STATE_CLOSING)
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event4")
+ await hass.async_block_till_done()
+ assert len(calls) == 4
+ assert calls[3].data["some"] == "is_closing - event - test_event4"
diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py
new file mode 100644
index 00000000000..39fdf3d3992
--- /dev/null
+++ b/tests/components/cover/test_reproduce_state.py
@@ -0,0 +1,198 @@
+"""Test reproduce state for Cover."""
+from homeassistant.components.cover import (
+ ATTR_CURRENT_POSITION,
+ ATTR_CURRENT_TILT_POSITION,
+ ATTR_POSITION,
+ ATTR_TILT_POSITION,
+)
+from homeassistant.const import (
+ SERVICE_CLOSE_COVER,
+ SERVICE_CLOSE_COVER_TILT,
+ SERVICE_OPEN_COVER,
+ SERVICE_OPEN_COVER_TILT,
+ SERVICE_SET_COVER_POSITION,
+ SERVICE_SET_COVER_TILT_POSITION,
+ STATE_CLOSED,
+ STATE_OPEN,
+)
+from homeassistant.core import State
+from tests.common import async_mock_service
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Cover states."""
+ hass.states.async_set("cover.entity_close", STATE_CLOSED, {})
+ hass.states.async_set(
+ "cover.entity_close_attr",
+ STATE_CLOSED,
+ {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0},
+ )
+ hass.states.async_set(
+ "cover.entity_close_tilt", STATE_CLOSED, {ATTR_CURRENT_TILT_POSITION: 50}
+ )
+ hass.states.async_set("cover.entity_open", STATE_OPEN, {})
+ hass.states.async_set(
+ "cover.entity_slightly_open", STATE_OPEN, {ATTR_CURRENT_POSITION: 50}
+ )
+ hass.states.async_set(
+ "cover.entity_open_attr",
+ STATE_OPEN,
+ {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 0},
+ )
+ hass.states.async_set(
+ "cover.entity_open_tilt",
+ STATE_OPEN,
+ {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50},
+ )
+ hass.states.async_set(
+ "cover.entity_entirely_open",
+ STATE_OPEN,
+ {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100},
+ )
+
+ close_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER)
+ open_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
+ close_tilt_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER_TILT)
+ open_tilt_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER_TILT)
+ position_calls = async_mock_service(hass, "cover", SERVICE_SET_COVER_POSITION)
+ position_tilt_calls = async_mock_service(
+ hass, "cover", SERVICE_SET_COVER_TILT_POSITION
+ )
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("cover.entity_close", STATE_CLOSED),
+ State(
+ "cover.entity_close_attr",
+ STATE_CLOSED,
+ {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0},
+ ),
+ State(
+ "cover.entity_close_tilt",
+ STATE_CLOSED,
+ {ATTR_CURRENT_TILT_POSITION: 50},
+ ),
+ State("cover.entity_open", STATE_OPEN),
+ State(
+ "cover.entity_slightly_open", STATE_OPEN, {ATTR_CURRENT_POSITION: 50}
+ ),
+ State(
+ "cover.entity_open_attr",
+ STATE_OPEN,
+ {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 0},
+ ),
+ State(
+ "cover.entity_open_tilt",
+ STATE_OPEN,
+ {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50},
+ ),
+ State(
+ "cover.entity_entirely_open",
+ STATE_OPEN,
+ {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100},
+ ),
+ ],
+ blocking=True,
+ )
+
+ assert len(close_calls) == 0
+ assert len(open_calls) == 0
+ assert len(close_tilt_calls) == 0
+ assert len(open_tilt_calls) == 0
+ assert len(position_calls) == 0
+ assert len(position_tilt_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("cover.entity_close", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(close_calls) == 0
+ assert len(open_calls) == 0
+ assert len(close_tilt_calls) == 0
+ assert len(open_tilt_calls) == 0
+ assert len(position_calls) == 0
+ assert len(position_tilt_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("cover.entity_close", STATE_OPEN),
+ State(
+ "cover.entity_close_attr",
+ STATE_OPEN,
+ {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50},
+ ),
+ State(
+ "cover.entity_close_tilt",
+ STATE_CLOSED,
+ {ATTR_CURRENT_TILT_POSITION: 100},
+ ),
+ State("cover.entity_open", STATE_CLOSED),
+ State("cover.entity_slightly_open", STATE_OPEN, {}),
+ State("cover.entity_open_attr", STATE_CLOSED, {}),
+ State(
+ "cover.entity_open_tilt", STATE_OPEN, {ATTR_CURRENT_TILT_POSITION: 0}
+ ),
+ State(
+ "cover.entity_entirely_open",
+ STATE_CLOSED,
+ {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0},
+ ),
+ # Should not raise
+ State("cover.non_existing", "on"),
+ ],
+ blocking=True,
+ )
+
+ valid_close_calls = [
+ {"entity_id": "cover.entity_open"},
+ {"entity_id": "cover.entity_open_attr"},
+ {"entity_id": "cover.entity_entirely_open"},
+ ]
+ assert len(close_calls) == 3
+ for call in close_calls:
+ assert call.domain == "cover"
+ assert call.data in valid_close_calls
+ valid_close_calls.remove(call.data)
+
+ valid_open_calls = [
+ {"entity_id": "cover.entity_close"},
+ {"entity_id": "cover.entity_slightly_open"},
+ {"entity_id": "cover.entity_open_tilt"},
+ ]
+ assert len(open_calls) == 3
+ for call in open_calls:
+ assert call.domain == "cover"
+ assert call.data in valid_open_calls
+ valid_open_calls.remove(call.data)
+
+ valid_close_tilt_calls = [
+ {"entity_id": "cover.entity_open_tilt"},
+ {"entity_id": "cover.entity_entirely_open"},
+ ]
+ assert len(close_tilt_calls) == 2
+ for call in close_tilt_calls:
+ assert call.domain == "cover"
+ assert call.data in valid_close_tilt_calls
+ valid_close_tilt_calls.remove(call.data)
+
+ assert len(open_tilt_calls) == 1
+ assert open_tilt_calls[0].domain == "cover"
+ assert open_tilt_calls[0].data == {"entity_id": "cover.entity_close_tilt"}
+
+ assert len(position_calls) == 1
+ assert position_calls[0].domain == "cover"
+ assert position_calls[0].data == {
+ "entity_id": "cover.entity_close_attr",
+ ATTR_POSITION: 50,
+ }
+
+ assert len(position_tilt_calls) == 1
+ assert position_tilt_calls[0].domain == "cover"
+ assert position_tilt_calls[0].data == {
+ "entity_id": "cover.entity_close_attr",
+ ATTR_TILT_POSITION: 50,
+ }
diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py
index 23d16ed35f4..be66b74c186 100644
--- a/tests/components/darksky/test_sensor.py
+++ b/tests/components/darksky/test_sensor.py
@@ -112,7 +112,10 @@ class TestDarkSkySetup(unittest.TestCase):
self.hass.stop()
@MockDependency("forecastio")
- @patch("forecastio.load_forecast", new=load_forecastMock)
+ @patch(
+ "homeassistant.components.darksky.sensor.forecastio.load_forecast",
+ new=load_forecastMock,
+ )
def test_setup_with_config(self, mock_forecastio):
"""Test the platform setup with configuration."""
setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL)
@@ -120,9 +123,7 @@ class TestDarkSkySetup(unittest.TestCase):
state = self.hass.states.get("sensor.dark_sky_summary")
assert state is not None
- @MockDependency("forecastio")
- @patch("forecastio.load_forecast", new=load_forecastMock)
- def test_setup_with_invalid_config(self, mock_forecastio):
+ def test_setup_with_invalid_config(self):
"""Test the platform setup with invalid configuration."""
setup_component(self.hass, "sensor", INVALID_CONFIG_MINIMAL)
@@ -130,7 +131,10 @@ class TestDarkSkySetup(unittest.TestCase):
assert state is None
@MockDependency("forecastio")
- @patch("forecastio.load_forecast", new=load_forecastMock)
+ @patch(
+ "homeassistant.components.darksky.sensor.forecastio.load_forecast",
+ new=load_forecastMock,
+ )
def test_setup_with_language_config(self, mock_forecastio):
"""Test the platform setup with language configuration."""
setup_component(self.hass, "sensor", VALID_CONFIG_LANG_DE)
@@ -138,9 +142,7 @@ class TestDarkSkySetup(unittest.TestCase):
state = self.hass.states.get("sensor.dark_sky_summary")
assert state is not None
- @MockDependency("forecastio")
- @patch("forecastio.load_forecast", new=load_forecastMock)
- def test_setup_with_invalid_language_config(self, mock_forecastio):
+ def test_setup_with_invalid_language_config(self):
"""Test the platform setup with language configuration."""
setup_component(self.hass, "sensor", INVALID_CONFIG_LANG)
@@ -164,7 +166,10 @@ class TestDarkSkySetup(unittest.TestCase):
assert not response
@MockDependency("forecastio")
- @patch("forecastio.load_forecast", new=load_forecastMock)
+ @patch(
+ "homeassistant.components.darksky.sensor.forecastio.load_forecast",
+ new=load_forecastMock,
+ )
def test_setup_with_alerts_config(self, mock_forecastio):
"""Test the platform setup with alert configuration."""
setup_component(self.hass, "sensor", VALID_CONFIG_ALERTS)
diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py
index ea28d3facb9..ca328f45839 100644
--- a/tests/components/darksky/test_weather.py
+++ b/tests/components/darksky/test_weather.py
@@ -6,6 +6,8 @@ from unittest.mock import patch
import forecastio
import requests_mock
+from requests.exceptions import ConnectionError
+
from homeassistant.components import weather
from homeassistant.util.unit_system import METRIC_SYSTEM
from homeassistant.setup import setup_component
@@ -48,3 +50,16 @@ class TestDarkSky(unittest.TestCase):
state = self.hass.states.get("weather.test")
assert state.state == "sunny"
+
+ @patch("forecastio.load_forecast", side_effect=ConnectionError())
+ def test_failed_setup(self, mock_load_forecast):
+ """Test to ensure that a network error does not break component state."""
+
+ assert setup_component(
+ self.hass,
+ weather.DOMAIN,
+ {"weather": {"name": "test", "platform": "darksky", "api_key": "foo"}},
+ )
+
+ state = self.hass.states.get("weather.test")
+ assert state.state == "unavailable"
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
index d0423c394a6..4045201bd18 100644
--- a/tests/components/deconz/test_config_flow.py
+++ b/tests/components/deconz/test_config_flow.py
@@ -387,7 +387,7 @@ async def test_hassio_confirm(hass):
async def test_option_flow(hass):
- """Test config flow selection of one of two bridges."""
+ """Test config flow options."""
entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None)
hass.config_entries._entries.append(entry)
diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py
index fa78ae94416..3c0e3b1eca7 100644
--- a/tests/components/device_automation/test_init.py
+++ b/tests/components/device_automation/test_init.py
@@ -170,6 +170,204 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r
assert _same_lists(triggers, expected_triggers)
+async def test_websocket_get_action_capabilities(
+ hass, hass_ws_client, device_reg, entity_reg
+):
+ """Test we get the expected action capabilities for an alarm through websocket."""
+ await async_setup_component(hass, "device_automation", {})
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(
+ "alarm_control_panel", "test", "5678", device_id=device_entry.id
+ )
+ expected_capabilities = {
+ "arm_away": {"extra_fields": []},
+ "arm_home": {"extra_fields": []},
+ "arm_night": {"extra_fields": []},
+ "disarm": {
+ "extra_fields": [{"name": "code", "optional": True, "type": "string"}]
+ },
+ "trigger": {"extra_fields": []},
+ }
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {"id": 1, "type": "device_automation/action/list", "device_id": device_entry.id}
+ )
+ msg = await client.receive_json()
+
+ assert msg["id"] == 1
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ actions = msg["result"]
+
+ id = 2
+ assert len(actions) == 5
+ for action in actions:
+ await client.send_json(
+ {
+ "id": id,
+ "type": "device_automation/action/capabilities",
+ "action": action,
+ }
+ )
+ msg = await client.receive_json()
+ assert msg["id"] == id
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ capabilities = msg["result"]
+ assert capabilities == expected_capabilities[action["type"]]
+ id = id + 1
+
+
+async def test_websocket_get_bad_action_capabilities(
+ hass, hass_ws_client, device_reg, entity_reg
+):
+ """Test we get no action capabilities for a non existing domain."""
+ await async_setup_component(hass, "device_automation", {})
+ expected_capabilities = {}
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 1,
+ "type": "device_automation/action/capabilities",
+ "action": {"domain": "beer"},
+ }
+ )
+ msg = await client.receive_json()
+ assert msg["id"] == 1
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ capabilities = msg["result"]
+ assert capabilities == expected_capabilities
+
+
+async def test_websocket_get_no_action_capabilities(
+ hass, hass_ws_client, device_reg, entity_reg
+):
+ """Test we get no action capabilities for a domain with no device action capabilities."""
+ await async_setup_component(hass, "device_automation", {})
+ expected_capabilities = {}
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 1,
+ "type": "device_automation/action/capabilities",
+ "action": {"domain": "deconz"},
+ }
+ )
+ msg = await client.receive_json()
+ assert msg["id"] == 1
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ capabilities = msg["result"]
+ assert capabilities == expected_capabilities
+
+
+async def test_websocket_get_condition_capabilities(
+ hass, hass_ws_client, device_reg, entity_reg
+):
+ """Test we get the expected condition capabilities for a light through websocket."""
+ await async_setup_component(hass, "device_automation", {})
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
+ expected_capabilities = {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 1,
+ "type": "device_automation/condition/list",
+ "device_id": device_entry.id,
+ }
+ )
+ msg = await client.receive_json()
+
+ assert msg["id"] == 1
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ conditions = msg["result"]
+
+ id = 2
+ assert len(conditions) == 2
+ for condition in conditions:
+ await client.send_json(
+ {
+ "id": id,
+ "type": "device_automation/condition/capabilities",
+ "condition": condition,
+ }
+ )
+ msg = await client.receive_json()
+ assert msg["id"] == id
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ capabilities = msg["result"]
+ assert capabilities == expected_capabilities
+ id = id + 1
+
+
+async def test_websocket_get_bad_condition_capabilities(
+ hass, hass_ws_client, device_reg, entity_reg
+):
+ """Test we get no condition capabilities for a non existing domain."""
+ await async_setup_component(hass, "device_automation", {})
+ expected_capabilities = {}
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 1,
+ "type": "device_automation/condition/capabilities",
+ "condition": {"domain": "beer"},
+ }
+ )
+ msg = await client.receive_json()
+ assert msg["id"] == 1
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ capabilities = msg["result"]
+ assert capabilities == expected_capabilities
+
+
+async def test_websocket_get_no_condition_capabilities(
+ hass, hass_ws_client, device_reg, entity_reg
+):
+ """Test we get no condition capabilities for a domain with no device condition capabilities."""
+ await async_setup_component(hass, "device_automation", {})
+ expected_capabilities = {}
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 1,
+ "type": "device_automation/condition/capabilities",
+ "condition": {"domain": "deconz"},
+ }
+ )
+ msg = await client.receive_json()
+ assert msg["id"] == 1
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ capabilities = msg["result"]
+ assert capabilities == expected_capabilities
+
+
async def test_websocket_get_trigger_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
@@ -204,6 +402,7 @@ async def test_websocket_get_trigger_capabilities(
triggers = msg["result"]
id = 2
+ assert len(triggers) == 2
for trigger in triggers:
await client.send_json(
{
diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py
index 57dfa183feb..195345dd489 100644
--- a/tests/components/dsmr/test_sensor.py
+++ b/tests/components/dsmr/test_sensor.py
@@ -11,9 +11,11 @@ from decimal import Decimal
from unittest.mock import Mock
import asynctest
+import pytest
+
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.dsmr.sensor import DerivativeDSMREntity
-import pytest
+
from tests.common import assert_setup_component
@@ -34,10 +36,11 @@ def mock_connection_factory(monkeypatch):
# apply the mock to both connection factories
monkeypatch.setattr(
- "dsmr_parser.clients.protocol.create_dsmr_reader", connection_factory
+ "homeassistant.components.dsmr.sensor.create_dsmr_reader", connection_factory
)
monkeypatch.setattr(
- "dsmr_parser.clients.protocol.create_tcp_dsmr_reader", connection_factory
+ "homeassistant.components.dsmr.sensor.create_tcp_dsmr_reader",
+ connection_factory,
)
return connection_factory, transport, protocol
@@ -158,7 +161,8 @@ def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory):
)
monkeypatch.setattr(
- "dsmr_parser.clients.protocol.create_dsmr_reader", first_fail_connection_factory
+ "homeassistant.components.dsmr.sensor.create_dsmr_reader",
+ first_fail_connection_factory,
)
yield from async_setup_component(hass, "sensor", {"sensor": config})
diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py
new file mode 100644
index 00000000000..0dcd38580b8
--- /dev/null
+++ b/tests/components/fan/test_reproduce_state.py
@@ -0,0 +1,89 @@
+"""Test reproduce state for Fan."""
+from homeassistant.core import State
+
+from tests.common import async_mock_service
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Fan states."""
+ hass.states.async_set("fan.entity_off", "off", {})
+ hass.states.async_set("fan.entity_on", "on", {})
+ hass.states.async_set("fan.entity_speed", "on", {"speed": "high"})
+ hass.states.async_set("fan.entity_oscillating", "on", {"oscillating": True})
+ hass.states.async_set("fan.entity_direction", "on", {"direction": "forward"})
+
+ turn_on_calls = async_mock_service(hass, "fan", "turn_on")
+ turn_off_calls = async_mock_service(hass, "fan", "turn_off")
+ set_direction_calls = async_mock_service(hass, "fan", "set_direction")
+ oscillate_calls = async_mock_service(hass, "fan", "oscillate")
+ set_speed_calls = async_mock_service(hass, "fan", "set_speed")
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("fan.entity_off", "off"),
+ State("fan.entity_on", "on"),
+ State("fan.entity_speed", "on", {"speed": "high"}),
+ State("fan.entity_oscillating", "on", {"oscillating": True}),
+ State("fan.entity_direction", "on", {"direction": "forward"}),
+ ],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+ assert len(set_direction_calls) == 0
+ assert len(oscillate_calls) == 0
+ assert len(set_speed_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("fan.entity_off", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+ assert len(set_direction_calls) == 0
+ assert len(oscillate_calls) == 0
+ assert len(set_speed_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("fan.entity_on", "off"),
+ State("fan.entity_off", "on"),
+ State("fan.entity_speed", "on", {"speed": "low"}),
+ State("fan.entity_oscillating", "on", {"oscillating": False}),
+ State("fan.entity_direction", "on", {"direction": "reverse"}),
+ # Should not raise
+ State("fan.non_existing", "on"),
+ ],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 1
+ assert turn_on_calls[0].domain == "fan"
+ assert turn_on_calls[0].data == {"entity_id": "fan.entity_off"}
+
+ assert len(set_direction_calls) == 1
+ assert set_direction_calls[0].domain == "fan"
+ assert set_direction_calls[0].data == {
+ "entity_id": "fan.entity_direction",
+ "direction": "reverse",
+ }
+
+ assert len(oscillate_calls) == 1
+ assert oscillate_calls[0].domain == "fan"
+ assert oscillate_calls[0].data == {
+ "entity_id": "fan.entity_oscillating",
+ "oscillating": False,
+ }
+
+ assert len(set_speed_calls) == 1
+ assert set_speed_calls[0].domain == "fan"
+ assert set_speed_calls[0].data == {"entity_id": "fan.entity_speed", "speed": "low"}
+
+ assert len(turn_off_calls) == 1
+ assert turn_off_calls[0].domain == "fan"
+ assert turn_off_calls[0].data == {"entity_id": "fan.entity_on"}
diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py
index fb35485f5c9..91871666f46 100644
--- a/tests/components/flux/test_switch.py
+++ b/tests/components/flux/test_switch.py
@@ -10,12 +10,14 @@ from homeassistant.const import (
SERVICE_TURN_ON,
SUN_EVENT_SUNRISE,
)
+from homeassistant.core import State
import homeassistant.util.dt as dt_util
from tests.common import (
assert_setup_component,
async_fire_time_changed,
async_mock_service,
+ mock_restore_cache,
)
from tests.components.light import common as common_light
from tests.components.switch import common
@@ -35,6 +37,52 @@ async def test_valid_config(hass):
},
)
+ state = hass.states.get("switch.flux")
+ assert state
+ assert state.state == "off"
+
+
+async def test_restore_state_last_on(hass):
+ """Test restoring state when the last state is on."""
+ mock_restore_cache(hass, [State("switch.flux", "on")])
+
+ assert await async_setup_component(
+ hass,
+ "switch",
+ {
+ "switch": {
+ "platform": "flux",
+ "name": "flux",
+ "lights": ["light.desk", "light.lamp"],
+ }
+ },
+ )
+
+ state = hass.states.get("switch.flux")
+ assert state
+ assert state.state == "on"
+
+
+async def test_restore_state_last_off(hass):
+ """Test restoring state when the last state is off."""
+ mock_restore_cache(hass, [State("switch.flux", "off")])
+
+ assert await async_setup_component(
+ hass,
+ "switch",
+ {
+ "switch": {
+ "platform": "flux",
+ "name": "flux",
+ "lights": ["light.desk", "light.lamp"],
+ }
+ },
+ )
+
+ state = hass.states.get("switch.flux")
+ assert state
+ assert state.state == "off"
+
async def test_valid_config_with_info(hass):
"""Test configuration."""
diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py
index 3a4c5333ba8..492290b9519 100644
--- a/tests/components/geo_rss_events/test_sensor.py
+++ b/tests/components/geo_rss_events/test_sensor.py
@@ -59,7 +59,7 @@ class TestGeoRssServiceUpdater(unittest.TestCase):
feed_entry.category = category
return feed_entry
- @mock.patch("georss_client.generic_feed.GenericFeed")
+ @mock.patch("homeassistant.components.geo_rss_events.sensor.GenericFeed")
def test_setup(self, mock_feed):
"""Test the general setup of the platform."""
# Set up some mock feed entries for this test.
@@ -122,7 +122,7 @@ class TestGeoRssServiceUpdater(unittest.TestCase):
ATTR_ICON: "mdi:alert",
}
- @mock.patch("georss_client.generic_feed.GenericFeed")
+ @mock.patch("homeassistant.components.geo_rss_events.sensor.GenericFeed")
def test_setup_with_categories(self, mock_feed):
"""Test the general setup of the platform."""
# Set up some mock feed entries for this test.
diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py
new file mode 100644
index 00000000000..488265f970b
--- /dev/null
+++ b/tests/components/glances/__init__.py
@@ -0,0 +1 @@
+"""Tests for Glances."""
diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py
new file mode 100644
index 00000000000..e5be52e6b33
--- /dev/null
+++ b/tests/components/glances/test_config_flow.py
@@ -0,0 +1,102 @@
+"""Tests for Glances config flow."""
+from unittest.mock import patch
+
+from glances_api import Glances
+
+from homeassistant.components.glances import config_flow
+from homeassistant.components.glances.const import DOMAIN
+from homeassistant.const import CONF_SCAN_INTERVAL
+
+from tests.common import MockConfigEntry, mock_coro
+
+NAME = "Glances"
+HOST = "0.0.0.0"
+USERNAME = "username"
+PASSWORD = "password"
+PORT = 61208
+VERSION = 3
+SCAN_INTERVAL = 10
+
+DEMO_USER_INPUT = {
+ "name": NAME,
+ "host": HOST,
+ "username": USERNAME,
+ "password": PASSWORD,
+ "version": VERSION,
+ "port": PORT,
+ "ssl": False,
+ "verify_ssl": True,
+}
+
+
+def init_config_flow(hass):
+ """Init a configuration flow."""
+ flow = config_flow.GlancesFlowHandler()
+ flow.hass = hass
+ return flow
+
+
+async def test_form(hass):
+ """Test config entry configured successfully."""
+ flow = init_config_flow(hass)
+
+ with patch("glances_api.Glances"), patch.object(
+ Glances, "get_data", return_value=mock_coro()
+ ):
+
+ result = await flow.async_step_user(DEMO_USER_INPUT)
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == NAME
+ assert result["data"] == DEMO_USER_INPUT
+
+
+async def test_form_cannot_connect(hass):
+ """Test to return error if we cannot connect."""
+ flow = init_config_flow(hass)
+
+ with patch("glances_api.Glances"):
+ result = await flow.async_step_user(DEMO_USER_INPUT)
+
+ assert result["type"] == "form"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_wrong_version(hass):
+ """Test to check if wrong version is entered."""
+ flow = init_config_flow(hass)
+
+ user_input = DEMO_USER_INPUT.copy()
+ user_input.update(version=1)
+ result = await flow.async_step_user(user_input)
+
+ assert result["type"] == "form"
+ assert result["errors"] == {"version": "wrong_version"}
+
+
+async def test_form_already_configured(hass):
+ """Test host is already configured."""
+ entry = MockConfigEntry(
+ domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60}
+ )
+ entry.add_to_hass(hass)
+
+ flow = init_config_flow(hass)
+ result = await flow.async_step_user(DEMO_USER_INPUT)
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_options(hass):
+ """Test options for Glances."""
+ entry = MockConfigEntry(
+ domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60}
+ )
+ entry.add_to_hass(hass)
+ flow = init_config_flow(hass)
+ options_flow = flow.async_get_options_flow(entry)
+
+ result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10})
+ assert result["type"] == "create_entry"
+ assert result["data"][CONF_SCAN_INTERVAL] == 10
diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py
index 8049ac4b0db..09522e9c86f 100644
--- a/tests/components/google_assistant/__init__.py
+++ b/tests/components/google_assistant/__init__.py
@@ -12,12 +12,23 @@ class MockConfig(helpers.AbstractConfig):
should_expose=None,
entity_config=None,
hass=None,
+ local_sdk_webhook_id=None,
+ local_sdk_user_id=None,
+ enabled=True,
):
"""Initialize config."""
super().__init__(hass)
self._should_expose = should_expose
self._secure_devices_pin = secure_devices_pin
self._entity_config = entity_config or {}
+ self._local_sdk_webhook_id = local_sdk_webhook_id
+ self._local_sdk_user_id = local_sdk_user_id
+ self._enabled = enabled
+
+ @property
+ def enabled(self):
+ """Return if Google is enabled."""
+ return self._enabled
@property
def secure_devices_pin(self):
@@ -29,6 +40,16 @@ class MockConfig(helpers.AbstractConfig):
"""Return secure devices pin."""
return self._entity_config
+ @property
+ def local_sdk_webhook_id(self):
+ """Return local SDK webhook id."""
+ return self._local_sdk_webhook_id
+
+ @property
+ def local_sdk_user_id(self):
+ """Return local SDK webhook id."""
+ return self._local_sdk_user_id
+
def should_expose(self, state):
"""Expose it all."""
return self._should_expose is None or self._should_expose(state)
diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py
index 6473e8964b8..b43e913ab27 100644
--- a/tests/components/google_assistant/test_google_assistant.py
+++ b/tests/components/google_assistant/test_google_assistant.py
@@ -3,7 +3,7 @@
import asyncio
import json
-from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
+from aiohttp.hdrs import AUTHORIZATION
import pytest
from homeassistant import core, const, setup
@@ -24,11 +24,6 @@ from . import DEMO_DEVICES
API_PASSWORD = "test1234"
-HA_HEADERS = {
- const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
- CONTENT_TYPE: const.CONTENT_TYPE_JSON,
-}
-
PROJECT_ID = "hasstest-1234"
CLIENT_ID = "helloworld"
ACCESS_TOKEN = "superdoublesecret"
diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py
new file mode 100644
index 00000000000..497b7b1f0ae
--- /dev/null
+++ b/tests/components/google_assistant/test_helpers.py
@@ -0,0 +1,130 @@
+"""Test Google Assistant helpers."""
+from unittest.mock import Mock
+from homeassistant.setup import async_setup_component
+from homeassistant.components.google_assistant import helpers
+from homeassistant.components.google_assistant.const import EVENT_COMMAND_RECEIVED
+from . import MockConfig
+
+from tests.common import async_capture_events, async_mock_service
+
+
+async def test_google_entity_sync_serialize_with_local_sdk(hass):
+ """Test sync serialize attributes of a GoogleEntity."""
+ hass.states.async_set("light.ceiling_lights", "off")
+ hass.config.api = Mock(port=1234, use_ssl=True)
+ config = MockConfig(
+ hass=hass,
+ local_sdk_webhook_id="mock-webhook-id",
+ local_sdk_user_id="mock-user-id",
+ )
+ entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights"))
+
+ serialized = await entity.sync_serialize()
+ assert "otherDeviceIds" not in serialized
+ assert "customData" not in serialized
+
+ config.async_enable_local_sdk()
+
+ serialized = await entity.sync_serialize()
+ assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}]
+ assert serialized["customData"] == {
+ "httpPort": 1234,
+ "httpSSL": True,
+ "proxyDeviceId": None,
+ "webhookId": "mock-webhook-id",
+ }
+
+
+async def test_config_local_sdk(hass, hass_client):
+ """Test the local SDK."""
+ command_events = async_capture_events(hass, EVENT_COMMAND_RECEIVED)
+ turn_on_calls = async_mock_service(hass, "light", "turn_on")
+ hass.states.async_set("light.ceiling_lights", "off")
+
+ assert await async_setup_component(hass, "webhook", {})
+
+ config = MockConfig(
+ hass=hass,
+ local_sdk_webhook_id="mock-webhook-id",
+ local_sdk_user_id="mock-user-id",
+ )
+
+ client = await hass_client()
+
+ config.async_enable_local_sdk()
+
+ resp = await client.post(
+ "/api/webhook/mock-webhook-id",
+ json={
+ "inputs": [
+ {
+ "context": {"locale_country": "US", "locale_language": "en"},
+ "intent": "action.devices.EXECUTE",
+ "payload": {
+ "commands": [
+ {
+ "devices": [{"id": "light.ceiling_lights"}],
+ "execution": [
+ {
+ "command": "action.devices.commands.OnOff",
+ "params": {"on": True},
+ }
+ ],
+ }
+ ],
+ "structureData": {},
+ },
+ }
+ ],
+ "requestId": "mock-req-id",
+ },
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result["requestId"] == "mock-req-id"
+
+ assert len(command_events) == 1
+ assert command_events[0].context.user_id == config.local_sdk_user_id
+
+ assert len(turn_on_calls) == 1
+ assert turn_on_calls[0].context is command_events[0].context
+
+ config.async_disable_local_sdk()
+
+ # Webhook is no longer active
+ resp = await client.post("/api/webhook/mock-webhook-id")
+ assert resp.status == 200
+ assert await resp.read() == b""
+
+
+async def test_config_local_sdk_if_disabled(hass, hass_client):
+ """Test the local SDK."""
+ assert await async_setup_component(hass, "webhook", {})
+
+ config = MockConfig(
+ hass=hass,
+ local_sdk_webhook_id="mock-webhook-id",
+ local_sdk_user_id="mock-user-id",
+ enabled=False,
+ )
+
+ client = await hass_client()
+
+ config.async_enable_local_sdk()
+
+ resp = await client.post(
+ "/api/webhook/mock-webhook-id", json={"requestId": "mock-req-id"}
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {
+ "payload": {"errorCode": "deviceTurnedOff"},
+ "requestId": "mock-req-id",
+ }
+
+ config.async_disable_local_sdk()
+
+ # Webhook is no longer active
+ resp = await client.post("/api/webhook/mock-webhook-id")
+ assert resp.status == 200
+ assert await resp.read() == b""
diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py
new file mode 100644
index 00000000000..4b26bbeba7f
--- /dev/null
+++ b/tests/components/google_assistant/test_http.py
@@ -0,0 +1,157 @@
+"""Test Google http services."""
+from datetime import datetime, timezone, timedelta
+from asynctest import patch, ANY
+
+from homeassistant.components.google_assistant.http import (
+ GoogleConfig,
+ _get_homegraph_jwt,
+ _get_homegraph_token,
+)
+from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA
+from homeassistant.components.google_assistant.const import (
+ REPORT_STATE_BASE_URL,
+ HOMEGRAPH_TOKEN_URL,
+)
+from homeassistant.auth.models import User
+
+DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA(
+ {
+ "project_id": "1234",
+ "service_account": {
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKYscIlwm7soDsHAz6L6YvUkCvkrX19rS6yeYOmovvhoK5WeYGWUsd8V72zmsyHB7XO94YgJVjvxfzn5K8bLePjFzwoSJjZvhBJ/ZQ05d8VmbvgyWUoPdG9oEa4fZ/lCYrXoaFdTot2xcJvrb/ZuiRl4s4eZpNeFYvVK/Am7UeFPAgMBAAECgYAUetOfzLYUudofvPCaKHu7tKZ5kQPfEa0w6BAPnBF1Mfl1JiDBRDMryFtKs6AOIAVwx00dY/Ex0BCbB3+Cr58H7t4NaPTJxCpmR09pK7o17B7xAdQv8+SynFNud9/5vQ5AEXMOLNwKiU7wpXT6Z7ZIibUBOR7ewsWgsHCDpN1iqQJBAOMODPTPSiQMwRAUHIc6GPleFSJnIz2PAoG3JOG9KFAL6RtIc19lob2ZXdbQdzKtjSkWo+O5W20WDNAl1k32h6MCQQC7W4ZCIY67mPbL6CxXfHjpSGF4Dr9VWJ7ZrKHr6XUoOIcEvsn/pHvWonjMdy93rQMSfOE8BKd/I1+GHRmNVgplAkAnSo4paxmsZVyfeKt7Jy2dMY+8tVZe17maUuQaAE7Sk00SgJYegwrbMYgQnWCTL39HBfj0dmYA2Zj8CCAuu6O7AkEAryFiYjaUAO9+4iNoL27+ZrFtypeeadyov7gKs0ZKaQpNyzW8A+Zwi7TbTeSqzic/E+z/bOa82q7p/6b7141xsQJBANCAcIwMcVb6KVCHlQbOtKspo5Eh4ZQi8bGl+IcwbQ6JSxeTx915IfAldgbuU047wOB04dYCFB2yLDiUGVXTifU=\n-----END PRIVATE KEY-----\n",
+ "client_email": "dummy@dummy.iam.gserviceaccount.com",
+ },
+ }
+)
+MOCK_TOKEN = {"access_token": "dummtoken", "expires_in": 3600}
+MOCK_JSON = {"devices": {}}
+MOCK_URL = "https://dummy"
+MOCK_HEADER = {
+ "Authorization": "Bearer {}".format(MOCK_TOKEN["access_token"]),
+ "X-GFE-SSL": "yes",
+}
+
+
+async def test_get_jwt(hass):
+ """Test signing of key."""
+
+ jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkdW1teUBkdW1teS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInNjb3BlIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9ob21lZ3JhcGgiLCJhdWQiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW4iLCJpYXQiOjE1NzEwMTEyMDAsImV4cCI6MTU3MTAxNDgwMH0.gG06SmY-zSvFwSrdFfqIdC6AnC22rwz-d2F2UDeWbywjdmFL_1zceL-OOLBwjD8MJr6nR0kmN_Osu7ml9-EzzZjJqsRUxMjGn2G8nSYHbv16R4FYIp62Ibvt6Jj_wdFobEPoy_5OJ28P5Hdu0giGMlFBJMy0Tc6MgEDZA-cwOBw"
+ res = _get_homegraph_jwt(
+ datetime(2019, 10, 14, tzinfo=timezone.utc),
+ DUMMY_CONFIG["service_account"]["client_email"],
+ DUMMY_CONFIG["service_account"]["private_key"],
+ )
+ assert res == jwt
+
+
+async def test_get_access_token(hass, aioclient_mock):
+ """Test the function to get access token."""
+ jwt = "dummyjwt"
+
+ aioclient_mock.post(
+ HOMEGRAPH_TOKEN_URL,
+ status=200,
+ json={"access_token": "1234", "expires_in": 3600},
+ )
+
+ await _get_homegraph_token(hass, jwt)
+ assert aioclient_mock.call_count == 1
+ assert aioclient_mock.mock_calls[0][3] == {
+ "Authorization": "Bearer {}".format(jwt),
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+
+
+async def test_update_access_token(hass):
+ """Test the function to update access token when expired."""
+ jwt = "dummyjwt"
+
+ config = GoogleConfig(hass, DUMMY_CONFIG)
+
+ base_time = datetime(2019, 10, 14, tzinfo=timezone.utc)
+ with patch(
+ "homeassistant.components.google_assistant.http._get_homegraph_token"
+ ) as mock_get_token, patch(
+ "homeassistant.components.google_assistant.http._get_homegraph_jwt"
+ ) as mock_get_jwt, patch(
+ "homeassistant.core.dt_util.utcnow"
+ ) as mock_utcnow:
+ mock_utcnow.return_value = base_time
+ mock_get_jwt.return_value = jwt
+ mock_get_token.return_value = MOCK_TOKEN
+
+ await config._async_update_token()
+ mock_get_token.assert_called_once()
+
+ mock_get_token.reset_mock()
+
+ mock_utcnow.return_value = base_time + timedelta(seconds=3600)
+ await config._async_update_token()
+ mock_get_token.assert_not_called()
+
+ mock_get_token.reset_mock()
+
+ mock_utcnow.return_value = base_time + timedelta(seconds=3601)
+ await config._async_update_token()
+ mock_get_token.assert_called_once()
+
+
+async def test_call_homegraph_api(hass, aioclient_mock, hass_storage):
+ """Test the function to call the homegraph api."""
+ config = GoogleConfig(hass, DUMMY_CONFIG)
+ with patch(
+ "homeassistant.components.google_assistant.http._get_homegraph_token"
+ ) as mock_get_token:
+ mock_get_token.return_value = MOCK_TOKEN
+
+ aioclient_mock.post(MOCK_URL, status=200, json={})
+
+ await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON)
+
+ assert mock_get_token.call_count == 1
+ assert aioclient_mock.call_count == 1
+
+ call = aioclient_mock.mock_calls[0]
+ assert call[2] == MOCK_JSON
+ assert call[3] == MOCK_HEADER
+
+
+async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage):
+ """Test the that the calls get retried with new token on 401."""
+ config = GoogleConfig(hass, DUMMY_CONFIG)
+ with patch(
+ "homeassistant.components.google_assistant.http._get_homegraph_token"
+ ) as mock_get_token:
+ mock_get_token.return_value = MOCK_TOKEN
+
+ aioclient_mock.post(MOCK_URL, status=401, json={})
+
+ await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON)
+
+ assert mock_get_token.call_count == 2
+ assert aioclient_mock.call_count == 2
+
+ call = aioclient_mock.mock_calls[0]
+ assert call[2] == MOCK_JSON
+ assert call[3] == MOCK_HEADER
+ call = aioclient_mock.mock_calls[1]
+ assert call[2] == MOCK_JSON
+ assert call[3] == MOCK_HEADER
+
+
+async def test_report_state(hass, aioclient_mock, hass_storage):
+ """Test the report state function."""
+ config = GoogleConfig(hass, DUMMY_CONFIG)
+ message = {"devices": {}}
+ owner = User(name="Test User", perm_lookup=None, groups=[], is_owner=True)
+
+ with patch.object(config, "async_call_homegraph_api") as mock_call, patch.object(
+ hass.auth, "async_get_owner"
+ ) as mock_get_owner:
+ mock_get_owner.return_value = owner
+
+ await config.async_report_state(message)
+ mock_call.assert_called_once_with(
+ REPORT_STATE_BASE_URL,
+ {"requestId": ANY, "agentUserId": owner.id, "payload": message},
+ )
diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py
index 6ecd4af446b..2f7fdb8e131 100644
--- a/tests/components/google_assistant/test_smart_home.py
+++ b/tests/components/google_assistant/test_smart_home.py
@@ -3,7 +3,7 @@ from unittest.mock import patch, Mock
import pytest
from homeassistant.core import State, EVENT_CALL_SERVICE
-from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS
+from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__
from homeassistant.setup import async_setup_component
from homeassistant.components import camera
from homeassistant.components.climate.const import (
@@ -734,3 +734,137 @@ async def test_trait_execute_adding_query_data(hass):
]
},
}
+
+
+async def test_identify(hass):
+ """Test identify message."""
+ result = await sh.async_handle_message(
+ hass,
+ BASIC_CONFIG,
+ None,
+ {
+ "requestId": REQ_ID,
+ "inputs": [
+ {
+ "intent": "action.devices.IDENTIFY",
+ "payload": {
+ "device": {
+ "mdnsScanData": {
+ "additionals": [
+ {
+ "type": "TXT",
+ "class": "IN",
+ "name": "devhome._home-assistant._tcp.local",
+ "ttl": 4500,
+ "data": [
+ "version=0.101.0.dev0",
+ "base_url=http://192.168.1.101:8123",
+ "requires_api_password=true",
+ ],
+ }
+ ]
+ }
+ },
+ "structureData": {},
+ },
+ }
+ ],
+ "devices": [
+ {
+ "id": "light.ceiling_lights",
+ "customData": {
+ "httpPort": 8123,
+ "httpSSL": False,
+ "proxyDeviceId": BASIC_CONFIG.agent_user_id,
+ "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219",
+ },
+ }
+ ],
+ },
+ )
+
+ assert result == {
+ "requestId": REQ_ID,
+ "payload": {
+ "device": {
+ "id": BASIC_CONFIG.agent_user_id,
+ "isLocalOnly": True,
+ "isProxy": True,
+ "deviceInfo": {
+ "hwVersion": "UNKNOWN_HW_VERSION",
+ "manufacturer": "Home Assistant",
+ "model": "Home Assistant",
+ "swVersion": __version__,
+ },
+ }
+ },
+ }
+
+
+async def test_reachable_devices(hass):
+ """Test REACHABLE_DEVICES intent."""
+ # Matching passed in device.
+ hass.states.async_set("light.ceiling_lights", "on")
+
+ # Unsupported entity
+ hass.states.async_set("not_supported.entity", "something")
+
+ # Excluded via config
+ hass.states.async_set("light.not_expose", "on")
+
+ # Not passed in as google_id
+ hass.states.async_set("light.not_mentioned", "on")
+
+ config = MockConfig(
+ should_expose=lambda state: state.entity_id != "light.not_expose"
+ )
+
+ result = await sh.async_handle_message(
+ hass,
+ config,
+ None,
+ {
+ "requestId": REQ_ID,
+ "inputs": [
+ {
+ "intent": "action.devices.REACHABLE_DEVICES",
+ "payload": {
+ "device": {
+ "proxyDevice": {
+ "id": "6a04f0f7-6125-4356-a846-861df7e01497",
+ "customData": "{}",
+ "proxyData": "{}",
+ }
+ },
+ "structureData": {},
+ },
+ }
+ ],
+ "devices": [
+ {
+ "id": "light.ceiling_lights",
+ "customData": {
+ "httpPort": 8123,
+ "httpSSL": False,
+ "proxyDeviceId": BASIC_CONFIG.agent_user_id,
+ "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219",
+ },
+ },
+ {
+ "id": "light.not_expose",
+ "customData": {
+ "httpPort": 8123,
+ "httpSSL": False,
+ "proxyDeviceId": BASIC_CONFIG.agent_user_id,
+ "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219",
+ },
+ },
+ {"id": BASIC_CONFIG.agent_user_id, "customData": {}},
+ ],
+ },
+ )
+
+ assert result == {
+ "requestId": REQ_ID,
+ "payload": {"devices": [{"verificationId": "light.ceiling_lights"}]},
+ }
diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py
index a5c527dacfe..d6ec24a7867 100644
--- a/tests/components/google_assistant/test_trait.py
+++ b/tests/components/google_assistant/test_trait.py
@@ -48,11 +48,11 @@ _LOGGER = logging.getLogger(__name__)
REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"
-BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID)
+BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID, None)
PIN_CONFIG = MockConfig(secure_devices_pin="1234")
-PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID)
+PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID, None)
async def test_brightness_light(hass):
diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py
index 8e2b6db777d..767ec59f366 100644
--- a/tests/components/hassio/__init__.py
+++ b/tests/components/hassio/__init__.py
@@ -1,4 +1,3 @@
"""Tests for Hassio component."""
-API_PASSWORD = "pass1234"
HASSIO_TOKEN = "123456"
diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py
index d7d50b97eb8..0e246cf1b46 100644
--- a/tests/components/hassio/conftest.py
+++ b/tests/components/hassio/conftest.py
@@ -9,7 +9,7 @@ from homeassistant.setup import async_setup_component
from homeassistant.components.hassio.handler import HassIO, HassioAPIError
from tests.common import mock_coro
-from . import API_PASSWORD, HASSIO_TOKEN
+from . import HASSIO_TOKEN
@pytest.fixture
@@ -39,23 +39,19 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock):
side_effect=HassioAPIError(),
):
hass.state = CoreState.starting
- hass.loop.run_until_complete(
- async_setup_component(
- hass, "hassio", {"http": {"api_password": API_PASSWORD}}
- )
- )
+ hass.loop.run_until_complete(async_setup_component(hass, "hassio", {}))
@pytest.fixture
def hassio_client(hassio_stubs, hass, hass_client):
"""Return a Hass.io HTTP client."""
- yield hass.loop.run_until_complete(hass_client())
+ return hass.loop.run_until_complete(hass_client())
@pytest.fixture
def hassio_noauth_client(hassio_stubs, hass, aiohttp_client):
"""Return a Hass.io HTTP client without auth."""
- yield hass.loop.run_until_complete(aiohttp_client(hass.http.app))
+ return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
@pytest.fixture
diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py
index 114935df3fc..480df508968 100644
--- a/tests/components/hassio/test_addon_panel.py
+++ b/tests/components/hassio/test_addon_panel.py
@@ -4,10 +4,8 @@ from unittest.mock import patch, Mock
import pytest
from homeassistant.setup import async_setup_component
-from homeassistant.const import HTTP_HEADER_HA_AUTH
from tests.common import mock_coro
-from . import API_PASSWORD
@pytest.fixture(autouse=True)
@@ -53,9 +51,7 @@ async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env):
"homeassistant.components.hassio.addon_panel._register_panel",
Mock(return_value=mock_coro()),
) as mock_panel:
- await async_setup_component(
- hass, "hassio", {"http": {"api_password": API_PASSWORD}}
- )
+ await async_setup_component(hass, "hassio", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 3
@@ -98,9 +94,7 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli
"homeassistant.components.hassio.addon_panel._register_panel",
Mock(return_value=mock_coro()),
) as mock_panel:
- await async_setup_component(
- hass, "hassio", {"http": {"api_password": API_PASSWORD}}
- )
+ await async_setup_component(hass, "hassio", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 3
@@ -113,14 +107,10 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli
hass_client = await hass_client()
- resp = await hass_client.post(
- "/api/hassio_push/panel/test2", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
- )
+ resp = await hass_client.post("/api/hassio_push/panel/test2")
assert resp.status == 400
- resp = await hass_client.post(
- "/api/hassio_push/panel/test1", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
- )
+ resp = await hass_client.post("/api/hassio_push/panel/test1")
assert resp.status == 200
assert mock_panel.call_count == 2
diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py
index a2839b297b8..1fb6d32ccf7 100644
--- a/tests/components/hassio/test_auth.py
+++ b/tests/components/hassio/test_auth.py
@@ -1,11 +1,9 @@
"""The tests for the hassio component."""
from unittest.mock import patch, Mock
-from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.exceptions import HomeAssistantError
from tests.common import mock_coro
-from . import API_PASSWORD
async def test_login_success(hass, hassio_client):
@@ -18,7 +16,6 @@ async def test_login_success(hass, hassio_client):
resp = await hassio_client.post(
"/api/hassio_auth",
json={"username": "test", "password": "123456", "addon": "samba"},
- headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
)
# Check we got right response
@@ -36,7 +33,6 @@ async def test_login_error(hass, hassio_client):
resp = await hassio_client.post(
"/api/hassio_auth",
json={"username": "test", "password": "123456", "addon": "samba"},
- headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
)
# Check we got right response
@@ -51,9 +47,7 @@ async def test_login_no_data(hass, hassio_client):
"HassAuthProvider.async_validate_login",
Mock(side_effect=HomeAssistantError()),
) as mock_login:
- resp = await hassio_client.post(
- "/api/hassio_auth", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
- )
+ resp = await hassio_client.post("/api/hassio_auth")
# Check we got right response
assert resp.status == 400
@@ -68,9 +62,7 @@ async def test_login_no_username(hass, hassio_client):
Mock(side_effect=HomeAssistantError()),
) as mock_login:
resp = await hassio_client.post(
- "/api/hassio_auth",
- json={"password": "123456", "addon": "samba"},
- headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
+ "/api/hassio_auth", json={"password": "123456", "addon": "samba"}
)
# Check we got right response
@@ -93,7 +85,6 @@ async def test_login_success_extra(hass, hassio_client):
"addon": "samba",
"path": "/share",
},
- headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
)
# Check we got right response
diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py
index 89f1483ffab..a1b4ae2e900 100644
--- a/tests/components/hassio/test_discovery.py
+++ b/tests/components/hassio/test_discovery.py
@@ -3,10 +3,9 @@ from unittest.mock import patch, Mock
from homeassistant.setup import async_setup_component
from homeassistant.components.hassio.handler import HassioAPIError
-from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_HEADER_HA_AUTH
+from homeassistant.const import EVENT_HOMEASSISTANT_START
from tests.common import mock_coro
-from . import API_PASSWORD
async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client):
@@ -101,9 +100,7 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client
Mock(return_value=mock_coro({"type": "abort"})),
) as mock_mqtt:
await hass.async_start()
- await async_setup_component(
- hass, "hassio", {"http": {"api_password": API_PASSWORD}}
- )
+ await async_setup_component(hass, "hassio", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 2
@@ -151,7 +148,6 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client):
) as mock_mqtt:
resp = await hassio_client.post(
"/api/hassio_push/discovery/testuuid",
- headers={HTTP_HEADER_HA_AUTH: API_PASSWORD},
json={"addon": "mosquitto", "service": "mqtt", "uuid": "testuuid"},
)
await hass.async_block_till_done()
diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py
index 8f77d6b6234..96d53f93c3a 100644
--- a/tests/components/hassio/test_http.py
+++ b/tests/components/hassio/test_http.py
@@ -4,19 +4,13 @@ from unittest.mock import patch
import pytest
-from homeassistant.const import HTTP_HEADER_HA_AUTH
-
-from . import API_PASSWORD
-
@asyncio.coroutine
def test_forward_request(hassio_client, aioclient_mock):
"""Test fetching normal path."""
aioclient_mock.post("http://127.0.0.1/beer", text="response")
- resp = yield from hassio_client.post(
- "/api/hassio/beer", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
- )
+ resp = yield from hassio_client.post("/api/hassio/beer")
# Check we got right response
assert resp.status == 200
@@ -87,9 +81,7 @@ def test_forward_log_request(hassio_client, aioclient_mock):
"""Test fetching normal log path doesn't remove ANSI color escape codes."""
aioclient_mock.get("http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m")
- resp = yield from hassio_client.get(
- "/api/hassio/beer/logs", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
- )
+ resp = yield from hassio_client.get("/api/hassio/beer/logs")
# Check we got right response
assert resp.status == 200
@@ -107,9 +99,7 @@ def test_bad_gateway_when_cannot_find_supervisor(hassio_client):
"homeassistant.components.hassio.http.async_timeout.timeout",
side_effect=asyncio.TimeoutError,
):
- resp = yield from hassio_client.get(
- "/api/hassio/addons/test/info", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}
- )
+ resp = yield from hassio_client.get("/api/hassio/addons/test/info")
assert resp.status == 502
diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py
index 02c018a0b49..c7c3f2bc5d5 100644
--- a/tests/components/homeassistant/test_scene.py
+++ b/tests/components/homeassistant/test_scene.py
@@ -28,3 +28,26 @@ async def test_reload_config_service(hass):
assert hass.states.get("scene.hallo") is None
assert hass.states.get("scene.bye") is not None
+
+
+async def test_apply_service(hass):
+ """Test the apply service."""
+ assert await async_setup_component(hass, "scene", {})
+ assert await async_setup_component(hass, "light", {"light": {"platform": "demo"}})
+
+ assert await hass.services.async_call(
+ "scene", "apply", {"entities": {"light.bed_light": "off"}}, blocking=True
+ )
+
+ assert hass.states.get("light.bed_light").state == "off"
+
+ assert await hass.services.async_call(
+ "scene",
+ "apply",
+ {"entities": {"light.bed_light": {"state": "on", "brightness": 50}}},
+ blocking=True,
+ )
+
+ state = hass.states.get("light.bed_light")
+ assert state.state == "on"
+ assert state.attributes["brightness"] == 50
diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py
index 61893af7008..97838eaa852 100644
--- a/tests/components/homekit/test_homekit.py
+++ b/tests/components/homekit/test_homekit.py
@@ -69,7 +69,7 @@ async def test_setup_min(hass):
assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}})
mock_homekit.assert_any_call(
- hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE
+ hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE, None
)
assert mock_homekit().setup.called is True
@@ -98,7 +98,7 @@ async def test_setup_auto_start_disabled(hass):
assert await setup.async_setup_component(hass, DOMAIN, config)
mock_homekit.assert_any_call(
- hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE
+ hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE, None
)
assert mock_homekit().setup.called is True
@@ -136,7 +136,11 @@ async def test_homekit_setup(hass, hk_driver):
path = hass.config.path(HOMEKIT_FILE)
assert isinstance(homekit.bridge, HomeBridge)
mock_driver.assert_called_with(
- hass, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path
+ hass,
+ address=IP_ADDRESS,
+ port=DEFAULT_PORT,
+ persist_file=path,
+ advertised_address=None,
)
assert homekit.driver.safe_mode is False
@@ -153,7 +157,30 @@ async def test_homekit_setup_ip_address(hass, hk_driver):
) as mock_driver:
await hass.async_add_job(homekit.setup)
mock_driver.assert_called_with(
- hass, address="172.0.0.0", port=DEFAULT_PORT, persist_file=ANY
+ hass,
+ address="172.0.0.0",
+ port=DEFAULT_PORT,
+ persist_file=ANY,
+ advertised_address=None,
+ )
+
+
+async def test_homekit_setup_advertise_ip(hass, hk_driver):
+ """Test setup with given IP address to advertise."""
+ homekit = HomeKit(
+ hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, "192.168.1.100"
+ )
+
+ with patch(
+ PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver
+ ) as mock_driver:
+ await hass.async_add_job(homekit.setup)
+ mock_driver.assert_called_with(
+ hass,
+ address="0.0.0.0",
+ port=DEFAULT_PORT,
+ persist_file=ANY,
+ advertised_address="192.168.1.100",
)
diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py
index d967d325561..8ad46e489d6 100644
--- a/tests/components/homekit/test_type_thermostats.py
+++ b/tests/components/homekit/test_type_thermostats.py
@@ -96,10 +96,10 @@ async def test_thermostat(hass, hk_driver, cls, events):
},
)
await hass.async_block_till_done()
- assert acc.char_target_temp.value == 22.0
+ assert acc.char_target_temp.value == 22.2
assert acc.char_current_heat_cool.value == 1
assert acc.char_target_heat_cool.value == 1
- assert acc.char_current_temp.value == 18.0
+ assert acc.char_current_temp.value == 17.8
assert acc.char_display_units.value == 0
hass.states.async_set(
@@ -432,7 +432,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events):
)
await hass.async_block_till_done()
assert acc.get_temperature_range() == (7.0, 35.0)
- assert acc.char_heating_thresh_temp.value == 20.0
+ assert acc.char_heating_thresh_temp.value == 20.1
assert acc.char_cooling_thresh_temp.value == 24.0
assert acc.char_current_temp.value == 23.0
assert acc.char_target_temp.value == 22.0
diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py
index 923cbaca42f..8898f988f9a 100644
--- a/tests/components/homekit/test_util.py
+++ b/tests/components/homekit/test_util.py
@@ -173,7 +173,7 @@ def test_convert_to_float():
def test_temperature_to_homekit():
"""Test temperature conversion from HA to HomeKit."""
assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5
- assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.5
+ assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4
def test_temperature_to_states():
diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py
new file mode 100644
index 00000000000..f60f8d659b5
--- /dev/null
+++ b/tests/components/homematicip_cloud/conftest.py
@@ -0,0 +1,148 @@
+"""Initializer helpers for HomematicIP fake server."""
+from asynctest import MagicMock, Mock, patch
+from homematicip.aio.auth import AsyncAuth
+from homematicip.aio.connection import AsyncConnection
+from homematicip.aio.home import AsyncHome
+import pytest
+
+from homeassistant import config_entries
+from homeassistant.components.homematicip_cloud import (
+ DOMAIN as HMIPC_DOMAIN,
+ async_setup as hmip_async_setup,
+ const as hmipc,
+ hap as hmip_hap,
+)
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeTemplate
+
+from tests.common import MockConfigEntry, mock_coro
+
+
+@pytest.fixture(name="mock_connection")
+def mock_connection_fixture() -> AsyncConnection:
+ """Return a mocked connection."""
+ connection = MagicMock(spec=AsyncConnection)
+
+ def _rest_call_side_effect(path, body=None):
+ return path, body
+
+ connection._restCall.side_effect = _rest_call_side_effect # pylint: disable=W0212
+ connection.api_call.return_value = mock_coro(True)
+ connection.init.side_effect = mock_coro(True)
+
+ return connection
+
+
+@pytest.fixture(name="hmip_config_entry")
+def hmip_config_entry_fixture() -> config_entries.ConfigEntry:
+ """Create a mock config entriy for homematic ip cloud."""
+ entry_data = {
+ hmipc.HMIPC_HAPID: HAPID,
+ hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN,
+ hmipc.HMIPC_NAME: "",
+ hmipc.HMIPC_PIN: HAPPIN,
+ }
+ config_entry = MockConfigEntry(
+ version=1,
+ domain=HMIPC_DOMAIN,
+ title=HAPID,
+ data=entry_data,
+ source="import",
+ connection_class=config_entries.CONN_CLASS_CLOUD_PUSH,
+ system_options={"disable_new_entities": False},
+ )
+
+ return config_entry
+
+
+@pytest.fixture(name="default_mock_home")
+def default_mock_home_fixture(mock_connection) -> AsyncHome:
+ """Create a fake homematic async home."""
+ return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock()
+
+
+@pytest.fixture(name="default_mock_hap")
+async def default_mock_hap_fixture(
+ hass: HomeAssistantType, mock_connection, hmip_config_entry
+) -> hmip_hap.HomematicipHAP:
+ """Create a mocked homematic access point."""
+ return await get_mock_hap(hass, mock_connection, hmip_config_entry)
+
+
+async def get_mock_hap(
+ hass: HomeAssistantType,
+ mock_connection,
+ hmip_config_entry: config_entries.ConfigEntry,
+) -> hmip_hap.HomematicipHAP:
+ """Create a mocked homematic access point."""
+ hass.config.components.add(HMIPC_DOMAIN)
+ hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry)
+ home_name = hmip_config_entry.data["name"]
+ mock_home = (
+ HomeTemplate(connection=mock_connection, home_name=home_name)
+ .init_home()
+ .get_async_home_mock()
+ )
+ with patch.object(hap, "get_hap", return_value=mock_coro(mock_home)):
+ assert await hap.async_setup()
+ mock_home.on_update(hap.async_update)
+ mock_home.on_create(hap.async_create_entity)
+
+ hass.data[HMIPC_DOMAIN] = {HAPID: hap}
+
+ await hass.async_block_till_done()
+
+ return hap
+
+
+@pytest.fixture(name="hmip_config")
+def hmip_config_fixture() -> ConfigType:
+ """Create a config for homematic ip cloud."""
+
+ entry_data = {
+ hmipc.HMIPC_HAPID: HAPID,
+ hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN,
+ hmipc.HMIPC_NAME: "",
+ hmipc.HMIPC_PIN: HAPPIN,
+ }
+
+ return {HMIPC_DOMAIN: [entry_data]}
+
+
+@pytest.fixture(name="dummy_config")
+def dummy_config_fixture() -> ConfigType:
+ """Create a dummy config."""
+ return {"blabla": None}
+
+
+@pytest.fixture(name="mock_hap_with_service")
+async def mock_hap_with_service_fixture(
+ hass: HomeAssistantType, default_mock_hap, dummy_config
+) -> hmip_hap.HomematicipHAP:
+ """Create a fake homematic access point with hass services."""
+ await hmip_async_setup(hass, dummy_config)
+ await hass.async_block_till_done()
+ hass.data[HMIPC_DOMAIN] = {HAPID: default_mock_hap}
+ return default_mock_hap
+
+
+@pytest.fixture(name="simple_mock_home")
+def simple_mock_home_fixture() -> AsyncHome:
+ """Return a simple AsyncHome Mock."""
+ return Mock(
+ spec=AsyncHome,
+ devices=[],
+ groups=[],
+ location=Mock(),
+ weather=Mock(create=True),
+ id=42,
+ dutyCycle=88,
+ connected=True,
+ )
+
+
+@pytest.fixture(name="simple_mock_auth")
+def simple_mock_auth_fixture() -> AsyncAuth:
+ """Return a simple AsyncAuth Mock."""
+ return Mock(spec=AsyncAuth, pin=HAPPIN, create=True)
diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py
new file mode 100644
index 00000000000..78c78ec0ab9
--- /dev/null
+++ b/tests/components/homematicip_cloud/helper.py
@@ -0,0 +1,144 @@
+"""Helper for HomematicIP Cloud Tests."""
+import json
+
+from asynctest import Mock
+from homematicip.aio.class_maps import (
+ TYPE_CLASS_MAP,
+ TYPE_GROUP_MAP,
+ TYPE_SECURITY_EVENT_MAP,
+)
+from homematicip.aio.device import AsyncDevice
+from homematicip.aio.group import AsyncGroup
+from homematicip.aio.home import AsyncHome
+from homematicip.home import Home
+
+from homeassistant.components.homematicip_cloud.device import (
+ ATTR_IS_GROUP,
+ ATTR_MODEL_TYPE,
+)
+
+from tests.common import load_fixture
+
+HAPID = "3014F7110000000000000001"
+HAPPIN = "5678"
+AUTH_TOKEN = "1234"
+HOME_JSON = "homematicip_cloud.json"
+
+
+def get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+):
+ """Get and test basic device."""
+ ha_state = hass.states.get(entity_id)
+ assert ha_state is not None
+ if device_model:
+ assert ha_state.attributes[ATTR_MODEL_TYPE] == device_model
+ assert ha_state.name == entity_name
+
+ hmip_device = default_mock_hap.hmip_device_by_entity_id.get(entity_id)
+
+ if hmip_device:
+ if isinstance(hmip_device, AsyncDevice):
+ assert ha_state.attributes[ATTR_IS_GROUP] is False
+ elif isinstance(hmip_device, AsyncGroup):
+ assert ha_state.attributes[ATTR_IS_GROUP] is True
+ return ha_state, hmip_device
+
+
+async def async_manipulate_test_data(
+ hass, hmip_device, attribute, new_value, channel=1, fire_device=None
+):
+ """Set new value on hmip device."""
+ if channel == 1:
+ setattr(hmip_device, attribute, new_value)
+ if hasattr(hmip_device, "functionalChannels"):
+ functional_channel = hmip_device.functionalChannels[channel]
+ setattr(functional_channel, attribute, new_value)
+
+ fire_target = hmip_device if fire_device is None else fire_device
+
+ if isinstance(fire_target, AsyncHome):
+ fire_target.fire_update_event(fire_target._rawJSONData) # pylint: disable=W0212
+ else:
+ fire_target.fire_update_event()
+
+ await hass.async_block_till_done()
+
+
+class HomeTemplate(Home):
+ """
+ Home template as builder for home mock.
+
+ It is based on the upstream libs home class to generate hmip devices
+ and groups based on the given homematicip_cloud.json.
+
+ All further testing activities should be done by using the AsyncHome mock,
+ that is generated by get_async_home_mock(self).
+
+ The class also generated mocks of devices and groups for further testing.
+ """
+
+ _typeClassMap = TYPE_CLASS_MAP
+ _typeGroupMap = TYPE_GROUP_MAP
+ _typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP
+
+ def __init__(self, connection=None, home_name=""):
+ """Init template with connection."""
+ super().__init__(connection=connection)
+ self.label = "Access Point"
+ self.name = home_name
+ self.model_type = "HmIP-HAP"
+ self.init_json_state = None
+
+ def init_home(self, json_path=HOME_JSON):
+ """Init template with json."""
+ self.init_json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8")
+ self.update_home(json_state=self.init_json_state, clearConfig=True)
+ return self
+
+ def update_home(self, json_state, clearConfig: bool = False):
+ """Update home and ensure that mocks are created."""
+ result = super().update_home(json_state, clearConfig)
+ self._generate_mocks()
+ return result
+
+ def _generate_mocks(self):
+ """Generate mocks for groups and devices."""
+ mock_devices = []
+ for device in self.devices:
+ mock_devices.append(_get_mock(device))
+ self.devices = mock_devices
+
+ mock_groups = []
+ for group in self.groups:
+ mock_groups.append(_get_mock(group))
+ self.groups = mock_groups
+
+ def download_configuration(self):
+ """Return the initial json config."""
+ return self.init_json_state
+
+ def get_async_home_mock(self):
+ """
+ Create Mock for Async_Home. based on template to be used for testing.
+
+ It adds collections of mocked devices and groups to the home objects,
+ and sets required attributes.
+ """
+ mock_home = Mock(
+ spec=AsyncHome, wraps=self, label="Access Point", modelType="HmIP-HAP"
+ )
+ mock_home.__dict__.update(self.__dict__)
+
+ return mock_home
+
+
+def _get_mock(instance):
+ """Create a mock and copy instance attributes over mock."""
+ if isinstance(instance, Mock):
+ instance.__dict__.update(instance._mock_wraps.__dict__) # pylint: disable=W0212
+ return instance
+
+ mock = Mock(spec=instance, wraps=instance)
+ mock.__dict__.update(instance.__dict__)
+ return mock
diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py
new file mode 100644
index 00000000000..2798a0879b7
--- /dev/null
+++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py
@@ -0,0 +1,148 @@
+"""Tests for HomematicIP Cloud alarm control panel."""
+from homematicip.base.enums import WindowState
+from homematicip.group import SecurityZoneGroup
+
+from homeassistant.components.alarm_control_panel import (
+ DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
+)
+from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
+from homeassistant.setup import async_setup_component
+
+from .helper import get_and_check_entity_basics
+
+
+def _get_security_zones(groups): # pylint: disable=W0221
+ """Get the security zones."""
+ for group in groups:
+ if isinstance(group, SecurityZoneGroup):
+ if group.label == "EXTERNAL":
+ external = group
+ elif group.label == "INTERNAL":
+ internal = group
+ return internal, external
+
+
+async def _async_manipulate_security_zones(
+ hass, home, internal_active, external_active, window_state
+):
+ """Set new values on hmip security zones."""
+ internal_zone, external_zone = _get_security_zones(home.groups)
+ external_zone.active = external_active
+ external_zone.windowState = window_state
+ internal_zone.active = internal_active
+
+ # Just one call to a security zone is required to refresh the ACP.
+ internal_zone.fire_update_event()
+
+ await hass.async_block_till_done()
+
+
+async def test_manually_configured_platform(hass):
+ """Test that we do not set up an access point."""
+ assert (
+ await async_setup_component(
+ hass,
+ ALARM_CONTROL_PANEL_DOMAIN,
+ {ALARM_CONTROL_PANEL_DOMAIN: {"platform": HMIPC_DOMAIN}},
+ )
+ is True
+ )
+ assert not hass.data.get(HMIPC_DOMAIN)
+
+
+async def test_hmip_alarm_control_panel(hass, default_mock_hap):
+ """Test HomematicipAlarmControlPanel."""
+ entity_id = "alarm_control_panel.hmip_alarm_control_panel"
+ entity_name = "HmIP Alarm Control Panel"
+ device_model = None
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "disarmed"
+ assert not hmip_device
+
+ home = default_mock_hap.home
+ service_call_counter = len(home.mock_calls)
+
+ await hass.services.async_call(
+ "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(home.mock_calls) == service_call_counter + 1
+ assert home.mock_calls[-1][0] == "set_security_zones_activation"
+ assert home.mock_calls[-1][1] == (True, True)
+ await _async_manipulate_security_zones(
+ hass,
+ home,
+ internal_active=True,
+ external_active=True,
+ window_state=WindowState.CLOSED,
+ )
+ assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_AWAY
+
+ await hass.services.async_call(
+ "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(home.mock_calls) == service_call_counter + 3
+ assert home.mock_calls[-1][0] == "set_security_zones_activation"
+ assert home.mock_calls[-1][1] == (False, True)
+ await _async_manipulate_security_zones(
+ hass,
+ home,
+ internal_active=False,
+ external_active=True,
+ window_state=WindowState.CLOSED,
+ )
+ assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_HOME
+
+ await hass.services.async_call(
+ "alarm_control_panel", "alarm_disarm", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(home.mock_calls) == service_call_counter + 5
+ assert home.mock_calls[-1][0] == "set_security_zones_activation"
+ assert home.mock_calls[-1][1] == (False, False)
+ await _async_manipulate_security_zones(
+ hass,
+ home,
+ internal_active=False,
+ external_active=False,
+ window_state=WindowState.CLOSED,
+ )
+ assert hass.states.get(entity_id).state is STATE_ALARM_DISARMED
+
+ await hass.services.async_call(
+ "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(home.mock_calls) == service_call_counter + 7
+ assert home.mock_calls[-1][0] == "set_security_zones_activation"
+ assert home.mock_calls[-1][1] == (True, True)
+ await _async_manipulate_security_zones(
+ hass,
+ home,
+ internal_active=True,
+ external_active=True,
+ window_state=WindowState.OPEN,
+ )
+ assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED
+
+ await hass.services.async_call(
+ "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(home.mock_calls) == service_call_counter + 9
+ assert home.mock_calls[-1][0] == "set_security_zones_activation"
+ assert home.mock_calls[-1][1] == (False, True)
+ await _async_manipulate_security_zones(
+ hass,
+ home,
+ internal_active=False,
+ external_active=True,
+ window_state=WindowState.OPEN,
+ )
+ assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED
diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py
new file mode 100644
index 00000000000..0760518171e
--- /dev/null
+++ b/tests/components/homematicip_cloud/test_binary_sensor.py
@@ -0,0 +1,305 @@
+"""Tests for HomematicIP Cloud binary sensor."""
+from homematicip.base.enums import SmokeDetectorAlarmType, WindowState
+
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
+from homeassistant.components.homematicip_cloud.binary_sensor import (
+ ATTR_ACCELERATION_SENSOR_MODE,
+ ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION,
+ ATTR_ACCELERATION_SENSOR_SENSITIVITY,
+ ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE,
+ ATTR_LOW_BATTERY,
+ ATTR_MOTION_DETECTED,
+)
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.setup import async_setup_component
+
+from .helper import async_manipulate_test_data, get_and_check_entity_basics
+
+
+async def test_manually_configured_platform(hass):
+ """Test that we do not set up an access point."""
+ assert (
+ await async_setup_component(
+ hass,
+ BINARY_SENSOR_DOMAIN,
+ {BINARY_SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}},
+ )
+ is True
+ )
+ assert not hass.data.get(HMIPC_DOMAIN)
+
+
+async def test_hmip_acceleration_sensor(hass, default_mock_hap):
+ """Test HomematicipAccelerationSensor."""
+ entity_id = "binary_sensor.garagentor"
+ entity_name = "Garagentor"
+ device_model = "HmIP-SAM"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_MODE] == "FLAT_DECT"
+ assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION] == "VERTICAL"
+ assert (
+ ha_state.attributes[ATTR_ACCELERATION_SENSOR_SENSITIVITY] == "SENSOR_RANGE_4G"
+ )
+ assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE] == 45
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await async_manipulate_test_data(
+ hass, hmip_device, "accelerationSensorTriggered", False
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+
+ await async_manipulate_test_data(
+ hass, hmip_device, "accelerationSensorTriggered", True
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+ assert len(hmip_device.mock_calls) == service_call_counter + 2
+
+
+async def test_hmip_contact_interface(hass, default_mock_hap):
+ """Test HomematicipContactInterface."""
+ entity_id = "binary_sensor.kontakt_schnittstelle_unterputz_1_fach"
+ entity_name = "Kontakt-Schnittstelle Unterputz – 1-fach"
+ device_model = "HmIP-FCI1"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+ await async_manipulate_test_data(hass, hmip_device, "windowState", None)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+
+async def test_hmip_shutter_contact(hass, default_mock_hap):
+ """Test HomematicipShutterContact."""
+ entity_id = "binary_sensor.fenstergriffsensor"
+ entity_name = "Fenstergriffsensor"
+ device_model = "HmIP-SRH"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ await async_manipulate_test_data(
+ hass, hmip_device, "windowState", WindowState.CLOSED
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+ await async_manipulate_test_data(hass, hmip_device, "windowState", None)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+
+async def test_hmip_motion_detector(hass, default_mock_hap):
+ """Test HomematicipMotionDetector."""
+ entity_id = "binary_sensor.bewegungsmelder_fur_55er_rahmen_innen"
+ entity_name = "Bewegungsmelder für 55er Rahmen – innen"
+ device_model = "HmIP-SMI55"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ await async_manipulate_test_data(hass, hmip_device, "motionDetected", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+
+async def test_hmip_presence_detector(hass, default_mock_hap):
+ """Test HomematicipPresenceDetector."""
+ entity_id = "binary_sensor.spi_1"
+ entity_name = "SPI_1"
+ device_model = "HmIP-SPI"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ await async_manipulate_test_data(hass, hmip_device, "presenceDetected", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+
+async def test_hmip_smoke_detector(hass, default_mock_hap):
+ """Test HomematicipSmokeDetector."""
+ entity_id = "binary_sensor.rauchwarnmelder"
+ entity_name = "Rauchwarnmelder"
+ device_model = "HmIP-SWSD"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ await async_manipulate_test_data(
+ hass,
+ hmip_device,
+ "smokeDetectorAlarmType",
+ SmokeDetectorAlarmType.PRIMARY_ALARM,
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+
+async def test_hmip_water_detector(hass, default_mock_hap):
+ """Test HomematicipWaterDetector."""
+ entity_id = "binary_sensor.wassersensor"
+ entity_name = "Wassersensor"
+ device_model = "HmIP-SWD"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True)
+ await async_manipulate_test_data(hass, hmip_device, "moistureDetected", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+ await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True)
+ await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+ await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", False)
+ await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+ await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", False)
+ await async_manipulate_test_data(hass, hmip_device, "moistureDetected", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+
+async def test_hmip_storm_sensor(hass, default_mock_hap):
+ """Test HomematicipStormSensor."""
+ entity_id = "binary_sensor.weather_sensor_plus_storm"
+ entity_name = "Weather Sensor – plus Storm"
+ device_model = "HmIP-SWO-PL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ await async_manipulate_test_data(hass, hmip_device, "storm", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+
+async def test_hmip_rain_sensor(hass, default_mock_hap):
+ """Test HomematicipRainSensor."""
+ entity_id = "binary_sensor.wettersensor_pro_raining"
+ entity_name = "Wettersensor - pro Raining"
+ device_model = "HmIP-SWO-PR"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ await async_manipulate_test_data(hass, hmip_device, "raining", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+
+async def test_hmip_sunshine_sensor(hass, default_mock_hap):
+ """Test HomematicipSunshineSensor."""
+ entity_id = "binary_sensor.wettersensor_pro_sunshine"
+ entity_name = "Wettersensor - pro Sunshine"
+ device_model = "HmIP-SWO-PR"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ assert ha_state.attributes["today_sunshine_duration_in_minutes"] == 100
+ await async_manipulate_test_data(hass, hmip_device, "sunshine", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+
+async def test_hmip_battery_sensor(hass, default_mock_hap):
+ """Test HomematicipSunshineSensor."""
+ entity_id = "binary_sensor.wohnungsture_battery"
+ entity_name = "Wohnungstüre Battery"
+ device_model = "HMIP-SWDO"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ await async_manipulate_test_data(hass, hmip_device, "lowBat", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+
+async def test_hmip_security_zone_sensor_group(hass, default_mock_hap):
+ """Test HomematicipSecurityZoneSensorGroup."""
+ entity_id = "binary_sensor.internal_securityzone"
+ entity_name = "INTERNAL SecurityZone"
+ device_model = "HmIP-SecurityZone"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ await async_manipulate_test_data(hass, hmip_device, "motionDetected", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+ assert ha_state.attributes[ATTR_MOTION_DETECTED] is True
+
+
+async def test_hmip_security_sensor_group(hass, default_mock_hap):
+ """Test HomematicipSecuritySensorGroup."""
+ entity_id = "binary_sensor.buro_sensors"
+ entity_name = "Büro Sensors"
+ device_model = None
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ assert not ha_state.attributes.get("low_bat")
+ await async_manipulate_test_data(hass, hmip_device, "lowBat", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+ assert ha_state.attributes[ATTR_LOW_BATTERY] is True
+
+ await async_manipulate_test_data(hass, hmip_device, "lowBat", False)
+ await async_manipulate_test_data(
+ hass,
+ hmip_device,
+ "smokeDetectorAlarmType",
+ SmokeDetectorAlarmType.PRIMARY_ALARM,
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+ assert (
+ ha_state.attributes["smoke_detector_alarm"]
+ == SmokeDetectorAlarmType.PRIMARY_ALARM
+ )
diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py
new file mode 100644
index 00000000000..80e4e74e451
--- /dev/null
+++ b/tests/components/homematicip_cloud/test_climate.py
@@ -0,0 +1,315 @@
+"""Tests for HomematicIP Cloud climate."""
+import datetime
+
+from homematicip.base.enums import AbsenceType
+from homematicip.functionalHomes import IndoorClimateHome
+
+from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
+from homeassistant.components.climate.const import (
+ ATTR_CURRENT_TEMPERATURE,
+ ATTR_PRESET_MODE,
+ ATTR_PRESET_MODES,
+ HVAC_MODE_AUTO,
+ HVAC_MODE_HEAT,
+ PRESET_AWAY,
+ PRESET_BOOST,
+ PRESET_ECO,
+ PRESET_NONE,
+)
+from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
+from homeassistant.setup import async_setup_component
+
+from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics
+
+
+async def test_manually_configured_platform(hass):
+ """Test that we do not set up an access point."""
+ assert (
+ await async_setup_component(
+ hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: {"platform": HMIPC_DOMAIN}}
+ )
+ is True
+ )
+ assert not hass.data.get(HMIPC_DOMAIN)
+
+
+async def test_hmip_heating_group(hass, default_mock_hap):
+ """Test HomematicipHeatingGroup."""
+ entity_id = "climate.badezimmer"
+ entity_name = "Badezimmer"
+ device_model = None
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == HVAC_MODE_AUTO
+ assert ha_state.attributes["current_temperature"] == 23.8
+ assert ha_state.attributes["min_temp"] == 5.0
+ assert ha_state.attributes["max_temp"] == 30.0
+ assert ha_state.attributes["temperature"] == 5.0
+ assert ha_state.attributes["current_humidity"] == 47
+ assert ha_state.attributes[ATTR_PRESET_MODE] == "STD"
+ assert ha_state.attributes[ATTR_PRESET_MODES] == [
+ PRESET_NONE,
+ PRESET_BOOST,
+ "STD",
+ "Winter",
+ ]
+
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "climate",
+ "set_temperature",
+ {"entity_id": entity_id, "temperature": 22.5},
+ blocking=True,
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+ assert hmip_device.mock_calls[-1][0] == "set_point_temperature"
+ assert hmip_device.mock_calls[-1][1] == (22.5,)
+ await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 22.5)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5
+
+ await hass.services.async_call(
+ "climate",
+ "set_hvac_mode",
+ {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT},
+ blocking=True,
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 3
+ assert hmip_device.mock_calls[-1][0] == "set_control_mode"
+ assert hmip_device.mock_calls[-1][1] == ("MANUAL",)
+ await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL")
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == HVAC_MODE_HEAT
+
+ await hass.services.async_call(
+ "climate",
+ "set_hvac_mode",
+ {"entity_id": entity_id, "hvac_mode": HVAC_MODE_AUTO},
+ blocking=True,
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 5
+ assert hmip_device.mock_calls[-1][0] == "set_control_mode"
+ assert hmip_device.mock_calls[-1][1] == ("AUTOMATIC",)
+ await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTO")
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == HVAC_MODE_AUTO
+
+ await hass.services.async_call(
+ "climate",
+ "set_preset_mode",
+ {"entity_id": entity_id, "preset_mode": PRESET_BOOST},
+ blocking=True,
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 7
+ assert hmip_device.mock_calls[-1][0] == "set_boost"
+ assert hmip_device.mock_calls[-1][1] == ()
+ await async_manipulate_test_data(hass, hmip_device, "boostMode", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST
+
+ await hass.services.async_call(
+ "climate",
+ "set_preset_mode",
+ {"entity_id": entity_id, "preset_mode": PRESET_NONE},
+ blocking=True,
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 9
+ assert hmip_device.mock_calls[-1][0] == "set_boost"
+ assert hmip_device.mock_calls[-1][1] == (False,)
+ await async_manipulate_test_data(hass, hmip_device, "boostMode", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_PRESET_MODE] == "STD"
+
+ # Not required for hmip, but a posiblity to send no temperature.
+ await hass.services.async_call(
+ "climate",
+ "set_temperature",
+ {"entity_id": entity_id, "target_temp_low": 10, "target_temp_high": 10},
+ blocking=True,
+ )
+ # No new service call should be in mock_calls.
+ assert len(hmip_device.mock_calls) == service_call_counter + 10
+ # Only fire event from last async_manipulate_test_data available.
+ assert hmip_device.mock_calls[-1][0] == "fire_update_event"
+
+ await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO")
+ await async_manipulate_test_data(
+ hass,
+ default_mock_hap.home.get_functionalHome(IndoorClimateHome),
+ "absenceType",
+ AbsenceType.VACATION,
+ fire_device=hmip_device,
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY
+
+ await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO")
+ await async_manipulate_test_data(
+ hass,
+ default_mock_hap.home.get_functionalHome(IndoorClimateHome),
+ "absenceType",
+ AbsenceType.PERIOD,
+ fire_device=hmip_device,
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO
+
+ # Not required for hmip, but a posiblity to send no temperature.
+ await hass.services.async_call(
+ "climate",
+ "set_preset_mode",
+ {"entity_id": entity_id, "preset_mode": "Winter"},
+ blocking=True,
+ )
+
+ assert len(hmip_device.mock_calls) == service_call_counter + 16
+ assert hmip_device.mock_calls[-1][0] == "set_active_profile"
+ assert hmip_device.mock_calls[-1][1] == (1,)
+
+
+async def test_hmip_climate_services(hass, mock_hap_with_service):
+ """Test HomematicipHeatingGroup."""
+
+ home = mock_hap_with_service.home
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "activate_eco_mode_with_duration",
+ {"duration": 60, "accesspoint_id": HAPID},
+ blocking=True,
+ )
+ assert home.mock_calls[-1][0] == "activate_absence_with_duration"
+ assert home.mock_calls[-1][1] == (60,)
+ assert len(home._connection.mock_calls) == 1 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "activate_eco_mode_with_duration",
+ {"duration": 60},
+ blocking=True,
+ )
+ assert home.mock_calls[-1][0] == "activate_absence_with_duration"
+ assert home.mock_calls[-1][1] == (60,)
+ assert len(home._connection.mock_calls) == 2 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "activate_eco_mode_with_period",
+ {"endtime": "2019-02-17 14:00", "accesspoint_id": HAPID},
+ blocking=True,
+ )
+ assert home.mock_calls[-1][0] == "activate_absence_with_period"
+ assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),)
+ assert len(home._connection.mock_calls) == 3 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "activate_eco_mode_with_period",
+ {"endtime": "2019-02-17 14:00"},
+ blocking=True,
+ )
+ assert home.mock_calls[-1][0] == "activate_absence_with_period"
+ assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),)
+ assert len(home._connection.mock_calls) == 4 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "activate_vacation",
+ {"endtime": "2019-02-17 14:00", "temperature": 18.5, "accesspoint_id": HAPID},
+ blocking=True,
+ )
+ assert home.mock_calls[-1][0] == "activate_vacation"
+ assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5)
+ assert len(home._connection.mock_calls) == 5 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "activate_vacation",
+ {"endtime": "2019-02-17 14:00", "temperature": 18.5},
+ blocking=True,
+ )
+ assert home.mock_calls[-1][0] == "activate_vacation"
+ assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5)
+ assert len(home._connection.mock_calls) == 6 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "deactivate_eco_mode",
+ {"accesspoint_id": HAPID},
+ blocking=True,
+ )
+ assert home.mock_calls[-1][0] == "deactivate_absence"
+ assert home.mock_calls[-1][1] == ()
+ assert len(home._connection.mock_calls) == 7 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud", "deactivate_eco_mode", blocking=True
+ )
+ assert home.mock_calls[-1][0] == "deactivate_absence"
+ assert home.mock_calls[-1][1] == ()
+ assert len(home._connection.mock_calls) == 8 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "deactivate_vacation",
+ {"accesspoint_id": HAPID},
+ blocking=True,
+ )
+ assert home.mock_calls[-1][0] == "deactivate_vacation"
+ assert home.mock_calls[-1][1] == ()
+ assert len(home._connection.mock_calls) == 9 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud", "deactivate_vacation", blocking=True
+ )
+ assert home.mock_calls[-1][0] == "deactivate_vacation"
+ assert home.mock_calls[-1][1] == ()
+ assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212
+
+ not_existing_hap_id = "5555F7110000000000000001"
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "deactivate_vacation",
+ {"accesspoint_id": not_existing_hap_id},
+ blocking=True,
+ )
+ assert home.mock_calls[-1][0] == "deactivate_vacation"
+ assert home.mock_calls[-1][1] == ()
+ # There is no further call on connection.
+ assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212
+
+
+async def test_hmip_heating_group_services(hass, mock_hap_with_service):
+ """Test HomematicipHeatingGroup services."""
+ entity_id = "climate.badezimmer"
+ entity_name = "Badezimmer"
+ device_model = None
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, mock_hap_with_service, entity_id, entity_name, device_model
+ )
+ assert ha_state
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "set_active_climate_profile",
+ {"climate_profile_index": 2, "entity_id": "climate.badezimmer"},
+ blocking=True,
+ )
+ assert hmip_device.mock_calls[-1][0] == "set_active_profile"
+ assert hmip_device.mock_calls[-1][1] == (1,)
+ assert len(hmip_device._connection.mock_calls) == 2 # pylint: disable=W0212
+
+ await hass.services.async_call(
+ "homematicip_cloud",
+ "set_active_climate_profile",
+ {"climate_profile_index": 2, "entity_id": "all"},
+ blocking=True,
+ )
+ assert hmip_device.mock_calls[-1][0] == "set_active_profile"
+ assert hmip_device.mock_calls[-1][1] == (1,)
+ assert len(hmip_device._connection.mock_calls) == 12 # pylint: disable=W0212
diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py
index c1bad855701..54cb309755d 100644
--- a/tests/components/homematicip_cloud/test_config_flow.py
+++ b/tests/components/homematicip_cloud/test_config_flow.py
@@ -1,8 +1,7 @@
"""Tests for HomematicIP Cloud config flow."""
from unittest.mock import patch
-from homeassistant.components.homematicip_cloud import hap as hmipc
-from homeassistant.components.homematicip_cloud import config_flow, const
+from homeassistant.components.homematicip_cloud import config_flow, const, hap as hmipc
from tests.common import MockConfigEntry, mock_coro
diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py
new file mode 100644
index 00000000000..22922303f9e
--- /dev/null
+++ b/tests/components/homematicip_cloud/test_cover.py
@@ -0,0 +1,155 @@
+"""Tests for HomematicIP Cloud cover."""
+from homeassistant.components.cover import (
+ ATTR_CURRENT_POSITION,
+ ATTR_CURRENT_TILT_POSITION,
+ DOMAIN as COVER_DOMAIN,
+)
+from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
+from homeassistant.const import STATE_CLOSED, STATE_OPEN
+from homeassistant.setup import async_setup_component
+
+from .helper import async_manipulate_test_data, get_and_check_entity_basics
+
+
+async def test_manually_configured_platform(hass):
+ """Test that we do not set up an access point."""
+ assert (
+ await async_setup_component(
+ hass, COVER_DOMAIN, {COVER_DOMAIN: {"platform": HMIPC_DOMAIN}}
+ )
+ is True
+ )
+ assert not hass.data.get(HMIPC_DOMAIN)
+
+
+async def test_hmip_cover_shutter(hass, default_mock_hap):
+ """Test HomematicipCoverShutte."""
+ entity_id = "cover.sofa_links"
+ entity_name = "Sofa links"
+ device_model = "HmIP-FBL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "closed"
+ assert ha_state.attributes["current_position"] == 0
+ assert ha_state.attributes["current_tilt_position"] == 0
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "cover", "open_cover", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+ assert hmip_device.mock_calls[-1][0] == "set_shutter_level"
+ assert hmip_device.mock_calls[-1][1] == (0,)
+ await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OPEN
+ assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100
+ assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0
+
+ await hass.services.async_call(
+ "cover",
+ "set_cover_position",
+ {"entity_id": entity_id, "position": "50"},
+ blocking=True,
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 3
+ assert hmip_device.mock_calls[-1][0] == "set_shutter_level"
+ assert hmip_device.mock_calls[-1][1] == (0.5,)
+ await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0.5)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OPEN
+ assert ha_state.attributes[ATTR_CURRENT_POSITION] == 50
+ assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0
+
+ await hass.services.async_call(
+ "cover", "close_cover", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 5
+ assert hmip_device.mock_calls[-1][0] == "set_shutter_level"
+ assert hmip_device.mock_calls[-1][1] == (1,)
+ await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 1)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_CLOSED
+ assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0
+ assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0
+
+ await hass.services.async_call(
+ "cover", "stop_cover", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 7
+ assert hmip_device.mock_calls[-1][0] == "set_shutter_stop"
+ assert hmip_device.mock_calls[-1][1] == ()
+
+ await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_CLOSED
+
+
+async def test_hmip_cover_slats(hass, default_mock_hap):
+ """Test HomematicipCoverSlats."""
+ entity_id = "cover.sofa_links"
+ entity_name = "Sofa links"
+ device_model = "HmIP-FBL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_CLOSED
+ assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0
+ assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+ assert hmip_device.mock_calls[-1][0] == "set_slats_level"
+ assert hmip_device.mock_calls[-1][1] == (0,)
+ await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0)
+ await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OPEN
+ assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100
+ assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100
+
+ await hass.services.async_call(
+ "cover",
+ "set_cover_tilt_position",
+ {"entity_id": entity_id, "tilt_position": "50"},
+ blocking=True,
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 4
+ assert hmip_device.mock_calls[-1][0] == "set_slats_level"
+ assert hmip_device.mock_calls[-1][1] == (0.5,)
+ await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OPEN
+ assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100
+ assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50
+
+ await hass.services.async_call(
+ "cover", "close_cover_tilt", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 6
+ assert hmip_device.mock_calls[-1][0] == "set_slats_level"
+ assert hmip_device.mock_calls[-1][1] == (1,)
+ await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OPEN
+ assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100
+ assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0
+
+ await hass.services.async_call(
+ "cover", "stop_cover_tilt", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 8
+ assert hmip_device.mock_calls[-1][0] == "set_shutter_stop"
+ assert hmip_device.mock_calls[-1][1] == ()
+
+ await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OPEN
diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py
new file mode 100644
index 00000000000..812f32a3344
--- /dev/null
+++ b/tests/components/homematicip_cloud/test_device.py
@@ -0,0 +1,132 @@
+"""Common tests for HomematicIP devices."""
+from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
+from .conftest import get_mock_hap
+from .helper import async_manipulate_test_data, get_and_check_entity_basics
+
+
+async def test_hmip_remove_device(hass, default_mock_hap):
+ """Test Remove of hmip device."""
+ entity_id = "light.treppe"
+ entity_name = "Treppe"
+ device_model = "HmIP-BSL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ assert hmip_device
+
+ device_registry = await dr.async_get_registry(hass)
+ entity_registry = await er.async_get_registry(hass)
+
+ pre_device_count = len(device_registry.devices)
+ pre_entity_count = len(entity_registry.entities)
+ pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id)
+
+ hmip_device.fire_remove_event()
+
+ await hass.async_block_till_done()
+
+ assert len(device_registry.devices) == pre_device_count - 1
+ assert len(entity_registry.entities) == pre_entity_count - 3
+ assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3
+
+
+async def test_hmip_remove_group(hass, default_mock_hap):
+ """Test Remove of hmip group."""
+ entity_id = "switch.strom_group"
+ entity_name = "Strom Group"
+ device_model = None
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ assert hmip_device
+
+ device_registry = await dr.async_get_registry(hass)
+ entity_registry = await er.async_get_registry(hass)
+
+ pre_device_count = len(device_registry.devices)
+ pre_entity_count = len(entity_registry.entities)
+ pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id)
+
+ hmip_device.fire_remove_event()
+
+ await hass.async_block_till_done()
+
+ assert len(device_registry.devices) == pre_device_count
+ assert len(entity_registry.entities) == pre_entity_count - 1
+ assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 1
+
+
+async def test_all_devices_unavailable_when_hap_not_connected(hass, default_mock_hap):
+ """Test make all devices unavaulable when hap is not connected."""
+ entity_id = "light.treppe"
+ entity_name = "Treppe"
+ device_model = "HmIP-BSL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ assert hmip_device
+
+ assert default_mock_hap.home.connected
+
+ await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False)
+
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_UNAVAILABLE
+
+
+async def test_hap_reconnected(hass, default_mock_hap):
+ """Test reconnect hap."""
+ entity_id = "light.treppe"
+ entity_name = "Treppe"
+ device_model = "HmIP-BSL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ assert hmip_device
+
+ assert default_mock_hap.home.connected
+
+ await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False)
+
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_UNAVAILABLE
+
+ default_mock_hap._accesspoint_connected = False # pylint: disable=W0212
+ await async_manipulate_test_data(hass, default_mock_hap.home, "connected", True)
+ await hass.async_block_till_done()
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+
+async def test_hap_with_name(hass, mock_connection, hmip_config_entry):
+ """Test hap with name."""
+ home_name = "TestName"
+ entity_id = f"light.{home_name.lower()}_treppe"
+ entity_name = f"{home_name} Treppe"
+ device_model = "HmIP-BSL"
+
+ hmip_config_entry.data["name"] = home_name
+ mock_hap = await get_mock_hap(hass, mock_connection, hmip_config_entry)
+ assert mock_hap
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert hmip_device
+ assert ha_state.state == STATE_ON
+ assert ha_state.attributes["friendly_name"] == entity_name
diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py
index 34afd19310f..324649ef515 100644
--- a/tests/components/homematicip_cloud/test_hap.py
+++ b/tests/components/homematicip_cloud/test_hap.py
@@ -1,11 +1,24 @@
"""Test HomematicIP Cloud accesspoint."""
-from unittest.mock import Mock, patch
+from asynctest import Mock, patch
+from homematicip.aio.auth import AsyncAuth
+from homematicip.base.base_connection import HmipConnectionError
import pytest
+from homeassistant.components.homematicip_cloud import (
+ DOMAIN as HMIPC_DOMAIN,
+ const,
+ errors,
+ hap as hmipc,
+)
+from homeassistant.components.homematicip_cloud.hap import (
+ HomematicipAuth,
+ HomematicipHAP,
+)
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.components.homematicip_cloud import hap as hmipc
-from homeassistant.components.homematicip_cloud import const, errors
+
+from .helper import HAPID, HAPPIN
+
from tests.common import mock_coro, mock_coro_func
@@ -18,7 +31,7 @@ async def test_auth_setup(hass):
}
hap = hmipc.HomematicipAuth(hass, config)
with patch.object(hap, "get_auth", return_value=mock_coro()):
- assert await hap.async_setup() is True
+ assert await hap.async_setup()
async def test_auth_setup_connection_error(hass):
@@ -30,7 +43,7 @@ async def test_auth_setup_connection_error(hass):
}
hap = hmipc.HomematicipAuth(hass, config)
with patch.object(hap, "get_auth", side_effect=errors.HmipcConnectionError):
- assert await hap.async_setup() is False
+ assert not await hap.async_setup()
async def test_auth_auth_check_and_register(hass):
@@ -49,10 +62,26 @@ async def test_auth_auth_check_and_register(hass):
), patch.object(
hap.auth, "confirmAuthToken", return_value=mock_coro()
):
- assert await hap.async_checkbutton() is True
+ assert await hap.async_checkbutton()
assert await hap.async_register() == "ABC"
+async def test_auth_auth_check_and_register_with_exception(hass):
+ """Test auth client registration."""
+ config = {
+ const.HMIPC_HAPID: "ABC123",
+ const.HMIPC_PIN: "123",
+ const.HMIPC_NAME: "hmip",
+ }
+ hap = hmipc.HomematicipAuth(hass, config)
+ hap.auth = Mock(spec=AsyncAuth)
+ with patch.object(
+ hap.auth, "isRequestAcknowledged", side_effect=HmipConnectionError
+ ), patch.object(hap.auth, "requestAuthToken", side_effect=HmipConnectionError):
+ assert not await hap.async_checkbutton()
+ assert await hap.async_register() is False
+
+
async def test_hap_setup_works(aioclient_mock):
"""Test a successful setup of a accesspoint."""
hass = Mock()
@@ -65,7 +94,7 @@ async def test_hap_setup_works(aioclient_mock):
}
hap = hmipc.HomematicipHAP(hass, entry)
with patch.object(hap, "get_hap", return_value=mock_coro(home)):
- assert await hap.async_setup() is True
+ assert await hap.async_setup()
assert hap.home is home
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8
@@ -94,8 +123,8 @@ async def test_hap_setup_connection_error():
), pytest.raises(ConfigEntryNotReady):
await hap.async_setup()
- assert len(hass.async_add_job.mock_calls) == 0
- assert len(hass.config_entries.flow.async_init.mock_calls) == 0
+ assert not hass.async_add_job.mock_calls
+ assert not hass.config_entries.flow.async_init.mock_calls
async def test_hap_reset_unloads_entry_if_setup():
@@ -111,13 +140,88 @@ async def test_hap_reset_unloads_entry_if_setup():
}
hap = hmipc.HomematicipHAP(hass, entry)
with patch.object(hap, "get_hap", return_value=mock_coro(home)):
- assert await hap.async_setup() is True
+ assert await hap.async_setup()
assert hap.home is home
- assert len(hass.services.async_register.mock_calls) == 0
+ assert not hass.services.async_register.mock_calls
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8
hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True)
await hap.async_reset()
assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 8
+
+
+async def test_hap_create(hass, hmip_config_entry, simple_mock_home):
+ """Mock AsyncHome to execute get_hap."""
+ hass.config.components.add(HMIPC_DOMAIN)
+ hap = HomematicipHAP(hass, hmip_config_entry)
+ assert hap
+ with patch(
+ "homeassistant.components.homematicip_cloud.hap.AsyncHome",
+ return_value=simple_mock_home,
+ ), patch.object(hap, "async_connect", return_value=mock_coro(None)):
+ assert await hap.async_setup()
+
+
+async def test_hap_create_exception(hass, hmip_config_entry, simple_mock_home):
+ """Mock AsyncHome to execute get_hap."""
+ hass.config.components.add(HMIPC_DOMAIN)
+ hap = HomematicipHAP(hass, hmip_config_entry)
+ assert hap
+
+ with patch.object(hap, "get_hap", side_effect=HmipConnectionError), pytest.raises(
+ HmipConnectionError
+ ):
+ await hap.async_setup()
+
+ simple_mock_home.init.side_effect = HmipConnectionError
+ with patch(
+ "homeassistant.components.homematicip_cloud.hap.AsyncHome",
+ return_value=simple_mock_home,
+ ), pytest.raises(ConfigEntryNotReady):
+ await hap.async_setup()
+
+
+async def test_auth_create(hass, simple_mock_auth):
+ """Mock AsyncAuth to execute get_auth."""
+ config = {
+ const.HMIPC_HAPID: HAPID,
+ const.HMIPC_PIN: HAPPIN,
+ const.HMIPC_NAME: "hmip",
+ }
+ hmip_auth = HomematicipAuth(hass, config)
+ assert hmip_auth
+
+ with patch(
+ "homeassistant.components.homematicip_cloud.hap.AsyncAuth",
+ return_value=simple_mock_auth,
+ ):
+ assert await hmip_auth.async_setup()
+ await hass.async_block_till_done()
+ assert hmip_auth.auth.pin == HAPPIN
+
+
+async def test_auth_create_exception(hass, simple_mock_auth):
+ """Mock AsyncAuth to execute get_auth."""
+ config = {
+ const.HMIPC_HAPID: HAPID,
+ const.HMIPC_PIN: HAPPIN,
+ const.HMIPC_NAME: "hmip",
+ }
+ hmip_auth = HomematicipAuth(hass, config)
+ simple_mock_auth.connectionRequest.side_effect = HmipConnectionError
+ assert hmip_auth
+ with patch(
+ "homeassistant.components.homematicip_cloud.hap.AsyncAuth",
+ return_value=simple_mock_auth,
+ ):
+ assert await hmip_auth.async_setup()
+ await hass.async_block_till_done()
+ assert not hmip_auth.auth
+
+ with patch(
+ "homeassistant.components.homematicip_cloud.hap.AsyncAuth",
+ return_value=simple_mock_auth,
+ ):
+ assert not await hmip_auth.get_auth(hass, HAPID, HAPPIN)
diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py
index d77d4a7e5b2..894db2e691b 100644
--- a/tests/components/homematicip_cloud/test_init.py
+++ b/tests/components/homematicip_cloud/test_init.py
@@ -2,10 +2,10 @@
from unittest.mock import patch
-from homeassistant.setup import async_setup_component
from homeassistant.components import homematicip_cloud as hmipc
+from homeassistant.setup import async_setup_component
-from tests.common import mock_coro, MockConfigEntry
+from tests.common import MockConfigEntry, mock_coro
async def test_config_with_accesspoint_passed_to_config_entry(hass):
@@ -53,7 +53,7 @@ async def test_config_already_registered_not_passed_to_config_entry(hass):
)
# No flow started
- assert len(mock_config_entries.flow.mock_calls) == 0
+ assert not mock_config_entries.flow.mock_calls
async def test_setup_entry_successful(hass):
diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py
new file mode 100644
index 00000000000..17e92d9d99d
--- /dev/null
+++ b/tests/components/homematicip_cloud/test_light.py
@@ -0,0 +1,223 @@
+"""Tests for HomematicIP Cloud light."""
+from homematicip.base.enums import RGBColorState
+
+from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
+from homeassistant.components.homematicip_cloud.light import (
+ ATTR_ENERGY_COUNTER,
+ ATTR_POWER_CONSUMPTION,
+)
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_COLOR_NAME,
+ DOMAIN as LIGHT_DOMAIN,
+)
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.setup import async_setup_component
+
+from .helper import async_manipulate_test_data, get_and_check_entity_basics
+
+
+async def test_manually_configured_platform(hass):
+ """Test that we do not set up an access point."""
+ assert (
+ await async_setup_component(
+ hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": HMIPC_DOMAIN}}
+ )
+ is True
+ )
+ assert not hass.data.get(HMIPC_DOMAIN)
+
+
+async def test_hmip_light(hass, default_mock_hap):
+ """Test HomematicipLight."""
+ entity_id = "light.treppe"
+ entity_name = "Treppe"
+ device_model = "HmIP-BSL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+
+ service_call_counter = len(hmip_device.mock_calls)
+ await hass.services.async_call(
+ "light", "turn_off", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+ assert hmip_device.mock_calls[-1][0] == "turn_off"
+ assert hmip_device.mock_calls[-1][1] == ()
+
+ await async_manipulate_test_data(hass, hmip_device, "on", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+ await hass.services.async_call(
+ "light", "turn_on", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 3
+ assert hmip_device.mock_calls[-1][0] == "turn_on"
+ assert hmip_device.mock_calls[-1][1] == ()
+
+ await async_manipulate_test_data(hass, hmip_device, "on", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+
+async def test_hmip_notification_light(hass, default_mock_hap):
+ """Test HomematicipNotificationLight."""
+ entity_id = "light.treppe_top_notification"
+ entity_name = "Treppe Top Notification"
+ device_model = "HmIP-BSL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ service_call_counter = len(hmip_device.mock_calls)
+
+ # Send all color via service call.
+ await hass.services.async_call(
+ "light", "turn_on", {"entity_id": entity_id}, blocking=True
+ )
+ assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level"
+ assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.RED, 1.0)
+
+ color_list = {
+ RGBColorState.WHITE: [0.0, 0.0],
+ RGBColorState.RED: [0.0, 100.0],
+ RGBColorState.YELLOW: [60.0, 100.0],
+ RGBColorState.GREEN: [120.0, 100.0],
+ RGBColorState.TURQUOISE: [180.0, 100.0],
+ RGBColorState.BLUE: [240.0, 100.0],
+ RGBColorState.PURPLE: [300.0, 100.0],
+ }
+
+ for color, hs_color in color_list.items():
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": entity_id, "hs_color": hs_color},
+ blocking=True,
+ )
+ assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level"
+ assert hmip_device.mock_calls[-1][1] == (2, color, 0.0392156862745098)
+
+ assert len(hmip_device.mock_calls) == service_call_counter + 8
+
+ assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level"
+ assert hmip_device.mock_calls[-1][1] == (
+ 2,
+ RGBColorState.PURPLE,
+ 0.0392156862745098,
+ )
+ await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1, 2)
+ await async_manipulate_test_data(
+ hass, hmip_device, "simpleRGBColorState", RGBColorState.PURPLE, 2
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+ assert ha_state.attributes[ATTR_COLOR_NAME] == RGBColorState.PURPLE
+ assert ha_state.attributes[ATTR_BRIGHTNESS] == 255
+
+ await hass.services.async_call(
+ "light", "turn_off", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 11
+ assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level"
+ assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.PURPLE, 0.0)
+ await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+ await async_manipulate_test_data(hass, hmip_device, "dimLevel", None, 2)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+ assert not ha_state.attributes.get(ATTR_BRIGHTNESS)
+
+
+async def test_hmip_dimmer(hass, default_mock_hap):
+ """Test HomematicipDimmer."""
+ entity_id = "light.schlafzimmerlicht"
+ entity_name = "Schlafzimmerlicht"
+ device_model = "HmIP-BDT"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "light", "turn_on", {"entity_id": entity_id}, blocking=True
+ )
+ assert hmip_device.mock_calls[-1][0] == "set_dim_level"
+ assert hmip_device.mock_calls[-1][1] == (1,)
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": entity_id, "brightness_pct": "100"},
+ blocking=True,
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 2
+ assert hmip_device.mock_calls[-1][0] == "set_dim_level"
+ assert hmip_device.mock_calls[-1][1] == (1.0,)
+ await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+ assert ha_state.attributes[ATTR_BRIGHTNESS] == 255
+
+ await hass.services.async_call(
+ "light", "turn_off", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 4
+ assert hmip_device.mock_calls[-1][0] == "set_dim_level"
+ assert hmip_device.mock_calls[-1][1] == (0,)
+ await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+ await async_manipulate_test_data(hass, hmip_device, "dimLevel", None)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+ assert not ha_state.attributes.get(ATTR_BRIGHTNESS)
+
+
+async def test_hmip_light_measuring(hass, default_mock_hap):
+ """Test HomematicipLightMeasuring."""
+ entity_id = "light.flur_oben"
+ entity_name = "Flur oben"
+ device_model = "HmIP-BSM"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "light", "turn_on", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+ assert hmip_device.mock_calls[-1][0] == "turn_on"
+ assert hmip_device.mock_calls[-1][1] == ()
+ await async_manipulate_test_data(hass, hmip_device, "on", True)
+ await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+ assert ha_state.attributes[ATTR_POWER_CONSUMPTION] == 50
+ assert ha_state.attributes[ATTR_ENERGY_COUNTER] == 6.33
+
+ await hass.services.async_call(
+ "light", "turn_off", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 4
+ assert hmip_device.mock_calls[-1][0] == "turn_off"
+ assert hmip_device.mock_calls[-1][1] == ()
+ await async_manipulate_test_data(hass, hmip_device, "on", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py
new file mode 100644
index 00000000000..8412cd19f4d
--- /dev/null
+++ b/tests/components/homematicip_cloud/test_sensor.py
@@ -0,0 +1,268 @@
+"""Tests for HomematicIP Cloud sensor."""
+from homematicip.base.enums import ValveState
+
+from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
+from homeassistant.components.homematicip_cloud.sensor import (
+ ATTR_LEFT_COUNTER,
+ ATTR_RIGHT_COUNTER,
+ ATTR_TEMPERATURE_OFFSET,
+ ATTR_WIND_DIRECTION,
+ ATTR_WIND_DIRECTION_VARIATION,
+)
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS
+from homeassistant.setup import async_setup_component
+
+from .helper import async_manipulate_test_data, get_and_check_entity_basics
+
+
+async def test_manually_configured_platform(hass):
+ """Test that we do not set up an access point."""
+ assert (
+ await async_setup_component(
+ hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}}
+ )
+ is True
+ )
+ assert not hass.data.get(HMIPC_DOMAIN)
+
+
+async def test_hmip_accesspoint_status(hass, default_mock_hap):
+ """Test HomematicipSwitch."""
+ entity_id = "sensor.access_point"
+ entity_name = "Access Point"
+ device_model = None
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+ assert hmip_device
+ assert ha_state.state == "8.0"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%"
+
+ await async_manipulate_test_data(hass, hmip_device, "dutyCycle", 17.3)
+
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "17.3"
+
+
+async def test_hmip_heating_thermostat(hass, default_mock_hap):
+ """Test HomematicipHeatingThermostat."""
+ entity_id = "sensor.heizkorperthermostat_heating"
+ entity_name = "Heizkörperthermostat Heating"
+ device_model = "HMIP-eTRV"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "0"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%"
+ await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.37)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "37"
+
+ await async_manipulate_test_data(hass, hmip_device, "valveState", "nn")
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "nn"
+
+ await async_manipulate_test_data(
+ hass, hmip_device, "valveState", ValveState.ADAPTION_DONE
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "37"
+
+ await async_manipulate_test_data(hass, hmip_device, "lowBat", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes["icon"] == "mdi:battery-outline"
+
+
+async def test_hmip_humidity_sensor(hass, default_mock_hap):
+ """Test HomematicipHumiditySensor."""
+ entity_id = "sensor.bwth_1_humidity"
+ entity_name = "BWTH 1 Humidity"
+ device_model = "HmIP-BWTH"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "40"
+ assert ha_state.attributes["unit_of_measurement"] == "%"
+ await async_manipulate_test_data(hass, hmip_device, "humidity", 45)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "45"
+
+
+async def test_hmip_temperature_sensor1(hass, default_mock_hap):
+ """Test HomematicipTemperatureSensor."""
+ entity_id = "sensor.bwth_1_temperature"
+ entity_name = "BWTH 1 Temperature"
+ device_model = "HmIP-BWTH"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "21.0"
+ assert ha_state.attributes["unit_of_measurement"] == TEMP_CELSIUS
+ await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 23.5)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "23.5"
+
+ assert not ha_state.attributes.get("temperature_offset")
+ await async_manipulate_test_data(hass, hmip_device, "temperatureOffset", 10)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10
+
+
+async def test_hmip_temperature_sensor2(hass, default_mock_hap):
+ """Test HomematicipTemperatureSensor."""
+ entity_id = "sensor.heizkorperthermostat_temperature"
+ entity_name = "Heizkörperthermostat Temperature"
+ device_model = "HMIP-eTRV"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "20.0"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
+ await async_manipulate_test_data(hass, hmip_device, "valveActualTemperature", 23.5)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "23.5"
+
+ assert not ha_state.attributes.get(ATTR_TEMPERATURE_OFFSET)
+ await async_manipulate_test_data(hass, hmip_device, "temperatureOffset", 10)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10
+
+
+async def test_hmip_power_sensor(hass, default_mock_hap):
+ """Test HomematicipPowerSensor."""
+ entity_id = "sensor.flur_oben_power"
+ entity_name = "Flur oben Power"
+ device_model = "HmIP-BSM"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "0.0"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT
+ await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 23.5)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "23.5"
+
+
+async def test_hmip_illuminance_sensor1(hass, default_mock_hap):
+ """Test HomematicipIlluminanceSensor."""
+ entity_id = "sensor.wettersensor_illuminance"
+ entity_name = "Wettersensor Illuminance"
+ device_model = "HmIP-SWO-B"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "4890.0"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx"
+ await async_manipulate_test_data(hass, hmip_device, "illumination", 231)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "231"
+
+
+async def test_hmip_illuminance_sensor2(hass, default_mock_hap):
+ """Test HomematicipIlluminanceSensor."""
+ entity_id = "sensor.lichtsensor_nord_illuminance"
+ entity_name = "Lichtsensor Nord Illuminance"
+ device_model = "HmIP-SLO"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "807.3"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx"
+ await async_manipulate_test_data(hass, hmip_device, "averageIllumination", 231)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "231"
+
+
+async def test_hmip_windspeed_sensor(hass, default_mock_hap):
+ """Test HomematicipWindspeedSensor."""
+ entity_id = "sensor.wettersensor_pro_windspeed"
+ entity_name = "Wettersensor - pro Windspeed"
+ device_model = "HmIP-SWO-PR"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "2.6"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "km/h"
+ await async_manipulate_test_data(hass, hmip_device, "windSpeed", 9.4)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "9.4"
+
+ assert ha_state.attributes[ATTR_WIND_DIRECTION_VARIATION] == 56.25
+ assert ha_state.attributes[ATTR_WIND_DIRECTION] == "WNW"
+
+ wind_directions = {
+ 25: "NNE",
+ 37.5: "NE",
+ 70: "ENE",
+ 92.5: "E",
+ 115: "ESE",
+ 137.5: "SE",
+ 160: "SSE",
+ 182.5: "S",
+ 205: "SSW",
+ 227.5: "SW",
+ 250: "WSW",
+ 272.5: "W",
+ 295: "WNW",
+ 317.5: "NW",
+ 340: "NNW",
+ 0: "N",
+ }
+
+ for direction, txt in wind_directions.items():
+ await async_manipulate_test_data(hass, hmip_device, "windDirection", direction)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_WIND_DIRECTION] == txt
+
+
+async def test_hmip_today_rain_sensor(hass, default_mock_hap):
+ """Test HomematicipTodayRainSensor."""
+ entity_id = "sensor.weather_sensor_plus_today_rain"
+ entity_name = "Weather Sensor – plus Today Rain"
+ device_model = "HmIP-SWO-PL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "3.9"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "mm"
+ await async_manipulate_test_data(hass, hmip_device, "todayRainCounter", 14.2)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "14.2"
+
+
+async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap):
+ """Test HomematicipPassageDetectorDeltaCounter."""
+ entity_id = "sensor.spdr_1"
+ entity_name = "SPDR_1"
+ device_model = "HmIP-SPDR"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "164"
+ assert ha_state.attributes[ATTR_LEFT_COUNTER] == 966
+ assert ha_state.attributes[ATTR_RIGHT_COUNTER] == 802
+ await async_manipulate_test_data(hass, hmip_device, "leftRightCounterDelta", 190)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == "190"
diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py
new file mode 100644
index 00000000000..9e33d1d9587
--- /dev/null
+++ b/tests/components/homematicip_cloud/test_switch.py
@@ -0,0 +1,173 @@
+"""Tests for HomematicIP Cloud switch."""
+from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
+from homeassistant.components.homematicip_cloud.device import (
+ ATTR_GROUP_MEMBER_UNREACHABLE,
+)
+from homeassistant.components.switch import (
+ ATTR_CURRENT_POWER_W,
+ ATTR_TODAY_ENERGY_KWH,
+ DOMAIN as SWITCH_DOMAIN,
+)
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.setup import async_setup_component
+
+from .helper import async_manipulate_test_data, get_and_check_entity_basics
+
+
+async def test_manually_configured_platform(hass):
+ """Test that we do not set up an access point."""
+ assert (
+ await async_setup_component(
+ hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": HMIPC_DOMAIN}}
+ )
+ is True
+ )
+ assert not hass.data.get(HMIPC_DOMAIN)
+
+
+async def test_hmip_switch(hass, default_mock_hap):
+ """Test HomematicipSwitch."""
+ entity_id = "switch.schrank"
+ entity_name = "Schrank"
+ device_model = "HMIP-PS"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "switch", "turn_off", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+ assert hmip_device.mock_calls[-1][0] == "turn_off"
+ assert hmip_device.mock_calls[-1][1] == ()
+ await async_manipulate_test_data(hass, hmip_device, "on", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+ await hass.services.async_call(
+ "switch", "turn_on", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 3
+ assert hmip_device.mock_calls[-1][0] == "turn_on"
+ assert hmip_device.mock_calls[-1][1] == ()
+ await async_manipulate_test_data(hass, hmip_device, "on", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+
+async def test_hmip_switch_measuring(hass, default_mock_hap):
+ """Test HomematicipSwitchMeasuring."""
+ entity_id = "switch.pc"
+ entity_name = "Pc"
+ device_model = "HMIP-PSM"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "switch", "turn_off", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+ assert hmip_device.mock_calls[-1][0] == "turn_off"
+ assert hmip_device.mock_calls[-1][1] == ()
+ await async_manipulate_test_data(hass, hmip_device, "on", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+ await hass.services.async_call(
+ "switch", "turn_on", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 3
+ assert hmip_device.mock_calls[-1][0] == "turn_on"
+ assert hmip_device.mock_calls[-1][1] == ()
+ await async_manipulate_test_data(hass, hmip_device, "on", True)
+ await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+ assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50
+ assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 36
+
+ await async_manipulate_test_data(hass, hmip_device, "energyCounter", None)
+ ha_state = hass.states.get(entity_id)
+ assert not ha_state.attributes.get(ATTR_TODAY_ENERGY_KWH)
+
+
+async def test_hmip_group_switch(hass, default_mock_hap):
+ """Test HomematicipGroupSwitch."""
+ entity_id = "switch.strom_group"
+ entity_name = "Strom Group"
+ device_model = None
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "switch", "turn_off", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+ assert hmip_device.mock_calls[-1][0] == "turn_off"
+ assert hmip_device.mock_calls[-1][1] == ()
+ await async_manipulate_test_data(hass, hmip_device, "on", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+ await hass.services.async_call(
+ "switch", "turn_on", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 3
+ assert hmip_device.mock_calls[-1][0] == "turn_on"
+ assert hmip_device.mock_calls[-1][1] == ()
+ await async_manipulate_test_data(hass, hmip_device, "on", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+ assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE)
+ await async_manipulate_test_data(hass, hmip_device, "unreach", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] is True
+
+
+async def test_hmip_multi_switch(hass, default_mock_hap):
+ """Test HomematicipMultiSwitch."""
+ entity_id = "switch.jalousien_1_kizi_2_schlazi_channel1"
+ entity_name = "Jalousien - 1 KiZi, 2 SchlaZi Channel1"
+ device_model = "HmIP-PCBS2"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_OFF
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "switch", "turn_on", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 1
+ assert hmip_device.mock_calls[-1][0] == "turn_on"
+ assert hmip_device.mock_calls[-1][1] == (1,)
+ await async_manipulate_test_data(hass, hmip_device, "on", True)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_ON
+
+ await hass.services.async_call(
+ "switch", "turn_off", {"entity_id": entity_id}, blocking=True
+ )
+ assert len(hmip_device.mock_calls) == service_call_counter + 3
+ assert hmip_device.mock_calls[-1][0] == "turn_off"
+ assert hmip_device.mock_calls[-1][1] == (1,)
+ await async_manipulate_test_data(hass, hmip_device, "on", False)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py
new file mode 100644
index 00000000000..9427a2d05bf
--- /dev/null
+++ b/tests/components/homematicip_cloud/test_weather.py
@@ -0,0 +1,96 @@
+"""Tests for HomematicIP Cloud weather."""
+from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
+from homeassistant.components.weather import (
+ ATTR_WEATHER_ATTRIBUTION,
+ ATTR_WEATHER_HUMIDITY,
+ ATTR_WEATHER_TEMPERATURE,
+ ATTR_WEATHER_WIND_BEARING,
+ ATTR_WEATHER_WIND_SPEED,
+ DOMAIN as WEATHER_DOMAIN,
+)
+from homeassistant.setup import async_setup_component
+
+from .helper import async_manipulate_test_data, get_and_check_entity_basics
+
+
+async def test_manually_configured_platform(hass):
+ """Test that we do not set up an access point."""
+ assert (
+ await async_setup_component(
+ hass, WEATHER_DOMAIN, {WEATHER_DOMAIN: {"platform": HMIPC_DOMAIN}}
+ )
+ is True
+ )
+ assert not hass.data.get(HMIPC_DOMAIN)
+
+
+async def test_hmip_weather_sensor(hass, default_mock_hap):
+ """Test HomematicipWeatherSensor."""
+ entity_id = "weather.weather_sensor_plus"
+ entity_name = "Weather Sensor – plus"
+ device_model = "HmIP-SWO-PL"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == ""
+ assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 4.3
+ assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 97
+ assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0
+ assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP"
+
+ await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1
+
+
+async def test_hmip_weather_sensor_pro(hass, default_mock_hap):
+ """Test HomematicipWeatherSensorPro."""
+ entity_id = "weather.wettersensor_pro"
+ entity_name = "Wettersensor - pro"
+ device_model = "HmIP-SWO-PR"
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "sunny"
+ assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 15.4
+ assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 65
+ assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 2.6
+ assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 295.0
+ assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP"
+
+ await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1)
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1
+
+
+async def test_hmip_home_weather(hass, default_mock_hap):
+ """Test HomematicipHomeWeather."""
+ entity_id = "weather.weather_1010_wien_osterreich"
+ entity_name = "Weather 1010 Wien, Österreich"
+ device_model = None
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, default_mock_hap, entity_id, entity_name, device_model
+ )
+ assert hmip_device
+ assert ha_state.state == "partlycloudy"
+ assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 16.6
+ assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 54
+ assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 8.6
+ assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 294
+ assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP"
+
+ await async_manipulate_test_data(
+ hass,
+ default_mock_hap.home.weather,
+ "temperature",
+ 28.3,
+ fire_device=default_mock_hap.home,
+ )
+
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 28.3
diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py
index d9246e685dc..481d7a010c9 100644
--- a/tests/components/html5/test_notify.py
+++ b/tests/components/html5/test_notify.py
@@ -87,7 +87,7 @@ class TestHtml5Notify:
assert service is not None
- @patch("pywebpush.WebPusher")
+ @patch("homeassistant.components.html5.notify.WebPusher")
def test_dismissing_message(self, mock_wp):
"""Test dismissing message."""
hass = MagicMock()
@@ -115,7 +115,7 @@ class TestHtml5Notify:
assert payload["dismiss"] is True
assert payload["tag"] == "test"
- @patch("pywebpush.WebPusher")
+ @patch("homeassistant.components.html5.notify.WebPusher")
def test_sending_message(self, mock_wp):
"""Test sending message."""
hass = MagicMock()
@@ -145,7 +145,7 @@ class TestHtml5Notify:
assert payload["body"] == "Hello"
assert payload["icon"] == "beer.png"
- @patch("pywebpush.WebPusher")
+ @patch("homeassistant.components.html5.notify.WebPusher")
def test_gcm_key_include(self, mock_wp):
"""Test if the gcm_key is only included for GCM endpoints."""
hass = MagicMock()
@@ -176,7 +176,7 @@ class TestHtml5Notify:
assert mock_wp.mock_calls[1][2]["gcm_key"] is not None
assert mock_wp.mock_calls[4][2]["gcm_key"] is None
- @patch("pywebpush.WebPusher")
+ @patch("homeassistant.components.html5.notify.WebPusher")
def test_fcm_key_include(self, mock_wp):
"""Test if the FCM header is included."""
hass = MagicMock()
@@ -201,7 +201,7 @@ class TestHtml5Notify:
# Get the keys passed to the WebPusher's send method
assert mock_wp.mock_calls[1][2]["headers"]["Authorization"] is not None
- @patch("pywebpush.WebPusher")
+ @patch("homeassistant.components.html5.notify.WebPusher")
def test_fcm_send_with_unknown_priority(self, mock_wp):
"""Test if the gcm_key is only included for GCM endpoints."""
hass = MagicMock()
@@ -226,7 +226,7 @@ class TestHtml5Notify:
# Get the keys passed to the WebPusher's send method
assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal"
- @patch("pywebpush.WebPusher")
+ @patch("homeassistant.components.html5.notify.WebPusher")
def test_fcm_no_targets(self, mock_wp):
"""Test if the gcm_key is only included for GCM endpoints."""
hass = MagicMock()
@@ -251,7 +251,7 @@ class TestHtml5Notify:
# Get the keys passed to the WebPusher's send method
assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal"
- @patch("pywebpush.WebPusher")
+ @patch("homeassistant.components.html5.notify.WebPusher")
def test_fcm_additional_data(self, mock_wp):
"""Test if the gcm_key is only included for GCM endpoints."""
hass = MagicMock()
@@ -475,7 +475,7 @@ async def test_callback_view_with_jwt(hass, hass_client):
registrations = {"device": SUBSCRIPTION_1}
client = await mock_client(hass, hass_client, registrations)
- with patch("pywebpush.WebPusher") as mock_wp:
+ with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp:
await hass.services.async_call(
"notify",
"notify",
@@ -511,7 +511,7 @@ async def test_send_fcm_without_targets(hass, hass_client):
"""Test that the notification is send with FCM without targets."""
registrations = {"device": SUBSCRIPTION_5}
await mock_client(hass, hass_client, registrations)
- with patch("pywebpush.WebPusher") as mock_wp:
+ with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp:
await hass.services.async_call(
"notify",
"notify",
diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py
index c4f73fd15a6..db5e1ea5c7a 100644
--- a/tests/components/http/__init__.py
+++ b/tests/components/http/__init__.py
@@ -6,6 +6,10 @@ from aiohttp import web
from homeassistant.components.http.const import KEY_REAL_IP
+# Relic from the past. Kept here so we can run negative tests.
+HTTP_HEADER_HA_AUTH = "X-HA-access"
+
+
def mock_real_ip(app):
"""Inject middleware to mock real IP.
diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py
index 842201beace..499ceab1556 100644
--- a/tests/components/http/test_auth.py
+++ b/tests/components/http/test_auth.py
@@ -11,10 +11,8 @@ from homeassistant.auth.providers import trusted_networks
from homeassistant.components.http.auth import setup_auth, async_sign_path
from homeassistant.components.http.const import KEY_AUTHENTICATED
from homeassistant.components.http.real_ip import setup_real_ip
-from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.setup import async_setup_component
-from . import mock_real_ip
-
+from . import mock_real_ip, HTTP_HEADER_HA_AUTH
API_PASSWORD = "test-password"
@@ -87,29 +85,29 @@ async def test_auth_middleware_loaded_by_default(hass):
assert len(mock_setup.mock_calls) == 1
-async def test_access_with_password_in_header(app, aiohttp_client, legacy_auth, hass):
+async def test_cant_access_with_password_in_header(
+ app, aiohttp_client, legacy_auth, hass
+):
"""Test access with password in header."""
setup_auth(hass, app)
client = await aiohttp_client(app)
- user = await get_legacy_user(hass.auth)
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
- assert req.status == 200
- assert await req.json() == {"user_id": user.id}
+ assert req.status == 401
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: "wrong-pass"})
assert req.status == 401
-async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, hass):
+async def test_cant_access_with_password_in_query(
+ app, aiohttp_client, legacy_auth, hass
+):
"""Test access with password in URL."""
setup_auth(hass, app)
client = await aiohttp_client(app)
- user = await get_legacy_user(hass.auth)
resp = await client.get("/", params={"api_password": API_PASSWORD})
- assert resp.status == 200
- assert await resp.json() == {"user_id": user.id}
+ assert resp.status == 401
resp = await client.get("/")
assert resp.status == 401
@@ -118,15 +116,13 @@ async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, h
assert resp.status == 401
-async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth):
+async def test_basic_auth_does_not_work(app, aiohttp_client, hass, legacy_auth):
"""Test access with basic authentication."""
setup_auth(hass, app)
client = await aiohttp_client(app)
- user = await get_legacy_user(hass.auth)
req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD))
- assert req.status == 200
- assert await req.json() == {"user_id": user.id}
+ assert req.status == 401
req = await client.get("/", auth=BasicAuth("wrong_username", API_PASSWORD))
assert req.status == 401
@@ -138,7 +134,7 @@ async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth):
assert req.status == 401
-async def test_access_with_trusted_ip(
+async def test_cannot_access_with_trusted_ip(
hass, app2, trusted_networks_auth, aiohttp_client, hass_owner_user
):
"""Test access with an untrusted ip address."""
@@ -155,8 +151,7 @@ async def test_access_with_trusted_ip(
for remote_addr in TRUSTED_ADDRESSES:
set_mock_ip(remote_addr)
resp = await client.get("/")
- assert resp.status == 200, "{} should be trusted".format(remote_addr)
- assert await resp.json() == {"user_id": hass_owner_user.id}
+ assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr)
async def test_auth_active_access_with_access_token_in_header(
@@ -209,29 +204,24 @@ async def test_auth_active_access_with_trusted_ip(
for remote_addr in TRUSTED_ADDRESSES:
set_mock_ip(remote_addr)
resp = await client.get("/")
- assert resp.status == 200, "{} should be trusted".format(remote_addr)
- assert await resp.json() == {"user_id": hass_owner_user.id}
+ assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr)
-async def test_auth_legacy_support_api_password_access(
+async def test_auth_legacy_support_api_password_cannot_access(
app, aiohttp_client, legacy_auth, hass
):
"""Test access using api_password if auth.support_legacy."""
setup_auth(hass, app)
client = await aiohttp_client(app)
- user = await get_legacy_user(hass.auth)
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
- assert req.status == 200
- assert await req.json() == {"user_id": user.id}
+ assert req.status == 401
resp = await client.get("/", params={"api_password": API_PASSWORD})
- assert resp.status == 200
- assert await resp.json() == {"user_id": user.id}
+ assert resp.status == 401
req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD))
- assert req.status == 200
- assert await req.json() == {"user_id": user.id}
+ assert req.status == 401
async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_token):
diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py
index fc31de4b950..f50afcef8a8 100644
--- a/tests/components/http/test_ban.py
+++ b/tests/components/http/test_ban.py
@@ -148,6 +148,8 @@ async def test_failed_login_attempts_counter(hass, aiohttp_client):
assert resp.status == 200
assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2
+ # This used to check that with trusted networks we reset login attempts
+ # We no longer support trusted networks.
resp = await client.get("/auth_true")
assert resp.status == 200
- assert remote_ip not in app[KEY_FAILED_LOGIN_ATTEMPTS]
+ assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2
diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py
index 99b9e0b6e9a..1cea900d971 100644
--- a/tests/components/http/test_cors.py
+++ b/tests/components/http/test_cors.py
@@ -13,11 +13,12 @@ from aiohttp.hdrs import (
)
import pytest
-from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.setup import async_setup_component
from homeassistant.components.http.cors import setup_cors
from homeassistant.components.http.view import HomeAssistantView
+from . import HTTP_HEADER_HA_AUTH
+
TRUSTED_ORIGIN = "https://home-assistant.io"
@@ -91,13 +92,13 @@ async def test_cors_preflight_allowed(client):
headers={
ORIGIN: TRUSTED_ORIGIN,
ACCESS_CONTROL_REQUEST_METHOD: "GET",
- ACCESS_CONTROL_REQUEST_HEADERS: "x-ha-access",
+ ACCESS_CONTROL_REQUEST_HEADERS: "x-requested-with",
},
)
assert req.status == 200
assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN
- assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == HTTP_HEADER_HA_AUTH.upper()
+ assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == "X-REQUESTED-WITH"
async def test_cors_middleware_with_cors_allowed_view(hass):
diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py
index d8e613df6df..ad8e3ac10fd 100644
--- a/tests/components/http/test_init.py
+++ b/tests/components/http/test_init.py
@@ -133,7 +133,7 @@ async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth):
resp = await client.get("/api/", params={"api_password": "test-password"})
- assert resp.status == 200
+ assert resp.status == 401
logs = caplog.text
# Ensure we don't log API passwords
diff --git a/tests/components/hydroquebec/__init__.py b/tests/components/hydroquebec/__init__.py
deleted file mode 100644
index 1342395d265..00000000000
--- a/tests/components/hydroquebec/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the hydroquebec component."""
diff --git a/tests/components/hydroquebec/test_sensor.py b/tests/components/hydroquebec/test_sensor.py
deleted file mode 100644
index 9b2dd5ab5b5..00000000000
--- a/tests/components/hydroquebec/test_sensor.py
+++ /dev/null
@@ -1,102 +0,0 @@
-"""The test for the hydroquebec sensor platform."""
-import asyncio
-import logging
-import sys
-from unittest.mock import MagicMock
-
-from homeassistant.bootstrap import async_setup_component
-from homeassistant.components.hydroquebec import sensor as hydroquebec
-from tests.common import assert_setup_component
-
-
-CONTRACT = "123456789"
-
-
-class HydroQuebecClientMock:
- """Fake Hydroquebec client."""
-
- def __init__(self, username, password, contract=None, httpsession=None):
- """Fake Hydroquebec client init."""
- pass
-
- def get_data(self, contract):
- """Return fake hydroquebec data."""
- return {CONTRACT: {"balance": 160.12}}
-
- def get_contracts(self):
- """Return fake hydroquebec contracts."""
- return [CONTRACT]
-
- @asyncio.coroutine
- def fetch_data(self):
- """Return fake fetching data."""
- pass
-
-
-class HydroQuebecClientMockError(HydroQuebecClientMock):
- """Fake Hydroquebec client error."""
-
- def get_contracts(self):
- """Return fake hydroquebec contracts."""
- return []
-
- @asyncio.coroutine
- def fetch_data(self):
- """Return fake fetching data."""
- raise PyHydroQuebecErrorMock("Fake Error")
-
-
-class PyHydroQuebecErrorMock(BaseException):
- """Fake PyHydroquebec Error."""
-
-
-class PyHydroQuebecClientFakeModule:
- """Fake pyfido.client module."""
-
- PyHydroQuebecError = PyHydroQuebecErrorMock
-
-
-class PyHydroQuebecFakeModule:
- """Fake pyfido module."""
-
- HydroQuebecClient = HydroQuebecClientMockError
-
-
-@asyncio.coroutine
-def test_hydroquebec_sensor(loop, hass):
- """Test the Hydroquebec number sensor."""
- sys.modules["pyhydroquebec"] = MagicMock()
- sys.modules["pyhydroquebec.client"] = MagicMock()
- sys.modules["pyhydroquebec.client.PyHydroQuebecError"] = PyHydroQuebecErrorMock
- import pyhydroquebec.client
-
- pyhydroquebec.HydroQuebecClient = HydroQuebecClientMock
- pyhydroquebec.client.PyHydroQuebecError = PyHydroQuebecErrorMock
- config = {
- "sensor": {
- "platform": "hydroquebec",
- "name": "hydro",
- "contract": CONTRACT,
- "username": "myusername",
- "password": "password",
- "monitored_variables": ["balance"],
- }
- }
- with assert_setup_component(1):
- yield from async_setup_component(hass, "sensor", config)
- state = hass.states.get("sensor.hydro_balance")
- assert state.state == "160.12"
- assert state.attributes.get("unit_of_measurement") == "CAD"
-
-
-@asyncio.coroutine
-def test_error(hass, caplog):
- """Test the Hydroquebec sensor errors."""
- caplog.set_level(logging.ERROR)
- sys.modules["pyhydroquebec"] = PyHydroQuebecFakeModule()
- sys.modules["pyhydroquebec.client"] = PyHydroQuebecClientFakeModule()
-
- config = {}
- fake_async_add_entities = MagicMock()
- yield from hydroquebec.async_setup_platform(hass, config, fake_async_add_entities)
- assert fake_async_add_entities.called is False
diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py
new file mode 100644
index 00000000000..71f0658923c
--- /dev/null
+++ b/tests/components/input_datetime/test_reproduce_state.py
@@ -0,0 +1,69 @@
+"""Test reproduce state for Input datetime."""
+from homeassistant.core import State
+
+from tests.common import async_mock_service
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Input datetime states."""
+ hass.states.async_set(
+ "input_datetime.entity_datetime",
+ "2010-10-10 01:20:00",
+ {"has_date": True, "has_time": True},
+ )
+ hass.states.async_set(
+ "input_datetime.entity_time", "01:20:00", {"has_date": False, "has_time": True}
+ )
+ hass.states.async_set(
+ "input_datetime.entity_date",
+ "2010-10-10",
+ {"has_date": True, "has_time": False},
+ )
+
+ datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime")
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("input_datetime.entity_datetime", "2010-10-10 01:20:00"),
+ State("input_datetime.entity_time", "01:20:00"),
+ State("input_datetime.entity_date", "2010-10-10"),
+ ],
+ blocking=True,
+ )
+
+ assert len(datetime_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("input_datetime.entity_datetime", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(datetime_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("input_datetime.entity_datetime", "2011-10-10 02:20:00"),
+ State("input_datetime.entity_time", "02:20:00"),
+ State("input_datetime.entity_date", "2011-10-10"),
+ # Should not raise
+ State("input_datetime.non_existing", "2010-10-10 01:20:00"),
+ ],
+ blocking=True,
+ )
+
+ valid_calls = [
+ {
+ "entity_id": "input_datetime.entity_datetime",
+ "datetime": "2011-10-10 02:20:00",
+ },
+ {"entity_id": "input_datetime.entity_time", "time": "02:20:00"},
+ {"entity_id": "input_datetime.entity_date", "date": "2011-10-10"},
+ ]
+ assert len(datetime_calls) == 3
+ for call in datetime_calls:
+ assert call.domain == "input_datetime"
+ assert call.data in valid_calls
+ valid_calls.remove(call.data)
diff --git a/tests/components/input_number/test_reproduce_state.py b/tests/components/input_number/test_reproduce_state.py
new file mode 100644
index 00000000000..37ab83f3204
--- /dev/null
+++ b/tests/components/input_number/test_reproduce_state.py
@@ -0,0 +1,62 @@
+"""Test reproduce state for Input number."""
+from homeassistant.core import State
+from homeassistant.setup import async_setup_component
+
+VALID_NUMBER1 = "19.0"
+VALID_NUMBER2 = "99.9"
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Input number states."""
+
+ assert await async_setup_component(
+ hass,
+ "input_number",
+ {
+ "input_number": {
+ "test_number": {"min": "5", "max": "100", "initial": VALID_NUMBER1}
+ }
+ },
+ )
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("input_number.test_number", VALID_NUMBER1),
+ # Should not raise
+ State("input_number.non_existing", "234"),
+ ],
+ blocking=True,
+ )
+
+ assert hass.states.get("input_number.test_number").state == VALID_NUMBER1
+
+ # Test reproducing with different state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("input_number.test_number", VALID_NUMBER2),
+ # Should not raise
+ State("input_number.non_existing", "234"),
+ ],
+ blocking=True,
+ )
+
+ assert hass.states.get("input_number.test_number").state == VALID_NUMBER2
+
+ # Test setting state to number out of range
+ await hass.helpers.state.async_reproduce_state(
+ [State("input_number.test_number", "150")], blocking=True
+ )
+
+ # The entity states should be unchanged after trying to set them to out-of-range number
+ assert hass.states.get("input_number.test_number").state == VALID_NUMBER2
+
+ await hass.helpers.state.async_reproduce_state(
+ [
+ # Test invalid state
+ State("input_number.test_number", "invalid_state"),
+ # Set to state it already is.
+ State("input_number.test_number", VALID_NUMBER2),
+ ],
+ blocking=True,
+ )
diff --git a/tests/components/input_select/test_reproduce_state.py b/tests/components/input_select/test_reproduce_state.py
new file mode 100644
index 00000000000..469c258cb4b
--- /dev/null
+++ b/tests/components/input_select/test_reproduce_state.py
@@ -0,0 +1,72 @@
+"""Test reproduce state for Input select."""
+from homeassistant.core import State
+from homeassistant.setup import async_setup_component
+
+VALID_OPTION1 = "Option A"
+VALID_OPTION2 = "Option B"
+VALID_OPTION3 = "Option C"
+VALID_OPTION4 = "Option D"
+VALID_OPTION5 = "Option E"
+VALID_OPTION6 = "Option F"
+INVALID_OPTION = "Option X"
+VALID_OPTION_SET1 = [VALID_OPTION1, VALID_OPTION2, VALID_OPTION3]
+VALID_OPTION_SET2 = [VALID_OPTION4, VALID_OPTION5, VALID_OPTION6]
+ENTITY = "input_select.test_select"
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Input select states."""
+
+ # Setup entity
+ assert await async_setup_component(
+ hass,
+ "input_select",
+ {
+ "input_select": {
+ "test_select": {"options": VALID_OPTION_SET1, "initial": VALID_OPTION1}
+ }
+ },
+ )
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State(ENTITY, VALID_OPTION1),
+ # Should not raise
+ State("input_select.non_existing", VALID_OPTION1),
+ ],
+ blocking=True,
+ )
+
+ # Test that entity is in desired state
+ assert hass.states.get(ENTITY).state == VALID_OPTION1
+
+ # Try reproducing with different state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State(ENTITY, VALID_OPTION3),
+ # Should not raise
+ State("input_select.non_existing", VALID_OPTION3),
+ ],
+ blocking=True,
+ )
+
+ # Test that we got the desired result
+ assert hass.states.get(ENTITY).state == VALID_OPTION3
+
+ # Test setting state to invalid state
+ await hass.helpers.state.async_reproduce_state(
+ [State(ENTITY, INVALID_OPTION)], blocking=True
+ )
+
+ # The entity state should be unchanged
+ assert hass.states.get(ENTITY).state == VALID_OPTION3
+
+ # Test setting a different option set
+ await hass.helpers.state.async_reproduce_state(
+ [State(ENTITY, VALID_OPTION5, {"options": VALID_OPTION_SET2})], blocking=True
+ )
+
+ # These should fail if options weren't changed to VALID_OPTION_SET2
+ assert hass.states.get(ENTITY).attributes == {"options": VALID_OPTION_SET2}
+ assert hass.states.get(ENTITY).state == VALID_OPTION5
diff --git a/tests/components/input_text/test_reproduce_state.py b/tests/components/input_text/test_reproduce_state.py
new file mode 100644
index 00000000000..fd75948d461
--- /dev/null
+++ b/tests/components/input_text/test_reproduce_state.py
@@ -0,0 +1,65 @@
+"""Test reproduce state for Input text."""
+from homeassistant.core import State
+from homeassistant.setup import async_setup_component
+
+VALID_TEXT1 = "Test text"
+VALID_TEXT2 = "LoremIpsum"
+INVALID_TEXT1 = "This text is too long!"
+INVALID_TEXT2 = "Short"
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Input text states."""
+
+ # Setup entity for testing
+ assert await async_setup_component(
+ hass,
+ "input_text",
+ {
+ "input_text": {
+ "test_text": {"min": "6", "max": "10", "initial": VALID_TEXT1}
+ }
+ },
+ )
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("input_text.test_text", VALID_TEXT1),
+ # Should not raise
+ State("input_text.non_existing", VALID_TEXT1),
+ ],
+ blocking=True,
+ )
+
+ # Test that entity is in desired state
+ assert hass.states.get("input_text.test_text").state == VALID_TEXT1
+
+ # Try reproducing with different state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("input_text.test_text", VALID_TEXT2),
+ # Should not raise
+ State("input_text.non_existing", VALID_TEXT2),
+ ],
+ blocking=True,
+ )
+
+ # Test that the state was changed
+ assert hass.states.get("input_text.test_text").state == VALID_TEXT2
+
+ # Test setting state to invalid state (length too long)
+ await hass.helpers.state.async_reproduce_state(
+ [State("input_text.test_text", INVALID_TEXT1)], blocking=True
+ )
+
+ # The entity state should be unchanged
+ assert hass.states.get("input_text.test_text").state == VALID_TEXT2
+
+ # Test setting state to invalid state (length too short)
+ await hass.helpers.state.async_reproduce_state(
+ [State("input_text.test_text", INVALID_TEXT2)], blocking=True
+ )
+
+ # The entity state should be unchanged
+ assert hass.states.get("input_text.test_text").state == VALID_TEXT2
diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py
index 48d4178e992..c65ca720235 100644
--- a/tests/components/integration/test_sensor.py
+++ b/tests/components/integration/test_sensor.py
@@ -200,4 +200,4 @@ async def test_suffix(hass):
assert state is not None
# Testing a network speed sensor at 1000 bytes/s over 10s = 10kbytes
- assert round(float(state.state), config["sensor"]["round"]) == 10.0
+ assert round(float(state.state)) == 10
diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py
index 8d72830b369..07e0b7cb192 100644
--- a/tests/components/jewish_calendar/test_sensor.py
+++ b/tests/components/jewish_calendar/test_sensor.py
@@ -1,5 +1,5 @@
"""The tests for the Jewish calendar sensors."""
-from datetime import time, timedelta
+from datetime import timedelta
from datetime import datetime as dt
import pytest
@@ -42,27 +42,17 @@ TEST_PARAMS = [
False,
'כ"ג אלול ה\' תשע"ח',
),
- (
- dt(2018, 9, 10),
- "UTC",
- 31.778,
- 35.235,
- "hebrew",
- "holiday_name",
- False,
- "א' ראש השנה",
- ),
+ (dt(2018, 9, 10), "UTC", 31.778, 35.235, "hebrew", "holiday", False, "א' ראש השנה"),
(
dt(2018, 9, 10),
"UTC",
31.778,
35.235,
"english",
- "holiday_name",
+ "holiday",
False,
"Rosh Hashana I",
),
- (dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", "holiday_type", False, 1),
(
dt(2018, 9, 8),
"UTC",
@@ -81,7 +71,7 @@ TEST_PARAMS = [
"hebrew",
"t_set_hakochavim",
True,
- time(19, 48),
+ dt(2018, 9, 8, 19, 48),
),
(
dt(2018, 9, 8),
@@ -91,7 +81,7 @@ TEST_PARAMS = [
"hebrew",
"t_set_hakochavim",
False,
- time(19, 21),
+ dt(2018, 9, 8, 19, 21),
),
(
dt(2018, 10, 14),
@@ -128,9 +118,8 @@ TEST_PARAMS = [
TEST_IDS = [
"date_output",
"date_output_hebrew",
- "holiday_name",
- "holiday_name_english",
- "holiday_type",
+ "holiday",
+ "holiday_english",
"torah_reading",
"first_stars_ny",
"first_stars_jerusalem",
@@ -183,7 +172,16 @@ async def test_jewish_calendar_sensor(
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert hass.states.get(f"sensor.test_{sensor}").state == str(result)
+ result = (
+ dt_util.as_utc(time_zone.localize(result)) if isinstance(result, dt) else result
+ )
+
+ sensor_object = hass.states.get(f"sensor.test_{sensor}")
+ assert sensor_object.state == str(result)
+
+ if sensor == "holiday":
+ assert sensor_object.attributes.get("type") == "YOM_TOV"
+ assert sensor_object.attributes.get("id") == "rosh_hashana_i"
SHABBAT_PARAMS = [
@@ -252,8 +250,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50),
"english_parshat_hashavua": "Vayeilech",
"hebrew_parshat_hashavua": "וילך",
- "english_holiday_name": "Erev Rosh Hashana",
- "hebrew_holiday_name": "ערב ראש השנה",
+ "english_holiday": "Erev Rosh Hashana",
+ "hebrew_holiday": "ערב ראש השנה",
},
),
make_nyc_test_params(
@@ -265,8 +263,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50),
"english_parshat_hashavua": "Vayeilech",
"hebrew_parshat_hashavua": "וילך",
- "english_holiday_name": "Rosh Hashana I",
- "hebrew_holiday_name": "א' ראש השנה",
+ "english_holiday": "Rosh Hashana I",
+ "hebrew_holiday": "א' ראש השנה",
},
),
make_nyc_test_params(
@@ -278,8 +276,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50),
"english_parshat_hashavua": "Vayeilech",
"hebrew_parshat_hashavua": "וילך",
- "english_holiday_name": "Rosh Hashana II",
- "hebrew_holiday_name": "ב' ראש השנה",
+ "english_holiday": "Rosh Hashana II",
+ "hebrew_holiday": "ב' ראש השנה",
},
),
make_nyc_test_params(
@@ -302,8 +300,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13),
"english_parshat_hashavua": "Bereshit",
"hebrew_parshat_hashavua": "בראשית",
- "english_holiday_name": "Hoshana Raba",
- "hebrew_holiday_name": "הושענא רבה",
+ "english_holiday": "Hoshana Raba",
+ "hebrew_holiday": "הושענא רבה",
},
),
make_nyc_test_params(
@@ -315,8 +313,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13),
"english_parshat_hashavua": "Bereshit",
"hebrew_parshat_hashavua": "בראשית",
- "english_holiday_name": "Shmini Atzeret",
- "hebrew_holiday_name": "שמיני עצרת",
+ "english_holiday": "Shmini Atzeret",
+ "hebrew_holiday": "שמיני עצרת",
},
),
make_nyc_test_params(
@@ -328,8 +326,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13),
"english_parshat_hashavua": "Bereshit",
"hebrew_parshat_hashavua": "בראשית",
- "english_holiday_name": "Simchat Torah",
- "hebrew_holiday_name": "שמחת תורה",
+ "english_holiday": "Simchat Torah",
+ "hebrew_holiday": "שמחת תורה",
},
),
make_jerusalem_test_params(
@@ -341,8 +339,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56),
"english_parshat_hashavua": "Bereshit",
"hebrew_parshat_hashavua": "בראשית",
- "english_holiday_name": "Hoshana Raba",
- "hebrew_holiday_name": "הושענא רבה",
+ "english_holiday": "Hoshana Raba",
+ "hebrew_holiday": "הושענא רבה",
},
),
make_jerusalem_test_params(
@@ -354,8 +352,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56),
"english_parshat_hashavua": "Bereshit",
"hebrew_parshat_hashavua": "בראשית",
- "english_holiday_name": "Shmini Atzeret",
- "hebrew_holiday_name": "שמיני עצרת",
+ "english_holiday": "Shmini Atzeret",
+ "hebrew_holiday": "שמיני עצרת",
},
),
make_jerusalem_test_params(
@@ -378,8 +376,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": "unknown",
"english_parshat_hashavua": "Bamidbar",
"hebrew_parshat_hashavua": "במדבר",
- "english_holiday_name": "Erev Shavuot",
- "hebrew_holiday_name": "ערב שבועות",
+ "english_holiday": "Erev Shavuot",
+ "hebrew_holiday": "ערב שבועות",
},
),
make_nyc_test_params(
@@ -391,8 +389,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 19),
"english_parshat_hashavua": "Nasso",
"hebrew_parshat_hashavua": "נשא",
- "english_holiday_name": "Shavuot",
- "hebrew_holiday_name": "שבועות",
+ "english_holiday": "Shavuot",
+ "hebrew_holiday": "שבועות",
},
),
make_jerusalem_test_params(
@@ -404,8 +402,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13),
"english_parshat_hashavua": "Ha'Azinu",
"hebrew_parshat_hashavua": "האזינו",
- "english_holiday_name": "Rosh Hashana I",
- "hebrew_holiday_name": "א' ראש השנה",
+ "english_holiday": "Rosh Hashana I",
+ "hebrew_holiday": "א' ראש השנה",
},
),
make_jerusalem_test_params(
@@ -417,8 +415,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13),
"english_parshat_hashavua": "Ha'Azinu",
"hebrew_parshat_hashavua": "האזינו",
- "english_holiday_name": "Rosh Hashana II",
- "hebrew_holiday_name": "ב' ראש השנה",
+ "english_holiday": "Rosh Hashana II",
+ "hebrew_holiday": "ב' ראש השנה",
},
),
make_jerusalem_test_params(
@@ -430,8 +428,8 @@ SHABBAT_PARAMS = [
"english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13),
"english_parshat_hashavua": "Ha'Azinu",
"hebrew_parshat_hashavua": "האזינו",
- "english_holiday_name": "",
- "hebrew_holiday_name": "",
+ "english_holiday": "",
+ "hebrew_holiday": "",
},
),
]
@@ -524,6 +522,12 @@ async def test_shabbat_times_sensor(
sensor_type = sensor_type.replace(f"{language}_", "")
+ result_value = (
+ dt_util.as_utc(result_value)
+ if isinstance(result_value, dt)
+ else result_value
+ )
+
assert hass.states.get(f"sensor.test_{sensor_type}").state == str(
result_value
), f"Value for {sensor_type}"
diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py
index 8009fbd6337..a9f4adddfab 100644
--- a/tests/components/light/test_device_condition.py
+++ b/tests/components/light/test_device_condition.py
@@ -1,11 +1,14 @@
"""The test for light device automation."""
+from datetime import timedelta
import pytest
+from unittest.mock import patch
from homeassistant.components.light import DOMAIN
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation
from homeassistant.helpers import device_registry
+import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
@@ -13,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
async_get_device_automations,
+ async_get_device_automation_capabilities,
)
@@ -63,6 +67,28 @@ async def test_get_conditions(hass, device_reg, entity_reg):
assert conditions == expected_conditions
+async def test_get_condition_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a light condition."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_capabilities = {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ for condition in conditions:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "condition", condition
+ )
+ assert capabilities == expected_capabilities
+
+
async def test_if_state(hass, calls):
"""Test for turn_on and turn_off conditions."""
platform = getattr(hass.components, f"test.{DOMAIN}")
@@ -134,3 +160,73 @@ async def test_if_state(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 2
assert calls[1].data["some"] == "is_off event - test_event2"
+
+
+async def test_if_fires_on_for_condition(hass, calls):
+ """Test for firing if condition is on with delay."""
+ point1 = dt_util.utcnow()
+ point2 = point1 + timedelta(seconds=10)
+ point3 = point2 + timedelta(seconds=10)
+
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ ent1, ent2, ent3 = platform.ENTITIES
+
+ with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow:
+ mock_utcnow.return_value = point1
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "is_off",
+ "for": {"seconds": 5},
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ ("platform", "event.event_type")
+ )
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ # Time travel 10 secs into the future
+ mock_utcnow.return_value = point2
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(ent1.entity_id, STATE_OFF)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ # Time travel 20 secs into the future
+ mock_utcnow.return_value = point3
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "is_off event - test_event1"
diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py
new file mode 100644
index 00000000000..92790890a4c
--- /dev/null
+++ b/tests/components/light/test_reproduce_state.py
@@ -0,0 +1,117 @@
+"""Test reproduce state for Light."""
+from homeassistant.core import State
+
+from tests.common import async_mock_service
+
+VALID_BRIGHTNESS = {"brightness": 180}
+VALID_WHITE_VALUE = {"white_value": 200}
+VALID_EFFECT = {"effect": "random"}
+VALID_COLOR_TEMP = {"color_temp": 240}
+VALID_HS_COLOR = {"hs_color": (345, 75)}
+VALID_RGB_COLOR = {"rgb_color": (255, 63, 111)}
+VALID_XY_COLOR = {"xy_color": (0.59, 0.274)}
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Light states."""
+ hass.states.async_set("light.entity_off", "off", {})
+ hass.states.async_set("light.entity_bright", "on", VALID_BRIGHTNESS)
+ hass.states.async_set("light.entity_white", "on", VALID_WHITE_VALUE)
+ hass.states.async_set("light.entity_effect", "on", VALID_EFFECT)
+ hass.states.async_set("light.entity_temp", "on", VALID_COLOR_TEMP)
+ hass.states.async_set("light.entity_hs", "on", VALID_HS_COLOR)
+ hass.states.async_set("light.entity_rgb", "on", VALID_RGB_COLOR)
+ hass.states.async_set("light.entity_xy", "on", VALID_XY_COLOR)
+
+ turn_on_calls = async_mock_service(hass, "light", "turn_on")
+ turn_off_calls = async_mock_service(hass, "light", "turn_off")
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("light.entity_off", "off"),
+ State("light.entity_bright", "on", VALID_BRIGHTNESS),
+ State("light.entity_white", "on", VALID_WHITE_VALUE),
+ State("light.entity_effect", "on", VALID_EFFECT),
+ State("light.entity_temp", "on", VALID_COLOR_TEMP),
+ State("light.entity_hs", "on", VALID_HS_COLOR),
+ State("light.entity_rgb", "on", VALID_RGB_COLOR),
+ State("light.entity_xy", "on", VALID_XY_COLOR),
+ ],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("light.entity_off", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("light.entity_xy", "off"),
+ State("light.entity_off", "on", VALID_BRIGHTNESS),
+ State("light.entity_bright", "on", VALID_WHITE_VALUE),
+ State("light.entity_white", "on", VALID_EFFECT),
+ State("light.entity_effect", "on", VALID_COLOR_TEMP),
+ State("light.entity_temp", "on", VALID_HS_COLOR),
+ State("light.entity_hs", "on", VALID_RGB_COLOR),
+ State("light.entity_rgb", "on", VALID_XY_COLOR),
+ ],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 7
+
+ expected_calls = []
+
+ expected_off = VALID_BRIGHTNESS
+ expected_off["entity_id"] = "light.entity_off"
+ expected_calls.append(expected_off)
+
+ expected_bright = VALID_WHITE_VALUE
+ expected_bright["entity_id"] = "light.entity_bright"
+ expected_calls.append(expected_bright)
+
+ expected_white = VALID_EFFECT
+ expected_white["entity_id"] = "light.entity_white"
+ expected_calls.append(expected_white)
+
+ expected_effect = VALID_COLOR_TEMP
+ expected_effect["entity_id"] = "light.entity_effect"
+ expected_calls.append(expected_effect)
+
+ expected_temp = VALID_HS_COLOR
+ expected_temp["entity_id"] = "light.entity_temp"
+ expected_calls.append(expected_temp)
+
+ expected_hs = VALID_RGB_COLOR
+ expected_hs["entity_id"] = "light.entity_hs"
+ expected_calls.append(expected_hs)
+
+ expected_rgb = VALID_XY_COLOR
+ expected_rgb["entity_id"] = "light.entity_rgb"
+ expected_calls.append(expected_rgb)
+
+ for call in turn_on_calls:
+ assert call.domain == "light"
+ found = False
+ for expected in expected_calls:
+ if call.data["entity_id"] == expected["entity_id"]:
+ # We found the matching entry
+ assert call.data == expected
+ found = True
+ break
+ # No entry found
+ assert found
+
+ assert len(turn_off_calls) == 1
+ assert turn_off_calls[0].domain == "light"
+ assert turn_off_calls[0].data == {"entity_id": "light.entity_xy"}
diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py
new file mode 100644
index 00000000000..2006f9b3ff1
--- /dev/null
+++ b/tests/components/lock/test_device_action.py
@@ -0,0 +1,170 @@
+"""The tests for Lock device actions."""
+import pytest
+
+from homeassistant.components.lock import DOMAIN
+from homeassistant.const import CONF_PLATFORM
+from homeassistant.setup import async_setup_component
+import homeassistant.components.automation as automation
+from homeassistant.helpers import device_registry
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+ async_get_device_automations,
+)
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+async def test_get_actions_support_open(hass, device_reg, entity_reg):
+ """Test we get the expected actions from a lock which supports open."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES["support_open"].unique_id,
+ device_id=device_entry.id,
+ )
+
+ expected_actions = [
+ {
+ "domain": DOMAIN,
+ "type": "lock",
+ "device_id": device_entry.id,
+ "entity_id": "lock.support_open_lock",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "unlock",
+ "device_id": device_entry.id,
+ "entity_id": "lock.support_open_lock",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "open",
+ "device_id": device_entry.id,
+ "entity_id": "lock.support_open_lock",
+ },
+ ]
+ actions = await async_get_device_automations(hass, "action", device_entry.id)
+ assert_lists_same(actions, expected_actions)
+
+
+async def test_get_actions_not_support_open(hass, device_reg, entity_reg):
+ """Test we get the expected actions from a lock which doesn't support open."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES["no_support_open"].unique_id,
+ device_id=device_entry.id,
+ )
+
+ expected_actions = [
+ {
+ "domain": DOMAIN,
+ "type": "lock",
+ "device_id": device_entry.id,
+ "entity_id": "lock.no_support_open_lock",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "unlock",
+ "device_id": device_entry.id,
+ "entity_id": "lock.no_support_open_lock",
+ },
+ ]
+ actions = await async_get_device_automations(hass, "action", device_entry.id)
+ assert_lists_same(actions, expected_actions)
+
+
+async def test_action(hass):
+ """Test for lock actions."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event_lock"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": "lock.entity",
+ "type": "lock",
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event_unlock"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": "lock.entity",
+ "type": "unlock",
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event_open"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "abcdefgh",
+ "entity_id": "lock.entity",
+ "type": "open",
+ },
+ },
+ ]
+ },
+ )
+
+ lock_calls = async_mock_service(hass, "lock", "lock")
+ unlock_calls = async_mock_service(hass, "lock", "unlock")
+ open_calls = async_mock_service(hass, "lock", "open")
+
+ hass.bus.async_fire("test_event_lock")
+ await hass.async_block_till_done()
+ assert len(lock_calls) == 1
+ assert len(unlock_calls) == 0
+ assert len(open_calls) == 0
+
+ hass.bus.async_fire("test_event_unlock")
+ await hass.async_block_till_done()
+ assert len(lock_calls) == 1
+ assert len(unlock_calls) == 1
+ assert len(open_calls) == 0
+
+ hass.bus.async_fire("test_event_open")
+ await hass.async_block_till_done()
+ assert len(lock_calls) == 1
+ assert len(unlock_calls) == 1
+ assert len(open_calls) == 1
diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py
new file mode 100644
index 00000000000..675f402e770
--- /dev/null
+++ b/tests/components/lock/test_device_condition.py
@@ -0,0 +1,126 @@
+"""The tests for Lock device conditions."""
+import pytest
+
+from homeassistant.components.lock import DOMAIN
+from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
+from homeassistant.setup import async_setup_component
+import homeassistant.components.automation as automation
+from homeassistant.helpers import device_registry
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+ async_get_device_automations,
+)
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock serivce."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_conditions(hass, device_reg, entity_reg):
+ """Test we get the expected conditions from a lock."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_locked",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_unlocked",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ ]
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ assert_lists_same(conditions, expected_conditions)
+
+
+async def test_if_state(hass, calls):
+ """Test for turn_on and turn_off conditions."""
+ hass.states.async_set("lock.entity", STATE_LOCKED)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "lock.entity",
+ "type": "is_locked",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_locked - {{ trigger.platform }} - {{ trigger.event.event_type }}"
+ },
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event2"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "lock.entity",
+ "type": "is_unlocked",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_unlocked - {{ trigger.platform }} - {{ trigger.event.event_type }}"
+ },
+ },
+ },
+ ]
+ },
+ )
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "is_locked - event - test_event1"
+
+ hass.states.async_set("lock.entity", STATE_UNLOCKED)
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "is_unlocked - event - test_event2"
diff --git a/tests/components/lock/test_reproduce_state.py b/tests/components/lock/test_reproduce_state.py
new file mode 100644
index 00000000000..a9b61fa1219
--- /dev/null
+++ b/tests/components/lock/test_reproduce_state.py
@@ -0,0 +1,53 @@
+"""Test reproduce state for Lock."""
+from homeassistant.core import State
+
+from tests.common import async_mock_service
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Lock states."""
+ hass.states.async_set("lock.entity_locked", "locked", {})
+ hass.states.async_set("lock.entity_unlocked", "unlocked", {})
+
+ lock_calls = async_mock_service(hass, "lock", "lock")
+ unlock_calls = async_mock_service(hass, "lock", "unlock")
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("lock.entity_locked", "locked"),
+ State("lock.entity_unlocked", "unlocked", {}),
+ ],
+ blocking=True,
+ )
+
+ assert len(lock_calls) == 0
+ assert len(unlock_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("lock.entity_locked", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(lock_calls) == 0
+ assert len(unlock_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("lock.entity_locked", "unlocked"),
+ State("lock.entity_unlocked", "locked"),
+ # Should not raise
+ State("lock.non_existing", "on"),
+ ],
+ blocking=True,
+ )
+
+ assert len(lock_calls) == 1
+ assert lock_calls[0].domain == "lock"
+ assert lock_calls[0].data == {"entity_id": "lock.entity_unlocked"}
+
+ assert len(unlock_calls) == 1
+ assert unlock_calls[0].domain == "lock"
+ assert unlock_calls[0].data == {"entity_id": "lock.entity_locked"}
diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py
index dfdaf80981f..892f4d60a44 100644
--- a/tests/components/melissa/test_init.py
+++ b/tests/components/melissa/test_init.py
@@ -1,14 +1,15 @@
"""The test for the Melissa Climate component."""
-from tests.common import MockDependency, mock_coro_func
-
from homeassistant.components import melissa
+from tests.common import MockDependency, mock_coro_func
+
VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}}
async def test_setup(hass):
"""Test setting up the Melissa component."""
with MockDependency("melissa") as mocked_melissa:
+ melissa.melissa = mocked_melissa
mocked_melissa.AsyncMelissa().async_connect = mock_coro_func()
await melissa.async_setup(hass, VALID_CONFIG)
diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py
index 12a43030aa8..bb734d2c03d 100644
--- a/tests/components/mqtt/test_cover.py
+++ b/tests/components/mqtt/test_cover.py
@@ -546,6 +546,27 @@ async def test_no_command_topic(hass, mqtt_mock):
assert hass.states.get("cover.test").attributes["supported_features"] == 240
+async def test_no_payload_stop(hass, mqtt_mock):
+ """Test with no stop payload."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "qos": 0,
+ "payload_open": "OPEN",
+ "payload_close": "CLOSE",
+ "payload_stop": None,
+ }
+ },
+ )
+
+ assert hass.states.get("cover.test").attributes["supported_features"] == 3
+
+
async def test_with_command_topic_and_tilt(hass, mqtt_mock):
"""Test with command topic and tilt config."""
assert await async_setup_component(
diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py
index caad12b3e39..71348fcf5cb 100644
--- a/tests/components/mqtt/test_device_tracker.py
+++ b/tests/components/mqtt/test_device_tracker.py
@@ -3,8 +3,11 @@ from asynctest import patch
import pytest
from homeassistant.components import device_tracker
-from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT
-from homeassistant.const import CONF_PLATFORM
+from homeassistant.components.device_tracker.const import (
+ ENTITY_ID_FORMAT,
+ SOURCE_TYPE_BLUETOOTH,
+)
+from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME
from homeassistant.setup import async_setup_component
from tests.common import async_fire_mqtt_message
@@ -156,3 +159,91 @@ async def test_multi_level_wildcard_topic_not_matching(hass, mock_device_tracker
async_fire_mqtt_message(hass, topic, location)
await hass.async_block_till_done()
assert hass.states.get(entity_id) is None
+
+
+async def test_matching_custom_payload_for_home_and_not_home(
+ hass, mock_device_tracker_conf
+):
+ """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home."""
+ dev_id = "paulus"
+ entity_id = ENTITY_ID_FORMAT.format(dev_id)
+ topic = "/location/paulus"
+ payload_home = "present"
+ payload_not_home = "not present"
+
+ hass.config.components = set(["mqtt", "zone"])
+ assert await async_setup_component(
+ hass,
+ device_tracker.DOMAIN,
+ {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: "mqtt",
+ "devices": {dev_id: topic},
+ "payload_home": payload_home,
+ "payload_not_home": payload_not_home,
+ }
+ },
+ )
+ async_fire_mqtt_message(hass, topic, payload_home)
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_HOME
+
+ async_fire_mqtt_message(hass, topic, payload_not_home)
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_NOT_HOME
+
+
+async def test_not_matching_custom_payload_for_home_and_not_home(
+ hass, mock_device_tracker_conf
+):
+ """Test not matching payload does not set state to home or not_home."""
+ dev_id = "paulus"
+ entity_id = ENTITY_ID_FORMAT.format(dev_id)
+ topic = "/location/paulus"
+ payload_home = "present"
+ payload_not_home = "not present"
+ payload_not_matching = "test"
+
+ hass.config.components = set(["mqtt", "zone"])
+ assert await async_setup_component(
+ hass,
+ device_tracker.DOMAIN,
+ {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: "mqtt",
+ "devices": {dev_id: topic},
+ "payload_home": payload_home,
+ "payload_not_home": payload_not_home,
+ }
+ },
+ )
+ async_fire_mqtt_message(hass, topic, payload_not_matching)
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state != STATE_HOME
+ assert hass.states.get(entity_id).state != STATE_NOT_HOME
+
+
+async def test_matching_source_type(hass, mock_device_tracker_conf):
+ """Test setting source type."""
+ dev_id = "paulus"
+ entity_id = ENTITY_ID_FORMAT.format(dev_id)
+ topic = "/location/paulus"
+ source_type = SOURCE_TYPE_BLUETOOTH
+ location = "work"
+
+ hass.config.components = set(["mqtt", "zone"])
+ assert await async_setup_component(
+ hass,
+ device_tracker.DOMAIN,
+ {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: "mqtt",
+ "devices": {dev_id: topic},
+ "source_type": source_type,
+ }
+ },
+ )
+
+ async_fire_mqtt_message(hass, topic, location)
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).attributes["source_type"] == SOURCE_TYPE_BLUETOOTH
diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py
index add27bebdeb..c35740407c7 100644
--- a/tests/components/mqtt/test_legacy_vacuum.py
+++ b/tests/components/mqtt/test_legacy_vacuum.py
@@ -5,10 +5,8 @@ import json
from homeassistant.components import mqtt, vacuum
from homeassistant.components.mqtt import CONF_COMMAND_TOPIC
from homeassistant.components.mqtt.discovery import async_start
-from homeassistant.components.mqtt.vacuum import (
- schema_legacy as mqttvacuum,
- services_to_strings,
-)
+from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum
+from homeassistant.components.mqtt.vacuum.schema import services_to_strings
from homeassistant.components.mqtt.vacuum.schema_legacy import (
ALL_SERVICES,
SERVICE_TO_STRING,
@@ -80,7 +78,7 @@ async def test_default_supported_features(hass, mqtt_mock):
async def test_all_commands(hass, mqtt_mock):
"""Test simple commands to the vacuum."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
)
@@ -221,7 +219,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock):
async def test_status(hass, mqtt_mock):
"""Test status updates from the vacuum."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
)
@@ -260,7 +258,7 @@ async def test_status(hass, mqtt_mock):
async def test_status_battery(hass, mqtt_mock):
"""Test status updates from the vacuum."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
)
@@ -277,7 +275,7 @@ async def test_status_battery(hass, mqtt_mock):
async def test_status_cleaning(hass, mqtt_mock):
"""Test status updates from the vacuum."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
)
@@ -294,7 +292,7 @@ async def test_status_cleaning(hass, mqtt_mock):
async def test_status_docked(hass, mqtt_mock):
"""Test status updates from the vacuum."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
)
@@ -311,7 +309,7 @@ async def test_status_docked(hass, mqtt_mock):
async def test_status_charging(hass, mqtt_mock):
"""Test status updates from the vacuum."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
)
@@ -328,7 +326,7 @@ async def test_status_charging(hass, mqtt_mock):
async def test_status_fan_speed(hass, mqtt_mock):
"""Test status updates from the vacuum."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
)
@@ -345,7 +343,7 @@ async def test_status_fan_speed(hass, mqtt_mock):
async def test_status_error(hass, mqtt_mock):
"""Test status updates from the vacuum."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
)
@@ -371,7 +369,7 @@ async def test_battery_template(hass, mqtt_mock):
config = deepcopy(DEFAULT_CONFIG)
config.update(
{
- mqttvacuum.CONF_SUPPORTED_FEATURES: mqttvacuum.services_to_strings(
+ mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
),
mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level",
@@ -390,7 +388,7 @@ async def test_battery_template(hass, mqtt_mock):
async def test_status_invalid_json(hass, mqtt_mock):
"""Test to make sure nothing breaks if the vacuum sends bad JSON."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
ALL_SERVICES, SERVICE_TO_STRING
)
diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py
index c210d773faf..71dff7ef3ac 100644
--- a/tests/components/mqtt/test_server.py
+++ b/tests/components/mqtt/test_server.py
@@ -19,9 +19,13 @@ class TestMQTT:
"""Stop everything that was started."""
self.hass.stop()
- @patch("passlib.apps.custom_app_context", Mock(return_value=""))
+ @patch(
+ "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="")
+ )
@patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock()))
- @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock()))
+ @patch(
+ "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock())
+ )
@patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro()))
@patch("homeassistant.components.mqtt.MQTT")
def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt):
@@ -41,9 +45,13 @@ class TestMQTT:
assert mock_mqtt.mock_calls[1][2]["username"] == "homeassistant"
assert mock_mqtt.mock_calls[1][2]["password"] == password
- @patch("passlib.apps.custom_app_context", Mock(return_value=""))
+ @patch(
+ "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="")
+ )
@patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock()))
- @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock()))
+ @patch(
+ "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock())
+ )
@patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro()))
@patch("homeassistant.components.mqtt.MQTT")
def test_creating_config_with_pass_and_http_pass(self, mock_mqtt):
@@ -57,12 +65,7 @@ class TestMQTT:
self.hass.config.api = MagicMock(api_password="api_password")
assert setup_component(
- self.hass,
- mqtt.DOMAIN,
- {
- "http": {"api_password": "http_secret"},
- mqtt.DOMAIN: {CONF_PASSWORD: password},
- },
+ self.hass, mqtt.DOMAIN, {mqtt.DOMAIN: {CONF_PASSWORD: password}}
)
self.hass.block_till_done()
assert mock_mqtt.called
diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py
index fe100bdcb6e..572c3b05752 100644
--- a/tests/components/mqtt/test_state_vacuum.py
+++ b/tests/components/mqtt/test_state_vacuum.py
@@ -5,11 +5,8 @@ import json
from homeassistant.components import mqtt, vacuum
from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC
from homeassistant.components.mqtt.discovery import async_start
-from homeassistant.components.mqtt.vacuum import (
- CONF_SCHEMA,
- schema_state as mqttvacuum,
- services_to_strings,
-)
+from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum
+from homeassistant.components.mqtt.vacuum.schema import services_to_strings
from homeassistant.components.mqtt.vacuum.schema_state import SERVICE_TO_STRING
from homeassistant.components.vacuum import (
ATTR_BATTERY_ICON,
@@ -259,7 +256,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock):
async def test_status_invalid_json(hass, mqtt_mock):
"""Test to make sure nothing breaks if the vacuum sends bad JSON."""
config = deepcopy(DEFAULT_CONFIG)
- config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings(
+ config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings(
mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING
)
diff --git a/tests/components/neato/__init__.py b/tests/components/neato/__init__.py
new file mode 100644
index 00000000000..7927918395c
--- /dev/null
+++ b/tests/components/neato/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Neato component."""
diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py
new file mode 100644
index 00000000000..3f4bd90d0c1
--- /dev/null
+++ b/tests/components/neato/test_config_flow.py
@@ -0,0 +1,161 @@
+"""Tests for the Neato config flow."""
+import pytest
+from unittest.mock import patch
+
+from pybotvac.exceptions import NeatoLoginException, NeatoRobotException
+
+from homeassistant import data_entry_flow
+from homeassistant.components.neato import config_flow
+from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+
+from tests.common import MockConfigEntry
+
+USERNAME = "myUsername"
+PASSWORD = "myPassword"
+VENDOR_NEATO = "neato"
+VENDOR_VORWERK = "vorwerk"
+VENDOR_INVALID = "invalid"
+
+
+@pytest.fixture(name="account")
+def mock_controller_login():
+ """Mock a successful login."""
+ with patch("homeassistant.components.neato.config_flow.Account", return_value=True):
+ yield
+
+
+def init_config_flow(hass):
+ """Init a configuration flow."""
+ flow = config_flow.NeatoConfigFlow()
+ flow.hass = hass
+ return flow
+
+
+async def test_user(hass, account):
+ """Test user config."""
+ flow = init_config_flow(hass)
+
+ result = await flow.async_step_user()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ result = await flow.async_step_user(
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == USERNAME
+ assert result["data"][CONF_USERNAME] == USERNAME
+ assert result["data"][CONF_PASSWORD] == PASSWORD
+ assert result["data"][CONF_VENDOR] == VENDOR_NEATO
+
+ result = await flow.async_step_user(
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_VORWERK}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == USERNAME
+ assert result["data"][CONF_USERNAME] == USERNAME
+ assert result["data"][CONF_PASSWORD] == PASSWORD
+ assert result["data"][CONF_VENDOR] == VENDOR_VORWERK
+
+
+async def test_import(hass, account):
+ """Test import step."""
+ flow = init_config_flow(hass)
+
+ result = await flow.async_step_import(
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == f"{USERNAME} (from configuration)"
+ assert result["data"][CONF_USERNAME] == USERNAME
+ assert result["data"][CONF_PASSWORD] == PASSWORD
+ assert result["data"][CONF_VENDOR] == VENDOR_NEATO
+
+
+async def test_abort_if_already_setup(hass, account):
+ """Test we abort if Neato is already setup."""
+ flow = init_config_flow(hass)
+ MockConfigEntry(
+ domain=NEATO_DOMAIN,
+ data={
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_VENDOR: VENDOR_NEATO,
+ },
+ ).add_to_hass(hass)
+
+ # Should fail, same USERNAME (import)
+ result = await flow.async_step_import(
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+ # Should fail, same USERNAME (flow)
+ result = await flow.async_step_user(
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_abort_on_invalid_credentials(hass):
+ """Test when we have invalid credentials."""
+ flow = init_config_flow(hass)
+
+ with patch(
+ "homeassistant.components.neato.config_flow.Account",
+ side_effect=NeatoLoginException(),
+ ):
+ result = await flow.async_step_user(
+ {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_VENDOR: VENDOR_NEATO,
+ }
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "invalid_credentials"}
+
+ result = await flow.async_step_import(
+ {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_VENDOR: VENDOR_NEATO,
+ }
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "invalid_credentials"
+
+
+async def test_abort_on_unexpected_error(hass):
+ """Test when we have an unexpected error."""
+ flow = init_config_flow(hass)
+
+ with patch(
+ "homeassistant.components.neato.config_flow.Account",
+ side_effect=NeatoRobotException(),
+ ):
+ result = await flow.async_step_user(
+ {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_VENDOR: VENDOR_NEATO,
+ }
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "unexpected_error"}
+
+ result = await flow.async_step_import(
+ {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_VENDOR: VENDOR_NEATO,
+ }
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "unexpected_error"
diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py
new file mode 100644
index 00000000000..444cbe8cc5d
--- /dev/null
+++ b/tests/components/neato/test_init.py
@@ -0,0 +1,119 @@
+"""Tests for the Neato init file."""
+import pytest
+from unittest.mock import patch
+
+from pybotvac.exceptions import NeatoLoginException
+
+from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry
+
+USERNAME = "myUsername"
+PASSWORD = "myPassword"
+VENDOR_NEATO = "neato"
+VENDOR_VORWERK = "vorwerk"
+VENDOR_INVALID = "invalid"
+
+VALID_CONFIG = {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_VENDOR: VENDOR_NEATO,
+}
+
+DIFFERENT_CONFIG = {
+ CONF_USERNAME: "anotherUsername",
+ CONF_PASSWORD: "anotherPassword",
+ CONF_VENDOR: VENDOR_VORWERK,
+}
+
+INVALID_CONFIG = {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_VENDOR: VENDOR_INVALID,
+}
+
+
+@pytest.fixture(name="config_flow")
+def mock_config_flow_login():
+ """Mock a successful login."""
+ with patch("homeassistant.components.neato.config_flow.Account", return_value=True):
+ yield
+
+
+@pytest.fixture(name="hub")
+def mock_controller_login():
+ """Mock a successful login."""
+ with patch("homeassistant.components.neato.Account", return_value=True):
+ yield
+
+
+async def test_no_config_entry(hass):
+ """There is nothing in configuration.yaml."""
+ res = await async_setup_component(hass, NEATO_DOMAIN, {})
+ assert res is True
+
+
+async def test_create_valid_config_entry(hass, config_flow, hub):
+ """There is something in configuration.yaml."""
+ assert hass.config_entries.async_entries(NEATO_DOMAIN) == []
+ assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG})
+ await hass.async_block_till_done()
+
+ entries = hass.config_entries.async_entries(NEATO_DOMAIN)
+ assert entries
+ assert entries[0].data[CONF_USERNAME] == USERNAME
+ assert entries[0].data[CONF_PASSWORD] == PASSWORD
+ assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO
+
+
+async def test_config_entries_in_sync(hass, hub):
+ """The config entry and configuration.yaml are in sync."""
+ MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass)
+
+ assert hass.config_entries.async_entries(NEATO_DOMAIN)
+ assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG})
+ await hass.async_block_till_done()
+
+ entries = hass.config_entries.async_entries(NEATO_DOMAIN)
+ assert entries
+ assert entries[0].data[CONF_USERNAME] == USERNAME
+ assert entries[0].data[CONF_PASSWORD] == PASSWORD
+ assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO
+
+
+async def test_config_entries_not_in_sync(hass, config_flow, hub):
+ """The config entry and configuration.yaml are not in sync."""
+ MockConfigEntry(domain=NEATO_DOMAIN, data=DIFFERENT_CONFIG).add_to_hass(hass)
+
+ assert hass.config_entries.async_entries(NEATO_DOMAIN)
+ assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG})
+ await hass.async_block_till_done()
+
+ entries = hass.config_entries.async_entries(NEATO_DOMAIN)
+ assert entries
+ assert entries[0].data[CONF_USERNAME] == USERNAME
+ assert entries[0].data[CONF_PASSWORD] == PASSWORD
+ assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO
+
+
+async def test_config_entries_not_in_sync_error(hass):
+ """The config entry and configuration.yaml are not in sync, the new configuration is wrong."""
+ MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass)
+
+ assert hass.config_entries.async_entries(NEATO_DOMAIN)
+ with patch(
+ "homeassistant.components.neato.config_flow.Account",
+ side_effect=NeatoLoginException(),
+ ):
+ assert not await async_setup_component(
+ hass, NEATO_DOMAIN, {NEATO_DOMAIN: DIFFERENT_CONFIG}
+ )
+ await hass.async_block_till_done()
+
+ entries = hass.config_entries.async_entries(NEATO_DOMAIN)
+ assert entries
+ assert entries[0].data[CONF_USERNAME] == USERNAME
+ assert entries[0].data[CONF_PASSWORD] == PASSWORD
+ assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO
diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py
index a6be3ac14f5..90a209fd897 100644
--- a/tests/components/nuheat/test_init.py
+++ b/tests/components/nuheat/test_init.py
@@ -1,11 +1,11 @@
"""NuHeat component tests."""
import unittest
-
from unittest.mock import patch
-from tests.common import get_test_home_assistant, MockDependency
from homeassistant.components import nuheat
+from tests.common import MockDependency, get_test_home_assistant
+
VALID_CONFIG = {
"nuheat": {"username": "warm", "password": "feet", "devices": "thermostat123"}
}
@@ -27,11 +27,12 @@ class TestNuHeat(unittest.TestCase):
@patch("homeassistant.helpers.discovery.load_platform")
def test_setup(self, mocked_nuheat, mocked_load):
"""Test setting up the NuHeat component."""
- nuheat.setup(self.hass, self.config)
+ with patch.object(nuheat, "nuheat", mocked_nuheat):
+ nuheat.setup(self.hass, self.config)
mocked_nuheat.NuHeat.assert_called_with("warm", "feet")
assert nuheat.DOMAIN in self.hass.data
- assert 2 == len(self.hass.data[nuheat.DOMAIN])
+ assert len(self.hass.data[nuheat.DOMAIN]) == 2
assert isinstance(
self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat())
)
diff --git a/tests/components/opentherm_gw/__init__.py b/tests/components/opentherm_gw/__init__.py
new file mode 100644
index 00000000000..2dfe9267651
--- /dev/null
+++ b/tests/components/opentherm_gw/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Opentherm Gateway integration."""
diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py
new file mode 100644
index 00000000000..89f2783cf71
--- /dev/null
+++ b/tests/components/opentherm_gw/test_config_flow.py
@@ -0,0 +1,209 @@
+"""Test the Opentherm Gateway config flow."""
+import asyncio
+from serial import SerialException
+from unittest.mock import patch
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES
+from homeassistant.components.opentherm_gw.const import (
+ DOMAIN,
+ CONF_FLOOR_TEMP,
+ CONF_PRECISION,
+)
+
+from pyotgw import OTGW_ABOUT
+from tests.common import mock_coro, MockConfigEntry
+
+
+async def test_form_user(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.opentherm_gw.async_setup",
+ return_value=mock_coro(True),
+ ) as mock_setup, patch(
+ "homeassistant.components.opentherm_gw.async_setup_entry",
+ return_value=mock_coro(True),
+ ) as mock_setup_entry, patch(
+ "pyotgw.pyotgw.connect",
+ return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}),
+ ) as mock_pyotgw_connect, patch(
+ "pyotgw.pyotgw.disconnect", return_value=mock_coro(None)
+ ) as mock_pyotgw_disconnect:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Test Entry 1"
+ assert result2["data"] == {
+ CONF_NAME: "Test Entry 1",
+ CONF_DEVICE: "/dev/ttyUSB0",
+ CONF_ID: "test_entry_1",
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+ assert len(mock_pyotgw_connect.mock_calls) == 1
+ assert len(mock_pyotgw_disconnect.mock_calls) == 1
+
+
+async def test_form_import(hass):
+ """Test import from existing config."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "homeassistant.components.opentherm_gw.async_setup",
+ return_value=mock_coro(True),
+ ) as mock_setup, patch(
+ "homeassistant.components.opentherm_gw.async_setup_entry",
+ return_value=mock_coro(True),
+ ) as mock_setup_entry, patch(
+ "pyotgw.pyotgw.connect",
+ return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}),
+ ) as mock_pyotgw_connect, patch(
+ "pyotgw.pyotgw.disconnect", return_value=mock_coro(None)
+ ) as mock_pyotgw_disconnect:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={CONF_ID: "legacy_gateway", CONF_DEVICE: "/dev/ttyUSB1"},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "legacy_gateway"
+ assert result["data"] == {
+ CONF_NAME: "legacy_gateway",
+ CONF_DEVICE: "/dev/ttyUSB1",
+ CONF_ID: "legacy_gateway",
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+ assert len(mock_pyotgw_connect.mock_calls) == 1
+ assert len(mock_pyotgw_disconnect.mock_calls) == 1
+
+
+async def test_form_duplicate_entries(hass):
+ """Test duplicate device or id errors."""
+ flow1 = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ flow2 = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ flow3 = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.opentherm_gw.async_setup",
+ return_value=mock_coro(True),
+ ) as mock_setup, patch(
+ "homeassistant.components.opentherm_gw.async_setup_entry",
+ return_value=mock_coro(True),
+ ) as mock_setup_entry, patch(
+ "pyotgw.pyotgw.connect",
+ return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}),
+ ) as mock_pyotgw_connect, patch(
+ "pyotgw.pyotgw.disconnect", return_value=mock_coro(None)
+ ) as mock_pyotgw_disconnect:
+ result1 = await hass.config_entries.flow.async_configure(
+ flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
+ )
+ result2 = await hass.config_entries.flow.async_configure(
+ flow2["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB1"}
+ )
+ result3 = await hass.config_entries.flow.async_configure(
+ flow3["flow_id"], {CONF_NAME: "Test Entry 2", CONF_DEVICE: "/dev/ttyUSB0"}
+ )
+ assert result1["type"] == "create_entry"
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "id_exists"}
+ assert result3["type"] == "form"
+ assert result3["errors"] == {"base": "already_configured"}
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+ assert len(mock_pyotgw_connect.mock_calls) == 1
+ assert len(mock_pyotgw_disconnect.mock_calls) == 1
+
+
+async def test_form_connection_timeout(hass):
+ """Test we handle connection timeout."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError)
+ ) as mock_connect:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"},
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "timeout"}
+ assert len(mock_connect.mock_calls) == 1
+
+
+async def test_form_connection_error(hass):
+ """Test we handle serial connection error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "serial_error"}
+ assert len(mock_connect.mock_calls) == 1
+
+
+async def test_options_form(hass):
+ """Test the options form."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Mock Gateway",
+ data={
+ CONF_NAME: "Mock Gateway",
+ CONF_DEVICE: "/dev/null",
+ CONF_ID: "mock_gateway",
+ },
+ options={},
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.flow.async_init(
+ entry.entry_id, context={"source": "test"}, data=None
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_FLOOR_TEMP: True, CONF_PRECISION: PRECISION_HALVES},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_PRECISION] == PRECISION_HALVES
+ assert result["data"][CONF_FLOOR_TEMP] is True
+
+ result = await hass.config_entries.options.flow.async_init(
+ entry.entry_id, context={"source": "test"}, data=None
+ )
+
+ result = await hass.config_entries.options.flow.async_configure(
+ result["flow_id"], user_input={CONF_PRECISION: 0}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_PRECISION] is None
+ assert result["data"][CONF_FLOOR_TEMP] is True
diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py
index 76863c61698..c4e2a54f69a 100644
--- a/tests/components/owntracks/test_config_flow.py
+++ b/tests/components/owntracks/test_config_flow.py
@@ -43,6 +43,7 @@ async def test_config_flow_unload(hass):
async def test_with_cloud_sub(hass):
"""Test creating a config flow while subscribed."""
+ hass.config.components.add("cloud")
with patch(
"homeassistant.components.cloud.async_active_subscription", return_value=True
), patch(
diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py
index 2a2178da9d5..c0d14f1efdc 100644
--- a/tests/components/plex/test_config_flow.py
+++ b/tests/components/plex/test_config_flow.py
@@ -46,14 +46,14 @@ async def test_bad_credentials(hass):
assert result["type"] == "form"
assert result["step_id"] == "start_website_auth"
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
- assert result["type"] == "external"
-
with patch(
"plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized
), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch(
"plexauth.PlexAuth.token", return_value="BAD TOKEN"
):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external"
+
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "external_done"
@@ -103,17 +103,17 @@ async def test_import_file_from_discovery(hass):
async def test_discovery(hass):
"""Test starting a flow from discovery."""
-
- result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
- context={"source": "discovery"},
- data={
- CONF_HOST: MOCK_SERVERS[0][CONF_HOST],
- CONF_PORT: MOCK_SERVERS[0][CONF_PORT],
- },
- )
- assert result["type"] == "abort"
- assert result["reason"] == "discovery_no_file"
+ with patch("homeassistant.components.plex.config_flow.load_json", return_value={}):
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "discovery"},
+ data={
+ CONF_HOST: MOCK_SERVERS[0][CONF_HOST],
+ CONF_PORT: MOCK_SERVERS[0][CONF_PORT],
+ },
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "discovery_no_file"
async def test_discovery_while_in_progress(hass):
@@ -192,12 +192,12 @@ async def test_unknown_exception(hass):
assert result["type"] == "form"
assert result["step_id"] == "start_website_auth"
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
- assert result["type"] == "external"
-
with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), asynctest.patch(
"plexauth.PlexAuth.initiate_auth"
), asynctest.patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external"
+
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "external_done"
@@ -217,14 +217,13 @@ async def test_no_servers_found(hass):
assert result["type"] == "form"
assert result["step_id"] == "start_website_auth"
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
- assert result["type"] == "external"
-
with patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0)
), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "external_done"
@@ -248,14 +247,14 @@ async def test_single_available_server(hass):
assert result["type"] == "form"
assert result["step_id"] == "start_website_auth"
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
- assert result["type"] == "external"
-
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch(
"plexapi.server.PlexServer", return_value=mock_plex_server
), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external"
+
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "external_done"
@@ -287,9 +286,6 @@ async def test_multiple_servers_with_selection(hass):
assert result["type"] == "form"
assert result["step_id"] == "start_website_auth"
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
- assert result["type"] == "external"
-
with patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2)
), patch(
@@ -299,6 +295,9 @@ async def test_multiple_servers_with_selection(hass):
), asynctest.patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external"
+
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "external_done"
@@ -349,9 +348,6 @@ async def test_adding_last_unconfigured_server(hass):
assert result["type"] == "form"
assert result["step_id"] == "start_website_auth"
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
- assert result["type"] == "external"
-
with patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2)
), patch(
@@ -361,6 +357,9 @@ async def test_adding_last_unconfigured_server(hass):
), asynctest.patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external"
+
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "external_done"
@@ -440,14 +439,14 @@ async def test_all_available_servers_configured(hass):
assert result["type"] == "form"
assert result["step_id"] == "start_website_auth"
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
- assert result["type"] == "external"
-
with patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2)
), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external"
+
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "external_done"
@@ -495,12 +494,12 @@ async def test_external_timed_out(hass):
assert result["type"] == "form"
assert result["step_id"] == "start_website_auth"
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
- assert result["type"] == "external"
-
with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch(
"plexauth.PlexAuth.token", return_value=None
):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external"
+
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "external_done"
@@ -520,12 +519,12 @@ async def test_callback_view(hass, aiohttp_client):
assert result["type"] == "form"
assert result["step_id"] == "start_website_auth"
- result = await hass.config_entries.flow.async_configure(result["flow_id"])
- assert result["type"] == "external"
-
with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external"
+
client = await aiohttp_client(hass.http.app)
forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}'
diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py
index 42f319e7343..81f81093a67 100644
--- a/tests/components/ps4/test_config_flow.py
+++ b/tests/components/ps4/test_config_flow.py
@@ -1,6 +1,8 @@
"""Define tests for the PlayStation 4 config flow."""
from unittest.mock import patch
+from pyps4_2ndscreen.errors import CredentialTimeout
+
from homeassistant import data_entry_flow
from homeassistant.components import ps4
from homeassistant.components.ps4.const import DEFAULT_NAME, DEFAULT_REGION
@@ -73,28 +75,28 @@ async def test_full_flow_implementation(hass):
manager = hass.config_entries
# User Step Started, results in Step Creds
- with patch("pyps4_homeassistant.Helper.port_bind", return_value=None):
+ with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "creds"
# Step Creds results with form in Step Mode.
- with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS):
+ with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS):
result = await flow.async_step_creds({})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "mode"
# Step Mode with User Input which is not manual, results in Step Link.
with patch(
- "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
+ "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
):
result = await flow.async_step_mode(MOCK_AUTO)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
# User Input results in created entry.
- with patch("pyps4_homeassistant.Helper.link", return_value=(True, True)), patch(
- "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
+ with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch(
+ "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
):
result = await flow.async_step_link(MOCK_CONFIG)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
@@ -126,20 +128,20 @@ async def test_multiple_flow_implementation(hass):
manager = hass.config_entries
# User Step Started, results in Step Creds
- with patch("pyps4_homeassistant.Helper.port_bind", return_value=None):
+ with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "creds"
# Step Creds results with form in Step Mode.
- with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS):
+ with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS):
result = await flow.async_step_creds({})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "mode"
# Step Mode with User Input which is not manual, results in Step Link.
with patch(
- "pyps4_homeassistant.Helper.has_devices",
+ "pyps4_2ndscreen.Helper.has_devices",
return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}],
):
result = await flow.async_step_mode(MOCK_AUTO)
@@ -147,8 +149,8 @@ async def test_multiple_flow_implementation(hass):
assert result["step_id"] == "link"
# User Input results in created entry.
- with patch("pyps4_homeassistant.Helper.link", return_value=(True, True)), patch(
- "pyps4_homeassistant.Helper.has_devices",
+ with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch(
+ "pyps4_2ndscreen.Helper.has_devices",
return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}],
):
result = await flow.async_step_link(MOCK_CONFIG)
@@ -175,8 +177,8 @@ async def test_multiple_flow_implementation(hass):
# Test additional flow.
# User Step Started, results in Step Mode:
- with patch("pyps4_homeassistant.Helper.port_bind", return_value=None), patch(
- "pyps4_homeassistant.Helper.has_devices",
+ with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None), patch(
+ "pyps4_2ndscreen.Helper.has_devices",
return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}],
):
result = await flow.async_step_user()
@@ -184,14 +186,14 @@ async def test_multiple_flow_implementation(hass):
assert result["step_id"] == "creds"
# Step Creds results with form in Step Mode.
- with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS):
+ with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS):
result = await flow.async_step_creds({})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "mode"
# Step Mode with User Input which is not manual, results in Step Link.
with patch(
- "pyps4_homeassistant.Helper.has_devices",
+ "pyps4_2ndscreen.Helper.has_devices",
return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}],
):
result = await flow.async_step_mode(MOCK_AUTO)
@@ -200,9 +202,9 @@ async def test_multiple_flow_implementation(hass):
# Step Link
with patch(
- "pyps4_homeassistant.Helper.has_devices",
+ "pyps4_2ndscreen.Helper.has_devices",
return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}],
- ), patch("pyps4_homeassistant.Helper.link", return_value=(True, True)):
+ ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)):
result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_TOKEN] == MOCK_CREDS
@@ -232,13 +234,13 @@ async def test_port_bind_abort(hass):
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
- with patch("pyps4_homeassistant.Helper.port_bind", return_value=MOCK_UDP_PORT):
+ with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_UDP_PORT):
reason = "port_987_bind_error"
result = await flow.async_step_user(user_input=None)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == reason
- with patch("pyps4_homeassistant.Helper.port_bind", return_value=MOCK_TCP_PORT):
+ with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT):
reason = "port_997_bind_error"
result = await flow.async_step_user(user_input=None)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -253,7 +255,7 @@ async def test_duplicate_abort(hass):
flow.creds = MOCK_CREDS
with patch(
- "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
+ "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
):
result = await flow.async_step_link(user_input=None)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -274,9 +276,9 @@ async def test_additional_device(hass):
assert len(manager.async_entries()) == 1
with patch(
- "pyps4_homeassistant.Helper.has_devices",
+ "pyps4_2ndscreen.Helper.has_devices",
return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}],
- ), patch("pyps4_homeassistant.Helper.link", return_value=(True, True)):
+ ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)):
result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_TOKEN] == MOCK_CREDS
@@ -296,7 +298,7 @@ async def test_no_devices_found_abort(hass):
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
- with patch("pyps4_homeassistant.Helper.has_devices", return_value=[]):
+ with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]):
result = await flow.async_step_link()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "no_devices_found"
@@ -310,8 +312,7 @@ async def test_manual_mode(hass):
# Step Mode with User Input: manual, results in Step Link.
with patch(
- "pyps4_homeassistant.Helper.has_devices",
- return_value=[{"host-ip": flow.m_device}],
+ "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": flow.m_device}]
):
result = await flow.async_step_mode(MOCK_MANUAL)
assert flow.m_device == MOCK_HOST
@@ -324,7 +325,7 @@ async def test_credential_abort(hass):
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
- with patch("pyps4_homeassistant.Helper.get_creds", return_value=None):
+ with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None):
result = await flow.async_step_creds({})
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "credential_error"
@@ -332,12 +333,10 @@ async def test_credential_abort(hass):
async def test_credential_timeout(hass):
"""Test that Credential Timeout shows error."""
- from pyps4_homeassistant.errors import CredentialTimeout
-
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
- with patch("pyps4_homeassistant.Helper.get_creds", side_effect=CredentialTimeout):
+ with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout):
result = await flow.async_step_creds({})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "credential_timeout"}
@@ -349,8 +348,8 @@ async def test_wrong_pin_error(hass):
flow.hass = hass
flow.location = MOCK_LOCATION
- with patch("pyps4_homeassistant.Helper.link", return_value=(True, False)), patch(
- "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
+ with patch("pyps4_2ndscreen.Helper.link", return_value=(True, False)), patch(
+ "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
):
result = await flow.async_step_link(MOCK_CONFIG)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -364,8 +363,8 @@ async def test_device_connection_error(hass):
flow.hass = hass
flow.location = MOCK_LOCATION
- with patch("pyps4_homeassistant.Helper.link", return_value=(False, True)), patch(
- "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
+ with patch("pyps4_2ndscreen.Helper.link", return_value=(False, True)), patch(
+ "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}]
):
result = await flow.async_step_link(MOCK_CONFIG)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py
index e4f2033c3cb..7bf93e37777 100644
--- a/tests/components/ps4/test_media_player.py
+++ b/tests/components/ps4/test_media_player.py
@@ -1,7 +1,7 @@
"""Tests for the PS4 media player platform."""
from unittest.mock import MagicMock, patch
-from pyps4_homeassistant.credential import get_ddp_message
+from pyps4_2ndscreen.credential import get_ddp_message
from homeassistant.components import ps4
from homeassistant.components.media_player.const import (
@@ -169,11 +169,6 @@ async def mock_ddp_response(hass, mock_status_data, games=None):
await hass.async_block_till_done()
-async def test_async_setup_platform_does_nothing():
- """Test setup platform does nothing (Uses config entries only)."""
- await ps4.media_player.async_setup_platform(None, None, None)
-
-
async def test_media_player_is_setup_correctly_with_entry(hass):
"""Test entity is setup correctly with entry correctly."""
mock_entity_id = await setup_mock_component(hass)
@@ -295,9 +290,7 @@ async def test_media_attributes_are_loaded(hass):
async def test_device_info_is_set_from_status_correctly(hass):
"""Test that device info is set correctly from status update."""
mock_d_registry = mock_device_registry(hass)
- with patch(
- "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_OFF
- ), patch(MOCK_SAVE, side_effect=MagicMock()):
+ with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_OFF):
mock_entity_id = await setup_mock_component(hass)
await hass.async_block_till_done()
@@ -447,9 +440,9 @@ async def test_media_stop(hass):
async def test_select_source(hass):
"""Test that select source service calls function with title."""
mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA}
- with patch(
- "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE
- ), patch(MOCK_LOAD, return_value=mock_data):
+ with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch(
+ MOCK_LOAD, return_value=mock_data
+ ):
mock_entity_id = await setup_mock_component(hass)
mock_func = "{}{}".format(
@@ -473,9 +466,9 @@ async def test_select_source(hass):
async def test_select_source_caps(hass):
"""Test that select source service calls function with upper case title."""
mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA}
- with patch(
- "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE
- ), patch(MOCK_LOAD, return_value=mock_data):
+ with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch(
+ MOCK_LOAD, return_value=mock_data
+ ):
mock_entity_id = await setup_mock_component(hass)
mock_func = "{}{}".format(
@@ -502,9 +495,9 @@ async def test_select_source_caps(hass):
async def test_select_source_id(hass):
"""Test that select source service calls function with Title ID."""
mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA}
- with patch(
- "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE
- ), patch(MOCK_LOAD, return_value=mock_data):
+ with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch(
+ MOCK_LOAD, return_value=mock_data
+ ):
mock_entity_id = await setup_mock_component(hass)
mock_func = "{}{}".format(
diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py
index 19b7566c37c..81e0423a723 100644
--- a/tests/components/recorder/test_migrate.py
+++ b/tests/components/recorder/test_migrate.py
@@ -23,9 +23,9 @@ def create_engine_test(*args, **kwargs):
async def test_schema_update_calls(hass):
"""Test that schema migrations occur in correct order."""
- with patch("sqlalchemy.create_engine", new=create_engine_test), patch(
- "homeassistant.components.recorder.migration._apply_update"
- ) as update:
+ with patch(
+ "homeassistant.components.recorder.create_engine", new=create_engine_test
+ ), patch("homeassistant.components.recorder.migration._apply_update") as update:
await async_setup_component(
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
)
diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py
index 1c676e203d2..7e06dcd1e5e 100644
--- a/tests/components/recorder/test_purge.py
+++ b/tests/components/recorder/test_purge.py
@@ -174,5 +174,5 @@ class TestRecorderPurge(unittest.TestCase):
self.hass.data[DATA_INSTANCE].block_till_done()
assert (
mock_logger.debug.mock_calls[3][1][0]
- == "Vacuuming SQLite to free space"
+ == "Vacuuming SQL DB to free space"
)
diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py
index d117678ccc7..50acb053347 100644
--- a/tests/components/rest/test_sensor.py
+++ b/tests/components/rest/test_sensor.py
@@ -76,6 +76,40 @@ class TestRestSensorSetup(unittest.TestCase):
)
assert 2 == mock_req.call_count
+ @requests_mock.Mocker()
+ def test_setup_minimum_resource_template(self, mock_req):
+ """Test setup with minimum configuration (resource_template)."""
+ mock_req.get("http://localhost", status_code=200)
+ with assert_setup_component(1, "sensor"):
+ assert setup_component(
+ self.hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource_template": "http://localhost",
+ }
+ },
+ )
+ assert mock_req.call_count == 2
+
+ @requests_mock.Mocker()
+ def test_setup_duplicate_resource(self, mock_req):
+ """Test setup with duplicate resources."""
+ mock_req.get("http://localhost", status_code=200)
+ with assert_setup_component(0, "sensor"):
+ assert setup_component(
+ self.hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "resource_template": "http://localhost",
+ }
+ },
+ )
+
@requests_mock.Mocker()
def test_setup_get(self, mock_req):
"""Test setup with valid configuration."""
@@ -152,6 +186,7 @@ class TestRestSensor(unittest.TestCase):
self.value_template = template("{{ value_json.key }}")
self.value_template.hass = self.hass
self.force_update = False
+ self.resource_template = None
self.sensor = rest.RestSensor(
self.hass,
@@ -162,6 +197,7 @@ class TestRestSensor(unittest.TestCase):
self.value_template,
[],
self.force_update,
+ self.resource_template,
)
def tearDown(self):
@@ -222,6 +258,7 @@ class TestRestSensor(unittest.TestCase):
None,
[],
self.force_update,
+ self.resource_template,
)
self.sensor.update()
assert "plain_state" == self.sensor.state
@@ -242,6 +279,7 @@ class TestRestSensor(unittest.TestCase):
None,
["key"],
self.force_update,
+ self.resource_template,
)
self.sensor.update()
assert "some_json_value" == self.sensor.device_state_attributes["key"]
@@ -261,6 +299,7 @@ class TestRestSensor(unittest.TestCase):
None,
["key"],
self.force_update,
+ self.resource_template,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
@@ -282,6 +321,7 @@ class TestRestSensor(unittest.TestCase):
None,
["key"],
self.force_update,
+ self.resource_template,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
@@ -303,6 +343,7 @@ class TestRestSensor(unittest.TestCase):
None,
["key"],
self.force_update,
+ self.resource_template,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
@@ -326,6 +367,7 @@ class TestRestSensor(unittest.TestCase):
self.value_template,
["key"],
self.force_update,
+ self.resource_template,
)
self.sensor.update()
diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py
index 2f50f34f140..b7ac5a4be8a 100644
--- a/tests/components/rest_command/test_init.py
+++ b/tests/components/rest_command/test_init.py
@@ -51,6 +51,7 @@ class TestRestCommandComponent:
self.config = {
rc.DOMAIN: {
"get_test": {"url": self.url, "method": "get"},
+ "patch_test": {"url": self.url, "method": "patch"},
"post_test": {"url": self.url, "method": "post"},
"put_test": {"url": self.url, "method": "put"},
"delete_test": {"url": self.url, "method": "delete"},
@@ -65,7 +66,7 @@ class TestRestCommandComponent:
def test_setup_tests(self):
"""Set up test config and test it."""
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
assert self.hass.services.has_service(rc.DOMAIN, "get_test")
@@ -75,7 +76,7 @@ class TestRestCommandComponent:
def test_rest_command_timeout(self, aioclient_mock):
"""Call a rest command with timeout."""
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
aioclient_mock.get(self.url, exc=asyncio.TimeoutError())
@@ -87,7 +88,7 @@ class TestRestCommandComponent:
def test_rest_command_aiohttp_error(self, aioclient_mock):
"""Call a rest command with aiohttp exception."""
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
aioclient_mock.get(self.url, exc=aiohttp.ClientError())
@@ -99,7 +100,7 @@ class TestRestCommandComponent:
def test_rest_command_http_error(self, aioclient_mock):
"""Call a rest command with status code 400."""
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
aioclient_mock.get(self.url, status=400)
@@ -114,7 +115,7 @@ class TestRestCommandComponent:
data = {"username": "test", "password": "123456"}
self.config[rc.DOMAIN]["get_test"].update(data)
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
aioclient_mock.get(self.url, content=b"success")
@@ -129,7 +130,7 @@ class TestRestCommandComponent:
data = {"payload": "test"}
self.config[rc.DOMAIN]["post_test"].update(data)
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
aioclient_mock.post(self.url, content=b"success")
@@ -142,7 +143,7 @@ class TestRestCommandComponent:
def test_rest_command_get(self, aioclient_mock):
"""Call a rest command with get."""
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
aioclient_mock.get(self.url, content=b"success")
@@ -154,7 +155,7 @@ class TestRestCommandComponent:
def test_rest_command_delete(self, aioclient_mock):
"""Call a rest command with delete."""
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
aioclient_mock.delete(self.url, content=b"success")
@@ -164,12 +165,28 @@ class TestRestCommandComponent:
assert len(aioclient_mock.mock_calls) == 1
+ def test_rest_command_patch(self, aioclient_mock):
+ """Call a rest command with patch."""
+ data = {"payload": "data"}
+ self.config[rc.DOMAIN]["patch_test"].update(data)
+
+ with assert_setup_component(5):
+ setup_component(self.hass, rc.DOMAIN, self.config)
+
+ aioclient_mock.patch(self.url, content=b"success")
+
+ self.hass.services.call(rc.DOMAIN, "patch_test", {})
+ self.hass.block_till_done()
+
+ assert len(aioclient_mock.mock_calls) == 1
+ assert aioclient_mock.mock_calls[0][2] == b"data"
+
def test_rest_command_post(self, aioclient_mock):
"""Call a rest command with post."""
data = {"payload": "data"}
self.config[rc.DOMAIN]["post_test"].update(data)
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
aioclient_mock.post(self.url, content=b"success")
@@ -185,7 +202,7 @@ class TestRestCommandComponent:
data = {"payload": "data"}
self.config[rc.DOMAIN]["put_test"].update(data)
- with assert_setup_component(4):
+ with assert_setup_component(5):
setup_component(self.hass, rc.DOMAIN, self.config)
aioclient_mock.put(self.url, content=b"success")
diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py
index 442ebebdffe..d1fdec579c9 100644
--- a/tests/components/rflink/test_binary_sensor.py
+++ b/tests/components/rflink/test_binary_sensor.py
@@ -8,14 +8,13 @@ from datetime import timedelta
from unittest.mock import patch
from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL
-
-import homeassistant.core as ha
from homeassistant.const import (
EVENT_STATE_CHANGED,
- STATE_ON,
STATE_OFF,
+ STATE_ON,
STATE_UNAVAILABLE,
)
+import homeassistant.core as ha
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py
index 858258e7efd..dc286502068 100644
--- a/tests/components/rflink/test_cover.py
+++ b/tests/components/rflink/test_cover.py
@@ -9,13 +9,13 @@ import logging
from homeassistant.components.rflink import EVENT_BUTTON_PRESSED
from homeassistant.const import (
- SERVICE_OPEN_COVER,
- SERVICE_CLOSE_COVER,
- STATE_OPEN,
- STATE_CLOSED,
ATTR_ENTITY_ID,
+ SERVICE_CLOSE_COVER,
+ SERVICE_OPEN_COVER,
+ STATE_CLOSED,
+ STATE_OPEN,
)
-from homeassistant.core import callback, State, CoreState
+from homeassistant.core import CoreState, State, callback
from tests.common import mock_restore_cache
from tests.components.rflink.test_init import mock_rflink
diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py
index 5e821fbdeb2..df96b0e87ae 100644
--- a/tests/components/rflink/test_init.py
+++ b/tests/components/rflink/test_init.py
@@ -5,14 +5,14 @@ from unittest.mock import Mock
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.rflink import (
CONF_RECONNECT_INTERVAL,
- SERVICE_SEND_COMMAND,
- RflinkCommand,
- TMP_ENTITY,
DATA_ENTITY_LOOKUP,
EVENT_KEY_COMMAND,
EVENT_KEY_SENSOR,
+ SERVICE_SEND_COMMAND,
+ TMP_ENTITY,
+ RflinkCommand,
)
-from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_STOP_COVER
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_STOP_COVER, SERVICE_TURN_OFF
async def mock_rflink(
@@ -46,7 +46,9 @@ async def mock_rflink(
return transport, protocol
mock_create = Mock(wraps=create_rflink_connection)
- monkeypatch.setattr("rflink.protocol.create_rflink_connection", mock_create)
+ monkeypatch.setattr(
+ "homeassistant.components.rflink.create_rflink_connection", mock_create
+ )
await async_setup_component(hass, "rflink", config)
await async_setup_component(hass, domain, config)
diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py
index ba4122724ce..b22730a3310 100644
--- a/tests/components/rflink/test_light.py
+++ b/tests/components/rflink/test_light.py
@@ -11,10 +11,10 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
- STATE_ON,
STATE_OFF,
+ STATE_ON,
)
-from homeassistant.core import callback, State, CoreState
+from homeassistant.core import CoreState, State, callback
from tests.common import mock_restore_cache
from tests.components.rflink.test_init import mock_rflink
@@ -313,10 +313,10 @@ async def test_signal_repetitions_cancelling(hass, monkeypatch):
await hass.async_block_till_done()
- assert protocol.send_command_ack.call_args_list[0][0][1] == "on"
- assert protocol.send_command_ack.call_args_list[1][0][1] == "off"
- assert protocol.send_command_ack.call_args_list[2][0][1] == "off"
- assert protocol.send_command_ack.call_args_list[3][0][1] == "off"
+ assert protocol.send_command_ack.call_args_list[0][0][1] == "off"
+ assert protocol.send_command_ack.call_args_list[1][0][1] == "on"
+ assert protocol.send_command_ack.call_args_list[2][0][1] == "on"
+ assert protocol.send_command_ack.call_args_list[3][0][1] == "on"
async def test_type_toggle(hass, monkeypatch):
diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py
index bf6c9e03fbc..3fea3ef6ef4 100644
--- a/tests/components/rflink/test_sensor.py
+++ b/tests/components/rflink/test_sensor.py
@@ -7,12 +7,13 @@ automatic sensor creation.
from homeassistant.components.rflink import (
CONF_RECONNECT_INTERVAL,
- TMP_ENTITY,
DATA_ENTITY_LOOKUP,
EVENT_KEY_COMMAND,
EVENT_KEY_SENSOR,
+ TMP_ENTITY,
)
from homeassistant.const import STATE_UNKNOWN
+
from tests.components.rflink.test_init import mock_rflink
DOMAIN = "sensor"
diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py
index 4503f1a232f..d1fced33208 100644
--- a/tests/components/rflink/test_switch.py
+++ b/tests/components/rflink/test_switch.py
@@ -10,10 +10,10 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
- STATE_ON,
STATE_OFF,
+ STATE_ON,
)
-from homeassistant.core import callback, State, CoreState
+from homeassistant.core import CoreState, State, callback
from tests.common import mock_restore_cache
from tests.components.rflink.test_init import mock_rflink
diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py
index 9fa71bdab67..d85ea5cf6f4 100644
--- a/tests/components/rfxtrx/test_cover.py
+++ b/tests/components/rfxtrx/test_cover.py
@@ -1,10 +1,11 @@
"""The tests for the Rfxtrx cover platform."""
import unittest
+import RFXtrx as rfxtrxmod
import pytest
-from homeassistant.setup import setup_component
from homeassistant.components import rfxtrx as rfxtrx_core
+from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant, mock_component
@@ -142,8 +143,6 @@ class TestCoverRfxtrx(unittest.TestCase):
},
)
- import RFXtrx as rfxtrxmod
-
rfxtrx_core.RFXOBJECT = rfxtrxmod.Core(
"", transport_protocol=rfxtrxmod.DummyTransport
)
diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py
index ac046b99897..ec457af7575 100644
--- a/tests/components/rfxtrx/test_init.py
+++ b/tests/components/rfxtrx/test_init.py
@@ -4,9 +4,10 @@ import unittest
import pytest
+from homeassistant.components import rfxtrx as rfxtrx
from homeassistant.core import callback
from homeassistant.setup import setup_component
-from homeassistant.components import rfxtrx as rfxtrx
+
from tests.common import get_test_home_assistant
diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py
index f3a6bcab1b1..a5230cc5f3c 100644
--- a/tests/components/rfxtrx/test_light.py
+++ b/tests/components/rfxtrx/test_light.py
@@ -1,10 +1,11 @@
"""The tests for the Rfxtrx light platform."""
import unittest
+import RFXtrx as rfxtrxmod
import pytest
-from homeassistant.setup import setup_component
from homeassistant.components import rfxtrx as rfxtrx_core
+from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant, mock_component
@@ -109,8 +110,6 @@ class TestLightRfxtrx(unittest.TestCase):
},
)
- import RFXtrx as rfxtrxmod
-
rfxtrx_core.RFXOBJECT = rfxtrxmod.Core(
"", transport_protocol=rfxtrxmod.DummyTransport
)
diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py
index 3f0cfead3e4..652c823e0cf 100644
--- a/tests/components/rfxtrx/test_sensor.py
+++ b/tests/components/rfxtrx/test_sensor.py
@@ -3,9 +3,9 @@ import unittest
import pytest
-from homeassistant.setup import setup_component
from homeassistant.components import rfxtrx as rfxtrx_core
from homeassistant.const import TEMP_CELSIUS
+from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant, mock_component
diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py
index dc955a198a7..66da197aae8 100644
--- a/tests/components/rfxtrx/test_switch.py
+++ b/tests/components/rfxtrx/test_switch.py
@@ -1,17 +1,18 @@
-"""The tests for the Rfxtrx switch platform."""
+"""The tests for the RFXtrx switch platform."""
import unittest
+import RFXtrx as rfxtrxmod
import pytest
-from homeassistant.setup import setup_component
from homeassistant.components import rfxtrx as rfxtrx_core
+from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant, mock_component
@pytest.mark.skipif("os.environ.get('RFXTRX') != 'RUN'")
class TestSwitchRfxtrx(unittest.TestCase):
- """Test the Rfxtrx switch platform."""
+ """Test the RFXtrx switch platform."""
def setUp(self):
"""Set up things to be run when tests are started."""
@@ -166,8 +167,6 @@ class TestSwitchRfxtrx(unittest.TestCase):
},
)
- import RFXtrx as rfxtrxmod
-
rfxtrx_core.RFXOBJECT = rfxtrxmod.Core(
"", transport_protocol=rfxtrxmod.DummyTransport
)
@@ -200,8 +199,6 @@ class TestSwitchRfxtrx(unittest.TestCase):
},
)
- import RFXtrx as rfxtrxmod
-
rfxtrx_core.RFXOBJECT = rfxtrxmod.Core(
"", transport_protocol=rfxtrxmod.DummyTransport
)
diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py
new file mode 100644
index 00000000000..f3ff15c3ad9
--- /dev/null
+++ b/tests/components/sensor/test_device_condition.py
@@ -0,0 +1,370 @@
+"""The test for sensor device automation."""
+import pytest
+
+from homeassistant.components.sensor import DOMAIN
+from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS
+from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM
+from homeassistant.setup import async_setup_component
+import homeassistant.components.automation as automation
+from homeassistant.helpers import device_registry
+
+from tests.common import (
+ MockConfigEntry,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+ async_get_device_automations,
+ async_get_device_automation_capabilities,
+)
+from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock serivce."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_conditions(hass, device_reg, entity_reg):
+ """Test we get the expected conditions from a sensor."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ for device_class in DEVICE_CLASSES:
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES[device_class].unique_id,
+ device_id=device_entry.id,
+ )
+
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": condition["type"],
+ "device_id": device_entry.id,
+ "entity_id": platform.ENTITIES[device_class].entity_id,
+ }
+ for device_class in DEVICE_CLASSES
+ for condition in ENTITY_CONDITIONS[device_class]
+ if device_class != "none"
+ ]
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ assert conditions == expected_conditions
+
+
+async def test_get_condition_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a sensor condition."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES["battery"].unique_id,
+ device_id=device_entry.id,
+ )
+
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ expected_capabilities = {
+ "extra_fields": [
+ {
+ "description": {"suffix": "%"},
+ "name": "above",
+ "optional": True,
+ "type": "float",
+ },
+ {
+ "description": {"suffix": "%"},
+ "name": "below",
+ "optional": True,
+ "type": "float",
+ },
+ ]
+ }
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ assert len(conditions) == 1
+ for condition in conditions:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "condition", condition
+ )
+ assert capabilities == expected_capabilities
+
+
+async def test_get_condition_capabilities_none(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a sensor condition."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ conditions = [
+ {
+ "condition": "device",
+ "device_id": "8770c43885354d5fa27604db6817f63f",
+ "domain": "sensor",
+ "entity_id": "sensor.beer",
+ "type": "is_battery_level",
+ },
+ {
+ "condition": "device",
+ "device_id": "8770c43885354d5fa27604db6817f63f",
+ "domain": "sensor",
+ "entity_id": platform.ENTITIES["none"].entity_id,
+ "type": "is_battery_level",
+ },
+ ]
+
+ expected_capabilities = {}
+ for condition in conditions:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "condition", condition
+ )
+ assert capabilities == expected_capabilities
+
+
+async def test_if_state_not_above_below(hass, calls, caplog):
+ """Test for bad value conditions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ sensor1 = platform.ENTITIES["battery"]
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": sensor1.entity_id,
+ "type": "is_battery_level",
+ }
+ ],
+ "action": {"service": "test.automation"},
+ }
+ ]
+ },
+ )
+ assert "must contain at least one of below, above" in caplog.text
+
+
+async def test_if_state_above(hass, calls):
+ """Test for value conditions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ sensor1 = platform.ENTITIES["battery"]
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": sensor1.entity_id,
+ "type": "is_battery_level",
+ "above": 10,
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "{{ trigger.%s }}"
+ % "}} - {{ trigger.".join(("platform", "event.event_type"))
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(sensor1.entity_id, 9)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(sensor1.entity_id, 11)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "event - test_event1"
+
+
+async def test_if_state_below(hass, calls):
+ """Test for value conditions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ sensor1 = platform.ENTITIES["battery"]
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": sensor1.entity_id,
+ "type": "is_battery_level",
+ "below": 10,
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "{{ trigger.%s }}"
+ % "}} - {{ trigger.".join(("platform", "event.event_type"))
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(sensor1.entity_id, 11)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(sensor1.entity_id, 9)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "event - test_event1"
+
+
+async def test_if_state_between(hass, calls):
+ """Test for value conditions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ sensor1 = platform.ENTITIES["battery"]
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": sensor1.entity_id,
+ "type": "is_battery_level",
+ "above": 10,
+ "below": 20,
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "{{ trigger.%s }}"
+ % "}} - {{ trigger.".join(("platform", "event.event_type"))
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(sensor1.entity_id, 9)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(sensor1.entity_id, 11)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "event - test_event1"
+
+ hass.states.async_set(sensor1.entity_id, 21)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ hass.states.async_set(sensor1.entity_id, 19)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "event - test_event1"
diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py
index 45452dc84a0..b7a921fff18 100644
--- a/tests/components/sensor/test_device_trigger.py
+++ b/tests/components/sensor/test_device_trigger.py
@@ -74,26 +74,84 @@ async def test_get_triggers(hass, device_reg, entity_reg):
if device_class != "none"
]
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert len(triggers) == 8
assert triggers == expected_triggers
async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
- """Test we get the expected capabilities from a binary_sensor trigger."""
+ """Test we get the expected capabilities from a sensor trigger."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES["battery"].unique_id,
+ device_id=device_entry.id,
+ )
+
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
expected_capabilities = {
"extra_fields": [
- {"name": "above", "optional": True, "type": "float"},
- {"name": "below", "optional": True, "type": "float"},
+ {
+ "description": {"suffix": "%"},
+ "name": "above",
+ "optional": True,
+ "type": "float",
+ },
+ {
+ "description": {"suffix": "%"},
+ "name": "below",
+ "optional": True,
+ "type": "float",
+ },
{"name": "for", "optional": True, "type": "positive_time_period_dict"},
]
}
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert len(triggers) == 1
+ for trigger in triggers:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "trigger", trigger
+ )
+ assert capabilities == expected_capabilities
+
+
+async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a sensor trigger."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ triggers = [
+ {
+ "platform": "device",
+ "device_id": "8770c43885354d5fa27604db6817f63f",
+ "domain": "sensor",
+ "entity_id": "sensor.beer",
+ "type": "is_battery_level",
+ },
+ {
+ "platform": "device",
+ "device_id": "8770c43885354d5fa27604db6817f63f",
+ "domain": "sensor",
+ "entity_id": platform.ENTITIES["none"].entity_id,
+ "type": "is_battery_level",
+ },
+ ]
+
+ expected_capabilities = {}
for trigger in triggers:
capabilities = await async_get_device_automation_capabilities(
hass, "trigger", trigger
diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py
index fce0129a7bf..521f1c6a6a8 100644
--- a/tests/components/smartthings/test_config_flow.py
+++ b/tests/components/smartthings/test_config_flow.py
@@ -205,6 +205,8 @@ async def test_cloudhook_app_created_then_show_wait_form(
hass, app, app_oauth_client, smartthings_mock
):
"""Test SmartApp is created with a cloudhoko and shows wait form."""
+ hass.config.components.add("cloud")
+
# Unload the endpoint so we can reload it under the cloud.
await smartapp.unload_smartapp_endpoint(hass)
diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py
index 9749ab9bb71..b8cd65f5a0b 100644
--- a/tests/components/smartthings/test_init.py
+++ b/tests/components/smartthings/test_init.py
@@ -268,6 +268,7 @@ async def test_remove_entry(hass, config_entry, smartthings_mock):
async def test_remove_entry_cloudhook(hass, config_entry, smartthings_mock):
"""Test that the installed app, app, and cloudhook are removed up."""
+ hass.config.components.add("cloud")
# Arrange
config_entry.add_to_hass(hass)
hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud"
diff --git a/tests/components/solarlog/__init__.py b/tests/components/solarlog/__init__.py
new file mode 100644
index 00000000000..9074cab8416
--- /dev/null
+++ b/tests/components/solarlog/__init__.py
@@ -0,0 +1 @@
+"""Tests for the solarlog integration."""
diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py
new file mode 100644
index 00000000000..86f3b05d975
--- /dev/null
+++ b/tests/components/solarlog/test_config_flow.py
@@ -0,0 +1,135 @@
+"""Test the solarlog config flow."""
+from unittest.mock import patch
+import pytest
+
+from homeassistant import data_entry_flow
+from homeassistant import config_entries, setup
+from homeassistant.components.solarlog import config_flow
+from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN
+from homeassistant.const import CONF_HOST, CONF_NAME
+
+from tests.common import MockConfigEntry, mock_coro
+
+NAME = "Solarlog test 1 2 3"
+HOST = "http://1.1.1.1"
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection",
+ return_value=mock_coro({"title": "solarlog test 1 2 3"}),
+ ), patch(
+ "homeassistant.components.solarlog.async_setup", return_value=mock_coro(True)
+ ) as mock_setup, patch(
+ "homeassistant.components.solarlog.async_setup_entry",
+ return_value=mock_coro(True),
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"host": HOST, "name": NAME}
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "solarlog_test_1_2_3"
+ assert result2["data"] == {"host": "http://1.1.1.1"}
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+@pytest.fixture(name="test_connect")
+def mock_controller():
+ """Mock a successfull _host_in_configuration_exists."""
+ with patch(
+ "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection",
+ side_effect=lambda *_: mock_coro(True),
+ ):
+ yield
+
+
+def init_config_flow(hass):
+ """Init a configuration flow."""
+ flow = config_flow.SolarLogConfigFlow()
+ flow.hass = hass
+ return flow
+
+
+async def test_user(hass, test_connect):
+ """Test user config."""
+ flow = init_config_flow(hass)
+
+ result = await flow.async_step_user()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ # tets with all provided
+ result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "solarlog_test_1_2_3"
+ assert result["data"][CONF_HOST] == HOST
+
+
+async def test_import(hass, test_connect):
+ """Test import step."""
+ flow = init_config_flow(hass)
+
+ # import with only host
+ result = await flow.async_step_import({CONF_HOST: HOST})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "solarlog"
+ assert result["data"][CONF_HOST] == HOST
+
+ # import with only name
+ result = await flow.async_step_import({CONF_NAME: NAME})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "solarlog_test_1_2_3"
+ assert result["data"][CONF_HOST] == DEFAULT_HOST
+
+ # import with host and name
+ result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "solarlog_test_1_2_3"
+ assert result["data"][CONF_HOST] == HOST
+
+
+async def test_abort_if_already_setup(hass, test_connect):
+ """Test we abort if the device is already setup."""
+ flow = init_config_flow(hass)
+ MockConfigEntry(
+ domain="solarlog", data={CONF_NAME: NAME, CONF_HOST: HOST}
+ ).add_to_hass(hass)
+
+ # Should fail, same HOST different NAME (default)
+ result = await flow.async_step_import(
+ {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+ # Should fail, same HOST and NAME
+ result = await flow.async_step_user({CONF_HOST: HOST, CONF_NAME: NAME})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_HOST: "already_configured"}
+
+ # SHOULD pass, diff HOST (without http://), different NAME
+ result = await flow.async_step_import(
+ {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "solarlog_test_7_8_9"
+ assert result["data"][CONF_HOST] == "http://2.2.2.2"
+
+ # SHOULD pass, diff HOST, same NAME
+ result = await flow.async_step_import(
+ {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "solarlog_test_1_2_3"
+ assert result["data"][CONF_HOST] == "http://2.2.2.2"
diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py
index cbc3784e3f5..d42e7b8e367 100644
--- a/tests/components/somfy/test_config_flow.py
+++ b/tests/components/somfy/test_config_flow.py
@@ -1,19 +1,35 @@
"""Tests for the Somfy config flow."""
import asyncio
-from unittest.mock import Mock, patch
+from unittest.mock import patch
-from pymfy.api.somfy_api import SomfyApi
+import pytest
-from homeassistant import data_entry_flow
+from homeassistant import data_entry_flow, setup, config_entries
from homeassistant.components.somfy import config_flow, DOMAIN
-from homeassistant.components.somfy.config_flow import register_flow_implementation
-from tests.common import MockConfigEntry, mock_coro
+from homeassistant.helpers import config_entry_oauth2_flow
+
+from tests.common import MockConfigEntry
CLIENT_SECRET_VALUE = "5678"
CLIENT_ID_VALUE = "1234"
-AUTH_URL = "http://somfy.com"
+
+@pytest.fixture()
+async def mock_impl(hass):
+ """Mock implementation."""
+ await setup.async_setup_component(hass, "http", {})
+
+ impl = config_entry_oauth2_flow.LocalOAuth2Implementation(
+ hass,
+ DOMAIN,
+ CLIENT_ID_VALUE,
+ CLIENT_SECRET_VALUE,
+ "https://accounts.somfy.com/oauth/oauth/v2/auth",
+ "https://accounts.somfy.com/oauth/oauth/v2/token",
+ )
+ config_flow.SomfyFlowHandler.async_register_implementation(hass, impl)
+ return impl
async def test_abort_if_no_configuration(hass):
@@ -30,47 +46,84 @@ async def test_abort_if_existing_entry(hass):
flow = config_flow.SomfyFlowHandler()
flow.hass = hass
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
- result = await flow.async_step_import()
- assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_setup"
-async def test_full_flow(hass):
- """Check classic use case."""
- hass.data[DOMAIN] = {}
- register_flow_implementation(hass, CLIENT_ID_VALUE, CLIENT_SECRET_VALUE)
- flow = config_flow.SomfyFlowHandler()
- flow.hass = hass
- hass.config.api = Mock(base_url="https://example.com")
- flow._get_authorization_url = Mock(return_value=mock_coro((AUTH_URL, "state")))
- result = await flow.async_step_import()
+async def test_full_flow(hass, aiohttp_client, aioclient_mock):
+ """Check full flow."""
+ assert await setup.async_setup_component(
+ hass,
+ "somfy",
+ {
+ "somfy": {
+ "client_id": CLIENT_ID_VALUE,
+ "client_secret": CLIENT_SECRET_VALUE,
+ },
+ "http": {"base_url": "https://example.com"},
+ },
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ "somfy", context={"source": config_entries.SOURCE_USER}
+ )
+ state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
- assert result["url"] == AUTH_URL
- result = await flow.async_step_auth("my_super_code")
- assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE
- assert result["step_id"] == "creation"
- assert flow.code == "my_super_code"
- with patch.object(
- SomfyApi, "request_token", return_value={"access_token": "super_token"}
- ):
- result = await flow.async_step_creation()
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["data"]["refresh_args"] == {
- "client_id": CLIENT_ID_VALUE,
- "client_secret": CLIENT_SECRET_VALUE,
+ assert result["url"] == (
+ "https://accounts.somfy.com/oauth/oauth/v2/auth"
+ f"?response_type=code&client_id={CLIENT_ID_VALUE}"
+ "&redirect_uri=https://example.com/auth/external/callback"
+ f"&state={state}"
+ )
+
+ client = await aiohttp_client(hass.http.app)
+ resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
+ assert resp.status == 200
+ assert resp.headers["content-type"] == "text/html; charset=utf-8"
+
+ aioclient_mock.post(
+ "https://accounts.somfy.com/oauth/oauth/v2/token",
+ json={
+ "refresh_token": "mock-refresh-token",
+ "access_token": "mock-access-token",
+ "type": "Bearer",
+ "expires_in": 60,
+ },
+ )
+
+ with patch("homeassistant.components.somfy.api.ConfigEntrySomfyApi"):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+
+ assert result["data"]["auth_implementation"] == "somfy"
+
+ result["data"]["token"].pop("expires_at")
+ assert result["data"]["token"] == {
+ "refresh_token": "mock-refresh-token",
+ "access_token": "mock-access-token",
+ "type": "Bearer",
+ "expires_in": 60,
}
- assert result["title"] == "Somfy"
- assert result["data"]["token"] == {"access_token": "super_token"}
+
+ assert "somfy" in hass.config.components
+ entry = hass.config_entries.async_entries("somfy")[0]
+ assert entry.state == config_entries.ENTRY_STATE_LOADED
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
-async def test_abort_if_authorization_timeout(hass):
+async def test_abort_if_authorization_timeout(hass, mock_impl):
"""Check Somfy authorization timeout."""
flow = config_flow.SomfyFlowHandler()
flow.hass = hass
- flow._get_authorization_url = Mock(side_effect=asyncio.TimeoutError)
- result = await flow.async_step_auth()
+
+ with patch.object(
+ mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError
+ ):
+ result = await flow.async_step_user()
+
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "authorize_url_timeout"
diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py
index 135b7279244..e0257585ad5 100644
--- a/tests/components/sonos/conftest.py
+++ b/tests/components/sonos/conftest.py
@@ -2,8 +2,8 @@
from asynctest.mock import Mock, patch as patch
import pytest
-from homeassistant.components.sonos import DOMAIN
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
+from homeassistant.components.sonos import DOMAIN
from homeassistant.const import CONF_HOSTS
from tests.common import MockConfigEntry
diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py
index b9ceaa49639..86ec90f32b8 100644
--- a/tests/components/sonos/test_init.py
+++ b/tests/components/sonos/test_init.py
@@ -2,8 +2,8 @@
from unittest.mock import patch
from homeassistant import config_entries, data_entry_flow
-from homeassistant.setup import async_setup_component
from homeassistant.components import sonos
+from homeassistant.setup import async_setup_component
from tests.common import mock_coro
diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py
index ec5861b536a..d21d3f01792 100644
--- a/tests/components/sonos/test_media_player.py
+++ b/tests/components/sonos/test_media_player.py
@@ -1,5 +1,5 @@
"""Tests for the Sonos Media Player platform."""
-from homeassistant.components.sonos import media_player, DOMAIN
+from homeassistant.components.sonos import DOMAIN, media_player
from homeassistant.setup import async_setup_component
diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py
index 28357ab34b5..afc7bebea09 100644
--- a/tests/components/sql/test_sensor.py
+++ b/tests/components/sql/test_sensor.py
@@ -1,11 +1,12 @@
"""The test for the sql sensor platform."""
import unittest
+
import pytest
import voluptuous as vol
from homeassistant.components.sql.sensor import validate_sql_select
-from homeassistant.setup import setup_component
from homeassistant.const import STATE_UNKNOWN
+from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant
diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py
index 6deb40c082d..4c7e9d29fee 100644
--- a/tests/components/statsd/test_init.py
+++ b/tests/components/statsd/test_init.py
@@ -2,15 +2,15 @@
import unittest
from unittest import mock
+import pytest
import voluptuous as vol
-from homeassistant.setup import setup_component
-import homeassistant.core as ha
import homeassistant.components.statsd as statsd
-from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED
+from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON
+import homeassistant.core as ha
+from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant
-import pytest
class TestStatsd(unittest.TestCase):
diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py
index 32ab36dc477..4c34ec0b341 100644
--- a/tests/components/stream/common.py
+++ b/tests/components/stream/common.py
@@ -1,8 +1,11 @@
"""Collection of test helpers."""
import io
+import av
+import numpy as np
+
from homeassistant.components.stream import Stream
-from homeassistant.components.stream.const import DOMAIN, ATTR_STREAMS
+from homeassistant.components.stream.const import ATTR_STREAMS, DOMAIN
def generate_h264_video():
@@ -11,8 +14,6 @@ def generate_h264_video():
See: http://docs.mikeboers.com/pyav/develop/cookbook/numpy.html
"""
- import numpy as np
- import av
duration = 5
fps = 24
diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py
index ac564ce7553..293f8d1e4cf 100644
--- a/tests/components/stream/test_hls.py
+++ b/tests/components/stream/test_hls.py
@@ -4,8 +4,8 @@ from urllib.parse import urlparse
import pytest
-from homeassistant.setup import async_setup_component
from homeassistant.components.stream import request_stream
+from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py
index 80d703c801b..0661a5a9738 100644
--- a/tests/components/stream/test_init.py
+++ b/tests/components/stream/test_init.py
@@ -1,16 +1,16 @@
"""The tests for stream."""
-from unittest.mock import patch, MagicMock
+from unittest.mock import MagicMock, patch
import pytest
-from homeassistant.const import CONF_FILENAME
from homeassistant.components.stream.const import (
+ ATTR_STREAMS,
+ CONF_LOOKBACK,
+ CONF_STREAM_SOURCE,
DOMAIN,
SERVICE_RECORD,
- CONF_STREAM_SOURCE,
- CONF_LOOKBACK,
- ATTR_STREAMS,
)
+from homeassistant.const import CONF_FILENAME
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py
index dce8b95d07c..95eeeecf7ad 100644
--- a/tests/components/stream/test_recorder.py
+++ b/tests/components/stream/test_recorder.py
@@ -2,11 +2,12 @@
from datetime import timedelta
from io import BytesIO
from unittest.mock import patch
+
import pytest
-from homeassistant.setup import async_setup_component
from homeassistant.components.stream.core import Segment
from homeassistant.components.stream.recorder import recorder_save_worker
+from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py
index e2ce5a373d2..e673527fada 100644
--- a/tests/components/switch/test_device_condition.py
+++ b/tests/components/switch/test_device_condition.py
@@ -1,20 +1,22 @@
"""The test for switch device automation."""
+from datetime import timedelta
import pytest
+from unittest.mock import patch
from homeassistant.components.switch import DOMAIN
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation
-from homeassistant.components.device_automation import (
- _async_get_device_automations as async_get_device_automations,
-)
from homeassistant.helpers import device_registry
+import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
async_mock_service,
mock_device_registry,
mock_registry,
+ async_get_device_automations,
+ async_get_device_automation_capabilities,
)
@@ -65,6 +67,28 @@ async def test_get_conditions(hass, device_reg, entity_reg):
assert conditions == expected_conditions
+async def test_get_condition_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a switch condition."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_capabilities = {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
+ conditions = await async_get_device_automations(hass, "condition", device_entry.id)
+ for condition in conditions:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "condition", condition
+ )
+ assert capabilities == expected_capabilities
+
+
async def test_if_state(hass, calls):
"""Test for turn_on and turn_off conditions."""
platform = getattr(hass.components, f"test.{DOMAIN}")
@@ -136,3 +160,73 @@ async def test_if_state(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 2
assert calls[1].data["some"] == "is_off event - test_event2"
+
+
+async def test_if_fires_on_for_condition(hass, calls):
+ """Test for firing if condition is on with delay."""
+ point1 = dt_util.utcnow()
+ point2 = point1 + timedelta(seconds=10)
+ point3 = point2 + timedelta(seconds=10)
+
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ ent1, ent2, ent3 = platform.ENTITIES
+
+ with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow:
+ mock_utcnow.return_value = point1
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "is_off",
+ "for": {"seconds": 5},
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ ("platform", "event.event_type")
+ )
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ # Time travel 10 secs into the future
+ mock_utcnow.return_value = point2
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set(ent1.entity_id, STATE_OFF)
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ # Time travel 20 secs into the future
+ mock_utcnow.return_value = point3
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "is_off event - test_event1"
diff --git a/tests/components/switch/test_reproduce_state.py b/tests/components/switch/test_reproduce_state.py
new file mode 100644
index 00000000000..4b6db84bfdd
--- /dev/null
+++ b/tests/components/switch/test_reproduce_state.py
@@ -0,0 +1,50 @@
+"""Test reproduce state for Switch."""
+from homeassistant.core import State
+
+from tests.common import async_mock_service
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Switch states."""
+ hass.states.async_set("switch.entity_off", "off", {})
+ hass.states.async_set("switch.entity_on", "on", {})
+
+ turn_on_calls = async_mock_service(hass, "switch", "turn_on")
+ turn_off_calls = async_mock_service(hass, "switch", "turn_off")
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [State("switch.entity_off", "off"), State("switch.entity_on", "on", {})],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("switch.entity_off", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("switch.entity_on", "off"),
+ State("switch.entity_off", "on", {}),
+ # Should not raise
+ State("switch.non_existing", "on"),
+ ],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 1
+ assert turn_on_calls[0].domain == "switch"
+ assert turn_on_calls[0].data == {"entity_id": "switch.entity_off"}
+
+ assert len(turn_off_calls) == 1
+ assert turn_off_calls[0].domain == "switch"
+ assert turn_off_calls[0].data == {"entity_id": "switch.entity_on"}
diff --git a/tests/components/timer/test_reproduce_state.py b/tests/components/timer/test_reproduce_state.py
new file mode 100644
index 00000000000..5539d8610c3
--- /dev/null
+++ b/tests/components/timer/test_reproduce_state.py
@@ -0,0 +1,84 @@
+"""Test reproduce state for Timer."""
+from homeassistant.components.timer import (
+ ATTR_DURATION,
+ SERVICE_CANCEL,
+ SERVICE_PAUSE,
+ SERVICE_START,
+ STATUS_ACTIVE,
+ STATUS_IDLE,
+ STATUS_PAUSED,
+)
+from homeassistant.core import State
+from tests.common import async_mock_service
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Timer states."""
+ hass.states.async_set("timer.entity_idle", STATUS_IDLE, {})
+ hass.states.async_set("timer.entity_paused", STATUS_PAUSED, {})
+ hass.states.async_set("timer.entity_active", STATUS_ACTIVE, {})
+ hass.states.async_set(
+ "timer.entity_active_attr", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"}
+ )
+
+ start_calls = async_mock_service(hass, "timer", SERVICE_START)
+ pause_calls = async_mock_service(hass, "timer", SERVICE_PAUSE)
+ cancel_calls = async_mock_service(hass, "timer", SERVICE_CANCEL)
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("timer.entity_idle", STATUS_IDLE),
+ State("timer.entity_paused", STATUS_PAUSED),
+ State("timer.entity_active", STATUS_ACTIVE),
+ State(
+ "timer.entity_active_attr", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"}
+ ),
+ ],
+ blocking=True,
+ )
+
+ assert len(start_calls) == 0
+ assert len(pause_calls) == 0
+ assert len(cancel_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("timer.entity_idle", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(start_calls) == 0
+ assert len(pause_calls) == 0
+ assert len(cancel_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("timer.entity_idle", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"}),
+ State("timer.entity_paused", STATUS_ACTIVE),
+ State("timer.entity_active", STATUS_IDLE),
+ State("timer.entity_active_attr", STATUS_PAUSED),
+ # Should not raise
+ State("timer.non_existing", "on"),
+ ],
+ blocking=True,
+ )
+
+ valid_start_calls = [
+ {"entity_id": "timer.entity_idle", ATTR_DURATION: "00:01:00"},
+ {"entity_id": "timer.entity_paused"},
+ ]
+ assert len(start_calls) == 2
+ for call in start_calls:
+ assert call.domain == "timer"
+ assert call.data in valid_start_calls
+ valid_start_calls.remove(call.data)
+
+ assert len(pause_calls) == 1
+ assert pause_calls[0].domain == "timer"
+ assert pause_calls[0].data == {"entity_id": "timer.entity_active_attr"}
+
+ assert len(cancel_calls) == 1
+ assert cancel_calls[0].domain == "timer"
+ assert cancel_calls[0].data == {"entity_id": "timer.entity_active"}
diff --git a/tests/components/tplink/test_device_tracker.py b/tests/components/tplink/test_device_tracker.py
deleted file mode 100644
index bbe73dc121a..00000000000
--- a/tests/components/tplink/test_device_tracker.py
+++ /dev/null
@@ -1,71 +0,0 @@
-"""The tests for the tplink device tracker platform."""
-
-import os
-import pytest
-
-from homeassistant.components.device_tracker.legacy import YAML_DEVICES
-from homeassistant.components.tplink.device_tracker import Tplink4DeviceScanner
-from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST
-import requests_mock
-
-
-@pytest.fixture(autouse=True)
-def setup_comp(hass):
- """Initialize components."""
- yaml_devices = hass.config.path(YAML_DEVICES)
- yield
- if os.path.isfile(yaml_devices):
- os.remove(yaml_devices)
-
-
-async def test_get_mac_addresses_from_both_bands(hass):
- """Test grabbing the mac addresses from 2.4 and 5 GHz clients pages."""
- with requests_mock.Mocker() as m:
- conf_dict = {
- CONF_PLATFORM: "tplink",
- CONF_HOST: "fake-host",
- CONF_USERNAME: "fake_user",
- CONF_PASSWORD: "fake_pass",
- }
-
- # Mock the token retrieval process
- FAKE_TOKEN = "fake_token"
- fake_auth_token_response = (
- "window.parent.location.href = "
- '"https://a/{}/userRpm/Index.htm";'.format(FAKE_TOKEN)
- )
-
- m.get(
- "http://{}/userRpm/LoginRpm.htm?Save=Save".format(conf_dict[CONF_HOST]),
- text=fake_auth_token_response,
- )
-
- FAKE_MAC_1 = "CA-FC-8A-C8-BB-53"
- FAKE_MAC_2 = "6C-48-83-21-46-8D"
- FAKE_MAC_3 = "77-98-75-65-B1-2B"
- mac_response_2_4 = "{} {}".format(FAKE_MAC_1, FAKE_MAC_2)
- mac_response_5 = "{}".format(FAKE_MAC_3)
-
- # Mock the 2.4 GHz clients page
- m.get(
- "http://{}/{}/userRpm/WlanStationRpm.htm".format(
- conf_dict[CONF_HOST], FAKE_TOKEN
- ),
- text=mac_response_2_4,
- )
-
- # Mock the 5 GHz clients page
- m.get(
- "http://{}/{}/userRpm/WlanStationRpm_5g.htm".format(
- conf_dict[CONF_HOST], FAKE_TOKEN
- ),
- text=mac_response_5,
- )
-
- tplink = Tplink4DeviceScanner(conf_dict)
-
- expected_mac_results = [
- mac.replace("-", ":") for mac in [FAKE_MAC_1, FAKE_MAC_2, FAKE_MAC_3]
- ]
-
- assert tplink.last_results == expected_mac_results
diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py
index df9bf2c2ca2..9428bf05483 100644
--- a/tests/components/tplink/test_init.py
+++ b/tests/components/tplink/test_init.py
@@ -1,21 +1,22 @@
"""Tests for the TP-Link component."""
-from typing import Dict, Any
+from typing import Any, Dict
from unittest.mock import MagicMock, patch
+from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug
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_DISCOVERY,
CONF_LIGHT,
CONF_SWITCH,
)
from homeassistant.const import CONF_HOST
from homeassistant.setup import async_setup_component
-from tests.common import MockDependency, MockConfigEntry, mock_coro
+
+from tests.common import MockConfigEntry, MockDependency, mock_coro
MOCK_PYHS100 = MockDependency("pyHS100")
@@ -25,7 +26,10 @@ async def test_creating_entry_tries_discover(hass):
with MOCK_PYHS100, patch(
"homeassistant.components.tplink.async_setup_entry",
return_value=mock_coro(True),
- ) as mock_setup, patch("pyHS100.Discover.discover", return_value={"host": 1234}):
+ ) as mock_setup, patch(
+ "homeassistant.components.tplink.common.Discover.discover",
+ return_value={"host": 1234},
+ ):
result = await hass.config_entries.flow.async_init(
tplink.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@@ -43,7 +47,9 @@ async def test_creating_entry_tries_discover(hass):
async def test_configuring_tplink_causes_discovery(hass):
"""Test that specifying empty config does discovery."""
- with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover:
+ with MOCK_PYHS100, patch(
+ "homeassistant.components.tplink.common.Discover.discover"
+ ) as discover:
discover.return_value = {"host": 1234}
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done()
@@ -61,8 +67,10 @@ async def test_configuring_tplink_causes_discovery(hass):
@pytest.mark.parametrize("count", [1, 2, 3])
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"
+ with patch(
+ "homeassistant.components.tplink.common.Discover.discover"
+ ) as discover, patch(
+ "homeassistant.components.tplink.common.SmartDevice._query_helper"
):
discovery_data = {
"123.123.123.{}".format(c): cls("123.123.123.123") for c in range(count)
@@ -104,8 +112,10 @@ class UnknownSmartDevice(SmartDevice):
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"
+ with patch(
+ "homeassistant.components.tplink.common.Discover.discover"
+ ) as discover, patch(
+ "homeassistant.components.tplink.common.SmartDevice._query_helper"
):
discover_device_fail = SmartPlug("123.123.123.123")
discover_device_fail.get_sysinfo = MagicMock(side_effect=SmartDeviceException())
@@ -139,11 +149,15 @@ async def test_configuring_devices_from_multiple_sources(hass):
async def test_is_dimmable(hass):
"""Test that is_dimmable switches are correctly added as lights."""
- with patch("pyHS100.Discover.discover") as discover, patch(
+ with patch(
+ "homeassistant.components.tplink.common.Discover.discover"
+ ) as discover, patch(
"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
+ ) as setup, patch(
+ "homeassistant.components.tplink.common.SmartDevice._query_helper"
+ ), patch(
+ "homeassistant.components.tplink.common.SmartPlug.is_dimmable", True
):
dimmable_switch = SmartPlug("123.123.123.123")
discover.return_value = {"host": dimmable_switch}
@@ -162,7 +176,9 @@ async def test_configuring_discovery_disabled(hass):
with MOCK_PYHS100, patch(
"homeassistant.components.tplink.async_setup_entry",
return_value=mock_coro(True),
- ) as mock_setup, patch("pyHS100.Discover.discover", return_value=[]) as discover:
+ ) as mock_setup, patch(
+ "homeassistant.components.tplink.common.Discover.discover", return_value=[]
+ ) as discover:
await async_setup_component(
hass, tplink.DOMAIN, {tplink.DOMAIN: {tplink.CONF_DISCOVERY: False}}
)
@@ -182,8 +198,10 @@ async def test_platforms_are_initialized(hass):
}
}
- with patch("pyHS100.Discover.discover") as discover, patch(
- "pyHS100.SmartDevice._query_helper"
+ with patch(
+ "homeassistant.components.tplink.common.Discover.discover"
+ ) as discover, patch(
+ "homeassistant.components.tplink.common.SmartDevice._query_helper"
), patch(
"homeassistant.components.tplink.light.async_setup_entry",
return_value=mock_coro(True),
@@ -191,7 +209,7 @@ async def test_platforms_are_initialized(hass):
"homeassistant.components.tplink.switch.async_setup_entry",
return_value=mock_coro(True),
) as switch_setup, patch(
- "pyHS100.SmartPlug.is_dimmable", False
+ "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False
):
# patching is_dimmable is necessray to avoid misdetection as light.
await async_setup_component(hass, tplink.DOMAIN, config)
@@ -221,7 +239,9 @@ async def test_unload(hass, platform):
entry = MockConfigEntry(domain=tplink.DOMAIN)
entry.add_to_hass(hass)
- with patch("pyHS100.SmartDevice._query_helper"), patch(
+ with patch(
+ "homeassistant.components.tplink.common.SmartDevice._query_helper"
+ ), patch(
"homeassistant.components.tplink.{}" ".async_setup_entry".format(platform),
return_value=mock_coro(True),
) as light_setup:
diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py
index e79f5c8ac96..28fbed9ff42 100644
--- a/tests/components/transmission/test_config_flow.py
+++ b/tests/components/transmission/test_config_flow.py
@@ -1,4 +1,4 @@
-"""Tests for Met.no config flow."""
+"""Tests for Transmission config flow."""
from datetime import timedelta
from unittest.mock import patch
@@ -31,6 +31,14 @@ PASSWORD = "password"
PORT = 9091
SCAN_INTERVAL = 10
+MOCK_ENTRY = {
+ CONF_NAME: NAME,
+ CONF_HOST: HOST,
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_PORT: PORT,
+}
+
@pytest.fixture(name="api")
def mock_transmission_api():
@@ -90,18 +98,10 @@ async def test_flow_works(hass, api):
assert result["data"][CONF_NAME] == NAME
assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_PORT] == PORT
- assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
+ # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
# test with all provided
- result = await flow.async_step_user(
- {
- CONF_NAME: NAME,
- CONF_HOST: HOST,
- CONF_USERNAME: USERNAME,
- CONF_PASSWORD: PASSWORD,
- CONF_PORT: PORT,
- }
- )
+ result = await flow.async_step_user(MOCK_ENTRY)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == NAME
@@ -110,7 +110,7 @@ async def test_flow_works(hass, api):
assert result["data"][CONF_USERNAME] == USERNAME
assert result["data"][CONF_PASSWORD] == PASSWORD
assert result["data"][CONF_PORT] == PORT
- assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
+ # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
async def test_options(hass):
@@ -118,14 +118,7 @@ async def test_options(hass):
entry = MockConfigEntry(
domain=DOMAIN,
title=CONF_NAME,
- data={
- "name": DEFAULT_NAME,
- "host": HOST,
- "username": USERNAME,
- "password": PASSWORD,
- "port": DEFAULT_PORT,
- "options": {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
- },
+ data=MOCK_ENTRY,
options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
)
flow = init_config_flow(hass)
@@ -157,7 +150,7 @@ async def test_import(hass, api):
assert result["data"][CONF_NAME] == DEFAULT_NAME
assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_PORT] == DEFAULT_PORT
- assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
+ assert result["data"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
# import with all
result = await flow.async_step_import(
@@ -177,18 +170,40 @@ async def test_import(hass, api):
assert result["data"][CONF_USERNAME] == USERNAME
assert result["data"][CONF_PASSWORD] == PASSWORD
assert result["data"][CONF_PORT] == PORT
- assert result["data"]["options"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL
+ assert result["data"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL
-async def test_integration_already_exists(hass, api):
- """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"}
+async def test_host_already_configured(hass, api):
+ """Test host is already configured."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=MOCK_ENTRY,
+ options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
)
+ entry.add_to_hass(hass)
+ flow = init_config_flow(hass)
+ result = await flow.async_step_user(MOCK_ENTRY)
+
assert result["type"] == "abort"
- assert result["reason"] == "one_instance_allowed"
+ assert result["reason"] == "already_configured"
+
+
+async def test_name_already_configured(hass, api):
+ """Test name is already configured."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=MOCK_ENTRY,
+ options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
+ )
+ entry.add_to_hass(hass)
+
+ mock_entry = MOCK_ENTRY.copy()
+ mock_entry[CONF_HOST] = "0.0.0.0"
+ flow = init_config_flow(hass)
+ result = await flow.async_step_user(mock_entry)
+
+ assert result["type"] == "form"
+ assert result["errors"] == {CONF_NAME: "name_exists"}
async def test_error_on_wrong_credentials(hass, auth_error):
diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py
new file mode 100644
index 00000000000..4baa00de7a7
--- /dev/null
+++ b/tests/components/transmission/test_init.py
@@ -0,0 +1,123 @@
+"""Tests for Transmission init."""
+
+from unittest.mock import patch
+
+import pytest
+from transmissionrpc.error import TransmissionError
+
+from homeassistant.components import transmission
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry, mock_coro
+
+MOCK_ENTRY = MockConfigEntry(
+ domain=transmission.DOMAIN,
+ data={
+ transmission.CONF_NAME: "Transmission",
+ transmission.CONF_HOST: "0.0.0.0",
+ transmission.CONF_USERNAME: "user",
+ transmission.CONF_PASSWORD: "pass",
+ transmission.CONF_PORT: 9091,
+ },
+)
+
+
+@pytest.fixture(name="api")
+def mock_transmission_api():
+ """Mock an api."""
+ with patch("transmissionrpc.Client"):
+ yield
+
+
+@pytest.fixture(name="auth_error")
+def mock_api_authentication_error():
+ """Mock an api."""
+ with patch(
+ "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized")
+ ):
+ yield
+
+
+@pytest.fixture(name="unknown_error")
+def mock_api_unknown_error():
+ """Mock an api."""
+ with patch("transmissionrpc.Client", side_effect=TransmissionError):
+ yield
+
+
+async def test_setup_with_no_config(hass):
+ """Test that we do not discover anything or try to set up a Transmission client."""
+ assert await async_setup_component(hass, transmission.DOMAIN, {}) is True
+ assert transmission.DOMAIN not in hass.data
+
+
+async def test_setup_with_config(hass, api):
+ """Test that we import the config and setup the client."""
+ config = {
+ transmission.DOMAIN: {
+ transmission.CONF_NAME: "Transmission",
+ transmission.CONF_HOST: "0.0.0.0",
+ transmission.CONF_USERNAME: "user",
+ transmission.CONF_PASSWORD: "pass",
+ transmission.CONF_PORT: 9091,
+ },
+ transmission.DOMAIN: {
+ transmission.CONF_NAME: "Transmission2",
+ transmission.CONF_HOST: "0.0.0.1",
+ transmission.CONF_USERNAME: "user",
+ transmission.CONF_PASSWORD: "pass",
+ transmission.CONF_PORT: 9091,
+ },
+ }
+ assert await async_setup_component(hass, transmission.DOMAIN, config) is True
+
+
+async def test_successful_config_entry(hass, api):
+ """Test that configured transmission is configured successfully."""
+
+ entry = MOCK_ENTRY
+ entry.add_to_hass(hass)
+
+ assert await transmission.async_setup_entry(hass, entry) is True
+ assert entry.options == {
+ transmission.CONF_SCAN_INTERVAL: transmission.DEFAULT_SCAN_INTERVAL
+ }
+
+
+async def test_setup_failed(hass):
+ """Test transmission failed due to an error."""
+
+ entry = MOCK_ENTRY
+ entry.add_to_hass(hass)
+
+ # test connection error raising ConfigEntryNotReady
+ with patch(
+ "transmissionrpc.Client",
+ side_effect=TransmissionError("111: Connection refused"),
+ ), pytest.raises(ConfigEntryNotReady):
+
+ await transmission.async_setup_entry(hass, entry)
+
+ # test Authentication error returning false
+
+ with patch(
+ "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized")
+ ):
+
+ assert await transmission.async_setup_entry(hass, entry) is False
+
+
+async def test_unload_entry(hass, api):
+ """Test removing transmission client."""
+ entry = MOCK_ENTRY
+ entry.add_to_hass(hass)
+
+ with patch.object(
+ hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True)
+ ) as unload_entry:
+ assert await transmission.async_setup_entry(hass, entry)
+
+ assert await transmission.async_unload_entry(hass, entry)
+ assert unload_entry.call_count == 2
+ assert entry.entry_id not in hass.data[transmission.DOMAIN]
diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py
new file mode 100644
index 00000000000..aea4d565f3d
--- /dev/null
+++ b/tests/components/unifi/test_config_flow.py
@@ -0,0 +1,265 @@
+"""Test UniFi config flow."""
+from asynctest import patch
+
+from homeassistant.components import unifi
+from homeassistant.components.unifi import config_flow
+from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID
+
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+
+from tests.common import MockConfigEntry
+
+import aiounifi
+
+
+async def test_flow_works(hass, aioclient_mock):
+ """Test config flow."""
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ aioclient_mock.post(
+ "https://1.2.3.4:1234/api/login",
+ json={"data": "login successful", "meta": {"rc": "ok"}},
+ headers={"content-type": "application/json"},
+ )
+
+ aioclient_mock.get(
+ "https://1.2.3.4:1234/api/self/sites",
+ json={
+ "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}],
+ "meta": {"rc": "ok"},
+ },
+ headers={"content-type": "application/json"},
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_HOST: "1.2.3.4",
+ CONF_USERNAME: "username",
+ CONF_PASSWORD: "password",
+ CONF_PORT: 1234,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "Site name"
+ assert result["data"] == {
+ CONF_CONTROLLER: {
+ CONF_HOST: "1.2.3.4",
+ CONF_USERNAME: "username",
+ CONF_PASSWORD: "password",
+ CONF_PORT: 1234,
+ CONF_SITE_ID: "site_id",
+ CONF_VERIFY_SSL: True,
+ }
+ }
+
+
+async def test_flow_works_multiple_sites(hass, aioclient_mock):
+ """Test config flow works when finding multiple sites."""
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ aioclient_mock.post(
+ "https://1.2.3.4:1234/api/login",
+ json={"data": "login successful", "meta": {"rc": "ok"}},
+ headers={"content-type": "application/json"},
+ )
+
+ aioclient_mock.get(
+ "https://1.2.3.4:1234/api/self/sites",
+ json={
+ "data": [
+ {"name": "default", "role": "admin", "desc": "site name"},
+ {"name": "site2", "role": "admin", "desc": "site2 name"},
+ ],
+ "meta": {"rc": "ok"},
+ },
+ headers={"content-type": "application/json"},
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_HOST: "1.2.3.4",
+ CONF_USERNAME: "username",
+ CONF_PASSWORD: "password",
+ CONF_PORT: 1234,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "site"
+ assert result["data_schema"]({"site": "site name"})
+ assert result["data_schema"]({"site": "site2 name"})
+
+
+async def test_flow_fails_site_already_configured(hass, aioclient_mock):
+ """Test config flow."""
+ entry = MockConfigEntry(
+ domain=unifi.DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}}
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ aioclient_mock.post(
+ "https://1.2.3.4:1234/api/login",
+ json={"data": "login successful", "meta": {"rc": "ok"}},
+ headers={"content-type": "application/json"},
+ )
+
+ aioclient_mock.get(
+ "https://1.2.3.4:1234/api/self/sites",
+ json={
+ "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}],
+ "meta": {"rc": "ok"},
+ },
+ headers={"content-type": "application/json"},
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_HOST: "1.2.3.4",
+ CONF_USERNAME: "username",
+ CONF_PASSWORD: "password",
+ CONF_PORT: 1234,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+
+ assert result["type"] == "abort"
+
+
+async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock):
+ """Test config flow."""
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.Unauthorized):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_HOST: "1.2.3.4",
+ CONF_USERNAME: "username",
+ CONF_PASSWORD: "password",
+ CONF_PORT: 1234,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"] == {"base": "faulty_credentials"}
+
+
+async def test_flow_fails_controller_unavailable(hass, aioclient_mock):
+ """Test config flow."""
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.RequestError):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_HOST: "1.2.3.4",
+ CONF_USERNAME: "username",
+ CONF_PASSWORD: "password",
+ CONF_PORT: 1234,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"] == {"base": "service_unavailable"}
+
+
+async def test_flow_fails_unknown_problem(hass, aioclient_mock):
+ """Test config flow."""
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ with patch("aiounifi.Controller.login", side_effect=Exception):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_HOST: "1.2.3.4",
+ CONF_USERNAME: "username",
+ CONF_PASSWORD: "password",
+ CONF_PORT: 1234,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+
+ assert result["type"] == "abort"
+
+
+async def test_option_flow(hass):
+ """Test config flow options."""
+ entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None)
+ hass.config_entries._entries.append(entry)
+
+ flow = await hass.config_entries.options._async_create_flow(
+ entry.entry_id, context={"source": "test"}, data=None
+ )
+
+ result = await flow.async_step_init()
+ assert result["type"] == "form"
+ assert result["step_id"] == "device_tracker"
+
+ result = await flow.async_step_device_tracker(
+ user_input={
+ config_flow.CONF_TRACK_CLIENTS: False,
+ config_flow.CONF_TRACK_WIRED_CLIENTS: False,
+ config_flow.CONF_TRACK_DEVICES: False,
+ config_flow.CONF_DETECTION_TIME: 100,
+ }
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "statistics_sensors"
+
+ result = await flow.async_step_statistics_sensors(
+ user_input={config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True}
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ config_flow.CONF_TRACK_CLIENTS: False,
+ config_flow.CONF_TRACK_WIRED_CLIENTS: False,
+ config_flow.CONF_TRACK_DEVICES: False,
+ config_flow.CONF_DETECTION_TIME: 100,
+ config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ }
diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py
index e73719205f7..2b64e56cd99 100644
--- a/tests/components/unifi/test_controller.py
+++ b/tests/components/unifi/test_controller.py
@@ -1,9 +1,14 @@
"""Test UniFi Controller."""
-from unittest.mock import Mock, patch
+from collections import deque
+from datetime import timedelta
+
+from asynctest import Mock, patch
import pytest
+from homeassistant import config_entries
from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.components import unifi
from homeassistant.components.unifi.const import (
CONF_CONTROLLER,
CONF_SITE_ID,
@@ -17,259 +22,362 @@ from homeassistant.const import (
CONF_USERNAME,
CONF_VERIFY_SSL,
)
-from homeassistant.components.unifi import controller, errors
+import aiounifi
-from tests.common import mock_coro
-
-CONTROLLER_SITES = {"site1": {"desc": "nice name", "name": "site", "role": "admin"}}
+CONTROLLER_HOST = {
+ "hostname": "controller_host",
+ "ip": "1.2.3.4",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "10:00:00:00:00:01",
+ "name": "Controller host",
+ "oui": "Producer",
+ "sw_mac": "00:00:00:00:01:01",
+ "sw_port": 1,
+ "wired-rx_bytes": 1234000000,
+ "wired-tx_bytes": 5678000000,
+}
CONTROLLER_DATA = {
CONF_HOST: "1.2.3.4",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
CONF_PORT: 1234,
- CONF_SITE_ID: "site",
- CONF_VERIFY_SSL: True,
+ CONF_SITE_ID: "site_id",
+ CONF_VERIFY_SSL: False,
}
ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA}
+SITES = {"Site name": {"desc": "Site name", "name": "site_id", "role": "admin"}}
-async def test_controller_setup():
+
+async def setup_unifi_integration(
+ hass,
+ config,
+ options,
+ sites,
+ clients_response,
+ devices_response,
+ clients_all_response,
+):
+ """Create the UniFi controller."""
+ if UNIFI_CONFIG not in hass.data:
+ hass.data[UNIFI_CONFIG] = []
+ hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass)
+ config_entry = config_entries.ConfigEntry(
+ version=1,
+ domain=unifi.DOMAIN,
+ title="Mock Title",
+ data=config,
+ source="test",
+ connection_class=config_entries.CONN_CLASS_LOCAL_POLL,
+ system_options={},
+ options=options,
+ entry_id=1,
+ )
+
+ mock_client_responses = deque()
+ mock_client_responses.append(clients_response)
+
+ mock_device_responses = deque()
+ mock_device_responses.append(devices_response)
+
+ mock_client_all_responses = deque()
+ mock_client_all_responses.append(clients_all_response)
+
+ mock_requests = []
+
+ async def mock_request(self, method, path, json=None):
+ mock_requests.append({"method": method, "path": path, "json": json})
+
+ if path == "s/{site}/stat/sta" and mock_client_responses:
+ return mock_client_responses.popleft()
+ if path == "s/{site}/stat/device" and mock_device_responses:
+ return mock_device_responses.popleft()
+ if path == "s/{site}/rest/user" and mock_client_all_responses:
+ return mock_client_all_responses.popleft()
+ return {}
+
+ with patch("aiounifi.Controller.login", return_value=True), patch(
+ "aiounifi.Controller.sites", return_value=sites
+ ), patch("aiounifi.Controller.request", new=mock_request):
+ await unifi.async_setup_entry(hass, config_entry)
+ await hass.async_block_till_done()
+ hass.config_entries._entries.append(config_entry)
+
+ controller_id = unifi.get_controller_id_from_config_entry(config_entry)
+ if controller_id not in hass.data[unifi.DOMAIN]:
+ return None
+ controller = hass.data[unifi.DOMAIN][controller_id]
+
+ controller.mock_client_responses = mock_client_responses
+ controller.mock_device_responses = mock_device_responses
+ controller.mock_client_all_responses = mock_client_all_responses
+ controller.mock_requests = mock_requests
+
+ return controller
+
+
+async def test_controller_setup(hass):
"""Successful setup."""
- hass = Mock()
- hass.data = {
- UNIFI_CONFIG: [
- {
- CONF_HOST: CONTROLLER_DATA[CONF_HOST],
- CONF_SITE_ID: "nice name",
- controller.CONF_BLOCK_CLIENT: ["mac"],
- controller.CONF_DONT_TRACK_CLIENTS: True,
- controller.CONF_DONT_TRACK_DEVICES: True,
- controller.CONF_DONT_TRACK_WIRED_CLIENTS: True,
- controller.CONF_DETECTION_TIME: 30,
- controller.CONF_SSID_FILTER: ["ssid"],
- }
- ],
- UNIFI_WIRELESS_CLIENTS: Mock(),
- }
- entry = Mock()
- entry.data = ENTRY_CONFIG
- entry.options = {}
- api = Mock()
- api.initialize.return_value = mock_coro(True)
- api.sites.return_value = mock_coro(CONTROLLER_SITES)
- api.clients = []
+ with patch(
+ "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
+ return_value=True,
+ ) as forward_entry_setup:
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
- unifi_controller = controller.UniFiController(hass, entry)
+ entry = controller.config_entry
+ assert len(forward_entry_setup.mock_calls) == len(
+ unifi.controller.SUPPORTED_PLATFORMS
+ )
+ assert forward_entry_setup.mock_calls[0][1] == (entry, "device_tracker")
+ assert forward_entry_setup.mock_calls[1][1] == (entry, "sensor")
+ assert forward_entry_setup.mock_calls[2][1] == (entry, "switch")
- with patch.object(controller, "get_controller", return_value=mock_coro(api)):
- assert await unifi_controller.async_setup() is True
+ assert controller.host == CONTROLLER_DATA[CONF_HOST]
+ assert controller.site == CONTROLLER_DATA[CONF_SITE_ID]
+ assert controller.site_name in SITES
+ assert controller.site_role == SITES[controller.site_name]["role"]
- assert unifi_controller.api is api
- assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2
- assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == (
- entry,
- "device_tracker",
+ assert (
+ controller.option_allow_bandwidth_sensors
+ == unifi.const.DEFAULT_ALLOW_BANDWIDTH_SENSORS
)
- assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == (
- entry,
- "switch",
+ assert controller.option_block_clients == unifi.const.DEFAULT_BLOCK_CLIENTS
+ assert controller.option_track_clients == unifi.const.DEFAULT_TRACK_CLIENTS
+ assert controller.option_track_devices == unifi.const.DEFAULT_TRACK_DEVICES
+ assert (
+ controller.option_track_wired_clients == unifi.const.DEFAULT_TRACK_WIRED_CLIENTS
)
+ assert controller.option_detection_time == timedelta(
+ seconds=unifi.const.DEFAULT_DETECTION_TIME
+ )
+ assert controller.option_ssid_filter == unifi.const.DEFAULT_SSID_FILTER
+
+ assert controller.mac is None
+
+ assert controller.signal_update == "unifi-update-1.2.3.4-site_id"
+ assert controller.signal_options_update == "unifi-options-1.2.3.4-site_id"
-async def test_controller_host():
- """Config entry host and controller host are the same."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
-
- unifi_controller = controller.UniFiController(hass, entry)
-
- assert unifi_controller.host == CONTROLLER_DATA[CONF_HOST]
-
-
-async def test_controller_site():
- """Config entry site and controller site are the same."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
-
- unifi_controller = controller.UniFiController(hass, entry)
-
- assert unifi_controller.site == CONTROLLER_DATA[CONF_SITE_ID]
-
-
-async def test_controller_mac():
+async def test_controller_mac(hass):
"""Test that it is possible to identify controller mac."""
- hass = Mock()
- hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()}
- hass.data[UNIFI_WIRELESS_CLIENTS].get_data.return_value = set()
- entry = Mock()
- entry.data = ENTRY_CONFIG
- entry.options = {}
- client = Mock()
- client.ip = "1.2.3.4"
- client.mac = "00:11:22:33:44:55"
- api = Mock()
- api.initialize.return_value = mock_coro(True)
- api.clients = {"client1": client}
- api.sites.return_value = mock_coro(CONTROLLER_SITES)
-
- unifi_controller = controller.UniFiController(hass, entry)
-
- with patch.object(controller, "get_controller", return_value=mock_coro(api)):
- assert await unifi_controller.async_setup() is True
-
- assert unifi_controller.mac == "00:11:22:33:44:55"
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[CONTROLLER_HOST],
+ devices_response=[],
+ clients_all_response=[],
+ )
+ assert controller.mac == "10:00:00:00:00:01"
-async def test_controller_no_mac():
- """Test that it works to not find the controllers mac."""
- hass = Mock()
- hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()}
- entry = Mock()
- entry.data = ENTRY_CONFIG
- entry.options = {}
- client = Mock()
- client.ip = "5.6.7.8"
- api = Mock()
- api.initialize.return_value = mock_coro(True)
- api.clients = {"client1": client}
- api.sites.return_value = mock_coro(CONTROLLER_SITES)
- api.clients = {}
+async def test_controller_import_config(hass):
+ """Test that import configuration.yaml instructions work."""
+ hass.data[UNIFI_CONFIG] = [
+ {
+ CONF_HOST: "1.2.3.4",
+ CONF_SITE_ID: "Site name",
+ unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ unifi.CONF_BLOCK_CLIENT: ["random mac"],
+ unifi.CONF_DONT_TRACK_CLIENTS: True,
+ unifi.CONF_DONT_TRACK_DEVICES: True,
+ unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True,
+ unifi.CONF_DETECTION_TIME: 150,
+ unifi.CONF_SSID_FILTER: ["SSID"],
+ }
+ ]
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
- unifi_controller = controller.UniFiController(hass, entry)
-
- with patch.object(controller, "get_controller", return_value=mock_coro(api)):
- assert await unifi_controller.async_setup() is True
-
- assert unifi_controller.mac is None
+ assert controller.option_allow_bandwidth_sensors is False
+ assert controller.option_block_clients == ["random mac"]
+ assert controller.option_track_clients is False
+ assert controller.option_track_devices is False
+ assert controller.option_track_wired_clients is False
+ assert controller.option_detection_time == timedelta(seconds=150)
+ assert controller.option_ssid_filter == ["SSID"]
-async def test_controller_not_accessible():
+async def test_controller_not_accessible(hass):
"""Retry to login gets scheduled when connection fails."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
- api = Mock()
- api.initialize.return_value = mock_coro(True)
-
- unifi_controller = controller.UniFiController(hass, entry)
-
with patch.object(
- controller, "get_controller", side_effect=errors.CannotConnect
+ unifi.controller, "get_controller", side_effect=unifi.errors.CannotConnect
), pytest.raises(ConfigEntryNotReady):
- await unifi_controller.async_setup()
+ await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
-async def test_controller_unknown_error():
+async def test_controller_unknown_error(hass):
"""Unknown errors are handled."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
- api = Mock()
- api.initialize.return_value = mock_coro(True)
-
- unifi_controller = controller.UniFiController(hass, entry)
-
- with patch.object(controller, "get_controller", side_effect=Exception):
- assert await unifi_controller.async_setup() is False
-
- assert not hass.helpers.event.async_call_later.mock_calls
+ with patch.object(unifi.controller, "get_controller", side_effect=Exception):
+ await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
+ assert hass.data[unifi.DOMAIN] == {}
-async def test_reset_if_entry_had_wrong_auth():
- """Calling reset when the entry contains wrong auth."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
+async def test_reset_after_successful_setup(hass):
+ """Calling reset when the entry has been setup."""
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
- unifi_controller = controller.UniFiController(hass, entry)
+ assert len(controller.listeners) == 5
+
+ result = await controller.async_reset()
+ await hass.async_block_till_done()
+
+ assert result is True
+ assert len(controller.listeners) == 0
+
+
+async def test_failed_update_failed_login(hass):
+ """Running update can handle a failed login."""
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
with patch.object(
- controller, "get_controller", side_effect=errors.AuthenticationRequired
+ controller.api.clients, "update", side_effect=aiounifi.LoginRequired
+ ), patch.object(controller.api, "login", side_effect=aiounifi.AiounifiException):
+ await controller.async_update()
+ await hass.async_block_till_done()
+
+ assert controller.available is False
+
+
+async def test_failed_update_successful_login(hass):
+ """Running update can login when requested."""
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
+
+ with patch.object(
+ controller.api.clients, "update", side_effect=aiounifi.LoginRequired
+ ), patch.object(controller.api, "login", return_value=Mock(True)):
+ await controller.async_update()
+ await hass.async_block_till_done()
+
+ assert controller.available is True
+
+
+async def test_failed_update(hass):
+ """Running update can login when requested."""
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
+
+ with patch.object(
+ controller.api.clients, "update", side_effect=aiounifi.AiounifiException
):
- assert await unifi_controller.async_setup() is False
+ await controller.async_update()
+ await hass.async_block_till_done()
- assert not hass.async_add_job.mock_calls
+ assert controller.available is False
- assert await unifi_controller.async_reset()
-
-
-async def test_reset_unloads_entry_if_setup():
- """Calling reset when the entry has been setup."""
- hass = Mock()
- hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()}
- entry = Mock()
- entry.data = ENTRY_CONFIG
- entry.options = {}
- api = Mock()
- api.initialize.return_value = mock_coro(True)
- api.sites.return_value = mock_coro(CONTROLLER_SITES)
- api.clients = []
-
- unifi_controller = controller.UniFiController(hass, entry)
-
- with patch.object(controller, "get_controller", return_value=mock_coro(api)):
- assert await unifi_controller.async_setup() is True
-
- assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2
-
- hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True)
- assert await unifi_controller.async_reset()
-
- assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 2
+ await controller.async_update()
+ await hass.async_block_till_done()
+ assert controller.available is True
async def test_get_controller(hass):
"""Successful call."""
- with patch("aiounifi.Controller.login", return_value=mock_coro()):
- assert await controller.get_controller(hass, **CONTROLLER_DATA)
+ with patch("aiounifi.Controller.login", return_value=Mock()):
+ assert await unifi.controller.get_controller(hass, **CONTROLLER_DATA)
async def test_get_controller_verify_ssl_false(hass):
"""Successful call with verify ssl set to false."""
controller_data = dict(CONTROLLER_DATA)
controller_data[CONF_VERIFY_SSL] = False
- with patch("aiounifi.Controller.login", return_value=mock_coro()):
- assert await controller.get_controller(hass, **controller_data)
+ with patch("aiounifi.Controller.login", return_value=Mock()):
+ assert await unifi.controller.get_controller(hass, **controller_data)
async def test_get_controller_login_failed(hass):
"""Check that get_controller can handle a failed login."""
- import aiounifi
-
result = None
with patch("aiounifi.Controller.login", side_effect=aiounifi.Unauthorized):
try:
- result = await controller.get_controller(hass, **CONTROLLER_DATA)
- except errors.AuthenticationRequired:
+ result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA)
+ except unifi.errors.AuthenticationRequired:
pass
assert result is None
async def test_get_controller_controller_unavailable(hass):
"""Check that get_controller can handle controller being unavailable."""
- import aiounifi
-
result = None
with patch("aiounifi.Controller.login", side_effect=aiounifi.RequestError):
try:
- result = await controller.get_controller(hass, **CONTROLLER_DATA)
- except errors.CannotConnect:
+ result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA)
+ except unifi.errors.CannotConnect:
pass
assert result is None
async def test_get_controller_unknown_error(hass):
"""Check that get_controller can handle unkown errors."""
- import aiounifi
-
result = None
with patch("aiounifi.Controller.login", side_effect=aiounifi.AiounifiException):
try:
- result = await controller.get_controller(hass, **CONTROLLER_DATA)
- except errors.AuthenticationRequired:
+ result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA)
+ except unifi.errors.AuthenticationRequired:
pass
assert result is None
diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py
index 3a2b37487af..29b16553757 100644
--- a/tests/components/unifi/test_device_tracker.py
+++ b/tests/components/unifi/test_device_tracker.py
@@ -1,42 +1,23 @@
"""The tests for the UniFi device tracker platform."""
-from collections import deque
from copy import copy
-
from datetime import timedelta
-from asynctest import Mock
-
-import pytest
-
-from aiounifi.clients import Clients, ClientsAll
-from aiounifi.devices import Devices
-
from homeassistant import config_entries
from homeassistant.components import unifi
from homeassistant.components.unifi.const import (
- CONF_CONTROLLER,
- CONF_SITE_ID,
CONF_SSID_FILTER,
CONF_TRACK_DEVICES,
CONF_TRACK_WIRED_CLIENTS,
- CONTROLLER_ID as CONF_CONTROLLER_ID,
- UNIFI_CONFIG,
- UNIFI_WIRELESS_CLIENTS,
-)
-from homeassistant.const import (
- CONF_HOST,
- CONF_PASSWORD,
- CONF_PORT,
- CONF_USERNAME,
- CONF_VERIFY_SSL,
- STATE_UNAVAILABLE,
)
+from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.helpers import entity_registry
from homeassistant.setup import async_setup_component
import homeassistant.components.device_tracker as device_tracker
import homeassistant.util.dt as dt_util
+from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration
+
DEFAULT_DETECTION_TIME = timedelta(seconds=300)
CLIENT_1 = {
@@ -93,79 +74,6 @@ DEVICE_2 = {
"version": "4.0.42.10433",
}
-CONTROLLER_DATA = {
- CONF_HOST: "mock-host",
- CONF_USERNAME: "mock-user",
- CONF_PASSWORD: "mock-pswd",
- CONF_PORT: 1234,
- CONF_SITE_ID: "mock-site",
- CONF_VERIFY_SSL: False,
-}
-
-ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA}
-
-CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site")
-
-
-@pytest.fixture
-def mock_controller(hass):
- """Mock a UniFi Controller."""
- hass.data[UNIFI_CONFIG] = {}
- hass.data[UNIFI_WIRELESS_CLIENTS] = Mock()
- controller = unifi.UniFiController(hass, None)
- controller.wireless_clients = set()
-
- controller.api = Mock()
- controller.mock_requests = []
-
- controller.mock_client_responses = deque()
- controller.mock_device_responses = deque()
- controller.mock_client_all_responses = deque()
-
- async def mock_request(method, path, **kwargs):
- kwargs["method"] = method
- kwargs["path"] = path
- controller.mock_requests.append(kwargs)
- if path == "s/{site}/stat/sta":
- return controller.mock_client_responses.popleft()
- if path == "s/{site}/stat/device":
- return controller.mock_device_responses.popleft()
- if path == "s/{site}/rest/user":
- return controller.mock_client_all_responses.popleft()
- return None
-
- controller.api.clients = Clients({}, mock_request)
- controller.api.devices = Devices({}, mock_request)
- controller.api.clients_all = ClientsAll({}, mock_request)
-
- return controller
-
-
-async def setup_controller(hass, mock_controller, options={}):
- """Load the UniFi switch platform with the provided controller."""
- hass.config.components.add(unifi.DOMAIN)
- hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller}
- config_entry = config_entries.ConfigEntry(
- 1,
- unifi.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_POLL,
- entry_id=1,
- system_options={},
- options=options,
- )
- hass.config_entries._entries.append(config_entry)
- mock_controller.config_entry = config_entry
-
- await mock_controller.async_update()
- await hass.config_entries.async_forward_entry_setup(
- config_entry, device_tracker.DOMAIN
- )
-
- await hass.async_block_till_done()
-
async def test_platform_manually_configured(hass):
"""Test that nothing happens when configuring unifi through device tracker platform."""
@@ -178,24 +86,32 @@ async def test_platform_manually_configured(hass):
assert unifi.DOMAIN not in hass.data
-async def test_no_clients(hass, mock_controller):
+async def test_no_clients(hass):
"""Test the update_clients function when no clients are found."""
- mock_controller.mock_client_responses.append({})
- mock_controller.mock_device_responses.append({})
+ await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
- await setup_controller(hass, mock_controller)
- assert len(mock_controller.mock_requests) == 2
assert len(hass.states.async_all()) == 2
-async def test_tracked_devices(hass, mock_controller):
+async def test_tracked_devices(hass):
"""Test the update_items function with some clients."""
- mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2, CLIENT_3])
- mock_controller.mock_device_responses.append([DEVICE_1, DEVICE_2])
- options = {CONF_SSID_FILTER: ["ssid"]}
-
- await setup_controller(hass, mock_controller, options)
- assert len(mock_controller.mock_requests) == 2
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={CONF_SSID_FILTER: ["ssid"]},
+ sites=SITES,
+ clients_response=[CLIENT_1, CLIENT_2, CLIENT_3],
+ devices_response=[DEVICE_1, DEVICE_2],
+ clients_all_response={},
+ )
assert len(hass.states.async_all()) == 5
client_1 = hass.states.get("device_tracker.client_1")
@@ -217,9 +133,9 @@ async def test_tracked_devices(hass, mock_controller):
client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
device_1_copy = copy(DEVICE_1)
device_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
- mock_controller.mock_client_responses.append([client_1_copy])
- mock_controller.mock_device_responses.append([device_1_copy])
- await mock_controller.async_update()
+ controller.mock_client_responses.append([client_1_copy])
+ controller.mock_device_responses.append([device_1_copy])
+ await controller.async_update()
await hass.async_block_till_done()
client_1 = hass.states.get("device_tracker.client_1")
@@ -230,19 +146,17 @@ async def test_tracked_devices(hass, mock_controller):
device_1_copy = copy(DEVICE_1)
device_1_copy["disabled"] = True
- mock_controller.mock_client_responses.append({})
- mock_controller.mock_device_responses.append([device_1_copy])
- await mock_controller.async_update()
+ controller.mock_client_responses.append({})
+ controller.mock_device_responses.append([device_1_copy])
+ await controller.async_update()
await hass.async_block_till_done()
device_1 = hass.states.get("device_tracker.device_1")
assert device_1.state == STATE_UNAVAILABLE
- mock_controller.config_entry.add_update_listener(
- mock_controller.async_options_updated
- )
+ controller.config_entry.add_update_listener(controller.async_options_updated)
hass.config_entries.async_update_entry(
- mock_controller.config_entry,
+ controller.config_entry,
options={
CONF_SSID_FILTER: [],
CONF_TRACK_WIRED_CLIENTS: False,
@@ -258,18 +172,23 @@ async def test_tracked_devices(hass, mock_controller):
assert device_1 is None
-async def test_wireless_client_go_wired_issue(hass, mock_controller):
+async def test_wireless_client_go_wired_issue(hass):
"""Test the solution to catch wireless device go wired UniFi issue.
UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired.
"""
client_1_client = copy(CLIENT_1)
client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
- mock_controller.mock_client_responses.append([client_1_client])
- mock_controller.mock_device_responses.append({})
- await setup_controller(hass, mock_controller)
- assert len(mock_controller.mock_requests) == 2
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=[client_1_client],
+ devices_response=[],
+ clients_all_response=[],
+ )
assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1")
@@ -278,9 +197,9 @@ async def test_wireless_client_go_wired_issue(hass, mock_controller):
client_1_client["is_wired"] = True
client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
- mock_controller.mock_client_responses.append([client_1_client])
- mock_controller.mock_device_responses.append({})
- await mock_controller.async_update()
+ controller.mock_client_responses.append([client_1_client])
+ controller.mock_device_responses.append({})
+ await controller.async_update()
await hass.async_block_till_done()
client_1 = hass.states.get("device_tracker.client_1")
@@ -288,65 +207,71 @@ async def test_wireless_client_go_wired_issue(hass, mock_controller):
client_1_client["is_wired"] = False
client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
- mock_controller.mock_client_responses.append([client_1_client])
- mock_controller.mock_device_responses.append({})
- await mock_controller.async_update()
+ controller.mock_client_responses.append([client_1_client])
+ controller.mock_device_responses.append({})
+ await controller.async_update()
await hass.async_block_till_done()
client_1 = hass.states.get("device_tracker.client_1")
assert client_1.state == "home"
-async def test_restoring_client(hass, mock_controller):
+async def test_restoring_client(hass):
"""Test the update_items function with some clients."""
- mock_controller.mock_client_responses.append([CLIENT_2])
- mock_controller.mock_device_responses.append({})
- mock_controller.mock_client_all_responses.append([CLIENT_1])
- options = {unifi.CONF_BLOCK_CLIENT: True}
-
config_entry = config_entries.ConfigEntry(
- 1,
- unifi.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_POLL,
- entry_id=1,
+ version=1,
+ domain=unifi.DOMAIN,
+ title="Mock Title",
+ data=ENTRY_CONFIG,
+ source="test",
+ connection_class=config_entries.CONN_CLASS_LOCAL_POLL,
system_options={},
+ options={},
+ entry_id=1,
)
registry = await entity_registry.async_get_registry(hass)
registry.async_get_or_create(
device_tracker.DOMAIN,
unifi.DOMAIN,
- "{}-mock-site".format(CLIENT_1["mac"]),
+ "{}-site_id".format(CLIENT_1["mac"]),
suggested_object_id=CLIENT_1["hostname"],
config_entry=config_entry,
)
registry.async_get_or_create(
device_tracker.DOMAIN,
unifi.DOMAIN,
- "{}-mock-site".format(CLIENT_2["mac"]),
+ "{}-site_id".format(CLIENT_2["mac"]),
suggested_object_id=CLIENT_2["hostname"],
config_entry=config_entry,
)
- await setup_controller(hass, mock_controller, options)
- assert len(mock_controller.mock_requests) == 3
+ await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={unifi.CONF_BLOCK_CLIENT: True},
+ sites=SITES,
+ clients_response=[CLIENT_2],
+ devices_response=[],
+ clients_all_response=[CLIENT_1],
+ )
assert len(hass.states.async_all()) == 4
device_1 = hass.states.get("device_tracker.client_1")
assert device_1 is not None
-async def test_dont_track_clients(hass, mock_controller):
+async def test_dont_track_clients(hass):
"""Test dont track clients config works."""
- mock_controller.mock_client_responses.append([CLIENT_1])
- mock_controller.mock_device_responses.append([DEVICE_1])
- options = {unifi.controller.CONF_TRACK_CLIENTS: False}
-
- await setup_controller(hass, mock_controller, options)
- assert len(mock_controller.mock_requests) == 2
+ await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={unifi.controller.CONF_TRACK_CLIENTS: False},
+ sites=SITES,
+ clients_response=[CLIENT_1],
+ devices_response=[DEVICE_1],
+ clients_all_response=[],
+ )
assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1")
@@ -357,14 +282,17 @@ async def test_dont_track_clients(hass, mock_controller):
assert device_1.state == "not_home"
-async def test_dont_track_devices(hass, mock_controller):
+async def test_dont_track_devices(hass):
"""Test dont track devices config works."""
- mock_controller.mock_client_responses.append([CLIENT_1])
- mock_controller.mock_device_responses.append([DEVICE_1])
- options = {unifi.controller.CONF_TRACK_DEVICES: False}
-
- await setup_controller(hass, mock_controller, options)
- assert len(mock_controller.mock_requests) == 2
+ await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={unifi.controller.CONF_TRACK_DEVICES: False},
+ sites=SITES,
+ clients_response=[CLIENT_1],
+ devices_response=[DEVICE_1],
+ clients_all_response=[],
+ )
assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1")
@@ -375,14 +303,17 @@ async def test_dont_track_devices(hass, mock_controller):
assert device_1 is None
-async def test_dont_track_wired_clients(hass, mock_controller):
+async def test_dont_track_wired_clients(hass):
"""Test dont track wired clients config works."""
- mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2])
- mock_controller.mock_device_responses.append({})
- options = {unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}
-
- await setup_controller(hass, mock_controller, options)
- assert len(mock_controller.mock_requests) == 2
+ await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False},
+ sites=SITES,
+ clients_response=[CLIENT_1, CLIENT_2],
+ devices_response=[],
+ clients_all_response=[],
+ )
assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1")
diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py
index ffd6d97e5b3..6b17b803390 100644
--- a/tests/components/unifi/test_init.py
+++ b/tests/components/unifi/test_init.py
@@ -2,20 +2,9 @@
from unittest.mock import Mock, patch
from homeassistant.components import unifi
-from homeassistant.components.unifi import config_flow
+
from homeassistant.setup import async_setup_component
-from homeassistant.components.unifi.const import (
- CONF_CONTROLLER,
- CONF_SITE_ID,
- CONTROLLER_ID as CONF_CONTROLLER_ID,
-)
-from homeassistant.const import (
- CONF_HOST,
- CONF_PASSWORD,
- CONF_PORT,
- CONF_USERNAME,
- CONF_VERIFY_SSL,
-)
+
from tests.common import mock_coro, MockConfigEntry
@@ -117,8 +106,7 @@ async def test_controller_fail_setup(hass):
mock_cntrlr.return_value.async_setup.return_value = mock_coro(False)
assert await unifi.async_setup_entry(hass, entry) is False
- controller_id = CONF_CONTROLLER_ID.format(host="0.0.0.0", site="default")
- assert controller_id in hass.data[unifi.DOMAIN]
+ assert hass.data[unifi.DOMAIN] == {}
async def test_controller_no_mac(hass):
@@ -184,164 +172,3 @@ async def test_unload_entry(hass):
assert await unifi.async_unload_entry(hass, entry)
assert len(mock_controller.return_value.async_reset.mock_calls) == 1
assert hass.data[unifi.DOMAIN] == {}
-
-
-async def test_flow_works(hass, aioclient_mock):
- """Test config flow."""
- flow = config_flow.UnifiFlowHandler()
- flow.hass = hass
-
- with patch("aiounifi.Controller") as mock_controller:
-
- def mock_constructor(
- host, username, password, port, site, websession, sslcontext
- ):
- """Fake the controller constructor."""
- mock_controller.host = host
- mock_controller.username = username
- mock_controller.password = password
- mock_controller.port = port
- mock_controller.site = site
- return mock_controller
-
- mock_controller.side_effect = mock_constructor
- mock_controller.login.return_value = mock_coro()
- mock_controller.sites.return_value = mock_coro(
- {"site1": {"name": "default", "role": "admin", "desc": "site name"}}
- )
-
- await flow.async_step_user(
- user_input={
- CONF_HOST: "1.2.3.4",
- CONF_USERNAME: "username",
- CONF_PASSWORD: "password",
- CONF_PORT: 1234,
- CONF_VERIFY_SSL: True,
- }
- )
-
- result = await flow.async_step_site(user_input={})
-
- assert mock_controller.host == "1.2.3.4"
- assert len(mock_controller.login.mock_calls) == 1
- assert len(mock_controller.sites.mock_calls) == 1
-
- assert result["type"] == "create_entry"
- assert result["title"] == "site name"
- assert result["data"] == {
- CONF_CONTROLLER: {
- CONF_HOST: "1.2.3.4",
- CONF_USERNAME: "username",
- CONF_PASSWORD: "password",
- CONF_PORT: 1234,
- CONF_SITE_ID: "default",
- CONF_VERIFY_SSL: True,
- }
- }
-
-
-async def test_controller_multiple_sites(hass):
- """Test config flow."""
- flow = config_flow.UnifiFlowHandler()
- flow.hass = hass
-
- flow.config = {
- CONF_HOST: "1.2.3.4",
- CONF_USERNAME: "username",
- CONF_PASSWORD: "password",
- }
- flow.sites = {
- "site1": {"name": "default", "role": "admin", "desc": "site name"},
- "site2": {"name": "site2", "role": "admin", "desc": "site2 name"},
- }
-
- result = await flow.async_step_site()
-
- assert result["type"] == "form"
- assert result["step_id"] == "site"
-
- assert result["data_schema"]({"site": "site name"})
- assert result["data_schema"]({"site": "site2 name"})
-
-
-async def test_controller_site_already_configured(hass):
- """Test config flow."""
- flow = config_flow.UnifiFlowHandler()
- flow.hass = hass
-
- entry = MockConfigEntry(
- domain=unifi.DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "default"}}
- )
- entry.add_to_hass(hass)
-
- flow.config = {
- CONF_HOST: "1.2.3.4",
- CONF_USERNAME: "username",
- CONF_PASSWORD: "password",
- }
- flow.desc = "site name"
- flow.sites = {"site1": {"name": "default", "role": "admin", "desc": "site name"}}
-
- result = await flow.async_step_site()
-
- assert result["type"] == "abort"
-
-
-async def test_user_credentials_faulty(hass, aioclient_mock):
- """Test config flow."""
- flow = config_flow.UnifiFlowHandler()
- flow.hass = hass
-
- with patch.object(
- config_flow, "get_controller", side_effect=unifi.errors.AuthenticationRequired
- ):
- result = await flow.async_step_user(
- {
- CONF_HOST: "1.2.3.4",
- CONF_USERNAME: "username",
- CONF_PASSWORD: "password",
- CONF_SITE_ID: "default",
- }
- )
-
- assert result["type"] == "form"
- assert result["errors"] == {"base": "faulty_credentials"}
-
-
-async def test_controller_is_unavailable(hass, aioclient_mock):
- """Test config flow."""
- flow = config_flow.UnifiFlowHandler()
- flow.hass = hass
-
- with patch.object(
- config_flow, "get_controller", side_effect=unifi.errors.CannotConnect
- ):
- result = await flow.async_step_user(
- {
- CONF_HOST: "1.2.3.4",
- CONF_USERNAME: "username",
- CONF_PASSWORD: "password",
- CONF_SITE_ID: "default",
- }
- )
-
- assert result["type"] == "form"
- assert result["errors"] == {"base": "service_unavailable"}
-
-
-async def test_controller_unkown_problem(hass, aioclient_mock):
- """Test config flow."""
- flow = config_flow.UnifiFlowHandler()
- flow.hass = hass
-
- with patch.object(config_flow, "get_controller", side_effect=Exception):
- result = await flow.async_step_user(
- {
- CONF_HOST: "1.2.3.4",
- CONF_USERNAME: "username",
- CONF_PASSWORD: "password",
- CONF_SITE_ID: "default",
- }
- )
-
- assert result["type"] == "abort"
diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py
new file mode 100644
index 00000000000..f591801a966
--- /dev/null
+++ b/tests/components/unifi/test_sensor.py
@@ -0,0 +1,112 @@
+"""UniFi sensor platform tests."""
+from copy import deepcopy
+
+from homeassistant.components import unifi
+from homeassistant.setup import async_setup_component
+
+import homeassistant.components.sensor as sensor
+
+from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration
+
+CLIENTS = [
+ {
+ "hostname": "Wired client hostname",
+ "ip": "10.0.0.1",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ "name": "Wired client name",
+ "oui": "Producer",
+ "sw_mac": "00:00:00:00:01:01",
+ "sw_port": 1,
+ "wired-rx_bytes": 1234000000,
+ "wired-tx_bytes": 5678000000,
+ },
+ {
+ "hostname": "Wireless client hostname",
+ "ip": "10.0.0.2",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:02",
+ "name": "Wireless client name",
+ "oui": "Producer",
+ "sw_mac": "00:00:00:00:01:01",
+ "sw_port": 2,
+ "rx_bytes": 1234000000,
+ "tx_bytes": 5678000000,
+ },
+]
+
+
+async def test_platform_manually_configured(hass):
+ """Test that we do not discover anything or try to set up a controller."""
+ assert (
+ await async_setup_component(
+ hass, sensor.DOMAIN, {sensor.DOMAIN: {"platform": "unifi"}}
+ )
+ is True
+ )
+ assert unifi.DOMAIN not in hass.data
+
+
+async def test_no_clients(hass):
+ """Test the update_clients function when no clients are found."""
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True},
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
+
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 2
+
+
+async def test_sensors(hass):
+ """Test the update_items function with some clients."""
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={
+ unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ unifi.const.CONF_TRACK_CLIENTS: False,
+ unifi.const.CONF_TRACK_DEVICES: False,
+ },
+ sites=SITES,
+ clients_response=CLIENTS,
+ devices_response=[],
+ clients_all_response=[],
+ )
+
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 6
+
+ wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
+ assert wired_client_rx.state == "1234.0"
+
+ wired_client_tx = hass.states.get("sensor.wired_client_name_tx")
+ assert wired_client_tx.state == "5678.0"
+
+ wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
+ assert wireless_client_rx.state == "1234.0"
+
+ wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
+ assert wireless_client_tx.state == "5678.0"
+
+ clients = deepcopy(CLIENTS)
+ clients[0]["is_wired"] = False
+ clients[1]["rx_bytes"] = 2345000000
+ clients[1]["tx_bytes"] = 6789000000
+
+ controller.mock_client_responses.append(clients)
+ await controller.async_update()
+ await hass.async_block_till_done()
+
+ wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
+ assert wireless_client_rx.state == "2345.0"
+
+ wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
+ assert wireless_client_tx.state == "6789.0"
diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py
index 7ea5e0680b9..3d754fb5dff 100644
--- a/tests/components/unifi/test_switch.py
+++ b/tests/components/unifi/test_switch.py
@@ -1,40 +1,25 @@
"""UniFi POE control platform tests."""
-from collections import deque
-from unittest.mock import Mock
-
-import pytest
-
-from tests.common import mock_coro
-
-import aiounifi
-from aiounifi.clients import Clients, ClientsAll
-from aiounifi.devices import Devices
+from copy import deepcopy
from homeassistant import config_entries
from homeassistant.components import unifi
-from homeassistant.components.unifi.const import (
- CONF_CONTROLLER,
- CONF_SITE_ID,
- CONTROLLER_ID as CONF_CONTROLLER_ID,
- UNIFI_CONFIG,
- UNIFI_WIRELESS_CLIENTS,
-)
from homeassistant.helpers import entity_registry
from homeassistant.setup import async_setup_component
-from homeassistant.const import (
- CONF_HOST,
- CONF_PASSWORD,
- CONF_PORT,
- CONF_USERNAME,
- CONF_VERIFY_SSL,
-)
import homeassistant.components.switch as switch
+from .test_controller import (
+ CONTROLLER_HOST,
+ ENTRY_CONFIG,
+ SITES,
+ setup_unifi_integration,
+)
+
CLIENT_1 = {
"hostname": "client_1",
"ip": "10.0.0.1",
"is_wired": True,
+ "last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
"name": "POE Client 1",
"oui": "Producer",
@@ -47,6 +32,7 @@ CLIENT_2 = {
"hostname": "client_2",
"ip": "10.0.0.2",
"is_wired": True,
+ "last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
"name": "POE Client 2",
"oui": "Producer",
@@ -59,6 +45,7 @@ CLIENT_3 = {
"hostname": "client_3",
"ip": "10.0.0.3",
"is_wired": True,
+ "last_seen": 1562600145,
"mac": "00:00:00:00:00:03",
"name": "Non-POE Client 3",
"oui": "Producer",
@@ -71,6 +58,7 @@ CLIENT_4 = {
"hostname": "client_4",
"ip": "10.0.0.4",
"is_wired": True,
+ "last_seen": 1562600145,
"mac": "00:00:00:00:00:04",
"name": "Non-POE Client 4",
"oui": "Producer",
@@ -79,23 +67,12 @@ CLIENT_4 = {
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
}
-CLOUDKEY = {
- "hostname": "client_1",
- "ip": "mock-host",
- "is_wired": True,
- "mac": "10:00:00:00:00:01",
- "name": "Cloud key",
- "oui": "Producer",
- "sw_mac": "00:00:00:00:01:01",
- "sw_port": 1,
- "wired-rx_bytes": 1234000000,
- "wired-tx_bytes": 5678000000,
-}
POE_SWITCH_CLIENTS = [
{
"hostname": "client_1",
"ip": "10.0.0.1",
"is_wired": True,
+ "last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
"name": "POE Client 1",
"oui": "Producer",
@@ -108,6 +85,7 @@ POE_SWITCH_CLIENTS = [
"hostname": "client_2",
"ip": "10.0.0.2",
"is_wired": True,
+ "last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
"name": "POE Client 2",
"oui": "Producer",
@@ -122,7 +100,8 @@ DEVICE_1 = {
"device_id": "mock-id",
"ip": "10.0.1.1",
"mac": "00:00:00:00:01:01",
- "type": "usw",
+ "last_seen": 1562600145,
+ "model": "US16P150",
"name": "mock-name",
"port_overrides": [],
"port_table": [
@@ -179,6 +158,9 @@ DEVICE_1 = {
"up": True,
},
],
+ "state": 1,
+ "type": "usw",
+ "version": "4.0.42.10433",
}
BLOCKED = {
@@ -204,139 +186,102 @@ UNBLOCKED = {
"oui": "Producer",
}
-CONTROLLER_DATA = {
- CONF_HOST: "mock-host",
- CONF_USERNAME: "mock-user",
- CONF_PASSWORD: "mock-pswd",
- CONF_PORT: 1234,
- CONF_SITE_ID: "mock-site",
- CONF_VERIFY_SSL: True,
-}
-
-ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA}
-
-CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site")
-
-
-@pytest.fixture
-def mock_controller(hass):
- """Mock a UniFi Controller."""
- hass.data[UNIFI_CONFIG] = {}
- hass.data[UNIFI_WIRELESS_CLIENTS] = Mock()
- controller = unifi.UniFiController(hass, None)
- controller.wireless_clients = set()
-
- controller._site_role = "admin"
-
- controller.api = Mock()
- controller.mock_requests = []
-
- controller.mock_client_responses = deque()
- controller.mock_device_responses = deque()
- controller.mock_client_all_responses = deque()
-
- async def mock_request(method, path, **kwargs):
- kwargs["method"] = method
- kwargs["path"] = path
- controller.mock_requests.append(kwargs)
- if path == "s/{site}/stat/sta":
- return controller.mock_client_responses.popleft()
- if path == "s/{site}/stat/device":
- return controller.mock_device_responses.popleft()
- if path == "s/{site}/rest/user":
- return controller.mock_client_all_responses.popleft()
- return None
-
- controller.api.clients = Clients({}, mock_request)
- controller.api.devices = Devices({}, mock_request)
- controller.api.clients_all = ClientsAll({}, mock_request)
-
- return controller
-
-
-async def setup_controller(hass, mock_controller, options={}):
- """Load the UniFi switch platform with the provided controller."""
- hass.config.components.add(unifi.DOMAIN)
- hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller}
- config_entry = config_entries.ConfigEntry(
- 1,
- unifi.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_POLL,
- entry_id=1,
- system_options={},
- options=options,
- )
- mock_controller.config_entry = config_entry
-
- await mock_controller.async_update()
- await hass.config_entries.async_forward_entry_setup(config_entry, "switch")
- # To flush out the service call to update the group
- await hass.async_block_till_done()
-
async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a bridge."""
+ """Test that we do not discover anything or try to set up a controller."""
assert (
await async_setup_component(
- hass, switch.DOMAIN, {"switch": {"platform": "unifi"}}
+ hass, switch.DOMAIN, {switch.DOMAIN: {"platform": "unifi"}}
)
is True
)
assert unifi.DOMAIN not in hass.data
-async def test_no_clients(hass, mock_controller):
+async def test_no_clients(hass):
"""Test the update_clients function when no clients are found."""
- mock_controller.mock_client_responses.append({})
- mock_controller.mock_device_responses.append({})
- await setup_controller(hass, mock_controller)
- assert len(mock_controller.mock_requests) == 2
- assert not hass.states.async_all()
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={
+ unifi.const.CONF_TRACK_CLIENTS: False,
+ unifi.const.CONF_TRACK_DEVICES: False,
+ },
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[],
+ )
+
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 2
-async def test_controller_not_client(hass, mock_controller):
+async def test_controller_not_client(hass):
"""Test that the controller doesn't become a switch."""
- mock_controller.mock_client_responses.append([CLOUDKEY])
- mock_controller.mock_device_responses.append([DEVICE_1])
- await setup_controller(hass, mock_controller)
- assert len(mock_controller.mock_requests) == 2
- assert not hass.states.async_all()
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={
+ unifi.const.CONF_TRACK_CLIENTS: False,
+ unifi.const.CONF_TRACK_DEVICES: False,
+ },
+ sites=SITES,
+ clients_response=[CONTROLLER_HOST],
+ devices_response=[DEVICE_1],
+ clients_all_response=[],
+ )
+
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 2
cloudkey = hass.states.get("switch.cloud_key")
assert cloudkey is None
-async def test_not_admin(hass, mock_controller):
+async def test_not_admin(hass):
"""Test that switch platform only work on an admin account."""
- mock_controller.mock_client_responses.append([CLIENT_1])
- mock_controller.mock_device_responses.append([])
+ sites = deepcopy(SITES)
+ sites["Site name"]["role"] = "not admin"
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={
+ unifi.const.CONF_TRACK_CLIENTS: False,
+ unifi.const.CONF_TRACK_DEVICES: False,
+ },
+ sites=sites,
+ clients_response=[CLIENT_1],
+ devices_response=[DEVICE_1],
+ clients_all_response=[],
+ )
- mock_controller._site_role = "viewer"
-
- await setup_controller(hass, mock_controller)
- assert len(mock_controller.mock_requests) == 2
- assert len(hass.states.async_all()) == 0
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 2
-async def test_switches(hass, mock_controller):
+async def test_switches(hass):
"""Test the update_items function with some clients."""
- mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_4])
- mock_controller.mock_device_responses.append([DEVICE_1])
- mock_controller.mock_client_all_responses.append([BLOCKED, UNBLOCKED, CLIENT_1])
- options = {unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]}
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={
+ unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]],
+ unifi.const.CONF_TRACK_CLIENTS: False,
+ unifi.const.CONF_TRACK_DEVICES: False,
+ },
+ sites=SITES,
+ clients_response=[CLIENT_1, CLIENT_4],
+ devices_response=[DEVICE_1],
+ clients_all_response=[BLOCKED, UNBLOCKED, CLIENT_1],
+ )
- await setup_controller(hass, mock_controller, options)
- assert len(mock_controller.mock_requests) == 3
- assert len(hass.states.async_all()) == 4
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 6
switch_1 = hass.states.get("switch.poe_client_1")
assert switch_1 is not None
assert switch_1.state == "on"
assert switch_1.attributes["power"] == "2.56"
- assert switch_1.attributes["received"] == 1234
- assert switch_1.attributes["sent"] == 5678
assert switch_1.attributes["switch"] == "00:00:00:00:01:01"
assert switch_1.attributes["port"] == 1
assert switch_1.attributes["poe_mode"] == "auto"
@@ -353,25 +298,78 @@ async def test_switches(hass, mock_controller):
assert unblocked.state == "on"
-async def test_new_client_discovered(hass, mock_controller):
+async def test_new_client_discovered_on_block_control(hass):
"""Test if 2nd update has a new client."""
- mock_controller.mock_client_responses.append([CLIENT_1])
- mock_controller.mock_device_responses.append([DEVICE_1])
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={
+ unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"]],
+ unifi.const.CONF_TRACK_CLIENTS: False,
+ unifi.const.CONF_TRACK_DEVICES: False,
+ },
+ sites=SITES,
+ clients_response=[],
+ devices_response=[],
+ clients_all_response=[BLOCKED],
+ )
- await setup_controller(hass, mock_controller)
- assert len(mock_controller.mock_requests) == 2
- assert len(hass.states.async_all()) == 2
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 4
- mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2])
- mock_controller.mock_device_responses.append([DEVICE_1])
+ controller.mock_client_all_responses.append([BLOCKED])
+
+ # Calling a service will trigger the updates to run
+ await hass.services.async_call(
+ "switch", "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True
+ )
+ assert len(controller.mock_requests) == 7
+ assert len(hass.states.async_all()) == 4
+ assert controller.mock_requests[3] == {
+ "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"},
+ "method": "post",
+ "path": "s/{site}/cmd/stamgr/",
+ }
+
+ await hass.services.async_call(
+ "switch", "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True
+ )
+ assert len(controller.mock_requests) == 11
+ assert controller.mock_requests[7] == {
+ "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"},
+ "method": "post",
+ "path": "s/{site}/cmd/stamgr/",
+ }
+
+
+async def test_new_client_discovered_on_poe_control(hass):
+ """Test if 2nd update has a new client."""
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={
+ unifi.const.CONF_TRACK_CLIENTS: False,
+ unifi.const.CONF_TRACK_DEVICES: False,
+ },
+ sites=SITES,
+ clients_response=[CLIENT_1],
+ devices_response=[DEVICE_1],
+ clients_all_response=[],
+ )
+
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 4
+
+ controller.mock_client_responses.append([CLIENT_1, CLIENT_2])
+ controller.mock_device_responses.append([DEVICE_1])
# Calling a service will trigger the updates to run
await hass.services.async_call(
"switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True
)
- assert len(mock_controller.mock_requests) == 5
- assert len(hass.states.async_all()) == 3
- assert mock_controller.mock_requests[2] == {
+ assert len(controller.mock_requests) == 6
+ assert len(hass.states.async_all()) == 5
+ assert controller.mock_requests[3] == {
"json": {
"port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}]
},
@@ -382,8 +380,8 @@ async def test_new_client_discovered(hass, mock_controller):
await hass.services.async_call(
"switch", "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True
)
- assert len(mock_controller.mock_requests) == 7
- assert mock_controller.mock_requests[5] == {
+ assert len(controller.mock_requests) == 9
+ assert controller.mock_requests[3] == {
"json": {
"port_overrides": [
{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"}
@@ -398,66 +396,24 @@ async def test_new_client_discovered(hass, mock_controller):
assert switch_2.state == "on"
-async def test_failed_update_successful_login(hass, mock_controller):
- """Running update can login when requested."""
- mock_controller.available = False
- mock_controller.api.clients.update = Mock()
- mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired
- mock_controller.api.login = Mock()
- mock_controller.api.login.return_value = mock_coro()
-
- await setup_controller(hass, mock_controller)
- assert len(mock_controller.mock_requests) == 0
-
- assert mock_controller.available is True
-
-
-async def test_failed_update_failed_login(hass, mock_controller):
- """Running update can handle a failed login."""
- mock_controller.api.clients.update = Mock()
- mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired
- mock_controller.api.login = Mock()
- mock_controller.api.login.side_effect = aiounifi.AiounifiException
-
- await setup_controller(hass, mock_controller)
- assert len(mock_controller.mock_requests) == 0
-
- assert mock_controller.available is False
-
-
-async def test_failed_update_unreachable_controller(hass, mock_controller):
- """Running update can handle a unreachable controller."""
- mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2])
- mock_controller.mock_device_responses.append([DEVICE_1])
-
- await setup_controller(hass, mock_controller)
-
- mock_controller.api.clients.update = Mock()
- mock_controller.api.clients.update.side_effect = aiounifi.AiounifiException
-
- # Calling a service will trigger the updates to run
- await hass.services.async_call(
- "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True
- )
-
- assert len(mock_controller.mock_requests) == 3
- assert len(hass.states.async_all()) == 3
-
- assert mock_controller.available is False
-
-
-async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller):
+async def test_ignore_multiple_poe_clients_on_same_port(hass):
"""Ignore when there are multiple POE driven clients on same port.
If there is a non-UniFi switch powered by POE,
clients will be transparently marked as having POE as well.
"""
- mock_controller.mock_client_responses.append(POE_SWITCH_CLIENTS)
- mock_controller.mock_device_responses.append([DEVICE_1])
- await setup_controller(hass, mock_controller)
- assert len(mock_controller.mock_requests) == 2
- # 1 All Lights group, 2 lights
- assert len(hass.states.async_all()) == 0
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={},
+ sites=SITES,
+ clients_response=POE_SWITCH_CLIENTS,
+ devices_response=[DEVICE_1],
+ clients_all_response=[],
+ )
+
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 5
switch_1 = hass.states.get("switch.poe_client_1")
switch_2 = hass.states.get("switch.poe_client_2")
@@ -465,22 +421,18 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller):
assert switch_2 is None
-async def test_restoring_client(hass, mock_controller):
+async def test_restoring_client(hass):
"""Test the update_items function with some clients."""
- mock_controller.mock_client_responses.append([CLIENT_2])
- mock_controller.mock_device_responses.append([DEVICE_1])
- mock_controller.mock_client_all_responses.append([CLIENT_1])
- options = {unifi.CONF_BLOCK_CLIENT: ["random mac"]}
-
config_entry = config_entries.ConfigEntry(
- 1,
- unifi.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_POLL,
- entry_id=1,
+ version=1,
+ domain=unifi.DOMAIN,
+ title="Mock Title",
+ data=ENTRY_CONFIG,
+ source="test",
+ connection_class=config_entries.CONN_CLASS_LOCAL_POLL,
system_options={},
+ options={},
+ entry_id=1,
)
registry = await entity_registry.async_get_registry(hass)
@@ -499,9 +451,22 @@ async def test_restoring_client(hass, mock_controller):
config_entry=config_entry,
)
- await setup_controller(hass, mock_controller, options)
- assert len(mock_controller.mock_requests) == 3
- assert len(hass.states.async_all()) == 3
+ controller = await setup_unifi_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={
+ unifi.CONF_BLOCK_CLIENT: ["random mac"],
+ unifi.const.CONF_TRACK_CLIENTS: False,
+ unifi.const.CONF_TRACK_DEVICES: False,
+ },
+ sites=SITES,
+ clients_response=[CLIENT_2],
+ devices_response=[DEVICE_1],
+ clients_all_response=[CLIENT_1],
+ )
+
+ assert len(controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 5
device_1 = hass.states.get("switch.client_1")
assert device_1 is not None
diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py
index 5f17606146b..5e2106ff208 100644
--- a/tests/components/upnp/test_init.py
+++ b/tests/components/upnp/test_init.py
@@ -59,17 +59,20 @@ async def test_async_setup_entry_default(hass):
}
with MockDependency("netdisco.discovery"), patch(
"homeassistant.components.upnp.get_local_ip", return_value="192.168.1.10"
- ):
+ ), patch.object(Device, "async_create_device") as create_device, patch.object(
+ Device, "async_create_device"
+ ) as create_device, patch.object(
+ Device, "async_discover", return_value=mock_coro([])
+ ) as async_discover:
await async_setup_component(hass, "http", config)
await async_setup_component(hass, "upnp", config)
await hass.async_block_till_done()
- # mock homeassistant.components.upnp.device.Device
- mock_device = MockDevice(udn)
- discovery_infos = [{"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}]
- with patch.object(Device, "async_create_device") as create_device, patch.object(
- Device, "async_discover"
- ) as async_discover: # noqa:E125
+ # mock homeassistant.components.upnp.device.Device
+ mock_device = MockDevice(udn)
+ discovery_infos = [
+ {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}
+ ]
create_device.return_value = mock_coro(return_value=mock_device)
async_discover.return_value = mock_coro(return_value=discovery_infos)
@@ -100,16 +103,17 @@ async def test_async_setup_entry_port_mapping(hass):
}
with MockDependency("netdisco.discovery"), patch(
"homeassistant.components.upnp.get_local_ip", return_value="192.168.1.10"
- ):
+ ), patch.object(Device, "async_create_device") as create_device, patch.object(
+ Device, "async_discover", return_value=mock_coro([])
+ ) as async_discover:
await async_setup_component(hass, "http", config)
await async_setup_component(hass, "upnp", config)
await hass.async_block_till_done()
- mock_device = MockDevice(udn)
- discovery_infos = [{"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}]
- with patch.object(Device, "async_create_device") as create_device, patch.object(
- Device, "async_discover"
- ) as async_discover: # noqa:E125
+ mock_device = MockDevice(udn)
+ discovery_infos = [
+ {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}
+ ]
create_device.return_value = mock_coro(return_value=mock_device)
async_discover.return_value = mock_coro(return_value=discovery_infos)
diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py
new file mode 100644
index 00000000000..d5a7051e6a6
--- /dev/null
+++ b/tests/components/vacuum/test_reproduce_state.py
@@ -0,0 +1,139 @@
+"""Test reproduce state for Vacuum."""
+from homeassistant.components.vacuum import (
+ ATTR_FAN_SPEED,
+ SERVICE_PAUSE,
+ SERVICE_RETURN_TO_BASE,
+ SERVICE_SET_FAN_SPEED,
+ SERVICE_START,
+ SERVICE_STOP,
+ STATE_CLEANING,
+ STATE_DOCKED,
+ STATE_RETURNING,
+)
+from homeassistant.const import (
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_IDLE,
+ STATE_OFF,
+ STATE_ON,
+ STATE_PAUSED,
+)
+from homeassistant.core import State
+
+from tests.common import async_mock_service
+
+FAN_SPEED_LOW = "low"
+FAN_SPEED_HIGH = "high"
+
+
+async def test_reproducing_states(hass, caplog):
+ """Test reproducing Vacuum states."""
+ hass.states.async_set("vacuum.entity_off", STATE_OFF, {})
+ hass.states.async_set("vacuum.entity_on", STATE_ON, {})
+ hass.states.async_set(
+ "vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW}
+ )
+ hass.states.async_set("vacuum.entity_cleaning", STATE_CLEANING, {})
+ hass.states.async_set("vacuum.entity_docked", STATE_DOCKED, {})
+ hass.states.async_set("vacuum.entity_idle", STATE_IDLE, {})
+ hass.states.async_set("vacuum.entity_returning", STATE_RETURNING, {})
+ hass.states.async_set("vacuum.entity_paused", STATE_PAUSED, {})
+
+ turn_on_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_ON)
+ turn_off_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_OFF)
+ start_calls = async_mock_service(hass, "vacuum", SERVICE_START)
+ pause_calls = async_mock_service(hass, "vacuum", SERVICE_PAUSE)
+ stop_calls = async_mock_service(hass, "vacuum", SERVICE_STOP)
+ return_calls = async_mock_service(hass, "vacuum", SERVICE_RETURN_TO_BASE)
+ fan_speed_calls = async_mock_service(hass, "vacuum", SERVICE_SET_FAN_SPEED)
+
+ # These calls should do nothing as entities already in desired state
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("vacuum.entity_off", STATE_OFF),
+ State("vacuum.entity_on", STATE_ON),
+ State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW}),
+ State("vacuum.entity_cleaning", STATE_CLEANING),
+ State("vacuum.entity_docked", STATE_DOCKED),
+ State("vacuum.entity_idle", STATE_IDLE),
+ State("vacuum.entity_returning", STATE_RETURNING),
+ State("vacuum.entity_paused", STATE_PAUSED),
+ ],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+ assert len(start_calls) == 0
+ assert len(pause_calls) == 0
+ assert len(stop_calls) == 0
+ assert len(return_calls) == 0
+ assert len(fan_speed_calls) == 0
+
+ # Test invalid state is handled
+ await hass.helpers.state.async_reproduce_state(
+ [State("vacuum.entity_off", "not_supported")], blocking=True
+ )
+
+ assert "not_supported" in caplog.text
+ assert len(turn_on_calls) == 0
+ assert len(turn_off_calls) == 0
+ assert len(start_calls) == 0
+ assert len(pause_calls) == 0
+ assert len(stop_calls) == 0
+ assert len(return_calls) == 0
+ assert len(fan_speed_calls) == 0
+
+ # Make sure correct services are called
+ await hass.helpers.state.async_reproduce_state(
+ [
+ State("vacuum.entity_off", STATE_ON),
+ State("vacuum.entity_on", STATE_OFF),
+ State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_HIGH}),
+ State("vacuum.entity_cleaning", STATE_PAUSED),
+ State("vacuum.entity_docked", STATE_CLEANING),
+ State("vacuum.entity_idle", STATE_DOCKED),
+ State("vacuum.entity_returning", STATE_CLEANING),
+ State("vacuum.entity_paused", STATE_IDLE),
+ # Should not raise
+ State("vacuum.non_existing", STATE_ON),
+ ],
+ blocking=True,
+ )
+
+ assert len(turn_on_calls) == 1
+ assert turn_on_calls[0].domain == "vacuum"
+ assert turn_on_calls[0].data == {"entity_id": "vacuum.entity_off"}
+
+ assert len(turn_off_calls) == 1
+ assert turn_off_calls[0].domain == "vacuum"
+ assert turn_off_calls[0].data == {"entity_id": "vacuum.entity_on"}
+
+ assert len(start_calls) == 2
+ entities = [
+ {"entity_id": "vacuum.entity_docked"},
+ {"entity_id": "vacuum.entity_returning"},
+ ]
+ for call in start_calls:
+ assert call.domain == "vacuum"
+ assert call.data in entities
+ entities.remove(call.data)
+
+ assert len(pause_calls) == 1
+ assert pause_calls[0].domain == "vacuum"
+ assert pause_calls[0].data == {"entity_id": "vacuum.entity_cleaning"}
+
+ assert len(stop_calls) == 1
+ assert stop_calls[0].domain == "vacuum"
+ assert stop_calls[0].data == {"entity_id": "vacuum.entity_paused"}
+
+ assert len(return_calls) == 1
+ assert return_calls[0].domain == "vacuum"
+ assert return_calls[0].data == {"entity_id": "vacuum.entity_idle"}
+
+ assert len(fan_speed_calls) == 1
+ assert fan_speed_calls[0].domain == "vacuum"
+ assert fan_speed_calls[0].data == {
+ "entity_id": "vacuum.entity_on_fan",
+ ATTR_FAN_SPEED: FAN_SPEED_HIGH,
+ }
diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py
index d71f15e6109..c2ee0930895 100644
--- a/tests/components/wake_on_lan/test_init.py
+++ b/tests/components/wake_on_lan/test_init.py
@@ -1,52 +1,46 @@
"""Tests for Wake On LAN component."""
-import asyncio
-from unittest import mock
-
import pytest
import voluptuous as vol
-from homeassistant.setup import async_setup_component
+from homeassistant.components import wake_on_lan
from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockDependency
-@pytest.fixture
-def mock_wakeonlan():
- """Mock mock_wakeonlan."""
- module = mock.MagicMock()
- with mock.patch.dict("sys.modules", {"wakeonlan": module}):
- yield module
-
-
-@asyncio.coroutine
-def test_send_magic_packet(hass, caplog, mock_wakeonlan):
+async def test_send_magic_packet(hass):
"""Test of send magic packet service call."""
- mac = "aa:bb:cc:dd:ee:ff"
- bc_ip = "192.168.255.255"
+ with MockDependency("wakeonlan") as mocked_wakeonlan:
+ mac = "aa:bb:cc:dd:ee:ff"
+ bc_ip = "192.168.255.255"
- yield from async_setup_component(hass, DOMAIN, {})
+ wake_on_lan.wakeonlan = mocked_wakeonlan
- yield from hass.services.async_call(
- DOMAIN,
- SERVICE_SEND_MAGIC_PACKET,
- {"mac": mac, "broadcast_address": bc_ip},
- blocking=True,
- )
- assert len(mock_wakeonlan.mock_calls) == 1
- assert mock_wakeonlan.mock_calls[-1][1][0] == mac
- assert mock_wakeonlan.mock_calls[-1][2]["ip_address"] == bc_ip
+ await async_setup_component(hass, DOMAIN, {})
- with pytest.raises(vol.Invalid):
- yield from hass.services.async_call(
+ await hass.services.async_call(
DOMAIN,
SERVICE_SEND_MAGIC_PACKET,
- {"broadcast_address": bc_ip},
+ {"mac": mac, "broadcast_address": bc_ip},
blocking=True,
)
- assert len(mock_wakeonlan.mock_calls) == 1
+ assert len(mocked_wakeonlan.mock_calls) == 1
+ assert mocked_wakeonlan.mock_calls[-1][1][0] == mac
+ assert mocked_wakeonlan.mock_calls[-1][2]["ip_address"] == bc_ip
- yield from hass.services.async_call(
- DOMAIN, SERVICE_SEND_MAGIC_PACKET, {"mac": mac}, blocking=True
- )
- assert len(mock_wakeonlan.mock_calls) == 2
- assert mock_wakeonlan.mock_calls[-1][1][0] == mac
- assert not mock_wakeonlan.mock_calls[-1][2]
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SEND_MAGIC_PACKET,
+ {"broadcast_address": bc_ip},
+ blocking=True,
+ )
+ assert len(mocked_wakeonlan.mock_calls) == 1
+
+ await hass.services.async_call(
+ DOMAIN, SERVICE_SEND_MAGIC_PACKET, {"mac": mac}, blocking=True
+ )
+ assert len(mocked_wakeonlan.mock_calls) == 2
+ assert mocked_wakeonlan.mock_calls[-1][1][0] == mac
+ assert not mocked_wakeonlan.mock_calls[-1][2]
diff --git a/tests/components/websocket_api/__init__.py b/tests/components/websocket_api/__init__.py
index 56def1b7fd9..4904270cc72 100644
--- a/tests/components/websocket_api/__init__.py
+++ b/tests/components/websocket_api/__init__.py
@@ -1,2 +1 @@
"""Tests for the websocket API."""
-API_PASSWORD = "test-password"
diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py
index 2ee28c0cb20..382de3142e8 100644
--- a/tests/components/websocket_api/conftest.py
+++ b/tests/components/websocket_api/conftest.py
@@ -5,8 +5,6 @@ from homeassistant.setup import async_setup_component
from homeassistant.components.websocket_api.http import URL
from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED
-from . import API_PASSWORD
-
@pytest.fixture
def websocket_client(hass, hass_ws_client, hass_access_token):
@@ -17,11 +15,7 @@ def websocket_client(hass, hass_ws_client, hass_access_token):
@pytest.fixture
def no_auth_websocket_client(hass, loop, aiohttp_client):
"""Websocket connection that requires authentication."""
- assert loop.run_until_complete(
- async_setup_component(
- hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
- )
- )
+ assert loop.run_until_complete(async_setup_component(hass, "websocket_api", {}))
client = loop.run_until_complete(aiohttp_client(hass.http.app))
ws = loop.run_until_complete(client.ws_connect(URL))
diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py
index 19b9cbb2196..00387506020 100644
--- a/tests/components/websocket_api/test_auth.py
+++ b/tests/components/websocket_api/test_auth.py
@@ -17,21 +17,10 @@ from homeassistant.setup import async_setup_component
from tests.common import mock_coro
-from . import API_PASSWORD
-
-async def test_auth_via_msg(no_auth_websocket_client, legacy_auth):
- """Test authenticating."""
- await no_auth_websocket_client.send_json(
- {"type": TYPE_AUTH, "api_password": API_PASSWORD}
- )
-
- msg = await no_auth_websocket_client.receive_json()
-
- assert msg["type"] == TYPE_AUTH_OK
-
-
-async def test_auth_events(hass, no_auth_websocket_client, legacy_auth):
+async def test_auth_events(
+ hass, no_auth_websocket_client, legacy_auth, hass_access_token
+):
"""Test authenticating."""
connected_evt = []
hass.helpers.dispatcher.async_dispatcher_connect(
@@ -42,7 +31,7 @@ async def test_auth_events(hass, no_auth_websocket_client, legacy_auth):
SIGNAL_WEBSOCKET_DISCONNECTED, lambda: disconnected_evt.append(1)
)
- await test_auth_via_msg(no_auth_websocket_client, legacy_auth)
+ await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token)
assert len(connected_evt) == 1
assert not disconnected_evt
@@ -60,7 +49,7 @@ async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client):
return_value=mock_coro(),
) as mock_process_wrong_login:
await no_auth_websocket_client.send_json(
- {"type": TYPE_AUTH, "api_password": API_PASSWORD + "wrong"}
+ {"type": TYPE_AUTH, "api_password": "wrong"}
)
msg = await no_auth_websocket_client.receive_json()
@@ -110,31 +99,25 @@ async def test_pre_auth_only_auth_allowed(no_auth_websocket_client):
assert msg["message"].startswith("Auth message incorrectly formatted")
-async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token):
+async def test_auth_active_with_token(
+ hass, no_auth_websocket_client, hass_access_token
+):
"""Test authenticating with a token."""
- assert await async_setup_component(
- hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
+ assert await async_setup_component(hass, "websocket_api", {})
+
+ await no_auth_websocket_client.send_json(
+ {"type": TYPE_AUTH, "access_token": hass_access_token}
)
- client = await aiohttp_client(hass.http.app)
-
- async with client.ws_connect(URL) as ws:
- auth_msg = await ws.receive_json()
- assert auth_msg["type"] == TYPE_AUTH_REQUIRED
-
- await ws.send_json({"type": TYPE_AUTH, "access_token": hass_access_token})
-
- auth_msg = await ws.receive_json()
- assert auth_msg["type"] == TYPE_AUTH_OK
+ auth_msg = await no_auth_websocket_client.receive_json()
+ assert auth_msg["type"] == TYPE_AUTH_OK
async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token):
"""Test authenticating with a token."""
refresh_token = await hass.auth.async_validate_access_token(hass_access_token)
refresh_token.user.is_active = False
- assert await async_setup_component(
- hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
- )
+ assert await async_setup_component(hass, "websocket_api", {})
client = await aiohttp_client(hass.http.app)
@@ -150,9 +133,7 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token
async def test_auth_active_with_password_not_allow(hass, aiohttp_client):
"""Test authenticating with a token."""
- assert await async_setup_component(
- hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
- )
+ assert await async_setup_component(hass, "websocket_api", {})
client = await aiohttp_client(hass.http.app)
@@ -160,7 +141,7 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client):
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_REQUIRED
- await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD})
+ await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"})
auth_msg = await ws.receive_json()
assert auth_msg["type"] == TYPE_AUTH_INVALID
@@ -168,28 +149,23 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client):
async def test_auth_legacy_support_with_password(hass, aiohttp_client, legacy_auth):
"""Test authenticating with a token."""
- assert await async_setup_component(
- hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
- )
+ assert await async_setup_component(hass, "websocket_api", {})
client = await aiohttp_client(hass.http.app)
async with client.ws_connect(URL) as ws:
- with patch("homeassistant.auth.AuthManager.support_legacy", return_value=True):
- auth_msg = await ws.receive_json()
- assert auth_msg["type"] == TYPE_AUTH_REQUIRED
+ auth_msg = await ws.receive_json()
+ assert auth_msg["type"] == TYPE_AUTH_REQUIRED
- await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD})
+ await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"})
- auth_msg = await ws.receive_json()
- assert auth_msg["type"] == TYPE_AUTH_OK
+ auth_msg = await ws.receive_json()
+ assert auth_msg["type"] == TYPE_AUTH_INVALID
async def test_auth_with_invalid_token(hass, aiohttp_client):
"""Test authenticating with a token."""
- assert await async_setup_component(
- hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
- )
+ assert await async_setup_component(hass, "websocket_api", {})
client = await aiohttp_client(hass.http.app)
diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py
index a39a0a0e7a6..1de5b8bb2c1 100644
--- a/tests/components/websocket_api/test_commands.py
+++ b/tests/components/websocket_api/test_commands.py
@@ -14,8 +14,6 @@ from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
-from . import API_PASSWORD
-
async def test_call_service(hass, websocket_client):
"""Test call service command."""
@@ -250,9 +248,7 @@ async def test_ping(websocket_client):
async def test_call_service_context_with_user(hass, aiohttp_client, hass_access_token):
"""Test that the user is set in the service call context."""
- assert await async_setup_component(
- hass, "websocket_api", {"http": {"api_password": API_PASSWORD}}
- )
+ assert await async_setup_component(hass, "websocket_api", {})
calls = async_mock_service(hass, "domain_test", "test_service")
client = await aiohttp_client(hass.http.app)
diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py
index 873b9e7269c..84b73060698 100644
--- a/tests/components/websocket_api/test_sensor.py
+++ b/tests/components/websocket_api/test_sensor.py
@@ -3,10 +3,12 @@
from homeassistant.bootstrap import async_setup_component
from tests.common import assert_setup_component
-from .test_auth import test_auth_via_msg
+from .test_auth import test_auth_active_with_token
-async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth):
+async def test_websocket_api(
+ hass, no_auth_websocket_client, hass_access_token, legacy_auth
+):
"""Test API streams."""
with assert_setup_component(1):
await async_setup_component(
@@ -16,7 +18,7 @@ async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth):
state = hass.states.get("sensor.connected_clients")
assert state.state == "0"
- await test_auth_via_msg(no_auth_websocket_client, legacy_auth)
+ await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token)
state = hass.states.get("sensor.connected_clients")
assert state.state == "1"
diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py
index 50d945e7fae..7997d01bd13 100644
--- a/tests/components/yandex_transport/test_yandex_transport_sensor.py
+++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py
@@ -38,14 +38,14 @@ TEST_CONFIG = {
}
FILTERED_ATTRS = {
- "т36": ["21:43", "21:47", "22:02"],
- "т47": ["21:40", "22:01"],
- "м10": ["21:48", "22:00"],
+ "т36": ["16:10", "16:17", "16:26"],
+ "т47": ["16:09", "16:10"],
+ "м10": ["16:12", "16:20"],
"stop_name": "7-й автобусный парк",
"attribution": "Data provided by maps.yandex.ru",
}
-RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds")
+RESULT_STATE = dt_util.utc_from_timestamp(1570972183).isoformat(timespec="seconds")
async def assert_setup_sensor(hass, config, count=1):
diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py
index fc29e4012cd..788faaaec73 100644
--- a/tests/components/zha/common.py
+++ b/tests/components/zha/common.py
@@ -3,6 +3,12 @@ import time
from unittest.mock import Mock, patch
from asynctest import CoroutineMock
+import zigpy.profiles.zha
+import zigpy.types
+import zigpy.zcl
+import zigpy.zcl.clusters.general
+import zigpy.zcl.foundation as zcl_f
+import zigpy.zdo.types
from homeassistant.components.zha.core.const import (
DATA_ZHA,
@@ -10,7 +16,6 @@ from homeassistant.components.zha.core.const import (
DATA_ZHA_CONFIG,
DATA_ZHA_DISPATCHERS,
)
-from homeassistant.components.zha.core.helpers import convert_ieee
from homeassistant.util import slugify
from tests.common import mock_coro
@@ -21,7 +26,7 @@ class FakeApplication:
def __init__(self):
"""Init fake application."""
- self.ieee = convert_ieee("00:15:8d:00:02:32:4f:32")
+ self.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32")
self.nwk = 0x087D
@@ -33,8 +38,6 @@ class FakeEndpoint:
def __init__(self, manufacturer, model):
"""Init fake endpoint."""
- from zigpy.profiles.zha import PROFILE_ID
-
self.device = None
self.endpoint_id = 1
self.in_clusters = {}
@@ -43,14 +46,12 @@ class FakeEndpoint:
self.status = 1
self.manufacturer = manufacturer
self.model = model
- self.profile_id = PROFILE_ID
+ self.profile_id = zigpy.profiles.zha.PROFILE_ID
self.device_type = None
def add_input_cluster(self, cluster_id):
"""Add an input cluster."""
- from zigpy.zcl import Cluster
-
- cluster = Cluster.from_id(self, cluster_id, is_server=True)
+ cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=True)
patch_cluster(cluster)
self.in_clusters[cluster_id] = cluster
if hasattr(cluster, "ep_attribute"):
@@ -58,9 +59,7 @@ class FakeEndpoint:
def add_output_cluster(self, cluster_id):
"""Add an output cluster."""
- from zigpy.zcl import Cluster
-
- cluster = Cluster.from_id(self, cluster_id, is_server=False)
+ cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=False)
patch_cluster(cluster)
self.out_clusters[cluster_id] = cluster
@@ -71,7 +70,6 @@ def patch_cluster(cluster):
cluster.configure_reporting = CoroutineMock(return_value=[0])
cluster.deserialize = Mock()
cluster.handle_cluster_request = Mock()
- cluster.handle_cluster_general_request = Mock()
cluster.read_attributes = CoroutineMock()
cluster.read_attributes_raw = Mock()
cluster.unbind = CoroutineMock(return_value=[0])
@@ -83,7 +81,7 @@ class FakeDevice:
def __init__(self, ieee, manufacturer, model):
"""Init fake device."""
self._application = APPLICATION
- self.ieee = convert_ieee(ieee)
+ self.ieee = zigpy.types.EUI64.convert(ieee)
self.nwk = 0xB79C
self.zdo = Mock()
self.endpoints = {0: self.zdo}
@@ -94,9 +92,7 @@ class FakeDevice:
self.initializing = False
self.manufacturer = manufacturer
self.model = model
- from zigpy.zdo.types import NodeDescriptor
-
- self.node_desc = NodeDescriptor()
+ self.node_desc = zigpy.zdo.types.NodeDescriptor()
def make_device(
@@ -150,11 +146,9 @@ async def async_init_zigpy_device(
def make_attribute(attrid, value, status=0):
"""Make an attribute."""
- from zigpy.zcl.foundation import Attribute, TypeValue
-
- attr = Attribute()
+ attr = zcl_f.Attribute()
attr.attrid = attrid
- attr.value = TypeValue()
+ attr.value = zcl_f.TypeValue()
attr.value.value = value
return attr
@@ -174,7 +168,7 @@ def make_entity_id(domain, device, cluster, use_suffix=True):
machine so that we can test state changes.
"""
ieee = device.ieee
- ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]])
+ ieeetail = "".join([f"{o:02x}" for o in ieee[:4]])
entity_id = "{}.{}_{}_{}_{}{}".format(
domain,
slugify(device.manufacturer),
@@ -202,21 +196,18 @@ async def async_test_device_join(
simulate pairing a new device to the network so that code pathways that
only trigger during device joins can be tested.
"""
- from zigpy.zcl.foundation import Status
- from zigpy.zcl.clusters.general import Basic
-
# create zigpy device mocking out the zigbee network operations
with patch(
"zigpy.zcl.Cluster.configure_reporting",
- return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]),
+ return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
):
with patch(
"zigpy.zcl.Cluster.bind",
- return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]),
+ return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
):
zigpy_device = await async_init_zigpy_device(
hass,
- [cluster_id, Basic.cluster_id],
+ [cluster_id, zigpy.zcl.clusters.general.Basic.cluster_id],
[],
device_type,
zha_gateway,
@@ -230,3 +221,12 @@ async def async_test_device_join(
domain, zigpy_device, cluster, use_suffix=device_type is None
)
assert hass.states.get(entity_id) is not None
+
+
+def make_zcl_header(command_id: int, global_command: bool = True) -> zcl_f.ZCLHeader:
+ """Cluster.handle_message() ZCL Header helper."""
+ if global_command:
+ frc = zcl_f.FrameControl(zcl_f.FrameType.GLOBAL_COMMAND)
+ else:
+ frc = zcl_f.FrameControl(zcl_f.FrameType.CLUSTER_COMMAND)
+ return zcl_f.ZCLHeader(frc, tsn=1, command_id=command_id)
diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py
index b836c55df17..e34ad208744 100644
--- a/tests/components/zha/conftest.py
+++ b/tests/components/zha/conftest.py
@@ -1,13 +1,16 @@
"""Test configuration for the ZHA component."""
from unittest.mock import patch
+
import pytest
+
from homeassistant import config_entries
-from homeassistant.components.zha.core.const import DOMAIN, DATA_ZHA, COMPONENTS
-from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
+from homeassistant.components.zha.core.const import COMPONENTS, DATA_ZHA, DOMAIN
from homeassistant.components.zha.core.gateway import ZHAGateway
from homeassistant.components.zha.core.registries import establish_device_mappings
-from .common import async_setup_entry
from homeassistant.components.zha.core.store import async_get_registry
+from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
+
+from .common import async_setup_entry
@pytest.fixture(name="config_entry")
diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py
index ae8e460b613..3fea9dfe088 100644
--- a/tests/components/zha/test_api.py
+++ b/tests/components/zha/test_api.py
@@ -1,33 +1,39 @@
"""Test ZHA API."""
import pytest
+import zigpy.zcl.clusters.general as general
+
from homeassistant.components.switch import DOMAIN
-from homeassistant.components.zha.api import async_load_api, TYPE, ID
+from homeassistant.components.websocket_api import const
+from homeassistant.components.zha.api import ID, TYPE, async_load_api
from homeassistant.components.zha.core.const import (
ATTR_CLUSTER_ID,
ATTR_CLUSTER_TYPE,
- CLUSTER_TYPE_IN,
+ ATTR_ENDPOINT_ID,
ATTR_IEEE,
+ ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_NAME,
ATTR_QUIRK_APPLIED,
- ATTR_MANUFACTURER,
- ATTR_ENDPOINT_ID,
+ CLUSTER_TYPE_IN,
)
-from homeassistant.components.websocket_api import const
+
from .common import async_init_zigpy_device
@pytest.fixture
async def zha_client(hass, config_entry, zha_gateway, hass_ws_client):
"""Test zha switch platform."""
- from zigpy.zcl.clusters.general import OnOff, Basic
# load the ZHA API
async_load_api(hass)
# create zigpy device
await async_init_zigpy_device(
- hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway
+ hass,
+ [general.OnOff.cluster_id, general.Basic.cluster_id],
+ [],
+ None,
+ zha_gateway,
)
# load up switch domain
diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py
index 47f81787acd..89dc1ae25a6 100644
--- a/tests/components/zha/test_binary_sensor.py
+++ b/tests/components/zha/test_binary_sensor.py
@@ -1,29 +1,37 @@
"""Test zha binary sensor."""
+import zigpy.zcl.clusters.general as general
+import zigpy.zcl.clusters.measurement as measurement
+import zigpy.zcl.clusters.security as security
+import zigpy.zcl.foundation as zcl_f
+
from homeassistant.components.binary_sensor import DOMAIN
-from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE
+from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+
from .common import (
+ async_enable_traffic,
async_init_zigpy_device,
+ async_test_device_join,
make_attribute,
make_entity_id,
- async_test_device_join,
- async_enable_traffic,
+ make_zcl_header,
)
async def test_binary_sensor(hass, config_entry, zha_gateway):
"""Test zha binary_sensor platform."""
- from zigpy.zcl.clusters.security import IasZone
- from zigpy.zcl.clusters.measurement import OccupancySensing
- from zigpy.zcl.clusters.general import Basic
# create zigpy devices
zigpy_device_zone = await async_init_zigpy_device(
- hass, [IasZone.cluster_id, Basic.cluster_id], [], None, zha_gateway
+ hass,
+ [security.IasZone.cluster_id, general.Basic.cluster_id],
+ [],
+ None,
+ zha_gateway,
)
zigpy_device_occupancy = await async_init_zigpy_device(
hass,
- [OccupancySensing.cluster_id, Basic.cluster_id],
+ [measurement.OccupancySensing.cluster_id, general.Basic.cluster_id],
[],
None,
zha_gateway,
@@ -67,20 +75,24 @@ async def test_binary_sensor(hass, config_entry, zha_gateway):
await async_test_iaszone_on_off(hass, zone_cluster, zone_entity_id)
# test new sensor join
- await async_test_device_join(hass, zha_gateway, OccupancySensing.cluster_id, DOMAIN)
+ await async_test_device_join(
+ hass, zha_gateway, measurement.OccupancySensing.cluster_id, DOMAIN
+ )
async def async_test_binary_sensor_on_off(hass, cluster, entity_id):
"""Test getting on and off messages for binary sensors."""
# binary sensor on
attr = make_attribute(0, 1)
- cluster.handle_message(1, 0x0A, [[attr]])
+ hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
+
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
# binary sensor off
attr.value.value = 0
- cluster.handle_message(0, 0x0A, [[attr]])
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py
index 25b0910931a..5e6bf51afd6 100644
--- a/tests/components/zha/test_config_flow.py
+++ b/tests/components/zha/test_config_flow.py
@@ -1,7 +1,9 @@
"""Tests for ZHA config flow."""
from asynctest import patch
+
from homeassistant.components.zha import config_flow
from homeassistant.components.zha.core.const import DOMAIN
+
from tests.common import MockConfigEntry
diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py
index 6e7bc6ab4b1..62884fe72ae 100644
--- a/tests/components/zha/test_device_action.py
+++ b/tests/components/zha/test_device_action.py
@@ -2,6 +2,9 @@
from unittest.mock import patch
import pytest
+import zigpy.zcl.clusters.general as general
+import zigpy.zcl.clusters.security as security
+import zigpy.zcl.foundation as zcl_f
import homeassistant.components.automation as automation
from homeassistant.components.device_automation import (
@@ -29,19 +32,21 @@ def calls(hass):
async def test_get_actions(hass, config_entry, zha_gateway):
"""Test we get the expected actions from a zha device."""
- from zigpy.zcl.clusters.general import Basic
- from zigpy.zcl.clusters.security import IasZone, IasWd
# create zigpy device
zigpy_device = await async_init_zigpy_device(
hass,
- [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id],
+ [
+ general.Basic.cluster_id,
+ security.IasZone.cluster_id,
+ security.IasWd.cluster_id,
+ ],
[],
None,
zha_gateway,
)
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
+ await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor")
await hass.async_block_till_done()
hass.config_entries._entries.append(config_entry)
@@ -64,15 +69,15 @@ async def test_get_actions(hass, config_entry, zha_gateway):
async def test_action(hass, config_entry, zha_gateway, calls):
"""Test for executing a zha device action."""
- from zigpy.zcl.clusters.general import Basic, OnOff
- from zigpy.zcl.clusters.security import IasZone, IasWd
- from zigpy.zcl.foundation import Status
-
# create zigpy device
zigpy_device = await async_init_zigpy_device(
hass,
- [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id],
- [OnOff.cluster_id],
+ [
+ general.Basic.cluster_id,
+ security.IasZone.cluster_id,
+ security.IasWd.cluster_id,
+ ],
+ [general.OnOff.cluster_id],
None,
zha_gateway,
)
@@ -96,7 +101,8 @@ async def test_action(hass, config_entry, zha_gateway, calls):
await async_enable_traffic(hass, zha_gateway, [zha_device])
with patch(
- "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS])
+ "zigpy.zcl.Cluster.request",
+ return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]),
):
assert await async_setup_component(
hass,
diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py
index 6a7638d9f86..446920eb2f9 100644
--- a/tests/components/zha/test_device_tracker.py
+++ b/tests/components/zha/test_device_tracker.py
@@ -1,44 +1,43 @@
"""Test ZHA Device Tracker."""
from datetime import timedelta
import time
+
+import zigpy.zcl.clusters.general as general
+import zigpy.zcl.foundation as zcl_f
+
from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER
-from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE
from homeassistant.components.zha.core.registries import (
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE,
)
+from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE
import homeassistant.util.dt as dt_util
+
from .common import (
+ async_enable_traffic,
async_init_zigpy_device,
+ async_test_device_join,
make_attribute,
make_entity_id,
- async_test_device_join,
- async_enable_traffic,
+ make_zcl_header,
)
+
from tests.common import async_fire_time_changed
async def test_device_tracker(hass, config_entry, zha_gateway):
"""Test zha device tracker platform."""
- from zigpy.zcl.clusters.general import (
- Basic,
- PowerConfiguration,
- BinaryInput,
- Identify,
- Ota,
- PollControl,
- )
# create zigpy device
zigpy_device = await async_init_zigpy_device(
hass,
[
- Basic.cluster_id,
- PowerConfiguration.cluster_id,
- Identify.cluster_id,
- PollControl.cluster_id,
- BinaryInput.cluster_id,
+ general.Basic.cluster_id,
+ general.PowerConfiguration.cluster_id,
+ general.Identify.cluster_id,
+ general.PollControl.cluster_id,
+ general.BinaryInput.cluster_id,
],
- [Identify.cluster_id, Ota.cluster_id],
+ [general.Identify.cluster_id, general.Ota.cluster_id],
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE,
zha_gateway,
)
@@ -67,10 +66,11 @@ async def test_device_tracker(hass, config_entry, zha_gateway):
# turn state flip
attr = make_attribute(0x0020, 23)
- cluster.handle_message(1, 0x0A, [[attr]])
+ hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
+ cluster.handle_message(hdr, [[attr]])
attr = make_attribute(0x0021, 200)
- cluster.handle_message(1, 0x0A, [[attr]])
+ cluster.handle_message(hdr, [[attr]])
zigpy_device.last_seen = time.time() + 10
next_update = dt_util.utcnow() + timedelta(seconds=30)
@@ -89,7 +89,7 @@ async def test_device_tracker(hass, config_entry, zha_gateway):
await async_test_device_join(
hass,
zha_gateway,
- PowerConfiguration.cluster_id,
+ general.PowerConfiguration.cluster_id,
DOMAIN,
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE,
)
diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py
index 2f4ddb6b8b2..75e8538c5bf 100644
--- a/tests/components/zha/test_device_trigger.py
+++ b/tests/components/zha/test_device_trigger.py
@@ -1,7 +1,6 @@
"""ZHA device automation trigger tests."""
-from unittest.mock import patch
-
import pytest
+import zigpy.zcl.clusters.general as general
import homeassistant.components.automation as automation
from homeassistant.components.switch import DOMAIN
@@ -11,7 +10,7 @@ from homeassistant.setup import async_setup_component
from .common import async_enable_traffic, async_init_zigpy_device
-from tests.common import async_mock_service, async_get_device_automations
+from tests.common import async_get_device_automations, async_mock_service
ON = 1
OFF = 0
@@ -45,11 +44,10 @@ def calls(hass):
async def test_triggers(hass, config_entry, zha_gateway):
"""Test zha device triggers."""
- from zigpy.zcl.clusters.general import OnOff, Basic
# create zigpy device
zigpy_device = await async_init_zigpy_device(
- hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway
+ hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
)
zigpy_device.device_automation_triggers = {
@@ -114,11 +112,10 @@ async def test_triggers(hass, config_entry, zha_gateway):
async def test_no_triggers(hass, config_entry, zha_gateway):
"""Test zha device with no triggers."""
- from zigpy.zcl.clusters.general import OnOff, Basic
# create zigpy device
zigpy_device = await async_init_zigpy_device(
- hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway
+ hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
)
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
@@ -137,11 +134,10 @@ async def test_no_triggers(hass, config_entry, zha_gateway):
async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls):
"""Test for remote triggers firing."""
- from zigpy.zcl.clusters.general import OnOff, Basic
# create zigpy device
zigpy_device = await async_init_zigpy_device(
- hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway
+ hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
)
zigpy_device.device_automation_triggers = {
@@ -197,13 +193,12 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls):
assert calls[0].data["message"] == "service called"
-async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls):
+async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, caplog):
"""Test for exception on event triggers firing."""
- from zigpy.zcl.clusters.general import OnOff, Basic
# create zigpy device
zigpy_device = await async_init_zigpy_device(
- hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway
+ hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
)
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
@@ -219,39 +214,37 @@ async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls):
ha_device_registry = await async_get_registry(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set())
- with patch("logging.Logger.error") as mock:
- await async_setup_component(
- hass,
- automation.DOMAIN,
- {
- automation.DOMAIN: [
- {
- "trigger": {
- "device_id": reg_device.id,
- "domain": "zha",
- "platform": "device",
- "type": "junk",
- "subtype": "junk",
- },
- "action": {
- "service": "test.automation",
- "data": {"message": "service called"},
- },
- }
- ]
- },
- )
- await hass.async_block_till_done()
- mock.assert_called_with("Error setting up trigger %s", "automation 0")
+ await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "device_id": reg_device.id,
+ "domain": "zha",
+ "platform": "device",
+ "type": "junk",
+ "subtype": "junk",
+ },
+ "action": {
+ "service": "test.automation",
+ "data": {"message": "service called"},
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert "Invalid config for [automation]" in caplog.text
-async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls):
+async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, caplog):
"""Test for exception on event triggers firing."""
- from zigpy.zcl.clusters.general import OnOff, Basic
# create zigpy device
zigpy_device = await async_init_zigpy_device(
- hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway
+ hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
)
zigpy_device.device_automation_triggers = {
@@ -275,27 +268,26 @@ async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls):
ha_device_registry = await async_get_registry(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set())
- with patch("logging.Logger.error") as mock:
- await async_setup_component(
- hass,
- automation.DOMAIN,
- {
- automation.DOMAIN: [
- {
- "trigger": {
- "device_id": reg_device.id,
- "domain": "zha",
- "platform": "device",
- "type": "junk",
- "subtype": "junk",
- },
- "action": {
- "service": "test.automation",
- "data": {"message": "service called"},
- },
- }
- ]
- },
- )
- await hass.async_block_till_done()
- mock.assert_called_with("Error setting up trigger %s", "automation 0")
+ await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "device_id": reg_device.id,
+ "domain": "zha",
+ "platform": "device",
+ "type": "junk",
+ "subtype": "junk",
+ },
+ "action": {
+ "service": "test.automation",
+ "data": {"message": "service called"},
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert "Invalid config for [automation]" in caplog.text
diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py
index 3fe5e7937c8..a196ba50ba7 100644
--- a/tests/components/zha/test_fan.py
+++ b/tests/components/zha/test_fan.py
@@ -1,28 +1,39 @@
"""Test zha fan."""
from unittest.mock import call, patch
+
+import zigpy.zcl.clusters.general as general
+import zigpy.zcl.clusters.hvac as hvac
+import zigpy.zcl.foundation as zcl_f
+
from homeassistant.components import fan
-from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE
from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED
-from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF
-from tests.common import mock_coro
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+)
+
from .common import (
+ async_enable_traffic,
async_init_zigpy_device,
+ async_test_device_join,
make_attribute,
make_entity_id,
- async_test_device_join,
- async_enable_traffic,
+ make_zcl_header,
)
+from tests.common import mock_coro
+
async def test_fan(hass, config_entry, zha_gateway):
"""Test zha fan platform."""
- from zigpy.zcl.clusters.hvac import Fan
- from zigpy.zcl.clusters.general import Basic
- from zigpy.zcl.foundation import Status
# create zigpy device
zigpy_device = await async_init_zigpy_device(
- hass, [Fan.cluster_id, Basic.cluster_id], [], None, zha_gateway
+ hass, [hvac.Fan.cluster_id, general.Basic.cluster_id], [], None, zha_gateway
)
# load up fan domain
@@ -44,20 +55,21 @@ async def test_fan(hass, config_entry, zha_gateway):
# turn on at fan
attr = make_attribute(0, 1)
- cluster.handle_message(1, 0x0A, [[attr]])
+ hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
# turn off at fan
attr.value.value = 0
- cluster.handle_message(0, 0x0A, [[attr]])
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA
with patch(
"zigpy.zcl.Cluster.write_attributes",
- return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]),
+ return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
):
# turn on via UI
await async_turn_on(hass, entity_id)
@@ -67,7 +79,7 @@ async def test_fan(hass, config_entry, zha_gateway):
# turn off from HA
with patch(
"zigpy.zcl.Cluster.write_attributes",
- return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]),
+ return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
):
# turn off via UI
await async_turn_off(hass, entity_id)
@@ -77,7 +89,7 @@ async def test_fan(hass, config_entry, zha_gateway):
# change speed from HA
with patch(
"zigpy.zcl.Cluster.write_attributes",
- return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]),
+ return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
):
# turn on via UI
await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH)
@@ -85,7 +97,7 @@ async def test_fan(hass, config_entry, zha_gateway):
assert cluster.write_attributes.call_args == call({"fan_mode": 3})
# test adding new fan to the network and HA
- await async_test_device_join(hass, zha_gateway, Fan.cluster_id, DOMAIN)
+ await async_test_device_join(hass, zha_gateway, hvac.Fan.cluster_id, DOMAIN)
async def async_turn_on(hass, entity_id, speed=None):
diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py
index 08c6cfe18cf..f0d9d4913e6 100644
--- a/tests/components/zha/test_light.py
+++ b/tests/components/zha/test_light.py
@@ -2,6 +2,11 @@
import asyncio
from unittest.mock import MagicMock, call, patch, sentinel
+import zigpy.profiles.zha
+import zigpy.types
+import zigpy.zcl.clusters.general as general
+import zigpy.zcl.foundation as zcl_f
+
from homeassistant.components.light import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
@@ -11,6 +16,7 @@ from .common import (
async_test_device_join,
make_attribute,
make_entity_id,
+ make_zcl_header,
)
from tests.common import mock_coro
@@ -21,24 +27,25 @@ OFF = 0
async def test_light(hass, config_entry, zha_gateway, monkeypatch):
"""Test zha light platform."""
- from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic
- from zigpy.zcl.foundation import Status
- from zigpy.profiles.zha import DeviceType
# create zigpy devices
zigpy_device_on_off = await async_init_zigpy_device(
hass,
- [OnOff.cluster_id, Basic.cluster_id],
+ [general.OnOff.cluster_id, general.Basic.cluster_id],
[],
- DeviceType.ON_OFF_LIGHT,
+ zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT,
zha_gateway,
)
zigpy_device_level = await async_init_zigpy_device(
hass,
- [OnOff.cluster_id, LevelControl.cluster_id, Basic.cluster_id],
+ [
+ general.OnOff.cluster_id,
+ general.LevelControl.cluster_id,
+ general.Basic.cluster_id,
+ ],
[],
- DeviceType.ON_OFF_LIGHT,
+ zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT,
zha_gateway,
ieee="00:0d:6f:11:0a:90:69:e7",
manufacturer="FakeLevelManufacturer",
@@ -61,12 +68,12 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch):
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])
+ MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS])
)
)
level_mock = MagicMock(
side_effect=asyncio.coroutine(
- MagicMock(return_value=[sentinel.data, Status.SUCCESS])
+ MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS])
)
)
monkeypatch.setattr(level_device_on_off_cluster, "request", on_off_mock)
@@ -115,7 +122,11 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch):
# test adding a new light to the network and HA
await async_test_device_join(
- hass, zha_gateway, OnOff.cluster_id, DOMAIN, device_type=DeviceType.ON_OFF_LIGHT
+ hass,
+ zha_gateway,
+ general.OnOff.cluster_id,
+ DOMAIN,
+ device_type=zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT,
)
@@ -123,13 +134,14 @@ async def async_test_on_off_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light."""
# turn on at light
attr = make_attribute(0, 1)
- cluster.handle_message(1, 0x0A, [[attr]])
+ hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
# turn off at light
attr.value.value = 0
- cluster.handle_message(0, 0x0A, [[attr]])
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
@@ -138,17 +150,17 @@ async def async_test_on_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light."""
# turn on at light
attr = make_attribute(0, 1)
- cluster.handle_message(1, 0x0A, [[attr]])
+ hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
async def async_test_on_off_from_hass(hass, cluster, entity_id):
"""Test on off functionality from hass."""
- from zigpy.zcl.foundation import Status
-
with patch(
- "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS])
+ "zigpy.zcl.Cluster.request",
+ return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]),
):
# turn on via UI
await hass.services.async_call(
@@ -164,10 +176,9 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id):
async def async_test_off_from_hass(hass, cluster, entity_id):
"""Test turning off the light from homeassistant."""
- from zigpy.zcl.foundation import Status
-
with patch(
- "zigpy.zcl.Cluster.request", return_value=mock_coro([0x01, Status.SUCCESS])
+ "zigpy.zcl.Cluster.request",
+ return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]),
):
# turn off via UI
await hass.services.async_call(
@@ -183,7 +194,6 @@ async def async_test_level_on_off_from_hass(
hass, on_off_cluster, level_cluster, entity_id
):
"""Test on off functionality from hass."""
- from zigpy import types
# turn on via UI
await hass.services.async_call(
@@ -208,7 +218,7 @@ async def async_test_level_on_off_from_hass(
assert level_cluster.request.call_args == call(
False,
4,
- (types.uint8_t, types.uint16_t),
+ (zigpy.types.uint8_t, zigpy.types.uint16_t),
254,
100.0,
expect_reply=True,
@@ -228,7 +238,7 @@ async def async_test_level_on_off_from_hass(
assert level_cluster.request.call_args == call(
False,
4,
- (types.uint8_t, types.uint16_t),
+ (zigpy.types.uint8_t, zigpy.types.uint16_t),
10,
0,
expect_reply=True,
@@ -243,7 +253,8 @@ async def async_test_level_on_off_from_hass(
async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state):
"""Test dimmer functionality from the light."""
attr = make_attribute(0, level)
- cluster.handle_message(1, 0x0A, [[attr]])
+ hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == expected_state
# hass uses None for brightness of 0 in state attributes
diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py
index 7381b557310..118526a1d85 100644
--- a/tests/components/zha/test_lock.py
+++ b/tests/components/zha/test_lock.py
@@ -1,27 +1,37 @@
"""Test zha lock."""
from unittest.mock import patch
-from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE
+
+import zigpy.zcl.clusters.closures as closures
+import zigpy.zcl.clusters.general as general
+import zigpy.zcl.foundation as zcl_f
+
from homeassistant.components.lock import DOMAIN
-from tests.common import mock_coro
+from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED
+
from .common import (
+ async_enable_traffic,
async_init_zigpy_device,
make_attribute,
make_entity_id,
- async_enable_traffic,
+ make_zcl_header,
)
+from tests.common import mock_coro
+
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
+ hass,
+ [closures.DoorLock.cluster_id, general.Basic.cluster_id],
+ [],
+ None,
+ zha_gateway,
)
# load up lock domain
@@ -43,13 +53,14 @@ async def test_lock(hass, config_entry, zha_gateway):
# set state to locked
attr = make_attribute(0, 1)
- cluster.handle_message(1, 0x0A, [[attr]])
+ hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
+ cluster.handle_message(hdr, [[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(0, 0x0A, [[attr]])
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_UNLOCKED
@@ -62,9 +73,9 @@ async def test_lock(hass, config_entry, zha_gateway):
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])):
+ with patch(
+ "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS])
+ ):
# lock via UI
await hass.services.async_call(
DOMAIN, "lock", {"entity_id": entity_id}, blocking=True
@@ -76,9 +87,9 @@ async def async_lock(hass, cluster, entity_id):
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])):
+ with patch(
+ "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS])
+ ):
# lock via UI
await hass.services.async_call(
DOMAIN, "unlock", {"entity_id": entity_id}, blocking=True
diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py
index faa44f34927..dec551f8d62 100644
--- a/tests/components/zha/test_sensor.py
+++ b/tests/components/zha/test_sensor.py
@@ -1,34 +1,34 @@
"""Test zha sensor."""
+import zigpy.zcl.clusters.general as general
+import zigpy.zcl.clusters.homeautomation as homeautomation
+import zigpy.zcl.clusters.measurement as measurement
+import zigpy.zcl.clusters.smartenergy as smartenergy
+import zigpy.zcl.foundation as zcl_f
+
from homeassistant.components.sensor import DOMAIN
-from homeassistant.const import STATE_UNKNOWN, STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
+
from .common import (
+ async_enable_traffic,
async_init_zigpy_device,
+ async_test_device_join,
make_attribute,
make_entity_id,
- async_test_device_join,
- async_enable_traffic,
+ make_zcl_header,
)
async def test_sensor(hass, config_entry, zha_gateway):
"""Test zha sensor platform."""
- from zigpy.zcl.clusters.measurement import (
- RelativeHumidity,
- TemperatureMeasurement,
- PressureMeasurement,
- IlluminanceMeasurement,
- )
- from zigpy.zcl.clusters.smartenergy import Metering
- from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
# list of cluster ids to create devices and sensor entities for
cluster_ids = [
- RelativeHumidity.cluster_id,
- TemperatureMeasurement.cluster_id,
- PressureMeasurement.cluster_id,
- IlluminanceMeasurement.cluster_id,
- Metering.cluster_id,
- ElectricalMeasurement.cluster_id,
+ measurement.RelativeHumidity.cluster_id,
+ measurement.TemperatureMeasurement.cluster_id,
+ measurement.PressureMeasurement.cluster_id,
+ measurement.IlluminanceMeasurement.cluster_id,
+ smartenergy.Metering.cluster_id,
+ homeautomation.ElectricalMeasurement.cluster_id,
]
# devices that were created from cluster_ids list above
@@ -59,33 +59,33 @@ async def test_sensor(hass, config_entry, zha_gateway):
assert hass.states.get(entity_id).state == STATE_UNKNOWN
# get the humidity device info and test the associated sensor logic
- device_info = zigpy_device_infos[RelativeHumidity.cluster_id]
+ device_info = zigpy_device_infos[measurement.RelativeHumidity.cluster_id]
await async_test_humidity(hass, device_info)
# get the temperature device info and test the associated sensor logic
- device_info = zigpy_device_infos[TemperatureMeasurement.cluster_id]
+ device_info = zigpy_device_infos[measurement.TemperatureMeasurement.cluster_id]
await async_test_temperature(hass, device_info)
# get the pressure device info and test the associated sensor logic
- device_info = zigpy_device_infos[PressureMeasurement.cluster_id]
+ device_info = zigpy_device_infos[measurement.PressureMeasurement.cluster_id]
await async_test_pressure(hass, device_info)
# get the illuminance device info and test the associated sensor logic
- device_info = zigpy_device_infos[IlluminanceMeasurement.cluster_id]
+ device_info = zigpy_device_infos[measurement.IlluminanceMeasurement.cluster_id]
await async_test_illuminance(hass, device_info)
# get the metering device info and test the associated sensor logic
- device_info = zigpy_device_infos[Metering.cluster_id]
+ device_info = zigpy_device_infos[smartenergy.Metering.cluster_id]
await async_test_metering(hass, device_info)
# get the electrical_measurement device info and test the associated
# sensor logic
- device_info = zigpy_device_infos[ElectricalMeasurement.cluster_id]
+ device_info = zigpy_device_infos[homeautomation.ElectricalMeasurement.cluster_id]
await async_test_electrical_measurement(hass, device_info)
# test joining a new temperature sensor to the network
await async_test_device_join(
- hass, zha_gateway, TemperatureMeasurement.cluster_id, DOMAIN
+ hass, zha_gateway, measurement.TemperatureMeasurement.cluster_id, DOMAIN
)
@@ -98,7 +98,6 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids):
A dict containing relevant device info for testing is returned. It contains
the entity id, zigpy device, and the zigbee cluster for the sensor.
"""
- from zigpy.zcl.clusters.general import Basic
device_infos = {}
counter = 0
@@ -107,7 +106,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids):
device_infos[cluster_id] = {"zigpy_device": None}
device_infos[cluster_id]["zigpy_device"] = await async_init_zigpy_device(
hass,
- [cluster_id, Basic.cluster_id],
+ [cluster_id, general.Basic.cluster_id],
[],
None,
zha_gateway,
@@ -177,7 +176,8 @@ async def send_attribute_report(hass, cluster, attrid, value):
device is paired to the zigbee network.
"""
attr = make_attribute(attrid, value)
- cluster.handle_message(1, 0x0A, [[attr]])
+ hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py
index ac6bc73b809..bf4ff3ed628 100644
--- a/tests/components/zha/test_switch.py
+++ b/tests/components/zha/test_switch.py
@@ -1,28 +1,37 @@
"""Test zha switch."""
from unittest.mock import call, patch
+
+import zigpy.zcl.clusters.general as general
+import zigpy.zcl.foundation as zcl_f
+
from homeassistant.components.switch import DOMAIN
-from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE
-from tests.common import mock_coro
+from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+
from .common import (
+ async_enable_traffic,
async_init_zigpy_device,
+ async_test_device_join,
make_attribute,
make_entity_id,
- async_test_device_join,
- async_enable_traffic,
+ make_zcl_header,
)
+from tests.common import mock_coro
+
ON = 1
OFF = 0
async def test_switch(hass, config_entry, zha_gateway):
"""Test zha switch platform."""
- from zigpy.zcl.clusters.general import OnOff, Basic
- from zigpy.zcl.foundation import Status
# create zigpy device
zigpy_device = await async_init_zigpy_device(
- hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway
+ hass,
+ [general.OnOff.cluster_id, general.Basic.cluster_id],
+ [],
+ None,
+ zha_gateway,
)
# load up switch domain
@@ -44,19 +53,21 @@ async def test_switch(hass, config_entry, zha_gateway):
# turn on at switch
attr = make_attribute(0, 1)
- cluster.handle_message(1, 0x0A, [[attr]])
+ hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
# turn off at switch
attr.value.value = 0
- cluster.handle_message(0, 0x0A, [[attr]])
+ cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA
with patch(
- "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS])
+ "zigpy.zcl.Cluster.request",
+ return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]),
):
# turn on via UI
await hass.services.async_call(
@@ -69,7 +80,8 @@ async def test_switch(hass, config_entry, zha_gateway):
# turn off from HA
with patch(
- "zigpy.zcl.Cluster.request", return_value=mock_coro([0x01, Status.SUCCESS])
+ "zigpy.zcl.Cluster.request",
+ return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]),
):
# turn off via UI
await hass.services.async_call(
@@ -81,4 +93,4 @@ async def test_switch(hass, config_entry, zha_gateway):
)
# test joining a new switch to the network and HA
- await async_test_device_join(hass, zha_gateway, OnOff.cluster_id, DOMAIN)
+ await async_test_device_join(hass, zha_gateway, general.OnOff.cluster_id, DOMAIN)
diff --git a/tests/fixtures/airly_no_station.json b/tests/fixtures/airly_no_station.json
new file mode 100644
index 00000000000..cc64934938f
--- /dev/null
+++ b/tests/fixtures/airly_no_station.json
@@ -0,0 +1,642 @@
+{
+ "current": {
+ "fromDateTime": "2019-10-02T05:53:00.608Z",
+ "tillDateTime": "2019-10-02T06:53:00.608Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ },
+ "history": [{
+ "fromDateTime": "2019-10-01T06:00:00.000Z",
+ "tillDateTime": "2019-10-01T07:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T07:00:00.000Z",
+ "tillDateTime": "2019-10-01T08:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T08:00:00.000Z",
+ "tillDateTime": "2019-10-01T09:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T09:00:00.000Z",
+ "tillDateTime": "2019-10-01T10:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T10:00:00.000Z",
+ "tillDateTime": "2019-10-01T11:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T11:00:00.000Z",
+ "tillDateTime": "2019-10-01T12:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T12:00:00.000Z",
+ "tillDateTime": "2019-10-01T13:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T13:00:00.000Z",
+ "tillDateTime": "2019-10-01T14:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T14:00:00.000Z",
+ "tillDateTime": "2019-10-01T15:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T15:00:00.000Z",
+ "tillDateTime": "2019-10-01T16:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T16:00:00.000Z",
+ "tillDateTime": "2019-10-01T17:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T17:00:00.000Z",
+ "tillDateTime": "2019-10-01T18:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T18:00:00.000Z",
+ "tillDateTime": "2019-10-01T19:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T19:00:00.000Z",
+ "tillDateTime": "2019-10-01T20:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T20:00:00.000Z",
+ "tillDateTime": "2019-10-01T21:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T21:00:00.000Z",
+ "tillDateTime": "2019-10-01T22:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T22:00:00.000Z",
+ "tillDateTime": "2019-10-01T23:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-01T23:00:00.000Z",
+ "tillDateTime": "2019-10-02T00:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T00:00:00.000Z",
+ "tillDateTime": "2019-10-02T01:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T01:00:00.000Z",
+ "tillDateTime": "2019-10-02T02:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T02:00:00.000Z",
+ "tillDateTime": "2019-10-02T03:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T03:00:00.000Z",
+ "tillDateTime": "2019-10-02T04:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T04:00:00.000Z",
+ "tillDateTime": "2019-10-02T05:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T05:00:00.000Z",
+ "tillDateTime": "2019-10-02T06:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }],
+ "forecast": [{
+ "fromDateTime": "2019-10-02T06:00:00.000Z",
+ "tillDateTime": "2019-10-02T07:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T07:00:00.000Z",
+ "tillDateTime": "2019-10-02T08:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T08:00:00.000Z",
+ "tillDateTime": "2019-10-02T09:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T09:00:00.000Z",
+ "tillDateTime": "2019-10-02T10:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T10:00:00.000Z",
+ "tillDateTime": "2019-10-02T11:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T11:00:00.000Z",
+ "tillDateTime": "2019-10-02T12:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T12:00:00.000Z",
+ "tillDateTime": "2019-10-02T13:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T13:00:00.000Z",
+ "tillDateTime": "2019-10-02T14:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T14:00:00.000Z",
+ "tillDateTime": "2019-10-02T15:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T15:00:00.000Z",
+ "tillDateTime": "2019-10-02T16:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T16:00:00.000Z",
+ "tillDateTime": "2019-10-02T17:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T17:00:00.000Z",
+ "tillDateTime": "2019-10-02T18:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T18:00:00.000Z",
+ "tillDateTime": "2019-10-02T19:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T19:00:00.000Z",
+ "tillDateTime": "2019-10-02T20:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T20:00:00.000Z",
+ "tillDateTime": "2019-10-02T21:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T21:00:00.000Z",
+ "tillDateTime": "2019-10-02T22:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T22:00:00.000Z",
+ "tillDateTime": "2019-10-02T23:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-02T23:00:00.000Z",
+ "tillDateTime": "2019-10-03T00:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-03T00:00:00.000Z",
+ "tillDateTime": "2019-10-03T01:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-03T01:00:00.000Z",
+ "tillDateTime": "2019-10-03T02:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-03T02:00:00.000Z",
+ "tillDateTime": "2019-10-03T03:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-03T03:00:00.000Z",
+ "tillDateTime": "2019-10-03T04:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-03T04:00:00.000Z",
+ "tillDateTime": "2019-10-03T05:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }, {
+ "fromDateTime": "2019-10-03T05:00:00.000Z",
+ "tillDateTime": "2019-10-03T06:00:00.000Z",
+ "values": [],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": null,
+ "level": "UNKNOWN",
+ "description": "There are no Airly sensors in this area yet.",
+ "advice": null,
+ "color": "#999999"
+ }],
+ "standards": []
+ }]
+}
\ No newline at end of file
diff --git a/tests/fixtures/airly_valid_station.json b/tests/fixtures/airly_valid_station.json
new file mode 100644
index 00000000000..656c62c04c2
--- /dev/null
+++ b/tests/fixtures/airly_valid_station.json
@@ -0,0 +1,1726 @@
+{
+ "current": {
+ "fromDateTime": "2019-10-02T05:54:57.204Z",
+ "tillDateTime": "2019-10-02T06:54:57.204Z",
+ "values": [{
+ "name": "PM1",
+ "value": 9.23
+ }, {
+ "name": "PM25",
+ "value": 13.71
+ }, {
+ "name": "PM10",
+ "value": 18.58
+ }, {
+ "name": "PRESSURE",
+ "value": 1000.87
+ }, {
+ "name": "HUMIDITY",
+ "value": 92.84
+ }, {
+ "name": "TEMPERATURE",
+ "value": 14.23
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 22.85,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Great air!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 54.84
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 37.17
+ }]
+ },
+ "history": [{
+ "fromDateTime": "2019-10-01T06:00:00.000Z",
+ "tillDateTime": "2019-10-01T07:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 5.95
+ }, {
+ "name": "PM25",
+ "value": 8.54
+ }, {
+ "name": "PM10",
+ "value": 11.46
+ }, {
+ "name": "PRESSURE",
+ "value": 1009.61
+ }, {
+ "name": "HUMIDITY",
+ "value": 97.6
+ }, {
+ "name": "TEMPERATURE",
+ "value": 9.71
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 14.24,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Green equals clean!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 34.18
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 22.91
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T07:00:00.000Z",
+ "tillDateTime": "2019-10-01T08:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 4.2
+ }, {
+ "name": "PM25",
+ "value": 5.88
+ }, {
+ "name": "PM10",
+ "value": 7.88
+ }, {
+ "name": "PRESSURE",
+ "value": 1009.13
+ }, {
+ "name": "HUMIDITY",
+ "value": 90.84
+ }, {
+ "name": "TEMPERATURE",
+ "value": 12.65
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 9.81,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Dear me, how wonderful!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 23.53
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 15.75
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T08:00:00.000Z",
+ "tillDateTime": "2019-10-01T09:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 3.63
+ }, {
+ "name": "PM25",
+ "value": 5.56
+ }, {
+ "name": "PM10",
+ "value": 7.71
+ }, {
+ "name": "PRESSURE",
+ "value": 1008.27
+ }, {
+ "name": "HUMIDITY",
+ "value": 84.61
+ }, {
+ "name": "TEMPERATURE",
+ "value": 15.57
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 9.26,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Dear me, how wonderful!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 22.23
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 15.42
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T09:00:00.000Z",
+ "tillDateTime": "2019-10-01T10:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 2.9
+ }, {
+ "name": "PM25",
+ "value": 3.93
+ }, {
+ "name": "PM10",
+ "value": 5.24
+ }, {
+ "name": "PRESSURE",
+ "value": 1007.57
+ }, {
+ "name": "HUMIDITY",
+ "value": 79.52
+ }, {
+ "name": "TEMPERATURE",
+ "value": 16.57
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 6.56,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe deep! The air is clean!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 15.74
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 10.48
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T10:00:00.000Z",
+ "tillDateTime": "2019-10-01T11:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 2.45
+ }, {
+ "name": "PM25",
+ "value": 3.33
+ }, {
+ "name": "PM10",
+ "value": 4.52
+ }, {
+ "name": "PRESSURE",
+ "value": 1006.75
+ }, {
+ "name": "HUMIDITY",
+ "value": 74.09
+ }, {
+ "name": "TEMPERATURE",
+ "value": 16.95
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 5.55,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "The air is grand today. ;)",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 13.31
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 9.04
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T11:00:00.000Z",
+ "tillDateTime": "2019-10-01T12:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 2.0
+ }, {
+ "name": "PM25",
+ "value": 2.93
+ }, {
+ "name": "PM10",
+ "value": 3.98
+ }, {
+ "name": "PRESSURE",
+ "value": 1005.71
+ }, {
+ "name": "HUMIDITY",
+ "value": 69.06
+ }, {
+ "name": "TEMPERATURE",
+ "value": 17.31
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 4.89,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Green equals clean!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 11.74
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 7.96
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T12:00:00.000Z",
+ "tillDateTime": "2019-10-01T13:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 1.92
+ }, {
+ "name": "PM25",
+ "value": 2.69
+ }, {
+ "name": "PM10",
+ "value": 3.68
+ }, {
+ "name": "PRESSURE",
+ "value": 1005.03
+ }, {
+ "name": "HUMIDITY",
+ "value": 65.08
+ }, {
+ "name": "TEMPERATURE",
+ "value": 17.47
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 4.49,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Enjoy life!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 10.77
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 7.36
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T13:00:00.000Z",
+ "tillDateTime": "2019-10-01T14:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 1.79
+ }, {
+ "name": "PM25",
+ "value": 2.57
+ }, {
+ "name": "PM10",
+ "value": 3.53
+ }, {
+ "name": "PRESSURE",
+ "value": 1004.26
+ }, {
+ "name": "HUMIDITY",
+ "value": 63.72
+ }, {
+ "name": "TEMPERATURE",
+ "value": 17.91
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 4.29,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Great air!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 10.29
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 7.06
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T14:00:00.000Z",
+ "tillDateTime": "2019-10-01T15:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 2.06
+ }, {
+ "name": "PM25",
+ "value": 3.08
+ }, {
+ "name": "PM10",
+ "value": 4.23
+ }, {
+ "name": "PRESSURE",
+ "value": 1003.46
+ }, {
+ "name": "HUMIDITY",
+ "value": 64.44
+ }, {
+ "name": "TEMPERATURE",
+ "value": 17.84
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 5.14,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "The air is grand today. ;)",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 12.33
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 8.47
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T15:00:00.000Z",
+ "tillDateTime": "2019-10-01T16:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 3.17
+ }, {
+ "name": "PM25",
+ "value": 4.61
+ }, {
+ "name": "PM10",
+ "value": 6.25
+ }, {
+ "name": "PRESSURE",
+ "value": 1003.18
+ }, {
+ "name": "HUMIDITY",
+ "value": 65.32
+ }, {
+ "name": "TEMPERATURE",
+ "value": 18.08
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 7.68,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Green, green, green!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 18.44
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 12.5
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T16:00:00.000Z",
+ "tillDateTime": "2019-10-01T17:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 4.17
+ }, {
+ "name": "PM25",
+ "value": 5.91
+ }, {
+ "name": "PM10",
+ "value": 8.06
+ }, {
+ "name": "PRESSURE",
+ "value": 1003.05
+ }, {
+ "name": "HUMIDITY",
+ "value": 66.14
+ }, {
+ "name": "TEMPERATURE",
+ "value": 17.04
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 9.84,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Enjoy life!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 23.62
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 16.11
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T17:00:00.000Z",
+ "tillDateTime": "2019-10-01T18:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 6.4
+ }, {
+ "name": "PM25",
+ "value": 10.93
+ }, {
+ "name": "PM10",
+ "value": 15.7
+ }, {
+ "name": "PRESSURE",
+ "value": 1002.85
+ }, {
+ "name": "HUMIDITY",
+ "value": 68.31
+ }, {
+ "name": "TEMPERATURE",
+ "value": 16.33
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 18.22,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "It couldn't be better ;)",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 43.74
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 31.4
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T18:00:00.000Z",
+ "tillDateTime": "2019-10-01T19:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 4.79
+ }, {
+ "name": "PM25",
+ "value": 7.41
+ }, {
+ "name": "PM10",
+ "value": 10.31
+ }, {
+ "name": "PRESSURE",
+ "value": 1002.52
+ }, {
+ "name": "HUMIDITY",
+ "value": 69.88
+ }, {
+ "name": "TEMPERATURE",
+ "value": 15.98
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 12.35,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Enjoy life!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 29.65
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 20.63
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T19:00:00.000Z",
+ "tillDateTime": "2019-10-01T20:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 5.99
+ }, {
+ "name": "PM25",
+ "value": 9.45
+ }, {
+ "name": "PM10",
+ "value": 13.22
+ }, {
+ "name": "PRESSURE",
+ "value": 1002.32
+ }, {
+ "name": "HUMIDITY",
+ "value": 70.47
+ }, {
+ "name": "TEMPERATURE",
+ "value": 15.76
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 15.74,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe deeply!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 37.78
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 26.44
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T20:00:00.000Z",
+ "tillDateTime": "2019-10-01T21:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 9.35
+ }, {
+ "name": "PM25",
+ "value": 14.67
+ }, {
+ "name": "PM10",
+ "value": 20.57
+ }, {
+ "name": "PRESSURE",
+ "value": 1002.46
+ }, {
+ "name": "HUMIDITY",
+ "value": 72.61
+ }, {
+ "name": "TEMPERATURE",
+ "value": 15.47
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 24.45,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "It couldn't be better ;)",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 58.68
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 41.13
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T21:00:00.000Z",
+ "tillDateTime": "2019-10-01T22:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 9.95
+ }, {
+ "name": "PM25",
+ "value": 15.37
+ }, {
+ "name": "PM10",
+ "value": 21.33
+ }, {
+ "name": "PRESSURE",
+ "value": 1002.59
+ }, {
+ "name": "HUMIDITY",
+ "value": 75.09
+ }, {
+ "name": "TEMPERATURE",
+ "value": 15.17
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 25.62,
+ "level": "LOW",
+ "description": "Air is quite good.",
+ "advice": "Take a breath!",
+ "color": "#D1CF1E"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 61.48
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 42.66
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T22:00:00.000Z",
+ "tillDateTime": "2019-10-01T23:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 10.16
+ }, {
+ "name": "PM25",
+ "value": 15.78
+ }, {
+ "name": "PM10",
+ "value": 21.97
+ }, {
+ "name": "PRESSURE",
+ "value": 1002.59
+ }, {
+ "name": "HUMIDITY",
+ "value": 77.68
+ }, {
+ "name": "TEMPERATURE",
+ "value": 14.9
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 26.31,
+ "level": "LOW",
+ "description": "Air is quite good.",
+ "advice": "Great air for a walk to the park!",
+ "color": "#D1CF1E"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 63.14
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 43.93
+ }]
+ }, {
+ "fromDateTime": "2019-10-01T23:00:00.000Z",
+ "tillDateTime": "2019-10-02T00:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 9.86
+ }, {
+ "name": "PM25",
+ "value": 15.14
+ }, {
+ "name": "PM10",
+ "value": 21.07
+ }, {
+ "name": "PRESSURE",
+ "value": 1002.49
+ }, {
+ "name": "HUMIDITY",
+ "value": 79.86
+ }, {
+ "name": "TEMPERATURE",
+ "value": 14.56
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 25.24,
+ "level": "LOW",
+ "description": "Air is quite good.",
+ "advice": "Leave the mask at home today!",
+ "color": "#D1CF1E"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 60.57
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 42.14
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T00:00:00.000Z",
+ "tillDateTime": "2019-10-02T01:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 9.77
+ }, {
+ "name": "PM25",
+ "value": 15.04
+ }, {
+ "name": "PM10",
+ "value": 20.97
+ }, {
+ "name": "PRESSURE",
+ "value": 1002.18
+ }, {
+ "name": "HUMIDITY",
+ "value": 81.77
+ }, {
+ "name": "TEMPERATURE",
+ "value": 14.13
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 25.07,
+ "level": "LOW",
+ "description": "Air is quite good.",
+ "advice": "Time for a walk with friends or activities with your family - because the air is clean!",
+ "color": "#D1CF1E"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 60.18
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 41.94
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T01:00:00.000Z",
+ "tillDateTime": "2019-10-02T02:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 9.67
+ }, {
+ "name": "PM25",
+ "value": 14.9
+ }, {
+ "name": "PM10",
+ "value": 20.67
+ }, {
+ "name": "PRESSURE",
+ "value": 1002.01
+ }, {
+ "name": "HUMIDITY",
+ "value": 84.5
+ }, {
+ "name": "TEMPERATURE",
+ "value": 13.7
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 24.84,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Great air!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 59.62
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 41.33
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T02:00:00.000Z",
+ "tillDateTime": "2019-10-02T03:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 7.17
+ }, {
+ "name": "PM25",
+ "value": 10.7
+ }, {
+ "name": "PM10",
+ "value": 14.58
+ }, {
+ "name": "PRESSURE",
+ "value": 1001.56
+ }, {
+ "name": "HUMIDITY",
+ "value": 88.55
+ }, {
+ "name": "TEMPERATURE",
+ "value": 13.44
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 17.83,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Catch your breath!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 42.8
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 29.17
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T03:00:00.000Z",
+ "tillDateTime": "2019-10-02T04:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 6.99
+ }, {
+ "name": "PM25",
+ "value": 10.23
+ }, {
+ "name": "PM10",
+ "value": 13.66
+ }, {
+ "name": "PRESSURE",
+ "value": 1001.34
+ }, {
+ "name": "HUMIDITY",
+ "value": 90.82
+ }, {
+ "name": "TEMPERATURE",
+ "value": 13.3
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 17.05,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Perfect air for exercising! Go for it!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 40.91
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 27.33
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T04:00:00.000Z",
+ "tillDateTime": "2019-10-02T05:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 7.82
+ }, {
+ "name": "PM25",
+ "value": 11.59
+ }, {
+ "name": "PM10",
+ "value": 15.77
+ }, {
+ "name": "PRESSURE",
+ "value": 1000.92
+ }, {
+ "name": "HUMIDITY",
+ "value": 91.8
+ }, {
+ "name": "TEMPERATURE",
+ "value": 13.34
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 19.32,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Dear me, how wonderful!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 46.36
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 31.54
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T05:00:00.000Z",
+ "tillDateTime": "2019-10-02T06:00:00.000Z",
+ "values": [{
+ "name": "PM1",
+ "value": 10.16
+ }, {
+ "name": "PM25",
+ "value": 15.35
+ }, {
+ "name": "PM10",
+ "value": 21.45
+ }, {
+ "name": "PRESSURE",
+ "value": 1000.82
+ }, {
+ "name": "HUMIDITY",
+ "value": 92.15
+ }, {
+ "name": "TEMPERATURE",
+ "value": 13.74
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 25.59,
+ "level": "LOW",
+ "description": "Air is quite good.",
+ "advice": "How about going for a walk?",
+ "color": "#D1CF1E"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 61.42
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 42.9
+ }]
+ }],
+ "forecast": [{
+ "fromDateTime": "2019-10-02T06:00:00.000Z",
+ "tillDateTime": "2019-10-02T07:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 13.28
+ }, {
+ "name": "PM10",
+ "value": 18.37
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 22.14,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "It couldn't be better ;)",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 53.13
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 36.73
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T07:00:00.000Z",
+ "tillDateTime": "2019-10-02T08:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 11.19
+ }, {
+ "name": "PM10",
+ "value": 15.65
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 18.65,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Enjoy life!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 44.76
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 31.31
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T08:00:00.000Z",
+ "tillDateTime": "2019-10-02T09:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 8.79
+ }, {
+ "name": "PM10",
+ "value": 12.8
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 14.65,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe deep! The air is clean!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 35.15
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 25.59
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T09:00:00.000Z",
+ "tillDateTime": "2019-10-02T10:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 5.46
+ }, {
+ "name": "PM10",
+ "value": 8.91
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 9.11,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe to fill your lungs!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 21.86
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 17.83
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T10:00:00.000Z",
+ "tillDateTime": "2019-10-02T11:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 2.26
+ }, {
+ "name": "PM10",
+ "value": 5.02
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 5.02,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Enjoy life!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 9.06
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 10.05
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T11:00:00.000Z",
+ "tillDateTime": "2019-10-02T12:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 1.06
+ }, {
+ "name": "PM10",
+ "value": 2.52
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 2.52,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "The air is great!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 4.22
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 5.05
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T12:00:00.000Z",
+ "tillDateTime": "2019-10-02T13:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 0.48
+ }, {
+ "name": "PM10",
+ "value": 1.94
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 1.94,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe as much as you can!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 1.94
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 3.89
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T13:00:00.000Z",
+ "tillDateTime": "2019-10-02T14:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 0.63
+ }, {
+ "name": "PM10",
+ "value": 2.26
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 2.26,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Enjoy life!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 2.53
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 4.52
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T14:00:00.000Z",
+ "tillDateTime": "2019-10-02T15:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 1.47
+ }, {
+ "name": "PM10",
+ "value": 3.39
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 3.39,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe as much as you can!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 5.87
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 6.78
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T15:00:00.000Z",
+ "tillDateTime": "2019-10-02T16:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 2.62
+ }, {
+ "name": "PM10",
+ "value": 5.02
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 5.02,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Great air!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 10.5
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 10.05
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T16:00:00.000Z",
+ "tillDateTime": "2019-10-02T17:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 3.89
+ }, {
+ "name": "PM10",
+ "value": 8.02
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 8.02,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Dear me, how wonderful!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 15.56
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 16.04
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T17:00:00.000Z",
+ "tillDateTime": "2019-10-02T18:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 6.26
+ }, {
+ "name": "PM10",
+ "value": 11.41
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 11.41,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "The air is great!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 25.05
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 22.83
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T18:00:00.000Z",
+ "tillDateTime": "2019-10-02T19:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 8.69
+ }, {
+ "name": "PM10",
+ "value": 14.48
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 14.48,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Zero dust - zero worries!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 34.76
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 28.96
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T19:00:00.000Z",
+ "tillDateTime": "2019-10-02T20:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 10.78
+ }, {
+ "name": "PM10",
+ "value": 16.86
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 17.97,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Zero dust - zero worries!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 43.13
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 33.72
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T20:00:00.000Z",
+ "tillDateTime": "2019-10-02T21:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 12.22
+ }, {
+ "name": "PM10",
+ "value": 18.19
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 20.36,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe to fill your lungs!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 48.88
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 36.38
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T21:00:00.000Z",
+ "tillDateTime": "2019-10-02T22:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 13.06
+ }, {
+ "name": "PM10",
+ "value": 18.62
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 21.77,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Dear me, how wonderful!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 52.25
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 37.24
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T22:00:00.000Z",
+ "tillDateTime": "2019-10-02T23:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 13.51
+ }, {
+ "name": "PM10",
+ "value": 18.49
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 22.52,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "The air is great!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 54.06
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 36.98
+ }]
+ }, {
+ "fromDateTime": "2019-10-02T23:00:00.000Z",
+ "tillDateTime": "2019-10-03T00:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 13.46
+ }, {
+ "name": "PM10",
+ "value": 17.63
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 22.44,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Green, green, green!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 53.85
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 35.26
+ }]
+ }, {
+ "fromDateTime": "2019-10-03T00:00:00.000Z",
+ "tillDateTime": "2019-10-03T01:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 13.05
+ }, {
+ "name": "PM10",
+ "value": 16.36
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 21.74,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Catch your breath!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 52.19
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 32.73
+ }]
+ }, {
+ "fromDateTime": "2019-10-03T01:00:00.000Z",
+ "tillDateTime": "2019-10-03T02:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 12.47
+ }, {
+ "name": "PM10",
+ "value": 15.16
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 20.79,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Green, green, green!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 49.9
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 30.32
+ }]
+ }, {
+ "fromDateTime": "2019-10-03T02:00:00.000Z",
+ "tillDateTime": "2019-10-03T03:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 11.99
+ }, {
+ "name": "PM10",
+ "value": 14.07
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 19.98,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe as much as you can!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 47.94
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 28.14
+ }]
+ }, {
+ "fromDateTime": "2019-10-03T03:00:00.000Z",
+ "tillDateTime": "2019-10-03T04:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 11.74
+ }, {
+ "name": "PM10",
+ "value": 13.67
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 19.56,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Dear me, how wonderful!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 46.95
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 27.34
+ }]
+ }, {
+ "fromDateTime": "2019-10-03T04:00:00.000Z",
+ "tillDateTime": "2019-10-03T05:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 11.44
+ }, {
+ "name": "PM10",
+ "value": 13.51
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 19.06,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe to fill your lungs!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 45.74
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 27.02
+ }]
+ }, {
+ "fromDateTime": "2019-10-03T05:00:00.000Z",
+ "tillDateTime": "2019-10-03T06:00:00.000Z",
+ "values": [{
+ "name": "PM25",
+ "value": 10.88
+ }, {
+ "name": "PM10",
+ "value": 13.38
+ }],
+ "indexes": [{
+ "name": "AIRLY_CAQI",
+ "value": 18.13,
+ "level": "VERY_LOW",
+ "description": "Great air here today!",
+ "advice": "Breathe as much as you can!",
+ "color": "#6BC926"
+ }],
+ "standards": [{
+ "name": "WHO",
+ "pollutant": "PM25",
+ "limit": 25.0,
+ "percent": 43.52
+ }, {
+ "name": "WHO",
+ "pollutant": "PM10",
+ "limit": 50.0,
+ "percent": 26.76
+ }]
+ }]
+}
\ No newline at end of file
diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json
new file mode 100644
index 00000000000..e17df9c2039
--- /dev/null
+++ b/tests/fixtures/homematicip_cloud.json
@@ -0,0 +1,5530 @@
+{
+ "clients": {
+ "00000000-0000-0000-0000-000000000000": {
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000000",
+ "label": "TEST-Client",
+ "clientType": "APP"
+ }
+ },
+ "devices": {
+ "3014F7110000000000000031": {
+ "availableFirmwareVersion": "1.2.1",
+ "firmwareVersion": "1.2.1",
+ "firmwareVersionInteger": 66049,
+ "functionalChannels": {
+ "0": {
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "coProUpdateFailure": false,
+ "configPending": false,
+ "deviceId": "3014F7110000000000000031",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "deviceUndervoltage": false,
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -88,
+ "rssiPeerValue": null,
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceCoProError": false,
+ "IFeatureDeviceCoProRestart": false,
+ "IFeatureDeviceCoProUpdate": false,
+ "IFeatureDeviceOverheated": false,
+ "IFeatureDeviceOverloaded": false,
+ "IFeatureDeviceTemperatureOutOfRange": false,
+ "IFeatureDeviceUndervoltage": false
+ },
+ "temperatureOutOfRange": false,
+ "unreach": false
+ },
+ "1": {
+ "accelerationSensorEventFilterPeriod": 3.0,
+ "accelerationSensorMode": "FLAT_DECT",
+ "accelerationSensorNeutralPosition": "VERTICAL",
+ "accelerationSensorSensitivity": "SENSOR_RANGE_4G",
+ "accelerationSensorTriggerAngle": 45,
+ "accelerationSensorTriggered": true,
+ "deviceId": "3014F7110000000000000031",
+ "functionalChannelType": "ACCELERATION_SENSOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "index": 1,
+ "label": "",
+ "notificationSoundTypeHighToLow": "SOUND_LONG",
+ "notificationSoundTypeLowToHigh": "SOUND_LONG"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000031",
+ "label": "Garagentor",
+ "lastStatusUpdate": 1567850423788,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 315,
+ "modelType": "HmIP-SAM",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000031",
+ "type": "ACCELERATION_SENSOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000052": {
+ "availableFirmwareVersion": "1.0.5",
+ "firmwareVersion": "1.0.5",
+ "firmwareVersionInteger": 65541,
+ "functionalChannels": {
+ "0": {
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "coProUpdateFailure": false,
+ "configPending": false,
+ "deviceId": "3014F7110000000000000052",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "deviceUndervoltage": false,
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -73,
+ "rssiPeerValue": null,
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceCoProError": false,
+ "IFeatureDeviceCoProRestart": false,
+ "IFeatureDeviceCoProUpdate": false,
+ "IFeatureDeviceOverheated": false,
+ "IFeatureDeviceOverloaded": false,
+ "IFeatureDeviceTemperatureOutOfRange": false,
+ "IFeatureDeviceUndervoltage": false
+ },
+ "temperatureOutOfRange": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000052",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "index": 1,
+ "label": ""
+ },
+ "2": {
+ "deviceId": "3014F7110000000000000052",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 2,
+ "label": ""
+ },
+ "3": {
+ "deviceId": "3014F7110000000000000052",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 2,
+ "groups": [],
+ "index": 3,
+ "label": ""
+ },
+ "4": {
+ "deviceId": "3014F7110000000000000052",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 2,
+ "groups": [],
+ "index": 4,
+ "label": ""
+ },
+ "5": {
+ "deviceId": "3014F7110000000000000052",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 3,
+ "groups": [],
+ "index": 5,
+ "label": ""
+ },
+ "6": {
+ "deviceId": "3014F7110000000000000052",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 3,
+ "groups": [],
+ "index": 6,
+ "label": ""
+ },
+ "7": {
+ "deviceId": "3014F7110000000000000052",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 4,
+ "groups": [],
+ "index": 7,
+ "label": ""
+ },
+ "8": {
+ "deviceId": "3014F7110000000000000052",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 4,
+ "groups": [],
+ "index": 8,
+ "label": ""
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000052",
+ "label": "Alarm-Melder",
+ "lastStatusUpdate": 1564733931898,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 336,
+ "modelType": "HmIP-MOD-RC8",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000052",
+ "type": "REMOTE_CONTROL_8_MODULE",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F71100000000FAL24C10": {
+ "availableFirmwareVersion": "1.6.2",
+ "firmwareVersion": "1.6.2",
+ "firmwareVersionInteger": 67074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "coolingEmergencyValue": 0.0,
+ "deviceId": "3014F71100000000FAL24C10",
+ "dutyCycle": false,
+ "frostProtectionTemperature": 8.0,
+ "functionalChannelType": "DEVICE_GLOBAL_PUMP_CONTROL",
+ "globalPumpControl": true,
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "heatingEmergencyValue": 0.25,
+ "heatingLoadType": "LOAD_BALANCING",
+ "heatingValveType": "NORMALLY_CLOSE",
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -73,
+ "rssiPeerValue": -74,
+ "unreach": false,
+ "valveProtectionDuration": 5,
+ "valveProtectionSwitchingInterval": 14
+ },
+ "1": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "index": 1,
+ "label": "",
+ "pumpFollowUpTime": 2,
+ "pumpLeadTime": 2,
+ "pumpProtectionDuration": 1,
+ "pumpProtectionSwitchingInterval": 14
+ },
+ "10": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 10,
+ "groups": [
+ ],
+ "index": 10,
+ "label": ""
+ },
+ "11": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "HEAT_DEMAND_CHANNEL",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 11,
+ "label": ""
+ },
+ "12": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "DEHUMIDIFIER_DEMAND_CHANNEL",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 12,
+ "label": ""
+ },
+ "2": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 2,
+ "groups": [
+ ],
+ "index": 2,
+ "label": ""
+ },
+ "3": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 3,
+ "groups": [
+ ],
+ "index": 3,
+ "label": ""
+ },
+ "4": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 4,
+ "groups": [
+ ],
+ "index": 4,
+ "label": ""
+ },
+ "5": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 5,
+ "groups": [
+ ],
+ "index": 5,
+ "label": ""
+ },
+ "6": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 6,
+ "groups": [
+ ],
+ "index": 6,
+ "label": ""
+ },
+ "7": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 7,
+ "groups": [
+ ],
+ "index": 7,
+ "label": ""
+ },
+ "8": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 8,
+ "groups": [
+ ],
+ "index": 8,
+ "label": ""
+ },
+ "9": {
+ "deviceId": "3014F71100000000FAL24C10",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 9,
+ "groups": [
+ ],
+ "index": 9,
+ "label": ""
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F71100000000FAL24C10",
+ "label": "Fu\u00dfbodenheizungsaktor",
+ "lastStatusUpdate": 1558461135830,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 280,
+ "modelType": "HmIP-FAL24-C10",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F71100000000FAL24C10",
+ "type": "FLOOR_TERMINAL_BLOCK_10",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F71100000000000BBL24": {
+ "availableFirmwareVersion": "1.6.2",
+ "firmwareVersion": "1.6.2",
+ "firmwareVersionInteger": 67074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F71100000000000BBL24",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000034"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -64,
+ "rssiPeerValue": -76,
+ "unreach": false
+ },
+ "1": {
+ "blindModeActive": true,
+ "bottomToTopReferenceTime": 54.88,
+ "changeOverDelay": 0.5,
+ "delayCompensationValue": 12.7,
+ "deviceId": "3014F71100000000000BBL24",
+ "endpositionAutoDetectionEnabled": true,
+ "functionalChannelType": "BLIND_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 1,
+ "label": "",
+ "previousShutterLevel": null,
+ "previousSlatsLevel": null,
+ "processing": false,
+ "profileMode": "AUTOMATIC",
+ "selfCalibrationInProgress": null,
+ "shutterLevel": 0.885,
+ "slatsLevel": 1.0,
+ "slatsReferenceTime": 1.6,
+ "supportingDelayCompensation": true,
+ "supportingEndpositionAutoDetection": true,
+ "supportingSelfCalibration": true,
+ "topToBottomReferenceTime": 53.68,
+ "userDesiredProfileMode": "MANUAL"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F71100000000000BBL24",
+ "label": "Jalousie Schiebet\u00fcr",
+ "lastStatusUpdate": 1558464454532,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 332,
+ "modelType": "HmIP-BBL",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F71100000000000BBL24",
+ "type": "BRAND_BLIND",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000BCBB11": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.10.10",
+ "firmwareVersionInteger": 68106,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000BCBB11",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -53,
+ "rssiPeerValue": -56,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000BCBB11",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 1,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "2": {
+ "deviceId": "3014F7110000000000BCBB11",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 2,
+ "groups": [
+ "00000000-0000-0000-0000-000000000038"
+ ],
+ "index": 2,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000BCBB11",
+ "label": "Jalousien - 1 KiZi, 2 SchlaZi",
+ "lastStatusUpdate": 1555621612744,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 357,
+ "modelType": "HmIP-PCBS2",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000BCBB11",
+ "type": "PRINTED_CIRCUIT_BOARD_SWITCH_2",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711ABCD0ABCD000002": {
+ "availableFirmwareVersion": "1.6.4",
+ "firmwareVersion": "1.6.4",
+ "firmwareVersionInteger": 67076,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711ABCD0ABCD000002",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000027"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -79,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F711ABCD0ABCD000002",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 1,
+ "label": "",
+ "on": true,
+ "profileMode": null,
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "2": {
+ "deviceId": "3014F711ABCD0ABCD000002",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 2,
+ "groups": [],
+ "index": 2,
+ "label": "",
+ "on": false,
+ "profileMode": null,
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "3": {
+ "deviceId": "3014F711ABCD0ABCD000002",
+ "functionalChannelType": "GENERIC_INPUT_CHANNEL",
+ "groupIndex": 3,
+ "groups": [],
+ "index": 3,
+ "label": ""
+ },
+ "4": {
+ "deviceId": "3014F711ABCD0ABCD000002",
+ "functionalChannelType": "GENERIC_INPUT_CHANNEL",
+ "groupIndex": 4,
+ "groups": [],
+ "index": 4,
+ "label": ""
+ },
+ "5": {
+ "analogOutputLevel": 12.5,
+ "deviceId": "3014F711ABCD0ABCD000002",
+ "functionalChannelType": "ANALOG_OUTPUT_CHANNEL",
+ "groupIndex": 0,
+ "groups": [],
+ "index": 5,
+ "label": ""
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711ABCD0ABCD000002",
+ "label": "Multi IO Box",
+ "lastStatusUpdate": 1552508702220,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 283,
+ "modelType": "HmIP-MIOB",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711ABCD0ABCD000002",
+ "type": "MULTI_IO_BOX",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F71100000000ABCDEF10": {
+ "automaticValveAdaptionNeeded": false,
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.6",
+ "firmwareVersionInteger": 65542,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F71100000000ABCDEF10",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000010"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -47,
+ "rssiPeerValue": -50,
+ "sabotage": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F71100000000ABCDEF10",
+ "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000013"
+ ],
+ "index": 1,
+ "label": "",
+ "setPointTemperature": 21.0,
+ "temperatureOffset": 0.0,
+ "valveActualTemperature": 21.6,
+ "valvePosition": 0.0,
+ "valveState": "ADAPTION_DONE"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F71100000000ABCDEF10",
+ "label": "Wohnzimmer 3",
+ "lastStatusUpdate": 1550912664486,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 325,
+ "modelType": "HmIP-eTRV-C",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F71100000000ABCDEF10",
+ "type": "HEATING_THERMOSTAT_COMPACT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F71100000000000TEST1": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.8.8",
+ "firmwareVersionInteger": 67592,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F71100000000000TEST1",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -51,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F71100000000000TEST1",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 1,
+ "label": ""
+ },
+ "2": {
+ "deviceId": "3014F71100000000000TEST1",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 2,
+ "label": ""
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F71100000000000TEST1",
+ "label": "Remote",
+ "lastStatusUpdate": 1550512733995,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 358,
+ "modelType": "HmIP-BRC2",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F71100000000000TEST1",
+ "type": "BRAND_PUSH_BUTTON",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000064": {
+ "availableFirmwareVersion": "1.0.6",
+ "firmwareVersion": "1.0.6",
+ "firmwareVersionInteger": 65542,
+ "functionalChannels": {
+ "0": {
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "coProUpdateFailure": false,
+ "configPending": false,
+ "deviceId": "3014F7110000000000000064",
+ "deviceOverheated": true,
+ "deviceOverloaded": false,
+ "deviceUndervoltage": false,
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000032",
+ "00000000-0000-0000-0000-000000000013"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -42,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceCoProError": false,
+ "IFeatureDeviceCoProRestart": false,
+ "IFeatureDeviceCoProUpdate": false,
+ "IFeatureDeviceOverheated": true,
+ "IFeatureDeviceOverloaded": false,
+ "IFeatureDeviceTemperatureOutOfRange": false,
+ "IFeatureDeviceUndervoltage": false
+ },
+ "temperatureOutOfRange": false,
+ "unreach": false
+ },
+ "1": {
+ "alarmContactType": "WINDOW_DOOR_CONTACT",
+ "contactType": "NORMALLY_CLOSE",
+ "deviceId": "3014F7110000000000000064",
+ "eventDelay": 0,
+ "functionalChannelType": "CONTACT_INTERFACE_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000033",
+ "00000000-0000-0000-0000-000000000010",
+ "00000000-0000-0000-0000-000000000013"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "CLOSED"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000064",
+ "label": "Schlie\u00dfer Magnet",
+ "lastStatusUpdate": 1524515854304,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 375,
+ "modelType": "HmIP-SCI",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000064",
+ "type": "SHUTTER_CONTACT_INTERFACE",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711BADCAFE000000001": {
+ "availableFirmwareVersion": "1.2.0",
+ "firmwareVersion": "1.2.0",
+ "firmwareVersionInteger": 66048,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711BADCAFE000000001",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -73,
+ "rssiPeerValue": -78,
+ "unreach": false
+ },
+ "1": {
+ "blindModeActive": true,
+ "bottomToTopReferenceTime": 41.0,
+ "changeOverDelay": 0.5,
+ "delayCompensationValue": 1.0,
+ "deviceId": "3014F711BADCAFE000000001",
+ "endpositionAutoDetectionEnabled": false,
+ "functionalChannelType": "BLIND_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 1,
+ "label": "",
+ "previousShutterLevel": null,
+ "previousSlatsLevel": null,
+ "processing": false,
+ "profileMode": "AUTOMATIC",
+ "selfCalibrationInProgress": null,
+ "shutterLevel": 1.0,
+ "slatsLevel": 1.0,
+ "slatsReferenceTime": 2.0,
+ "supportingDelayCompensation": false,
+ "supportingEndpositionAutoDetection": false,
+ "supportingSelfCalibration": false,
+ "topToBottomReferenceTime": 41.0,
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711BADCAFE000000001",
+ "label": "Sofa links",
+ "lastStatusUpdate": 1548616026922,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 333,
+ "modelType": "HmIP-FBL",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711BADCAFE000000001",
+ "type": "FULL_FLUSH_BLIND",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000055": {
+ "availableFirmwareVersion": "1.2.4",
+ "firmwareVersion": "1.2.4",
+ "firmwareVersionInteger": 66052,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000055",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000034"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -76,
+ "rssiPeerValue": -77,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 21.0,
+ "deviceId": "3014F7110000000000000055",
+ "display": "SETPOINT",
+ "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000035"
+ ],
+ "humidity": 40,
+ "index": 1,
+ "label": "",
+ "vaporAmount": 6.177718198711658,
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 21.5,
+ "temperatureOffset": 0.0
+ },
+ "2": {
+ "deviceId": "3014F7110000000000000055",
+ "frostProtectionTemperature": 8.0,
+ "functionalChannelType": "INTERNAL_SWITCH_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000035"
+ ],
+ "heatingValveType": "NORMALLY_CLOSE",
+ "index": 2,
+ "internalSwitchOutputEnabled": true,
+ "label": "",
+ "valveProtectionDuration": 5,
+ "valveProtectionSwitchingInterval": 14
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000055",
+ "label": "BWTH 1",
+ "lastStatusUpdate": 1547283716818,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 305,
+ "modelType": "HmIP-BWTH",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000055",
+ "type": "BRAND_WALL_MOUNTED_THERMOSTAT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711ABCDEF0000000014": {
+ "availableFirmwareVersion": "1.4.2",
+ "firmwareVersion": "1.4.2",
+ "firmwareVersionInteger": 66562,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711ABCDEF0000000014",
+ "dutyCycle": null,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000033"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": null,
+ "rssiPeerValue": null,
+ "unreach": null
+ },
+ "1": {
+ "deviceId": "3014F711ABCDEF0000000014",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "index": 1,
+ "label": ""
+ },
+ "2": {
+ "deviceId": "3014F711ABCDEF0000000014",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "index": 2,
+ "label": ""
+ },
+ "3": {
+ "deviceId": "3014F711ABCDEF0000000014",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 2,
+ "groups": [],
+ "index": 3,
+ "label": ""
+ },
+ "4": {
+ "deviceId": "3014F711ABCDEF0000000014",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 2,
+ "groups": [],
+ "index": 4,
+ "label": ""
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711ABCDEF0000000014",
+ "label": "FFB 1",
+ "lastStatusUpdate": 0,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 266,
+ "modelType": "HmIP-KRC4",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F711ABCDEF0000000014",
+ "type": "KEY_REMOTE_CONTROL_4",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711BSL0000000000050": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.2",
+ "firmwareVersionInteger": 65538,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711BSL0000000000050",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -67,
+ "rssiPeerValue": -70,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F711BSL0000000000050",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "index": 1,
+ "label": "",
+ "on": true,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "2": {
+ "deviceId": "3014F711BSL0000000000050",
+ "dimLevel": 0.0,
+ "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL",
+ "groupIndex": 2,
+ "groups": [],
+ "index": 2,
+ "label": "",
+ "on": null,
+ "profileMode": "AUTOMATIC",
+ "simpleRGBColorState": "RED",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "3": {
+ "deviceId": "3014F711BSL0000000000050",
+ "dimLevel": 1.0,
+ "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL",
+ "groupIndex": 3,
+ "groups": [],
+ "index": 3,
+ "label": "",
+ "on": true,
+ "profileMode": "AUTOMATIC",
+ "simpleRGBColorState": "GREEN",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711BSL0000000000050",
+ "label": "Treppe",
+ "lastStatusUpdate": 1548431183264,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 360,
+ "modelType": "HmIP-BSL",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711BSL0000000000050",
+ "type": "BRAND_SWITCH_NOTIFICATION_LIGHT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711SLO0000000000026": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.16",
+ "firmwareVersionInteger": 65552,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711SLO0000000000026",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -60,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "averageIllumination": 807.3,
+ "currentIllumination": 785.2,
+ "deviceId": "3014F711SLO0000000000026",
+ "functionalChannelType": "LIGHT_SENSOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "highestIllumination": 837.1,
+ "index": 1,
+ "label": "",
+ "lowestIllumination": 785.2
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711SLO0000000000026",
+ "label": "Lichtsensor Nord",
+ "lastStatusUpdate": 1548494235548,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 308,
+ "modelType": "HmIP-SLO",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F711SLO0000000000026",
+ "type": "LIGHT_SENSOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000054": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.0",
+ "firmwareVersionInteger": 65536,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000054",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000053"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -76,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000054",
+ "functionalChannelType": "PASSAGE_DETECTOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000055"
+ ],
+ "index": 1,
+ "label": "",
+ "leftCounter": 966,
+ "leftRightCounterDelta": 164,
+ "passageBlindtime": 1.5,
+ "passageDirection": "LEFT",
+ "passageSensorSensitivity": 50.0,
+ "passageTimeout": 0.5,
+ "rightCounter": 802
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000054",
+ "label": "SPDR_1",
+ "lastStatusUpdate": 1547282742305,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 304,
+ "modelType": "HmIP-SPDR",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000054",
+ "type": "PASSAGE_DETECTOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711000000000AAAAA25": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.12",
+ "firmwareVersionInteger": 65548,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711000000000AAAAA25",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_PERMANENT_FULL_RX",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000010"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "permanentFullRx": true,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -46,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F711000000000AAAAA25",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000048",
+ "00000000-0000-0000-0000-000000000034"
+ ],
+ "index": 1,
+ "label": ""
+ },
+ "2": {
+ "deviceId": "3014F711000000000AAAAA25",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000048",
+ "00000000-0000-0000-0000-000000000034"
+ ],
+ "index": 2,
+ "label": ""
+ },
+ "3": {
+ "currentIllumination": null,
+ "deviceId": "3014F711000000000AAAAA25",
+ "functionalChannelType": "MOTION_DETECTION_CHANNEL",
+ "groupIndex": 2,
+ "groups": [],
+ "illumination": 14.2,
+ "index": 3,
+ "label": "",
+ "motionBufferActive": true,
+ "motionDetected": false,
+ "motionDetectionSendInterval": "SECONDS_240",
+ "numberOfBrightnessMeasurements": 7
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711000000000AAAAA25",
+ "label": "Bewegungsmelder für 55er Rahmen – innen",
+ "lastStatusUpdate": 1546776387401,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 338,
+ "modelType": "HmIP-SMI55",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F711000000000AAAAA25",
+ "type": "MOTION_DETECTOR_PUSH_BUTTON",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000038": {
+ "availableFirmwareVersion": "1.0.18",
+ "firmwareVersion": "1.0.18",
+ "firmwareVersionInteger": 65554,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000038",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -55,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 4.3,
+ "deviceId": "3014F7110000000000000038",
+ "functionalChannelType": "WEATHER_SENSOR_PLUS_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "humidity": 97,
+ "vaporAmount": 6.177718198711658,
+ "illumination": 26.4,
+ "illuminationThresholdSunshine": 3500.0,
+ "index": 1,
+ "label": "",
+ "raining": false,
+ "storm": false,
+ "sunshine": false,
+ "todayRainCounter": 3.8999999999999773,
+ "todaySunshineDuration": 0,
+ "totalRainCounter": 544.0999999999999,
+ "totalSunshineDuration": 132057,
+ "windSpeed": 15.0,
+ "windValueType": "CURRENT_VALUE",
+ "yesterdayRainCounter": 25.600000000000023,
+ "yesterdaySunshineDuration": 0
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000038",
+ "label": "Weather Sensor – plus",
+ "lastStatusUpdate": 1546789939739,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 351,
+ "modelType": "HmIP-SWO-PL",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000038",
+ "type": "WEATHER_SENSOR_PLUS",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000BBBBB1": {
+ "availableFirmwareVersion": "1.6.2",
+ "firmwareVersion": "1.6.2",
+ "firmwareVersionInteger": 67074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "coolingEmergencyValue": 0.0,
+ "deviceId": "3014F7110000000000BBBBB1",
+ "dutyCycle": false,
+ "frostProtectionTemperature": 8.0,
+ "functionalChannelType": "DEVICE_GLOBAL_PUMP_CONTROL",
+ "globalPumpControl": true,
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000007"
+ ],
+ "heatingEmergencyValue": 0.25,
+ "heatingLoadType": "LOAD_BALANCING",
+ "heatingValveType": "NORMALLY_CLOSE",
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -62,
+ "rssiPeerValue": null,
+ "unreach": false,
+ "valveProtectionDuration": 5,
+ "valveProtectionSwitchingInterval": 14
+ },
+ "1": {
+ "deviceId": "3014F7110000000000BBBBB1",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "index": 1,
+ "label": "",
+ "pumpFollowUpTime": 2,
+ "pumpLeadTime": 2,
+ "pumpProtectionDuration": 1,
+ "pumpProtectionSwitchingInterval": 14
+ },
+ "2": {
+ "deviceId": "3014F7110000000000BBBBB1",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 2,
+ "groups": [
+ "00000000-0000-0000-0000-000000000008"
+ ],
+ "index": 2,
+ "label": ""
+ },
+ "3": {
+ "deviceId": "3014F7110000000000BBBBB1",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 3,
+ "groups": [
+ "00000000-0000-0000-0000-000000000009"
+ ],
+ "index": 3,
+ "label": ""
+ },
+ "4": {
+ "deviceId": "3014F7110000000000BBBBB1",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 4,
+ "groups": [
+ "00000000-0000-0000-0000-000000000010"
+ ],
+ "index": 4,
+ "label": ""
+ },
+ "5": {
+ "deviceId": "3014F7110000000000BBBBB1",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 5,
+ "groups": [
+ "00000000-0000-0000-0000-000000000011"
+ ],
+ "index": 5,
+ "label": ""
+ },
+ "6": {
+ "deviceId": "3014F7110000000000BBBBB1",
+ "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL",
+ "groupIndex": 6,
+ "groups": [],
+ "index": 6,
+ "label": ""
+ },
+ "7": {
+ "deviceId": "3014F7110000000000BBBBB1",
+ "functionalChannelType": "HEAT_DEMAND_CHANNEL",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000012",
+ "00000000-0000-0000-0000-000000000013"
+ ],
+ "index": 7,
+ "label": ""
+ },
+ "8": {
+ "deviceId": "3014F7110000000000BBBBB1",
+ "functionalChannelType": "DEHUMIDIFIER_DEMAND_CHANNEL",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000014"
+ ],
+ "index": 8,
+ "label": ""
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000BBBBB1",
+ "label": "Fußbodenheizungsaktor",
+ "lastStatusUpdate": 1545746610807,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 277,
+ "modelType": "HmIP-FAL230-C6",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000BBBBB1",
+ "type": "FLOOR_TERMINAL_BLOCK_6",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000BBBBB8": {
+ "availableFirmwareVersion": "1.2.16",
+ "firmwareVersion": "1.2.16",
+ "firmwareVersionInteger": 66064,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000BBBBB8",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -59,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000BBBBB8",
+ "functionalChannelType": "ALARM_SIREN_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 1,
+ "label": ""
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000BBBBB8",
+ "label": "Alarmsirene",
+ "lastStatusUpdate": 1544480290322,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 298,
+ "modelType": "HmIP-ASIR",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000BBBBB8",
+ "type": "ALARM_SIREN_INDOOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711000000000000BB11": {
+ "availableFirmwareVersion": "1.4.8",
+ "firmwareVersion": "1.4.8",
+ "firmwareVersionInteger": 66568,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711000000000000BB11",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -56,
+ "rssiPeerValue": -52,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "currentIllumination": null,
+ "deviceId": "3014F711000000000000BB11",
+ "functionalChannelType": "MOTION_DETECTION_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "illumination": 0.1,
+ "index": 1,
+ "label": "",
+ "motionBufferActive": false,
+ "motionDetected": true,
+ "motionDetectionSendInterval": "SECONDS_480",
+ "numberOfBrightnessMeasurements": 7
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711000000000000BB11",
+ "label": "Wohnzimmer",
+ "lastStatusUpdate": 1544480290322,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 291,
+ "modelType": "HmIP-SMI",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000011",
+ "type": "MOTION_DETECTOR_INDOOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F71100000000000BBB17": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.2",
+ "firmwareVersionInteger": 65538,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F71100000000000BBB17",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -70,
+ "rssiPeerValue": -67,
+ "unreach": false
+ },
+ "1": {
+ "currentIllumination": null,
+ "deviceId": "3014F71100000000000BBB17",
+ "functionalChannelType": "MOTION_DETECTION_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "illumination": 233.4,
+ "index": 1,
+ "label": "",
+ "motionBufferActive": true,
+ "motionDetected": true,
+ "motionDetectionSendInterval": "SECONDS_240",
+ "numberOfBrightnessMeasurements": 7
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F71100000000000BBB17",
+ "label": "Außen Küche",
+ "lastStatusUpdate": 1546776559553,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 302,
+ "modelType": "HmIP-SMO-A",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F71100000000000BBB17",
+ "type": "MOTION_DETECTOR_OUTDOOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000050": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.2",
+ "firmwareVersionInteger": "65538",
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000050",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_INCORRECT_POSITIONED",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000020"
+ ],
+ "incorrectPositioned": true,
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -65,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "acousticAlarmSignal": "FREQUENCY_RISING",
+ "acousticAlarmTiming": "ONCE_PER_MINUTE",
+ "acousticWaterAlarmTrigger": "WATER_DETECTION",
+ "deviceId": "3014F7110000000000000050",
+ "functionalChannelType": "WATER_SENSOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000023"
+ ],
+ "inAppWaterAlarmTrigger": "WATER_MOISTURE_DETECTION",
+ "index": 1,
+ "label": "",
+ "moistureDetected": false,
+ "sirenWaterAlarmTrigger": "WATER_MOISTURE_DETECTION",
+ "waterlevelDetected": false
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000050",
+ "label": "Wassersensor",
+ "lastStatusUpdate": 1530802738493,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 353,
+ "modelType": "HmIP-SWD",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000050",
+ "type": "WATER_SENSOR",
+ "updateState": "UP_TO_DATE"
+
+ },
+ "3014F7110000000000000000": {
+ "availableFirmwareVersion": "1.16.8",
+ "firmwareVersion": "1.16.8",
+ "firmwareVersionInteger": 69640,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000000",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000004",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -85,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000000",
+ "eventDelay": 0,
+ "functionalChannelType": "SHUTTER_CONTACT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000006",
+ "00000000-0000-0000-0000-000000000007",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "OPEN"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000000",
+ "label": "Balkontüre",
+ "lastStatusUpdate": 1524516526498,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 258,
+ "modelType": "HMIP-SWDO",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000000",
+ "type": "SHUTTER_CONTACT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000005551": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.2.12",
+ "firmwareVersionInteger": 66060,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000005551",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -73,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000005551",
+ "eventDelay": 0,
+ "functionalChannelType": "SHUTTER_CONTACT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000010",
+ "00000000-0000-0000-0000-000000000007"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "CLOSED"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000005551",
+ "label": "Eingangst\u00fcrkontakt",
+ "lastStatusUpdate": 1524515854304,
+ "liveUpdateState": "UP_TO_DATE",
+ "manufacturerCode": 1,
+ "modelId": 340,
+ "modelType": "HmIP-SWDM",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000005551",
+ "type": "SHUTTER_CONTACT_MAGNETIC",
+ "updateState": "BACKGROUND_UPDATE_NOT_SUPPORTED"
+ },
+ "3014F7110000000000000001": {
+ "availableFirmwareVersion": "1.16.8",
+ "firmwareVersion": "1.16.8",
+ "firmwareVersionInteger": 69640,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000001",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000008",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -64,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000001",
+ "eventDelay": 0,
+ "functionalChannelType": "SHUTTER_CONTACT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000009",
+ "00000000-0000-0000-0000-000000000010",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "CLOSED"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000001",
+ "label": "Fenster",
+ "lastStatusUpdate": 1524515854304,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 258,
+ "modelType": "HMIP-SWDO",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000001",
+ "type": "SHUTTER_CONTACT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000002": {
+ "availableFirmwareVersion": "1.16.8",
+ "firmwareVersion": "1.16.8",
+ "firmwareVersionInteger": 69640,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000002",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000004",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -95,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000002",
+ "eventDelay": 0,
+ "functionalChannelType": "SHUTTER_CONTACT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000006",
+ "00000000-0000-0000-0000-000000000007",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "CLOSED"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000002",
+ "label": "Balkonfenster",
+ "lastStatusUpdate": 1524516088763,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 258,
+ "modelType": "HMIP-SWDO",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000002",
+ "type": "SHUTTER_CONTACT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000003": {
+ "availableFirmwareVersion": "1.16.8",
+ "firmwareVersion": "1.16.8",
+ "firmwareVersionInteger": 69640,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000003",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000004",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -78,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000003",
+ "eventDelay": 0,
+ "functionalChannelType": "SHUTTER_CONTACT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000006",
+ "00000000-0000-0000-0000-000000000007",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "CLOSED"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000003",
+ "label": "Küche",
+ "lastStatusUpdate": 1524514836466,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 258,
+ "modelType": "HMIP-SWDO",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000003",
+ "type": "SHUTTER_CONTACT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000004": {
+ "availableFirmwareVersion": "1.16.8",
+ "firmwareVersion": "1.16.8",
+ "firmwareVersionInteger": 69640,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000004",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000011",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -56,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000004",
+ "eventDelay": 0,
+ "functionalChannelType": "SHUTTER_CONTACT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000012",
+ "00000000-0000-0000-0000-000000000013",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "OPEN"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000004",
+ "label": "Fenster",
+ "lastStatusUpdate": 1524512404032,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 258,
+ "modelType": "HMIP-SWDO",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000004",
+ "type": "SHUTTER_CONTACT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000005": {
+ "availableFirmwareVersion": "1.16.8",
+ "firmwareVersion": "1.16.8",
+ "firmwareVersionInteger": 69640,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000005",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000004",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -80,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000005",
+ "eventDelay": 0,
+ "functionalChannelType": "SHUTTER_CONTACT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000006",
+ "00000000-0000-0000-0000-000000000007",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "OPEN"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000005",
+ "label": "Wohnzimmer",
+ "lastStatusUpdate": 0,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 258,
+ "modelType": "HMIP-SWDO",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000005",
+ "type": "SHUTTER_CONTACT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000006": {
+ "availableFirmwareVersion": "1.16.8",
+ "firmwareVersion": "1.16.8",
+ "firmwareVersionInteger": 69640,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000006",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000014",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -76,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000006",
+ "eventDelay": 0,
+ "functionalChannelType": "SHUTTER_CONTACT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000015",
+ "00000000-0000-0000-0000-000000000005"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "CLOSED"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000006",
+ "label": "Wohnungstüre",
+ "lastStatusUpdate": 1524516489316,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 258,
+ "modelType": "HMIP-SWDO",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000006",
+ "type": "SHUTTER_CONTACT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000007": {
+ "availableFirmwareVersion": "1.16.8",
+ "firmwareVersion": "1.16.8",
+ "firmwareVersionInteger": 69640,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000007",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000014",
+ "00000000-0000-0000-0000-000000000016"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -56,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000007",
+ "eventDelay": 0,
+ "functionalChannelType": "SHUTTER_CONTACT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000016",
+ "00000000-0000-0000-0000-000000000015"
+ ],
+ "index": 1,
+ "label": "",
+ "windowState": "CLOSED"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000007",
+ "label": "Vorzimmer",
+ "lastStatusUpdate": 1524515489257,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 258,
+ "modelType": "HMIP-SWDO",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000007",
+ "type": "SHUTTER_CONTACT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000108": {
+ "availableFirmwareVersion": "1.12.6",
+ "firmwareVersion": "1.12.6",
+ "firmwareVersionInteger": 68614,
+ "functionalChannels": {
+ "0": {
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "coProUpdateFailure": false,
+ "configPending": false,
+ "deviceId": "3014F7110000000000000108",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "deviceUndervoltage": false,
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000009"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -68,
+ "rssiPeerValue": -63,
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceCoProError": false,
+ "IFeatureDeviceCoProRestart": false,
+ "IFeatureDeviceCoProUpdate": false,
+ "IFeatureDeviceOverheated": true,
+ "IFeatureDeviceOverloaded": false,
+ "IFeatureDeviceTemperatureOutOfRange": false,
+ "IFeatureDeviceUndervoltage": false
+ },
+ "temperatureOutOfRange": false,
+ "unreach": false
+ },
+ "1": {
+ "currentPowerConsumption": 0.0,
+ "deviceId": "3014F7110000000000000108",
+ "energyCounter": 6.333200000000001,
+ "functionalChannelType": "SWITCH_MEASURING_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000023"
+ ],
+ "index": 1,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000108",
+ "label": "Flur oben",
+ "lastStatusUpdate": 1570365990392,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 288,
+ "modelType": "HmIP-BSM",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000108",
+ "type": "BRAND_SWITCH_MEASURING",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000109": {
+ "availableFirmwareVersion": "1.6.2",
+ "firmwareVersion": "1.6.2",
+ "firmwareVersionInteger": 67074,
+ "functionalChannels": {
+ "0": {
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "coProUpdateFailure": false,
+ "configPending": false,
+ "deviceId": "3014F7110000000000000109",
+ "deviceOverheated": null,
+ "deviceOverloaded": false,
+ "deviceUndervoltage": false,
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000029"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -80,
+ "rssiPeerValue": -73,
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceCoProError": false,
+ "IFeatureDeviceCoProRestart": false,
+ "IFeatureDeviceCoProUpdate": false,
+ "IFeatureDeviceOverheated": true,
+ "IFeatureDeviceOverloaded": false,
+ "IFeatureDeviceTemperatureOutOfRange": false,
+ "IFeatureDeviceUndervoltage": false
+ },
+ "temperatureOutOfRange": false,
+ "unreach": false
+ },
+ "1": {
+ "currentPowerConsumption": 0.0,
+ "deviceId": "3014F7110000000000000109",
+ "energyCounter": 0.0011,
+ "functionalChannelType": "SWITCH_MEASURING_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000030"
+ ],
+ "index": 1,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000011",
+ "label": "Ausschalter Terrasse Bewegungsmelder",
+ "lastStatusUpdate": 1570366291250,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 289,
+ "modelType": "HmIP-FSM",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000109",
+ "type": "FULL_FLUSH_SWITCH_MEASURING",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000008": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "2.6.2",
+ "firmwareVersionInteger": 132610,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000008",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000017"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": true,
+ "routerModuleSupported": true,
+ "rssiDeviceValue": -48,
+ "rssiPeerValue": -49,
+ "unreach": false
+ },
+ "1": {
+ "currentPowerConsumption": 195.3,
+ "deviceId": "3014F7110000000000000008",
+ "energyCounter": 35.536,
+ "functionalChannelType": "SWITCH_MEASURING_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000018"
+ ],
+ "index": 1,
+ "label": "",
+ "on": true,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000008",
+ "label": "Pc",
+ "lastStatusUpdate": 1524516554056,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 262,
+ "modelType": "HMIP-PSM",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000008",
+ "type": "PLUGABLE_SWITCH_MEASURING",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000009": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "2.6.2",
+ "firmwareVersionInteger": 132610,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000009",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000017"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": true,
+ "routerModuleSupported": true,
+ "rssiDeviceValue": -60,
+ "rssiPeerValue": -66,
+ "unreach": false
+ },
+ "1": {
+ "currentPowerConsumption": 0.0,
+ "deviceId": "3014F7110000000000000009",
+ "energyCounter": 0.4754,
+ "functionalChannelType": "SWITCH_MEASURING_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000018"
+ ],
+ "index": 1,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000009",
+ "label": "Brunnen",
+ "lastStatusUpdate": 1524515786303,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 262,
+ "modelType": "HMIP-PSM",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000009",
+ "type": "PLUGABLE_SWITCH_MEASURING",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000010": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "2.6.2",
+ "firmwareVersionInteger": 132610,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000010",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000017"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": true,
+ "routerModuleSupported": true,
+ "rssiDeviceValue": -47,
+ "rssiPeerValue": -49,
+ "unreach": false
+ },
+ "1": {
+ "currentPowerConsumption": 2.04,
+ "deviceId": "3014F7110000000000000010",
+ "energyCounter": 1.5343,
+ "functionalChannelType": "SWITCH_MEASURING_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000018"
+ ],
+ "index": 1,
+ "label": "",
+ "on": true,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000010",
+ "label": "Büro",
+ "lastStatusUpdate": 1524513613922,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 262,
+ "modelType": "HMIP-PSM",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000010",
+ "type": "PLUGABLE_SWITCH_MEASURING",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000110": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "2.6.2",
+ "firmwareVersionInteger": 132610,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000110",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000017"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": true,
+ "routerModuleSupported": true,
+ "rssiDeviceValue": -47,
+ "rssiPeerValue": -49,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000110",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000018"
+ ],
+ "index": 1,
+ "label": "",
+ "on": true,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000110",
+ "label": "Schrank",
+ "lastStatusUpdate": 1524513613922,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 262,
+ "modelType": "HMIP-PS",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000110",
+ "type": "PLUGABLE_SWITCH",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000011": {
+ "automaticValveAdaptionNeeded": false,
+ "availableFirmwareVersion": "2.0.2",
+ "firmwareVersion": "2.0.2",
+ "firmwareVersionInteger": 131074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000011",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000011"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -54,
+ "rssiPeerValue": -51,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000011",
+ "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000012"
+ ],
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 5.0,
+ "temperatureOffset": 0.0,
+ "valvePosition": 0.0,
+ "valveState": "ADAPTION_DONE"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000011",
+ "label": "Heizung",
+ "lastStatusUpdate": 1524516360178,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 269,
+ "modelType": "HMIP-eTRV",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000011",
+ "type": "HEATING_THERMOSTAT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000012": {
+ "automaticValveAdaptionNeeded": false,
+ "availableFirmwareVersion": "2.0.2",
+ "firmwareVersion": "2.0.2",
+ "firmwareVersionInteger": 131074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000012",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000008"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -46,
+ "rssiPeerValue": -54,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000012",
+ "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000010"
+ ],
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 19.0,
+ "temperatureOffset": 0.0,
+ "valvePosition": 0.0,
+ "valveState": "ADAPTION_DONE"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000012",
+ "label": "Heizkörperthermostat",
+ "lastStatusUpdate": 1524514105832,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 269,
+ "modelType": "HMIP-eTRV",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000012",
+ "type": "HEATING_THERMOSTAT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000013": {
+ "automaticValveAdaptionNeeded": false,
+ "availableFirmwareVersion": "2.0.2",
+ "firmwareVersion": "2.0.2",
+ "firmwareVersionInteger": 131074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000013",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000014"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -58,
+ "rssiPeerValue": -58,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000013",
+ "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000019"
+ ],
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 5.0,
+ "temperatureOffset": 0.0,
+ "valvePosition": 0.0,
+ "valveState": "ADAPTION_DONE"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000013",
+ "label": "Heizkörperthermostat",
+ "lastStatusUpdate": 1524514007132,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 269,
+ "modelType": "HMIP-eTRV",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000013",
+ "type": "HEATING_THERMOSTAT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000014": {
+ "automaticValveAdaptionNeeded": false,
+ "availableFirmwareVersion": "2.0.2",
+ "firmwareVersion": "2.0.2",
+ "firmwareVersionInteger": 131074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000014",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000004"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": true,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -60,
+ "rssiPeerValue": -58,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000014",
+ "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000007"
+ ],
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 5.0,
+ "temperatureOffset": 0.0,
+ "valvePosition": 0.0,
+ "valveState": "ADAPTION_DONE"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000014",
+ "label": "Küche-Heizung",
+ "lastStatusUpdate": 1524513898337,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 269,
+ "modelType": "HMIP-eTRV",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000014",
+ "type": "HEATING_THERMOSTAT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000015": {
+ "automaticValveAdaptionNeeded": false,
+ "availableFirmwareVersion": "2.0.2",
+ "firmwareVersion": "2.0.2",
+ "firmwareVersionInteger": 131074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000015",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000004"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": true,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -65,
+ "rssiPeerValue": -66,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000015",
+ "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000007"
+ ],
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 5.0,
+ "temperatureOffset": 0.0,
+ "valvePosition": 0.0,
+ "valveState": "ADAPTION_DONE"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000015",
+ "label": "Wohnzimmer-Heizung",
+ "lastStatusUpdate": 1524513950325,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 269,
+ "modelType": "HMIP-eTRV",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000015",
+ "type": "HEATING_THERMOSTAT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000016": {
+ "automaticValveAdaptionNeeded": false,
+ "availableFirmwareVersion": "2.0.2",
+ "firmwareVersion": "2.0.2",
+ "firmwareVersionInteger": 131074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000016",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000020"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -50,
+ "rssiPeerValue": -51,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000016",
+ "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000021"
+ ],
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 5.0,
+ "temperatureOffset": 0.0,
+ "valvePosition": 0.0,
+ "valveState": "ADAPTION_DONE"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000016",
+ "label": "Heizkörperthermostat",
+ "lastStatusUpdate": 1524514626157,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 269,
+ "modelType": "HMIP-eTRV",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000016",
+ "type": "HEATING_THERMOSTAT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000017": {
+ "automaticValveAdaptionNeeded": false,
+ "availableFirmwareVersion": "2.0.2",
+ "firmwareVersion": "2.0.2",
+ "firmwareVersionInteger": 131074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000017",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000004"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": true,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -67,
+ "rssiPeerValue": -62,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000017",
+ "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000007"
+ ],
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 5.0,
+ "temperatureOffset": 0.0,
+ "valvePosition": 0.0,
+ "valveState": "ADAPTION_DONE"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000017",
+ "label": "Balkon-Heizung",
+ "lastStatusUpdate": 1524511331830,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 269,
+ "modelType": "HMIP-eTRV",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000017",
+ "type": "HEATING_THERMOSTAT",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000018": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.11",
+ "firmwareVersionInteger": 65547,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000018",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000004",
+ "00000000-0000-0000-0000-000000000016"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -67,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000018",
+ "functionalChannelType": "SMOKE_DETECTOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000022",
+ "00000000-0000-0000-0000-000000000006"
+ ],
+ "index": 1,
+ "label": "",
+ "smokeDetectorAlarmType": "IDLE_OFF"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000018",
+ "label": "Rauchwarnmelder",
+ "lastStatusUpdate": 1524461072721,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 296,
+ "modelType": "HmIP-SWSD",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000018",
+ "type": "SMOKE_DETECTOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000019": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.11",
+ "firmwareVersionInteger": 65547,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000019",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000008",
+ "00000000-0000-0000-0000-000000000016"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -50,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000019",
+ "functionalChannelType": "SMOKE_DETECTOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000022",
+ "00000000-0000-0000-0000-000000000009"
+ ],
+ "index": 1,
+ "label": "",
+ "smokeDetectorAlarmType": "IDLE_OFF"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000019",
+ "label": "Rauchwarnmelder",
+ "lastStatusUpdate": 1524480981494,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 296,
+ "modelType": "HmIP-SWSD",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000019",
+ "type": "SMOKE_DETECTOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000020": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.11",
+ "firmwareVersionInteger": 65547,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000020",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000011",
+ "00000000-0000-0000-0000-000000000016"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -54,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000020",
+ "functionalChannelType": "SMOKE_DETECTOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000013",
+ "00000000-0000-0000-0000-000000000022"
+ ],
+ "index": 1,
+ "label": "",
+ "smokeDetectorAlarmType": "IDLE_OFF"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000020",
+ "label": "Rauchwarnmelder",
+ "lastStatusUpdate": 1524456324824,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 296,
+ "modelType": "HmIP-SWSD",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000020",
+ "type": "SMOKE_DETECTOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000021": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.11",
+ "firmwareVersionInteger": 65547,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000021",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000014",
+ "00000000-0000-0000-0000-000000000016"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -80,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F7110000000000000021",
+ "functionalChannelType": "SMOKE_DETECTOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000022",
+ "00000000-0000-0000-0000-000000000015"
+ ],
+ "index": 1,
+ "label": "",
+ "smokeDetectorAlarmType": "IDLE_OFF"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000021",
+ "label": "Rauchwarnmelder",
+ "lastStatusUpdate": 1524443129876,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 296,
+ "modelType": "HmIP-SWSD",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000021",
+ "type": "SMOKE_DETECTOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000022": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.8.0",
+ "firmwareVersionInteger": 67584,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000022",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000011"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -76,
+ "rssiPeerValue": -63,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 24.7,
+ "deviceId": "3014F7110000000000000022",
+ "display": "ACTUAL_HUMIDITY",
+ "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000012"
+ ],
+ "humidity": 43,
+ "vaporAmount": 6.177718198711658,
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 5.0,
+ "temperatureOffset": 0.0
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000022",
+ "label": "Wandthermostat",
+ "lastStatusUpdate": 1524516534382,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 297,
+ "modelType": "HmIP-WTH-2",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000022",
+ "type": "WALL_MOUNTED_THERMOSTAT_PRO",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000023": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.8.0",
+ "firmwareVersionInteger": 67584,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000023",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000008"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -61,
+ "rssiPeerValue": -58,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 24.5,
+ "deviceId": "3014F7110000000000000023",
+ "display": "ACTUAL_HUMIDITY",
+ "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000010"
+ ],
+ "humidity": 46,
+ "vaporAmount": 6.177718198711658,
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 19.0,
+ "temperatureOffset": 0.0
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000023",
+ "label": "Wandthermostat",
+ "lastStatusUpdate": 1524516454116,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 297,
+ "modelType": "HmIP-WTH-2",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000023",
+ "type": "WALL_MOUNTED_THERMOSTAT_PRO",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000024": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.8.0",
+ "firmwareVersionInteger": 67584,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000024",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000004"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -75,
+ "rssiPeerValue": -85,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 23.6,
+ "deviceId": "3014F7110000000000000024",
+ "display": "ACTUAL",
+ "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000007"
+ ],
+ "humidity": 45,
+ "vaporAmount": 6.177718198711658,
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 5.0,
+ "temperatureOffset": 0.0
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000024",
+ "label": "Wandthermostat",
+ "lastStatusUpdate": 1524516436601,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 297,
+ "modelType": "HmIP-WTH-2",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000024",
+ "type": "WALL_MOUNTED_THERMOSTAT_PRO",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000025": {
+ "availableFirmwareVersion": "1.8.0",
+ "firmwareVersion": "1.8.0",
+ "firmwareVersionInteger": 67584,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000025",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000020"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -46,
+ "rssiPeerValue": -47,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 23.8,
+ "deviceId": "3014F7110000000000000025",
+ "display": "ACTUAL_HUMIDITY",
+ "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000021"
+ ],
+ "humidity": 47,
+ "vaporAmount": 6.177718198711658,
+ "index": 1,
+ "label": "",
+ "valveActualTemperature": 20.0,
+ "setPointTemperature": 5.0,
+ "temperatureOffset": 0.0
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000025",
+ "label": "Wandthermostat",
+ "lastStatusUpdate": 1524516556479,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 297,
+ "modelType": "HmIP-WTH-2",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000025",
+ "type": "WALL_MOUNTED_THERMOSTAT_PRO",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F7110000000000000029": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.14",
+ "firmwareVersionInteger": 65550,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F7110000000000000029",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000019"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -46,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "binaryBehaviorType": "NORMALLY_CLOSE",
+ "deviceId": "3014F7110000000000000029",
+ "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000020"
+ ],
+ "index": 1,
+ "label": "",
+ "multiModeInputMode": "KEY_BEHAVIOR",
+ "windowState": "CLOSED"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F7110000000000000029",
+ "label": "Kontakt-Schnittstelle Unterputz – 1-fach",
+ "lastStatusUpdate": 1547923306429,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 382,
+ "modelType": "HmIP-FCI1",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F7110000000000000029",
+ "type": "FULL_FLUSH_CONTACT_INTERFACE",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711AAAA000000000001": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.10",
+ "firmwareVersionInteger": 65546,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711AAAA000000000001",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000008"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -68,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 15.4,
+ "deviceId": "3014F711AAAA000000000001",
+ "functionalChannelType": "WEATHER_SENSOR_PRO_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-AAAA-0000-0000-000000000001"
+ ],
+ "humidity": 65,
+ "vaporAmount": 6.177718198711658,
+ "illumination": 4153.0,
+ "illuminationThresholdSunshine": 10.0,
+ "index": 1,
+ "label": "",
+ "raining": false,
+ "storm": false,
+ "sunshine": true,
+ "todayRainCounter": 6.5,
+ "todaySunshineDuration": 100,
+ "totalRainCounter": 6.5,
+ "totalSunshineDuration": 100,
+ "weathervaneAlignmentNeeded": false,
+ "windDirection": 295.0,
+ "windDirectionVariation": 56.25,
+ "windSpeed": 2.6,
+ "windValueType": "AVERAGE_VALUE",
+ "yesterdayRainCounter": 0.0,
+ "yesterdaySunshineDuration": 0
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711AAAA000000000001",
+ "label": "Wettersensor - pro",
+ "lastStatusUpdate": 1524513950325,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 352,
+ "modelType": "HmIP-SWO-PR",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711AAAA000000000001",
+ "type": "WEATHER_SENSOR_PRO",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711AAAA000000000002": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.6",
+ "firmwareVersionInteger": 65542,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711AAAA000000000002",
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "dutyCycle": false,
+ "groups": [
+ "00000000-0000-0000-0000-000000000008"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -55,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 15.1,
+ "deviceId": "3014F711AAAA000000000002",
+ "functionalChannelType": "CLIMATE_SENSOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-AAAA-0000-0000-000000000001"
+ ],
+ "humidity": 70,
+ "vaporAmount": 6.177718198711658,
+ "index": 1,
+ "label": ""
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711AAAA000000000002",
+ "label": "Temperatur- und Luftfeuchtigkeitssensor - außen",
+ "lastStatusUpdate": 1524513950325,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 314,
+ "modelType": "HmIP-STHO",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711AAAA000000000002",
+ "type": "TEMPERATURE_HUMIDITY_SENSOR_OUTDOOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711AAAA000000000003": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.10",
+ "firmwareVersionInteger": 65546,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711AAAA000000000003",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [ "00000000-0000-0000-0000-000000000008" ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -77,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 15.2,
+ "deviceId": "3014F711AAAA000000000003",
+ "functionalChannelType": "WEATHER_SENSOR_CHANNEL",
+ "groupIndex": 1,
+ "groups": [ "00000000-AAAA-0000-0000-000000000001" ],
+ "humidity": 42,
+ "vaporAmount": 6.177718198711658,
+ "illumination": 4890.0,
+ "illuminationThresholdSunshine": 3500.0,
+ "index": 1,
+ "label": "",
+ "storm": false,
+ "sunshine": true,
+ "todaySunshineDuration": 51,
+ "totalSunshineDuration": 54,
+ "windSpeed": 6.6,
+ "windValueType": "MAX_VALUE",
+ "yesterdaySunshineDuration": 3
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711AAAA000000000003",
+ "label": "Wettersensor",
+ "lastStatusUpdate": 1524513950325,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 350,
+ "modelType": "HmIP-SWO-B",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711AAAA000000000003",
+ "type": "WEATHER_SENSOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711AAAA000000000004": {
+ "availableFirmwareVersion": "1.2.10",
+ "firmwareVersion": "1.2.10",
+ "firmwareVersionInteger": 66058,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711AAAA000000000004",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [ "00000000-0000-0000-0000-000000000008" ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -54,
+ "rssiPeerValue": null,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F711AAAA000000000004",
+ "eventDelay": 0,
+ "functionalChannelType": "ROTARY_HANDLE_CHANNEL",
+ "groupIndex": 1,
+ "groups": [ "00000000-0000-0000-0000-000000000009" ],
+ "index": 1,
+ "label": "",
+ "windowState": "TILTED"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711AAAA000000000004",
+ "label": "Fenstergriffsensor",
+ "lastStatusUpdate": 1524816385462,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 286,
+ "modelType": "HmIP-SRH",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711AAAA000000000004",
+ "type": "ROTARY_HANDLE_SENSOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711AAAA000000000005": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.4.8",
+ "firmwareVersionInteger": 66568,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711AAAA000000000005",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000008"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -44,
+ "rssiPeerValue": -42,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F711AAAA000000000005",
+ "dimLevel": 0.0,
+ "functionalChannelType": "DIMMER_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000008"
+ ],
+ "index": 1,
+ "label": "",
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711AAAA000000000005",
+ "label": "Schlafzimmerlicht",
+ "lastStatusUpdate": 1524816385462,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 290,
+ "modelType": "HmIP-BDT",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711AAAA000000000005",
+ "type": "BRAND_DIMMER",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711BBBBBBBBBBBBB017": {
+ "availableFirmwareVersion": "1.0.19",
+ "firmwareVersion": "1.0.19",
+ "firmwareVersionInteger": 65555,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711BBBBBBBBBBBBB017",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -61,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F711BBBBBBBBBBBBB017",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 1,
+ "label": ""
+ },
+ "2": {
+ "deviceId": "3014F711BBBBBBBBBBBBB017",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 2,
+ "label": ""
+ },
+ "3": {
+ "deviceId": "3014F711BBBBBBBBBBBBB017",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 2,
+ "groups": [
+ ],
+ "index": 3,
+ "label": ""
+ },
+ "4": {
+ "deviceId": "3014F711BBBBBBBBBBBBB017",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 2,
+ "groups": [
+ ],
+ "index": 4,
+ "label": ""
+ },
+ "5": {
+ "deviceId": "3014F711BBBBBBBBBBBBB017",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 3,
+ "groups": [
+ ],
+ "index": 5,
+ "label": ""
+ },
+ "6": {
+ "deviceId": "3014F711BBBBBBBBBBBBB017",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 3,
+ "groups": [
+ ],
+ "index": 6,
+ "label": ""
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711BBBBBBBBBBBBB017",
+ "label": "Wandtaster - 6-fach",
+ "lastStatusUpdate": 1544475961687,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 300,
+ "modelType": "HmIP-WRC6",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB017",
+ "type": "PUSH_BUTTON_6",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711BBBBBBBBBBBBB016": {
+ "availableFirmwareVersion": "1.0.19",
+ "firmwareVersion": "1.0.19",
+ "firmwareVersionInteger": 65555,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711BBBBBBBBBBBBB016",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -42,
+ "rssiPeerValue": null,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F711BBBBBBBBBBBBB016",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 1,
+ "label": ""
+ },
+ "2": {
+ "deviceId": "3014F711BBBBBBBBBBBBB016",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ ],
+ "index": 2,
+ "label": ""
+ },
+ "3": {
+ "deviceId": "3014F711BBBBBBBBBBBBB016",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 2,
+ "groups": [
+ ],
+ "index": 3,
+ "label": ""
+ },
+ "4": {
+ "deviceId": "3014F711BBBBBBBBBBBBB016",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 2,
+ "groups": [
+ ],
+ "index": 4,
+ "label": ""
+ },
+ "5": {
+ "deviceId": "3014F711BBBBBBBBBBBBB016",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 3,
+ "groups": [
+ ],
+ "index": 5,
+ "label": ""
+ },
+ "6": {
+ "deviceId": "3014F711BBBBBBBBBBBBB016",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 3,
+ "groups": [
+ ],
+ "index": 6,
+ "label": ""
+ },
+ "7": {
+ "deviceId": "3014F711BBBBBBBBBBBBB016",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 4,
+ "groups": [
+ ],
+ "index": 7,
+ "label": ""
+ },
+ "8": {
+ "deviceId": "3014F711BBBBBBBBBBBBB016",
+ "functionalChannelType": "SINGLE_KEY_CHANNEL",
+ "groupIndex": 4,
+ "groups": [
+ ],
+ "index": 8,
+ "label": ""
+ }
+
+
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711BBBBBBBBBBBBB016",
+ "label": "Fernbedienung - 8 Tasten",
+ "lastStatusUpdate": 1544479483638,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 299,
+ "modelType": "HmIP-RC8",
+ "oem": "eQ-3",
+ "permanentlyReachable": false,
+ "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB016",
+ "type": "REMOTE_CONTROL_8",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711AAAAAAAAAAAAAA51": {
+ "availableFirmwareVersion": "1.4.0",
+ "firmwareVersion": "1.4.0",
+ "firmwareVersionInteger": 66560,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711AAAAAAAAAAAAAA51",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_SABOTAGE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000021",
+ "00000000-0000-0000-0000-000000000060"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -62,
+ "rssiPeerValue": -61,
+ "sabotage": false,
+ "unreach": false
+ },
+ "1": {
+ "currentIllumination": null,
+ "deviceId": "3014F711AAAAAAAAAAAAAA51",
+ "functionalChannelType": "PRESENCE_DETECTION_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000022",
+ "00000000-0000-0000-0000-000000000060"
+ ],
+ "illumination": 1.8,
+ "index": 1,
+ "label": "",
+ "motionBufferActive": false,
+ "motionDetectionSendInterval": "SECONDS_240",
+ "numberOfBrightnessMeasurements": 7,
+ "presenceDetected": false
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711AAAAAAAAAAAAAA51",
+ "label": "SPI_1",
+ "lastStatusUpdate": 1542758692234,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 303,
+ "modelType": "HmIP-SPI",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711AAAAAAAAAAAAAA51",
+ "type": "PRESENCE_DETECTOR_INDOOR",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711ACBCDABCADCA66": {
+ "availableFirmwareVersion": "1.6.2",
+ "firmwareVersion": "1.6.2",
+ "firmwareVersionInteger": 67074,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711ACBCDABCADCA66",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000024"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -78,
+ "rssiPeerValue": -77,
+ "unreach": false
+ },
+ "1": {
+ "bottomToTopReferenceTime": 30.080000000000002,
+ "changeOverDelay": 0.5,
+ "delayCompensationValue": 12.7,
+ "deviceId": "3014F711ACBCDABCADCA66",
+ "endpositionAutoDetectionEnabled": true,
+ "functionalChannelType": "SHUTTER_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000069",
+ "00000000-0000-0000-0000-000000000070"
+ ],
+ "index": 1,
+ "label": "",
+ "previousShutterLevel": null,
+ "processing": false,
+ "profileMode": "AUTOMATIC",
+ "selfCalibrationInProgress": null,
+ "shutterLevel": 1.0,
+ "supportingDelayCompensation": true,
+ "supportingEndpositionAutoDetection": true,
+ "supportingSelfCalibration": true,
+ "topToBottomReferenceTime": 24.68,
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711ACBCDABCADCA66",
+ "label": "BROLL_1",
+ "lastStatusUpdate": 1542756558785,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 323,
+ "modelType": "HmIP-BROLL",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711ACBCDABCADCA66",
+ "type": "BRAND_SHUTTER",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711BBBBBBBBBBBBB18": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.8.12",
+ "firmwareVersionInteger": 67596,
+ "functionalChannels": {
+ "0": {
+ "configPending": false,
+ "deviceId": "3014F711BBBBBBBBBBBBB18",
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [
+ "00000000-0000-0000-0000-000000000041"
+ ],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -35,
+ "rssiPeerValue": -36,
+ "unreach": false
+ },
+ "1": {
+ "deviceId": "3014F711BBBBBBBBBBBBB18",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 1,
+ "groups": [
+ "00000000-0000-0000-0000-000000000042"
+ ],
+ "index": 1,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "2": {
+ "deviceId": "3014F711BBBBBBBBBBBBB18",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 2,
+ "groups": [
+ "00000000-0000-0000-0000-000000000042",
+ "00000000-0000-0000-0000-000000000040"
+ ],
+ "index": 2,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "3": {
+ "deviceId": "3014F711BBBBBBBBBBBBB18",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 3,
+ "groups": [
+ "00000000-0000-0000-0000-000000000042"
+ ],
+ "index": 3,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "4": {
+ "deviceId": "3014F711BBBBBBBBBBBBB18",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 4,
+ "groups": [
+ "00000000-0000-0000-0000-000000000042"
+ ],
+ "index": 4,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "5": {
+ "deviceId": "3014F711BBBBBBBBBBBBB18",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 5,
+ "groups": [
+ "00000000-0000-0000-0000-000000000042"
+ ],
+ "index": 5,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "6": {
+ "deviceId": "3014F711BBBBBBBBBBBBB18",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 6,
+ "groups": [
+ "00000000-0000-0000-0000-000000000042"
+ ],
+ "index": 6,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "7": {
+ "deviceId": "3014F711BBBBBBBBBBBBB18",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 7,
+ "groups": [
+ "00000000-0000-0000-0000-000000000042"
+ ],
+ "index": 7,
+ "label": "",
+ "on": false,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "8": {
+ "deviceId": "3014F711BBBBBBBBBBBBB18",
+ "functionalChannelType": "SWITCH_CHANNEL",
+ "groupIndex": 8,
+ "groups": [
+ "00000000-0000-0000-0000-000000000042"
+ ],
+ "index": 8,
+ "label": "",
+ "on": true,
+ "profileMode": "AUTOMATIC",
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711BBBBBBBBBBBBB18",
+ "label": "ioBroker",
+ "lastStatusUpdate": 1543746604446,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 307,
+ "modelType": "HmIP-MOD-OC8",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB18",
+ "type": "OPEN_COLLECTOR_8_MODULE",
+ "updateState": "UP_TO_DATE"
+ }
+ },
+ "groups": {
+ "00000000-0000-0000-0000-000000000020": {
+ "channels": [
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000025"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000016"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000050"
+ }
+ ],
+ "configPending": false,
+ "dutyCycle": false,
+ "groups": [
+ "00000000-0000-0000-0000-000000000021"
+ ],
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000020",
+ "incorrectPositioned": null,
+ "label": "Badezimmer",
+ "lastStatusUpdate": 1524516556479,
+ "lowBat": false,
+ "metaGroupId": null,
+ "sabotage": null,
+ "type": "META",
+ "unreach": false
+ },
+ "00000000-0000-0000-0000-000000000012": {
+ "activeProfile": "PROFILE_1",
+ "actualTemperature": 24.7,
+ "boostDuration": 15,
+ "boostMode": false,
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000004"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000022"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000011"
+ }
+ ],
+ "controlMode": "AUTOMATIC",
+ "controllable": true,
+ "cooling": false,
+ "coolingAllowed": false,
+ "coolingIgnored": false,
+ "dutyCycle": false,
+ "ecoAllowed": true,
+ "ecoIgnored": false,
+ "externalClockCoolingTemperature": 23.0,
+ "externalClockEnabled": false,
+ "externalClockHeatingTemperature": 19.0,
+ "floorHeatingMode": "FLOOR_HEATING_STANDARD",
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "humidity": 43,
+ "humidityLimitEnabled": true,
+ "humidityLimitValue": 60,
+ "id": "00000000-0000-0000-0000-000000000012",
+ "label": "Schlafzimmer",
+ "lastSetPointReachedTimestamp": 1557767559939,
+ "lastSetPointUpdatedTimestamp": 1557767559939,
+ "lastStatusUpdate": 1524516534382,
+ "lowBat": false,
+ "maxTemperature": 30.0,
+ "metaGroupId": "00000000-0000-0000-0000-000000000011",
+ "minTemperature": 5.0,
+ "partyMode": false,
+ "profiles": {
+ "PROFILE_1": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000012",
+ "index": "PROFILE_1",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000023",
+ "visible": true
+ },
+ "PROFILE_2": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000012",
+ "index": "PROFILE_2",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000024",
+ "visible": true
+ },
+ "PROFILE_3": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000012",
+ "index": "PROFILE_3",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000025",
+ "visible": false
+ },
+ "PROFILE_4": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000012",
+ "index": "PROFILE_4",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000026",
+ "visible": true
+ },
+ "PROFILE_5": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000012",
+ "index": "PROFILE_5",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000027",
+ "visible": true
+ },
+ "PROFILE_6": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000012",
+ "index": "PROFILE_6",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000028",
+ "visible": false
+ }
+ },
+ "setPointTemperature": 5.0,
+ "type": "HEATING",
+ "unreach": false,
+ "valvePosition": 0.0,
+ "valveSilentModeEnabled": false,
+ "valveSilentModeSupported": false,
+ "heatingFailureSupported": true,
+ "windowOpenTemperature": 5.0,
+ "windowState": "OPEN"
+ },
+ "00000000-0000-0000-0000-000000000016": {
+ "active": false,
+ "channels": [
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000021"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000020"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000007"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000007"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000019"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000018"
+ }
+ ],
+ "configPending": false,
+ "dutyCycle": false,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000016",
+ "ignorableDevices": [],
+ "label": "INTERNAL",
+ "lastStatusUpdate": 1524515489257,
+ "lowBat": false,
+ "metaGroupId": null,
+ "motionDetected": null,
+ "presenceDetected": null,
+ "sabotage": false,
+ "silent": true,
+ "type": "SECURITY_ZONE",
+ "unreach": false,
+ "windowState": "CLOSED",
+ "zoneAssignmentIndex": "ALARM_MODE_ZONE_3"
+ },
+ "00000000-0000-0000-0000-000000000017": {
+ "channels": [
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000008"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000009"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000010"
+ }
+ ],
+ "configPending": false,
+ "dutyCycle": false,
+ "groups": [
+ "00000000-0000-0000-0000-000000000018"
+ ],
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000017",
+ "incorrectPositioned": null,
+ "label": "Strom",
+ "lastStatusUpdate": 1524516554056,
+ "lowBat": null,
+ "metaGroupId": null,
+ "sabotage": null,
+ "type": "META",
+ "unreach": false
+ },
+ "00000000-0000-0000-0000-000000000029": {
+ "channels": [],
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000029",
+ "label": "HEATING_TEMPERATURE_LIMITER",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "type": "HEATING_TEMPERATURE_LIMITER",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000030": {
+ "boilerFollowUpTime": 0,
+ "boilerLeadTime": 0,
+ "channels": [],
+ "dutyCycle": null,
+ "heatDemand": null,
+ "heatDemandRuleEnabled": false,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000030",
+ "label": "HEATING_COOLING_DEMAND_BOILER",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "on": null,
+ "triggered": false,
+ "type": "HEATING_COOLING_DEMAND_BOILER",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000010": {
+ "activeProfile": "PROFILE_1",
+ "actualTemperature": 24.5,
+ "boostDuration": 15,
+ "boostMode": false,
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000001"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000023"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000012"
+ }
+ ],
+ "controlMode": "AUTOMATIC",
+ "controllable": true,
+ "cooling": false,
+ "coolingAllowed": false,
+ "coolingIgnored": false,
+ "dutyCycle": false,
+ "ecoAllowed": true,
+ "ecoIgnored": false,
+ "externalClockCoolingTemperature": 23.0,
+ "externalClockEnabled": false,
+ "externalClockHeatingTemperature": 19.0,
+ "floorHeatingMode": "FLOOR_HEATING_STANDARD",
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "humidity": 46,
+ "humidityLimitEnabled": true,
+ "humidityLimitValue": 60,
+ "id": "00000000-0000-0000-0000-000000000010",
+ "label": "Büro",
+ "lastSetPointReachedTimestamp": 1557767559939,
+ "lastSetPointUpdatedTimestamp": 1557767559939,
+ "lastStatusUpdate": 1524516454116,
+ "lowBat": false,
+ "maxTemperature": 30.0,
+ "metaGroupId": "00000000-0000-0000-0000-000000000008",
+ "minTemperature": 5.0,
+ "partyMode": false,
+ "profiles": {
+ "PROFILE_1": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000010",
+ "index": "PROFILE_1",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000031",
+ "visible": true
+ },
+ "PROFILE_2": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000010",
+ "index": "PROFILE_2",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000032",
+ "visible": true
+ },
+ "PROFILE_3": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000010",
+ "index": "PROFILE_3",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000033",
+ "visible": false
+ },
+ "PROFILE_4": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000010",
+ "index": "PROFILE_4",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000034",
+ "visible": true
+ },
+ "PROFILE_5": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000010",
+ "index": "PROFILE_5",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000035",
+ "visible": true
+ },
+ "PROFILE_6": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000010",
+ "index": "PROFILE_6",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000036",
+ "visible": false
+ }
+ },
+ "setPointTemperature": 19.0,
+ "type": "HEATING",
+ "unreach": false,
+ "valvePosition": 0.0,
+ "valveSilentModeEnabled": false,
+ "valveSilentModeSupported": false,
+ "heatingFailureSupported": true,
+ "windowOpenTemperature": 5.0,
+ "windowState": "CLOSED"
+ },
+ "00000000-0000-0000-0000-000000000018": {
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000010"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000009"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000008"
+ }
+ ],
+ "dimLevel": null,
+ "dutyCycle": false,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000018",
+ "label": "Strom",
+ "lastStatusUpdate": 1524516554056,
+ "lowBat": null,
+ "metaGroupId": "00000000-0000-0000-0000-000000000017",
+ "on": true,
+ "processing": null,
+ "shutterLevel": null,
+ "slatsLevel": null,
+ "type": "SWITCHING",
+ "unreach": false
+ },
+ "00000000-0000-0000-0000-000000000009": {
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000001"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000019"
+ }
+ ],
+ "dutyCycle": false,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000009",
+ "label": "Büro",
+ "lastStatusUpdate": 1524515854304,
+ "lowBat": false,
+ "metaGroupId": "00000000-0000-0000-0000-000000000008",
+ "motionDetected": null,
+ "presenceDetected": null,
+ "moistureDetected": null,
+ "waterlevelDetected": null,
+ "powerMainsFailure": null,
+ "sabotage": false,
+ "smokeDetectorAlarmType": "IDLE_OFF",
+ "type": "SECURITY",
+ "unreach": false,
+ "windowState": "CLOSED"
+ },
+ "00000000-0000-0000-0000-000000000013": {
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000004"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000020"
+ }
+ ],
+ "dutyCycle": false,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000013",
+ "label": "Schlafzimmer",
+ "lastStatusUpdate": 1524512404032,
+ "lowBat": false,
+ "metaGroupId": "00000000-0000-0000-0000-000000000011",
+ "motionDetected": null,
+ "presenceDetected": null,
+ "moistureDetected": null,
+ "waterlevelDetected": null,
+ "powerMainsFailure": null,
+ "sabotage": false,
+ "smokeDetectorAlarmType": "IDLE_OFF",
+ "type": "SECURITY",
+ "unreach": false,
+ "windowState": "OPEN"
+ },
+ "00000000-0000-0000-0000-000000000005": {
+ "active": false,
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000001"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000002"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000001"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000002"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000003"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000006"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000006"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000003"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000005"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000000"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000004"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000005"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000000"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000004"
+ }
+ ],
+ "configPending": false,
+ "dutyCycle": false,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000005",
+ "ignorableDevices": [],
+ "label": "EXTERNAL",
+ "lastStatusUpdate": 1524516526498,
+ "lowBat": false,
+ "metaGroupId": null,
+ "motionDetected": null,
+ "presenceDetected": null,
+ "sabotage": false,
+ "silent": true,
+ "type": "SECURITY_ZONE",
+ "unreach": false,
+ "windowState": "OPEN",
+ "zoneAssignmentIndex": "ALARM_MODE_ZONE_2"
+ },
+ "00000000-0000-0000-0000-000000000022": {
+ "acousticFeedbackEnabled": true,
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000020"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000018"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000021"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000019"
+ }
+ ],
+ "dimLevel": null,
+ "dutyCycle": false,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000022",
+ "label": "SIREN",
+ "lastStatusUpdate": 1524480981494,
+ "lowBat": false,
+ "metaGroupId": null,
+ "on": false,
+ "onTime": 180.0,
+ "signalAcoustic": "FREQUENCY_RISING",
+ "signalOptical": "DOUBLE_FLASHING_REPEATING",
+ "smokeDetectorAlarmType": "IDLE_OFF",
+ "type": "ALARM_SWITCHING",
+ "unreach": false
+ },
+ "00000000-0000-0000-0000-000000000037": {
+ "channels": [],
+ "dimLevel": null,
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000037",
+ "label": "COMING_HOME",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "on": null,
+ "type": "LINKED_SWITCHING",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000021": {
+ "activeProfile": "PROFILE_1",
+ "actualTemperature": 23.8,
+ "boostDuration": 15,
+ "boostMode": false,
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000025"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000016"
+ }
+ ],
+ "controlMode": "AUTOMATIC",
+ "controllable": true,
+ "cooling": false,
+ "coolingAllowed": false,
+ "coolingIgnored": false,
+ "dutyCycle": false,
+ "ecoAllowed": true,
+ "ecoIgnored": false,
+ "externalClockCoolingTemperature": 23.0,
+ "externalClockEnabled": false,
+ "externalClockHeatingTemperature": 19.0,
+ "floorHeatingMode": "FLOOR_HEATING_STANDARD",
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "humidity": 47,
+ "humidityLimitEnabled": true,
+ "humidityLimitValue": 60,
+ "id": "00000000-0000-0000-0000-000000000021",
+ "label": "Badezimmer",
+ "lastSetPointReachedTimestamp": 1557767559939,
+ "lastSetPointUpdatedTimestamp": 1557767559939,
+ "lastStatusUpdate": 1524516556479,
+ "lowBat": false,
+ "maxTemperature": 30.0,
+ "metaGroupId": "00000000-0000-0000-0000-000000000020",
+ "minTemperature": 5.0,
+ "partyMode": false,
+ "profiles": {
+ "PROFILE_1": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000021",
+ "index": "PROFILE_1",
+ "name": "STD",
+ "profileId": "00000000-0000-0000-0000-000000000038",
+ "visible": true
+ },
+ "PROFILE_2": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000021",
+ "index": "PROFILE_2",
+ "name": "Winter",
+ "profileId": "00000000-0000-0000-0000-000000000039",
+ "visible": true
+ },
+ "PROFILE_3": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000021",
+ "index": "PROFILE_3",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000040",
+ "visible": false
+ },
+ "PROFILE_4": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000021",
+ "index": "PROFILE_4",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000041",
+ "visible": true
+ },
+ "PROFILE_5": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000021",
+ "index": "PROFILE_5",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000042",
+ "visible": false
+ },
+ "PROFILE_6": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000021",
+ "index": "PROFILE_6",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000043",
+ "visible": false
+ }
+ },
+ "setPointTemperature": 5.0,
+ "type": "HEATING",
+ "unreach": false,
+ "valvePosition": 0.0,
+ "valveSilentModeEnabled": false,
+ "valveSilentModeSupported": false,
+ "heatingFailureSupported": true,
+ "windowOpenTemperature": 5.0,
+ "windowState": null
+ },
+ "00000000-0000-0000-0000-000000000006": {
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000005"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000002"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000000"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000018"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000003"
+ }
+ ],
+ "dutyCycle": false,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000006",
+ "label": "Wohnzimmer",
+ "lastStatusUpdate": 1524516526498,
+ "lowBat": false,
+ "metaGroupId": "00000000-0000-0000-0000-000000000004",
+ "motionDetected": null,
+ "presenceDetected": null,
+ "moistureDetected": null,
+ "waterlevelDetected": null,
+ "powerMainsFailure": null,
+ "sabotage": false,
+ "smokeDetectorAlarmType": "IDLE_OFF",
+ "type": "SECURITY",
+ "unreach": false,
+ "windowState": "OPEN"
+ },
+ "00000000-0000-0000-0000-000000000044": {
+ "channels": [],
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000044",
+ "label": "INBOX",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "type": "INBOX",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000045": {
+ "channels": [],
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000045",
+ "label": "HEATING_HUMIDITY_LIMITER",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "type": "HEATING_HUMIDITY_LIMITER",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000008": {
+ "channels": [
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000001"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000012"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000023"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000019"
+ }
+ ],
+ "configPending": false,
+ "dutyCycle": false,
+ "groups": [
+ "00000000-0000-0000-0000-000000000010",
+ "00000000-0000-0000-0000-000000000009"
+ ],
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000008",
+ "incorrectPositioned": null,
+ "label": "Büro",
+ "lastStatusUpdate": 1524516454116,
+ "lowBat": false,
+ "metaGroupId": null,
+ "sabotage": false,
+ "type": "META",
+ "unreach": false
+ },
+ "00000000-0000-0000-0000-000000000011": {
+ "channels": [
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000022"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000004"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000020"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000011"
+ }
+ ],
+ "configPending": false,
+ "dutyCycle": false,
+ "groups": [
+ "00000000-0000-0000-0000-000000000012",
+ "00000000-0000-0000-0000-000000000013"
+ ],
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000011",
+ "incorrectPositioned": null,
+ "label": "Schlafzimmer",
+ "lastStatusUpdate": 1524516534382,
+ "lowBat": false,
+ "metaGroupId": null,
+ "sabotage": false,
+ "type": "META",
+ "unreach": false
+ },
+ "00000000-0000-0000-0000-000000000046": {
+ "channels": [],
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000046",
+ "label": "HEATING_CHANGEOVER",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "on": null,
+ "type": "HEATING_CHANGEOVER",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000014": {
+ "channels": [
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000021"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000007"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000006"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000013"
+ }
+ ],
+ "configPending": false,
+ "dutyCycle": false,
+ "groups": [
+ "00000000-0000-0000-0000-000000000015",
+ "00000000-0000-0000-0000-000000000019"
+ ],
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000014",
+ "incorrectPositioned": null,
+ "label": "Vorzimmer",
+ "lastStatusUpdate": 1524516489316,
+ "lowBat": false,
+ "metaGroupId": null,
+ "sabotage": false,
+ "type": "META",
+ "unreach": false
+ },
+ "00000000-0000-0000-0000-000000000007": {
+ "activeProfile": "PROFILE_1",
+ "actualTemperature": 23.6,
+ "boostDuration": 15,
+ "boostMode": false,
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000005"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000002"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000000"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000024"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000017"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000015"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000014"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000003"
+ }
+ ],
+ "controlMode": "AUTOMATIC",
+ "controllable": true,
+ "cooling": false,
+ "coolingAllowed": false,
+ "coolingIgnored": false,
+ "dutyCycle": false,
+ "ecoAllowed": true,
+ "ecoIgnored": false,
+ "externalClockCoolingTemperature": 23.0,
+ "externalClockEnabled": false,
+ "externalClockHeatingTemperature": 19.0,
+ "floorHeatingMode": "FLOOR_HEATING_STANDARD",
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "humidity": 45,
+ "humidityLimitEnabled": true,
+ "humidityLimitValue": 60,
+ "id": "00000000-0000-0000-0000-000000000007",
+ "label": "Wohnzimmer",
+ "lastSetPointReachedTimestamp": 1557767559939,
+ "lastSetPointUpdatedTimestamp": 1557767559939,
+ "lastStatusUpdate": 1524516526498,
+ "lowBat": false,
+ "maxTemperature": 30.0,
+ "metaGroupId": "00000000-0000-0000-0000-000000000004",
+ "minTemperature": 5.0,
+ "partyMode": false,
+ "profiles": {
+ "PROFILE_1": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000007",
+ "index": "PROFILE_1",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000047",
+ "visible": true
+ },
+ "PROFILE_2": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000007",
+ "index": "PROFILE_2",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000048",
+ "visible": true
+ },
+ "PROFILE_3": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000007",
+ "index": "PROFILE_3",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000049",
+ "visible": false
+ },
+ "PROFILE_4": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000007",
+ "index": "PROFILE_4",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000050",
+ "visible": true
+ },
+ "PROFILE_5": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000007",
+ "index": "PROFILE_5",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000051",
+ "visible": true
+ },
+ "PROFILE_6": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000007",
+ "index": "PROFILE_6",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000052",
+ "visible": false
+ }
+ },
+ "setPointTemperature": 5.0,
+ "type": "HEATING",
+ "unreach": false,
+ "valvePosition": 0.0,
+ "valveSilentModeEnabled": false,
+ "valveSilentModeSupported": false,
+ "heatingFailureSupported": true,
+ "windowOpenTemperature": 5.0,
+ "windowState": "OPEN"
+ },
+ "00000000-0000-0000-0000-000000000053": {
+ "channels": [],
+ "dimLevel": null,
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000053",
+ "label": "PANIC",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "on": null,
+ "type": "LINKED_SWITCHING",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000054": {
+ "channels": [],
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000054",
+ "label": "HEATING_EXTERNAL_CLOCK",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "type": "HEATING_EXTERNAL_CLOCK",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000055": {
+ "channels": [],
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000055",
+ "label": "HEATING_DEHUMIDIFIER",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "on": null,
+ "type": "HEATING_DEHUMIDIFIER",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000056": {
+ "acousticFeedbackEnabled": true,
+ "channels": [],
+ "dimLevel": null,
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000056",
+ "label": "ALARM",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "on": null,
+ "onTime": 7200.0,
+ "signalAcoustic": "FREQUENCY_RISING",
+ "signalOptical": "DOUBLE_FLASHING_REPEATING",
+ "smokeDetectorAlarmType": null,
+ "type": "ALARM_SWITCHING",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000057": {
+ "channels": [],
+ "dutyCycle": null,
+ "heatDemand": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000057",
+ "label": "HEATING_COOLING_DEMAND_PUMP",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "on": null,
+ "pumpFollowUpTime": 2,
+ "pumpLeadTime": 2,
+ "pumpProtectionDuration": 1,
+ "pumpProtectionSwitchingInterval": 14,
+ "type": "HEATING_COOLING_DEMAND_PUMP",
+ "unreach": null
+ },
+ "00000000-0000-0000-0000-000000000015": {
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000007"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000006"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000021"
+ }
+ ],
+ "dutyCycle": false,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000015",
+ "label": "Vorzimmer",
+ "lastStatusUpdate": 1524516489316,
+ "lowBat": false,
+ "metaGroupId": "00000000-0000-0000-0000-000000000014",
+ "motionDetected": null,
+ "presenceDetected": null,
+ "moistureDetected": null,
+ "waterlevelDetected": null,
+ "powerMainsFailure": null,
+ "sabotage": false,
+ "smokeDetectorAlarmType": "IDLE_OFF",
+ "type": "SECURITY",
+ "unreach": false,
+ "windowState": "CLOSED"
+ },
+ "00000000-0000-0000-0000-000000000004": {
+ "channels": [
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000024"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000005"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000002"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000000"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000014"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000003"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000017"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000015"
+ },
+ {
+ "channelIndex": 0,
+ "deviceId": "3014F7110000000000000018"
+ }
+ ],
+ "configPending": false,
+ "dutyCycle": false,
+ "groups": [
+ "00000000-0000-0000-0000-000000000006",
+ "00000000-0000-0000-0000-000000000007"
+ ],
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000004",
+ "incorrectPositioned": null,
+ "label": "Wohnzimmer",
+ "lastStatusUpdate": 1524516526498,
+ "lowBat": false,
+ "metaGroupId": null,
+ "sabotage": false,
+ "type": "META",
+ "unreach": false
+ },
+ "00000000-0000-0000-0000-000000000019": {
+ "activeProfile": "PROFILE_1",
+ "actualTemperature": null,
+ "boostDuration": 15,
+ "boostMode": false,
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000013"
+ }
+ ],
+ "controlMode": "AUTOMATIC",
+ "controllable": true,
+ "cooling": null,
+ "coolingAllowed": false,
+ "coolingIgnored": false,
+ "dutyCycle": false,
+ "ecoAllowed": true,
+ "ecoIgnored": false,
+ "externalClockCoolingTemperature": 23.0,
+ "externalClockEnabled": false,
+ "externalClockHeatingTemperature": 19.0,
+ "floorHeatingMode": "FLOOR_HEATING_STANDARD",
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "humidity": null,
+ "humidityLimitEnabled": true,
+ "humidityLimitValue": 60,
+ "id": "00000000-0000-0000-0000-000000000019",
+ "label": "Vorzimmer",
+ "lastSetPointReachedTimestamp": 1557767559939,
+ "lastSetPointUpdatedTimestamp": 1557767559939,
+ "lastStatusUpdate": 1524514007132,
+ "lowBat": false,
+ "maxTemperature": 30.0,
+ "metaGroupId": "00000000-0000-0000-0000-000000000014",
+ "minTemperature": 5.0,
+ "partyMode": false,
+ "profiles": {
+ "PROFILE_1": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000019",
+ "index": "PROFILE_1",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000058",
+ "visible": true
+ },
+ "PROFILE_2": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000019",
+ "index": "PROFILE_2",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000059",
+ "visible": false
+ },
+ "PROFILE_3": {
+ "enabled": true,
+ "groupId": "00000000-0000-0000-0000-000000000019",
+ "index": "PROFILE_3",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000060",
+ "visible": false
+ },
+ "PROFILE_4": {
+ "enabled": false,
+ "groupId": "00000000-0000-0000-0000-000000000019",
+ "index": "PROFILE_4",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000061",
+ "visible": true
+ },
+ "PROFILE_5": {
+ "enabled": false,
+ "groupId": "00000000-0000-0000-0000-000000000019",
+ "index": "PROFILE_5",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000062",
+ "visible": false
+ },
+ "PROFILE_6": {
+ "enabled": false,
+ "groupId": "00000000-0000-0000-0000-000000000019",
+ "index": "PROFILE_6",
+ "name": "",
+ "profileId": "00000000-0000-0000-0000-000000000063",
+ "visible": false
+ }
+ },
+ "setPointTemperature": 5.0,
+ "type": "HEATING",
+ "unreach": false,
+ "valvePosition": 0.0,
+ "valveSilentModeEnabled": false,
+ "valveSilentModeSupported": false,
+ "heatingFailureSupported": true,
+ "windowOpenTemperature": 5.0,
+ "windowState": null
+ },
+ "00000000-AAAA-0000-0000-000000000001": {
+ "actualTemperature": 15.4,
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F711AAAA000000000003"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F711AAAA000000000002"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F711AAAA000000000001"
+ }
+ ],
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "humidity": 65,
+ "id": "00000000-AAAA-0000-0000-000000000001",
+ "illumination": 4703.0,
+ "label": "Terrasse",
+ "lastStatusUpdate": 1520770214834,
+ "lowBat": false,
+ "metaGroupId": "76df95a5-afa5-45ee-b817-f724ffaf04a1",
+ "raining": false,
+ "type": "ENVIRONMENT",
+ "unreach": false,
+ "windSpeed": 29.1
+ },
+ "00000000-BBBB-0000-0000-000000000052": {
+ "channels": [],
+ "checkInterval": 600,
+ "dutyCycle": null,
+ "enabled": true,
+ "heatingFailureValidationResult": "NO_HEATING_FAILURE",
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-BBBB-0000-0000-000000000052",
+ "label": "HEATING_FAILURE_ALERT_RULE_GROUP",
+ "lastExecutionTimestamp": 1550773800084,
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "triggered": false,
+ "type": "HEATING_FAILURE_ALERT_RULE_GROUP",
+ "unreach": null,
+ "validationTimeout": 86400000
+ },
+ "00000000-AAAA-0000-0000-000000000068": {
+ "acousticFeedbackEnabled": true,
+ "channels": [],
+ "dimLevel": null,
+ "dutyCycle": null,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-AAAA-0000-0000-000000000068",
+ "label": "BACKUP_ALARM_SIREN",
+ "lastStatusUpdate": 0,
+ "lowBat": null,
+ "metaGroupId": null,
+ "on": null,
+ "onTime": 180.0,
+ "signalAcoustic": "FREQUENCY_RISING",
+ "signalOptical": "DISABLE_OPTICAL_SIGNAL",
+ "smokeDetectorAlarmType": null,
+ "type": "SECURITY_BACKUP_ALARM_SWITCHING",
+ "unreach": null
+ },
+ "00000000-0000-0000-AAAA-000000000029": {
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000023"
+ }
+ ],
+ "dutyCycle": false,
+ "enabled": true,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "humidityLowerThreshold": 40,
+ "humidityUpperThreshold": 60,
+ "humidityValidationResult": "LESSER_LOWER_THRESHOLD",
+ "id": "00000000-0000-0000-AAAA-000000000029",
+ "label": "B\u00fcro",
+ "lastExecutionTimestamp": 1551387905665,
+ "lastStatusUpdate": 1551388104260,
+ "lowBat": false,
+ "metaGroupId": "00000000-0000-0000-0000-000000000008",
+ "outdoorClimateSensor": null,
+ "triggered": false,
+ "type": "HUMIDITY_WARNING_RULE_GROUP",
+ "unreach": false,
+ "ventilationRecommended": true
+ },
+ "00000000-0000-0000-0000-000000000049": {
+ "channels": [
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000038"
+ },
+ {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000023"
+ }
+ ],
+ "dutyCycle": false,
+ "enabled": true,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "humidityLowerThreshold": 30,
+ "humidityUpperThreshold": 60,
+ "humidityValidationResult": null,
+ "id": "00000000-0000-0000-0000-000000000049",
+ "label": "Schlafzimmer",
+ "lastExecutionTimestamp": 0,
+ "lastStatusUpdate": 1551003370150,
+ "lowBat": false,
+ "metaGroupId": "00000000-0000-0000-0000-000000000008",
+ "outdoorClimateSensor": {
+ "channelIndex": 1,
+ "deviceId": "3014F7110000000000000038"
+ },
+ "triggered": false,
+ "type": "HUMIDITY_WARNING_RULE_GROUP",
+ "unreach": false,
+ "ventilationRecommended": false
+ }
+ },
+ "home": {
+ "apExchangeClientId": null,
+ "apExchangeState": "NONE",
+ "availableAPVersion": null,
+ "carrierSense": null,
+ "clients": [
+ "00000000-0000-0000-0000-000000000000"
+ ],
+ "connected": true,
+ "currentAPVersion": "1.2.4",
+ "deviceUpdateStrategy": "AUTOMATICALLY_IF_POSSIBLE",
+ "dutyCycle": 8.0,
+ "functionalHomes": {
+ "INDOOR_CLIMATE": {
+ "absenceEndTime": null,
+ "absenceType": "NOT_ABSENT",
+ "active": true,
+ "coolingEnabled": false,
+ "ecoDuration": "PERMANENT",
+ "ecoTemperature": 17.0,
+ "floorHeatingSpecificGroups": {
+ "HEATING_CHANGEOVER": "00000000-0000-0000-0000-000000000046",
+ "HEATING_COOLING_DEMAND_BOILER": "00000000-0000-0000-0000-000000000030",
+ "HEATING_COOLING_DEMAND_PUMP": "00000000-0000-0000-0000-000000000057",
+ "HEATING_DEHUMIDIFIER": "00000000-0000-0000-0000-000000000055",
+ "HEATING_EXTERNAL_CLOCK": "00000000-0000-0000-0000-000000000054",
+ "HEATING_HUMIDITY_LIMITER": "00000000-0000-0000-0000-000000000045",
+ "HEATING_TEMPERATURE_LIMITER": "00000000-0000-0000-0000-000000000029"
+ },
+ "functionalGroups": [
+ "00000000-0000-0000-0000-000000000012",
+ "00000000-0000-0000-0000-000000000007",
+ "00000000-0000-0000-0000-000000000019",
+ "00000000-0000-0000-0000-000000000010",
+ "00000000-0000-0000-0000-000000000021"
+ ],
+ "optimumStartStopEnabled": false,
+ "solution": "INDOOR_CLIMATE"
+ },
+ "LIGHT_AND_SHADOW": {
+ "active": true,
+ "extendedLinkedShutterGroups": [],
+ "extendedLinkedSwitchingGroups": [],
+ "functionalGroups": [
+ "00000000-0000-0000-0000-000000000018"
+ ],
+ "shutterProfileGroups": [],
+ "solution": "LIGHT_AND_SHADOW",
+ "switchingProfileGroups": []
+ },
+ "SECURITY_AND_ALARM": {
+ "activationInProgress": false,
+ "active": true,
+ "alarmActive": false,
+ "alarmEventDeviceId": "3014F7110000000000000007",
+ "alarmEventTimestamp": 1524504122047,
+ "alarmSecurityJournalEntryType": "SENSOR_EVENT",
+ "functionalGroups": [
+ "00000000-0000-0000-0000-000000000013",
+ "00000000-0000-0000-0000-000000000006",
+ "00000000-0000-0000-0000-000000000015",
+ "00000000-0000-0000-0000-000000000009"
+ ],
+ "intrusionAlertThroughSmokeDetectors": false,
+ "securitySwitchingGroups": {
+ "ALARM": "00000000-0000-0000-0000-000000000056",
+ "BACKUP_ALARM_SIREN": "00000000-AAAA-0000-0000-000000000068",
+ "COMING_HOME": "00000000-0000-0000-0000-000000000037",
+ "PANIC": "00000000-0000-0000-0000-000000000053",
+ "SIREN": "00000000-0000-0000-0000-000000000022"
+ },
+ "securityZoneActivationMode": "ACTIVATION_WITH_DEVICE_IGNORELIST",
+ "securityZones": {
+ "EXTERNAL": "00000000-0000-0000-0000-000000000005",
+ "INTERNAL": "00000000-0000-0000-0000-000000000016"
+ },
+ "solution": "SECURITY_AND_ALARM",
+ "zoneActivationDelay": 0.0
+ },
+ "WEATHER_AND_ENVIRONMENT": {
+ "active": true,
+ "functionalGroups": [
+ "00000000-AAAA-0000-0000-000000000001"
+ ],
+ "solution": "WEATHER_AND_ENVIRONMENT"
+ }
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "inboxGroup": "00000000-0000-0000-0000-000000000044",
+ "lastReadyForUpdateTimestamp": 1522319489138,
+ "location": {
+ "city": "1010 Wien, Österreich",
+ "latitude": "48.208088",
+ "longitude": "16.358608"
+ },
+ "metaGroups": [
+ "00000000-0000-0000-0000-000000000011",
+ "00000000-0000-0000-0000-000000000008",
+ "00000000-0000-0000-0000-000000000014",
+ "00000000-0000-0000-0000-000000000004",
+ "00000000-0000-0000-0000-000000000017",
+ "00000000-0000-0000-0000-000000000020"
+ ],
+ "pinAssigned": false,
+ "powerMeterCurrency": "EUR",
+ "powerMeterUnitPrice": 0.0,
+ "ruleGroups": [
+ "00000000-0000-0000-0000-000000000057",
+ "00000000-0000-0000-0000-000000000030"
+ ],
+ "ruleMetaDatas": {
+ "00000000-0000-0000-0000-000000000065": {
+ "active": true,
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "00000000-0000-0000-0000-000000000065",
+ "label": "Alarmanlage",
+ "ruleErrorCategories": [],
+ "type": "SIMPLE"
+ }
+ },
+ "timeZoneId": "Europe/Vienna",
+ "updateState": "UP_TO_DATE",
+ "voiceControlSettings": {
+ "allowedActiveSecurityZoneIds": []
+ },
+ "weather": {
+ "humidity": 54,
+ "maxTemperature": 16.6,
+ "minTemperature": 16.6,
+ "temperature": 16.6,
+ "vaporAmount": 5.465858858389302,
+ "weatherCondition": "LIGHT_CLOUDY",
+ "weatherDayTime": "NIGHT",
+ "windDirection": 294,
+ "windSpeed": 8.568
+ }
+ }
+}
diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json
index c5e4857297a..3189d7a9d9b 100644
--- a/tests/fixtures/yandex_transport_reply.json
+++ b/tests/fixtures/yandex_transport_reply.json
@@ -1,2106 +1,1560 @@
{
- "data": {
- "geometries": [
- {
- "type": "Point",
- "coordinates": [
- 37.565280044,
- 55.851959656
- ]
- }
- ],
- "geometry": {
- "type": "Point",
- "coordinates": [
- 37.565280044,
- 55.851959656
+ "data": {
+ "geometries": [
+ {
+ "type": "Point",
+ "coordinates": [
+ 37.565280044,
+ 55.851959656
+ ]
+ }
+ ],
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 37.565280044,
+ 55.851959656
+ ]
+ },
+ "properties": {
+ "name": "7-й автобусный парк",
+ "description": "7-й автобусный парк",
+ "currentTime": 1570971868567,
+ "tzOffset": 10800,
+ "StopMetaData": {
+ "id": "stop__9639579",
+ "name": "7-й автобусный парк",
+ "type": "urban",
+ "region": {
+ "id": 213,
+ "type": 6,
+ "parent_id": 1,
+ "capital_id": 0,
+ "geo_parent_id": 0,
+ "city_id": 213,
+ "name": "moscow",
+ "native_name": "",
+ "iso_name": "RU MOW",
+ "is_main": true,
+ "en_name": "Moscow",
+ "short_en_name": "MSK",
+ "phone_code": "495 499",
+ "phone_code_old": "095",
+ "zip_code": "",
+ "population": 12506468,
+ "synonyms": "Moskau, Moskva",
+ "latitude": 55.753215,
+ "longitude": 37.622504,
+ "latitude_size": 0.878654,
+ "longitude_size": 1.164423,
+ "zoom": 10,
+ "tzname": "Europe/Moscow",
+ "official_languages": "ru",
+ "widespread_languages": "ru",
+ "suggest_list": [],
+ "is_eu": false,
+ "services_names": [
+ "bs",
+ "yaca",
+ "weather",
+ "afisha",
+ "maps",
+ "tv",
+ "ad",
+ "etrain",
+ "subway",
+ "delivery",
+ "route"
+ ],
+ "ename": "moscow",
+ "bounds": [
+ [
+ 37.0402925,
+ 55.31141404514547
+ ],
+ [
+ 38.2047155,
+ 56.190068045145466
]
- },
- "properties": {
- "name": "7-й автобусный парк",
- "description": "7-й автобусный парк",
- "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)",
- "StopMetaData": {
- "id": "stop__9639579",
- "name": "7-й автобусный парк",
- "type": "urban",
- "region": {
- "id": 213,
- "type": 6,
- "parent_id": 1,
- "capital_id": 0,
- "geo_parent_id": 0,
- "city_id": 213,
- "name": "moscow",
- "native_name": "",
- "iso_name": "RU MOW",
- "is_main": true,
- "en_name": "Moscow",
- "short_en_name": "MSK",
- "phone_code": "495 499",
- "phone_code_old": "095",
- "zip_code": "",
- "population": 12506468,
- "synonyms": "Moskau, Moskva",
- "latitude": 55.753215,
- "longitude": 37.622504,
- "latitude_size": 0.878654,
- "longitude_size": 1.164423,
- "zoom": 10,
- "tzname": "Europe/Moscow",
- "official_languages": "ru",
- "widespread_languages": "ru",
- "suggest_list": [],
- "is_eu": false,
- "services_names": [
- "bs",
- "yaca",
- "weather",
- "afisha",
- "maps",
- "tv",
- "ad",
- "etrain",
- "subway",
- "delivery",
- "route"
- ],
- "ename": "moscow",
- "bounds": [
- [
- 37.0402925,
- 55.31141404514547
- ],
- [
- 38.2047155,
- 56.190068045145466
- ]
- ],
- "names": {
- "ablative": "",
- "accusative": "Москву",
- "dative": "Москве",
- "directional": "",
- "genitive": "Москвы",
- "instrumental": "Москвой",
- "locative": "",
- "nominative": "Москва",
- "preposition": "в",
- "prepositional": "Москве"
- },
- "parent": {
- "id": 1,
- "type": 5,
- "parent_id": 3,
- "capital_id": 213,
- "geo_parent_id": 0,
- "city_id": 213,
- "name": "moscow-and-moscow-oblast",
- "native_name": "",
- "iso_name": "RU-MOS",
- "is_main": true,
- "en_name": "Moscow and Moscow Oblast",
- "short_en_name": "RU-MOS",
- "phone_code": "495 496 498 499",
- "phone_code_old": "",
- "zip_code": "",
- "population": 7503385,
- "synonyms": "Московская область, Подмосковье, Podmoskovye",
- "latitude": 55.815792,
- "longitude": 37.380031,
- "latitude_size": 2.705659,
- "longitude_size": 5.060749,
- "zoom": 8,
- "tzname": "Europe/Moscow",
- "official_languages": "ru",
- "widespread_languages": "ru",
- "suggest_list": [
- 213,
- 10716,
- 10747,
- 10758,
- 20728,
- 10740,
- 10738,
- 20523,
- 10735,
- 10734,
- 10743,
- 21622
- ],
- "is_eu": false,
- "services_names": [
- "bs",
- "yaca",
- "ad"
- ],
- "ename": "moscow-and-moscow-oblast",
- "bounds": [
- [
- 34.8496565,
- 54.439456064325434
- ],
- [
- 39.9104055,
- 57.14511506432543
- ]
- ],
- "names": {
- "ablative": "",
- "accusative": "Москву и Московскую область",
- "dative": "Москве и Московской области",
- "directional": "",
- "genitive": "Москвы и Московской области",
- "instrumental": "Москвой и Московской областью",
- "locative": "",
- "nominative": "Москва и Московская область",
- "preposition": "в",
- "prepositional": "Москве и Московской области"
- },
- "parent": {
- "id": 225,
- "type": 3,
- "parent_id": 10001,
- "capital_id": 213,
- "geo_parent_id": 0,
- "city_id": 213,
- "name": "russia",
- "native_name": "",
- "iso_name": "RU",
- "is_main": false,
- "en_name": "Russia",
- "short_en_name": "RU",
- "phone_code": "7",
- "phone_code_old": "",
- "zip_code": "",
- "population": 146880432,
- "synonyms": "Russian Federation,Российская Федерация",
- "latitude": 61.698653,
- "longitude": 99.505405,
- "latitude_size": 40.700127,
- "longitude_size": 171.643239,
- "zoom": 3,
- "tzname": "",
- "official_languages": "ru",
- "widespread_languages": "ru",
- "suggest_list": [
- 213,
- 2,
- 65,
- 54,
- 47,
- 43,
- 66,
- 51,
- 56,
- 172,
- 39,
- 62
- ],
- "is_eu": false,
- "services_names": [
- "bs",
- "yaca",
- "ad"
- ],
- "ename": "russia",
- "bounds": [
- [
- 13.683785499999999,
- 35.290400699917846
- ],
- [
- -174.6729755,
- 75.99052769991785
- ]
- ],
- "names": {
- "ablative": "",
- "accusative": "Россию",
- "dative": "России",
- "directional": "",
- "genitive": "России",
- "instrumental": "Россией",
- "locative": "",
- "nominative": "Россия",
- "preposition": "в",
- "prepositional": "России"
- }
- }
- }
- },
- "Transport": [
- {
- "lineId": "2036925416",
- "name": "194",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "2036927196",
- "EssentialStops": [
- {
- "id": "stop__9711780",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9648742",
- "name": "Коровино"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659860",
- "tzOffset": 10800,
- "text": "21:51"
- }
- },
- {
- "Scheduled": {
- "value": "1568660760",
- "tzOffset": 10800,
- "text": "22:06"
- }
- },
- {
- "Scheduled": {
- "value": "1568661840",
- "tzOffset": 10800,
- "text": "22:24"
- }
- }
- ],
- "departureTime": "21:51"
- }
- }
- ],
- "threadId": "2036927196",
- "EssentialStops": [
- {
- "id": "stop__9711780",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9648742",
- "name": "Коровино"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659860",
- "tzOffset": 10800,
- "text": "21:51"
- }
- },
- {
- "Scheduled": {
- "value": "1568660760",
- "tzOffset": 10800,
- "text": "22:06"
- }
- },
- {
- "Scheduled": {
- "value": "1568661840",
- "tzOffset": 10800,
- "text": "22:24"
- }
- }
- ],
- "departureTime": "21:51"
- }
- },
- {
- "lineId": "213_114_bus_mosgortrans",
- "name": "114",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213B_114_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9647199",
- "name": "Метро Войковская"
- },
- {
- "id": "stop__9639588",
- "name": "Коровинское шоссе"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "15 мин",
- "value": 900,
- "begin": {
- "value": "1568603405",
- "tzOffset": 10800,
- "text": "6:10"
- },
- "end": {
- "value": "1568672165",
- "tzOffset": 10800,
- "text": "1:16"
- }
- }
- }
- }
- ],
- "threadId": "213B_114_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9647199",
- "name": "Метро Войковская"
- },
- {
- "id": "stop__9639588",
- "name": "Коровинское шоссе"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "15 мин",
- "value": 900,
- "begin": {
- "value": "1568603405",
- "tzOffset": 10800,
- "text": "6:10"
- },
- "end": {
- "value": "1568672165",
- "tzOffset": 10800,
- "text": "1:16"
- }
- }
- }
- },
- {
- "lineId": "213_154_bus_mosgortrans",
- "name": "154",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213B_154_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9642548",
- "name": "ВДНХ (южная)"
- },
- {
- "id": "stop__9711744",
- "name": "Станция Ховрино"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659260",
- "tzOffset": 10800,
- "text": "21:41"
- },
- "Estimated": {
- "value": "1568659252",
- "tzOffset": 10800,
- "text": "21:40"
- },
- "vehicleId": "codd%5Fnew|1054764%5F191500"
- },
- {
- "Scheduled": {
- "value": "1568660580",
- "tzOffset": 10800,
- "text": "22:03"
- }
- },
- {
- "Scheduled": {
- "value": "1568661900",
- "tzOffset": 10800,
- "text": "22:25"
- }
- }
- ],
- "departureTime": "21:41"
- }
- }
- ],
- "threadId": "213B_154_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9642548",
- "name": "ВДНХ (южная)"
- },
- {
- "id": "stop__9711744",
- "name": "Станция Ховрино"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659260",
- "tzOffset": 10800,
- "text": "21:41"
- },
- "Estimated": {
- "value": "1568659252",
- "tzOffset": 10800,
- "text": "21:40"
- },
- "vehicleId": "codd%5Fnew|1054764%5F191500"
- },
- {
- "Scheduled": {
- "value": "1568660580",
- "tzOffset": 10800,
- "text": "22:03"
- }
- },
- {
- "Scheduled": {
- "value": "1568661900",
- "tzOffset": 10800,
- "text": "22:25"
- }
- }
- ],
- "departureTime": "21:41"
- }
- },
- {
- "lineId": "213_179_bus_mosgortrans",
- "name": "179",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213B_179_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9647199",
- "name": "Метро Войковская"
- },
- {
- "id": "stop__9639480",
- "name": "Платформа Лианозово"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659920",
- "tzOffset": 10800,
- "text": "21:52"
- },
- "Estimated": {
- "value": "1568659351",
- "tzOffset": 10800,
- "text": "21:42"
- },
- "vehicleId": "codd%5Fnew|59832%5F31359"
- },
- {
- "Scheduled": {
- "value": "1568660760",
- "tzOffset": 10800,
- "text": "22:06"
- }
- },
- {
- "Scheduled": {
- "value": "1568661660",
- "tzOffset": 10800,
- "text": "22:21"
- }
- }
- ],
- "departureTime": "21:52"
- }
- }
- ],
- "threadId": "213B_179_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9647199",
- "name": "Метро Войковская"
- },
- {
- "id": "stop__9639480",
- "name": "Платформа Лианозово"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659920",
- "tzOffset": 10800,
- "text": "21:52"
- },
- "Estimated": {
- "value": "1568659351",
- "tzOffset": 10800,
- "text": "21:42"
- },
- "vehicleId": "codd%5Fnew|59832%5F31359"
- },
- {
- "Scheduled": {
- "value": "1568660760",
- "tzOffset": 10800,
- "text": "22:06"
- }
- },
- {
- "Scheduled": {
- "value": "1568661660",
- "tzOffset": 10800,
- "text": "22:21"
- }
- }
- ],
- "departureTime": "21:52"
- }
- },
- {
- "lineId": "213_191m_minibus_default",
- "name": "591",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213A_191m_minibus_default",
- "EssentialStops": [
- {
- "id": "stop__9647199",
- "name": "Метро Войковская"
- },
- {
- "id": "stop__9711744",
- "name": "Станция Ховрино"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568660525",
- "tzOffset": 10800,
- "text": "22:02"
- },
- "vehicleId": "codd%5Fnew|38278%5F9345312"
- }
- ],
- "Frequency": {
- "text": "22 мин",
- "value": 1320,
- "begin": {
- "value": "1568602033",
- "tzOffset": 10800,
- "text": "5:47"
- },
- "end": {
- "value": "1568672233",
- "tzOffset": 10800,
- "text": "1:17"
- }
- }
- }
- }
- ],
- "threadId": "213A_191m_minibus_default",
- "EssentialStops": [
- {
- "id": "stop__9647199",
- "name": "Метро Войковская"
- },
- {
- "id": "stop__9711744",
- "name": "Станция Ховрино"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568660525",
- "tzOffset": 10800,
- "text": "22:02"
- },
- "vehicleId": "codd%5Fnew|38278%5F9345312"
- }
- ],
- "Frequency": {
- "text": "22 мин",
- "value": 1320,
- "begin": {
- "value": "1568602033",
- "tzOffset": 10800,
- "text": "5:47"
- },
- "end": {
- "value": "1568672233",
- "tzOffset": 10800,
- "text": "1:17"
- }
- }
- }
- },
- {
- "lineId": "213_206m_minibus_default",
- "name": "206к",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213A_206m_minibus_default",
- "EssentialStops": [
- {
- "id": "stop__9640756",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9640553",
- "name": "Лобненская улица"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "22 мин",
- "value": 1320,
- "begin": {
- "value": "1568601239",
- "tzOffset": 10800,
- "text": "5:33"
- },
- "end": {
- "value": "1568671439",
- "tzOffset": 10800,
- "text": "1:03"
- }
- }
- }
- }
- ],
- "threadId": "213A_206m_minibus_default",
- "EssentialStops": [
- {
- "id": "stop__9640756",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9640553",
- "name": "Лобненская улица"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "22 мин",
- "value": 1320,
- "begin": {
- "value": "1568601239",
- "tzOffset": 10800,
- "text": "5:33"
- },
- "end": {
- "value": "1568671439",
- "tzOffset": 10800,
- "text": "1:03"
- }
- }
- }
- },
- {
- "lineId": "213_215_bus_mosgortrans",
- "name": "215",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213B_215_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9711780",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9711744",
- "name": "Станция Ховрино"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "27 мин",
- "value": 1620,
- "begin": {
- "value": "1568601276",
- "tzOffset": 10800,
- "text": "5:34"
- },
- "end": {
- "value": "1568671476",
- "tzOffset": 10800,
- "text": "1:04"
- }
- }
- }
- }
- ],
- "threadId": "213B_215_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9711780",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9711744",
- "name": "Станция Ховрино"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "27 мин",
- "value": 1620,
- "begin": {
- "value": "1568601276",
- "tzOffset": 10800,
- "text": "5:34"
- },
- "end": {
- "value": "1568671476",
- "tzOffset": 10800,
- "text": "1:04"
- }
- }
- }
- },
- {
- "lineId": "213_282_bus_mosgortrans",
- "name": "282",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213A_282_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9641102",
- "name": "Улица Корнейчука"
- },
- {
- "id": "2532226085",
- "name": "Метро Войковская"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568659888",
- "tzOffset": 10800,
- "text": "21:51"
- },
- "vehicleId": "codd%5Fnew|34874%5F9345408"
- }
- ],
- "Frequency": {
- "text": "15 мин",
- "value": 900,
- "begin": {
- "value": "1568602180",
- "tzOffset": 10800,
- "text": "5:49"
- },
- "end": {
- "value": "1568673460",
- "tzOffset": 10800,
- "text": "1:37"
- }
- }
- }
- }
- ],
- "threadId": "213A_282_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9641102",
- "name": "Улица Корнейчука"
- },
- {
- "id": "2532226085",
- "name": "Метро Войковская"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568659888",
- "tzOffset": 10800,
- "text": "21:51"
- },
- "vehicleId": "codd%5Fnew|34874%5F9345408"
- }
- ],
- "Frequency": {
- "text": "15 мин",
- "value": 900,
- "begin": {
- "value": "1568602180",
- "tzOffset": 10800,
- "text": "5:49"
- },
- "end": {
- "value": "1568673460",
- "tzOffset": 10800,
- "text": "1:37"
- }
- }
- }
- },
- {
- "lineId": "213_294m_minibus_default",
- "name": "994",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213A_294m_minibus_default",
- "EssentialStops": [
- {
- "id": "stop__9640756",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9649459",
- "name": "Метро Алтуфьево"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "30 мин",
- "value": 1800,
- "begin": {
- "value": "1568601527",
- "tzOffset": 10800,
- "text": "5:38"
- },
- "end": {
- "value": "1568671727",
- "tzOffset": 10800,
- "text": "1:08"
- }
- }
- }
- }
- ],
- "threadId": "213A_294m_minibus_default",
- "EssentialStops": [
- {
- "id": "stop__9640756",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9649459",
- "name": "Метро Алтуфьево"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "30 мин",
- "value": 1800,
- "begin": {
- "value": "1568601527",
- "tzOffset": 10800,
- "text": "5:38"
- },
- "end": {
- "value": "1568671727",
- "tzOffset": 10800,
- "text": "1:08"
- }
- }
- }
- },
- {
- "lineId": "213_36_trolleybus_mosgortrans",
- "name": "т36",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213A_36_trolleybus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9642550",
- "name": "ВДНХ (южная)"
- },
- {
- "id": "stop__9640641",
- "name": "Дмитровское шоссе, 155"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659680",
- "tzOffset": 10800,
- "text": "21:48"
- },
- "Estimated": {
- "value": "1568659426",
- "tzOffset": 10800,
- "text": "21:43"
- },
- "vehicleId": "codd%5Fnew|1084829%5F430260"
- },
- {
- "Scheduled": {
- "value": "1568660520",
- "tzOffset": 10800,
- "text": "22:02"
- },
- "Estimated": {
- "value": "1568659656",
- "tzOffset": 10800,
- "text": "21:47"
- },
- "vehicleId": "codd%5Fnew|1117016%5F430280"
- },
- {
- "Scheduled": {
- "value": "1568661900",
- "tzOffset": 10800,
- "text": "22:25"
- },
- "Estimated": {
- "value": "1568660538",
- "tzOffset": 10800,
- "text": "22:02"
- },
- "vehicleId": "codd%5Fnew|1054576%5F430226"
- }
- ],
- "departureTime": "21:48"
- }
- }
- ],
- "threadId": "213A_36_trolleybus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9642550",
- "name": "ВДНХ (южная)"
- },
- {
- "id": "stop__9640641",
- "name": "Дмитровское шоссе, 155"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659680",
- "tzOffset": 10800,
- "text": "21:48"
- },
- "Estimated": {
- "value": "1568659426",
- "tzOffset": 10800,
- "text": "21:43"
- },
- "vehicleId": "codd%5Fnew|1084829%5F430260"
- },
- {
- "Scheduled": {
- "value": "1568660520",
- "tzOffset": 10800,
- "text": "22:02"
- },
- "Estimated": {
- "value": "1568659656",
- "tzOffset": 10800,
- "text": "21:47"
- },
- "vehicleId": "codd%5Fnew|1117016%5F430280"
- },
- {
- "Scheduled": {
- "value": "1568661900",
- "tzOffset": 10800,
- "text": "22:25"
- },
- "Estimated": {
- "value": "1568660538",
- "tzOffset": 10800,
- "text": "22:02"
- },
- "vehicleId": "codd%5Fnew|1054576%5F430226"
- }
- ],
- "departureTime": "21:48"
- }
- },
- {
- "lineId": "213_47_trolleybus_mosgortrans",
- "name": "т47",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213B_47_trolleybus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9639568",
- "name": "Бескудниковский переулок"
- },
- {
- "id": "stop__9641903",
- "name": "Бескудниковский переулок"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659980",
- "tzOffset": 10800,
- "text": "21:53"
- },
- "Estimated": {
- "value": "1568659253",
- "tzOffset": 10800,
- "text": "21:40"
- },
- "vehicleId": "codd%5Fnew|1112219%5F430329"
- },
- {
- "Scheduled": {
- "value": "1568660940",
- "tzOffset": 10800,
- "text": "22:09"
- },
- "Estimated": {
- "value": "1568660519",
- "tzOffset": 10800,
- "text": "22:01"
- },
- "vehicleId": "codd%5Fnew|1139620%5F430382"
- },
- {
- "Scheduled": {
- "value": "1568663580",
- "tzOffset": 10800,
- "text": "22:53"
- }
- }
- ],
- "departureTime": "21:53"
- }
- }
- ],
- "threadId": "213B_47_trolleybus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9639568",
- "name": "Бескудниковский переулок"
- },
- {
- "id": "stop__9641903",
- "name": "Бескудниковский переулок"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659980",
- "tzOffset": 10800,
- "text": "21:53"
- },
- "Estimated": {
- "value": "1568659253",
- "tzOffset": 10800,
- "text": "21:40"
- },
- "vehicleId": "codd%5Fnew|1112219%5F430329"
- },
- {
- "Scheduled": {
- "value": "1568660940",
- "tzOffset": 10800,
- "text": "22:09"
- },
- "Estimated": {
- "value": "1568660519",
- "tzOffset": 10800,
- "text": "22:01"
- },
- "vehicleId": "codd%5Fnew|1139620%5F430382"
- },
- {
- "Scheduled": {
- "value": "1568663580",
- "tzOffset": 10800,
- "text": "22:53"
- }
- }
- ],
- "departureTime": "21:53"
- }
- },
- {
- "lineId": "213_56_trolleybus_mosgortrans",
- "name": "т56",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213A_56_trolleybus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9639561",
- "name": "Коровинское шоссе"
- },
- {
- "id": "stop__9639588",
- "name": "Коровинское шоссе"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568660675",
- "tzOffset": 10800,
- "text": "22:04"
- },
- "vehicleId": "codd%5Fnew|146304%5F31207"
- }
- ],
- "Frequency": {
- "text": "8 мин",
- "value": 480,
- "begin": {
- "value": "1568606244",
- "tzOffset": 10800,
- "text": "6:57"
- },
- "end": {
- "value": "1568670144",
- "tzOffset": 10800,
- "text": "0:42"
- }
- }
- }
- }
- ],
- "threadId": "213A_56_trolleybus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9639561",
- "name": "Коровинское шоссе"
- },
- {
- "id": "stop__9639588",
- "name": "Коровинское шоссе"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568660675",
- "tzOffset": 10800,
- "text": "22:04"
- },
- "vehicleId": "codd%5Fnew|146304%5F31207"
- }
- ],
- "Frequency": {
- "text": "8 мин",
- "value": 480,
- "begin": {
- "value": "1568606244",
- "tzOffset": 10800,
- "text": "6:57"
- },
- "end": {
- "value": "1568670144",
- "tzOffset": 10800,
- "text": "0:42"
- }
- }
- }
- },
- {
- "lineId": "213_63_bus_mosgortrans",
- "name": "63",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213A_63_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9640554",
- "name": "Лобненская улица"
- },
- {
- "id": "stop__9640553",
- "name": "Лобненская улица"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568659369",
- "tzOffset": 10800,
- "text": "21:42"
- },
- "vehicleId": "codd%5Fnew|38921%5F9215306"
- },
- {
- "Estimated": {
- "value": "1568660136",
- "tzOffset": 10800,
- "text": "21:55"
- },
- "vehicleId": "codd%5Fnew|38918%5F9215303"
- }
- ],
- "Frequency": {
- "text": "17 мин",
- "value": 1020,
- "begin": {
- "value": "1568600987",
- "tzOffset": 10800,
- "text": "5:29"
- },
- "end": {
- "value": "1568670227",
- "tzOffset": 10800,
- "text": "0:43"
- }
- }
- }
- }
- ],
- "threadId": "213A_63_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9640554",
- "name": "Лобненская улица"
- },
- {
- "id": "stop__9640553",
- "name": "Лобненская улица"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568659369",
- "tzOffset": 10800,
- "text": "21:42"
- },
- "vehicleId": "codd%5Fnew|38921%5F9215306"
- },
- {
- "Estimated": {
- "value": "1568660136",
- "tzOffset": 10800,
- "text": "21:55"
- },
- "vehicleId": "codd%5Fnew|38918%5F9215303"
- }
- ],
- "Frequency": {
- "text": "17 мин",
- "value": 1020,
- "begin": {
- "value": "1568600987",
- "tzOffset": 10800,
- "text": "5:29"
- },
- "end": {
- "value": "1568670227",
- "tzOffset": 10800,
- "text": "0:43"
- }
- }
- }
- },
- {
- "lineId": "213_677_bus_mosgortrans",
- "name": "677",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213B_677_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9639495",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9639480",
- "name": "Платформа Лианозово"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568659369",
- "tzOffset": 10800,
- "text": "21:42"
- },
- "vehicleId": "codd%5Fnew|11731%5F31376"
- }
- ],
- "Frequency": {
- "text": "4 мин",
- "value": 240,
- "begin": {
- "value": "1568600940",
- "tzOffset": 10800,
- "text": "5:29"
- },
- "end": {
- "value": "1568672640",
- "tzOffset": 10800,
- "text": "1:24"
- }
- }
- }
- }
- ],
- "threadId": "213B_677_bus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9639495",
- "name": "Метро Петровско-Разумовская"
- },
- {
- "id": "stop__9639480",
- "name": "Платформа Лианозово"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568659369",
- "tzOffset": 10800,
- "text": "21:42"
- },
- "vehicleId": "codd%5Fnew|11731%5F31376"
- }
- ],
- "Frequency": {
- "text": "4 мин",
- "value": 240,
- "begin": {
- "value": "1568600940",
- "tzOffset": 10800,
- "text": "5:29"
- },
- "end": {
- "value": "1568672640",
- "tzOffset": 10800,
- "text": "1:24"
- }
- }
- }
- },
- {
- "lineId": "213_692_bus_mosgortrans",
- "name": "692",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "2036928706",
- "EssentialStops": [
- {
- "id": "3163417967",
- "name": "Платформа Дегунино"
- },
- {
- "id": "3163417967",
- "name": "Платформа Дегунино"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568660280",
- "tzOffset": 10800,
- "text": "21:58"
- },
- "Estimated": {
- "value": "1568660255",
- "tzOffset": 10800,
- "text": "21:57"
- },
- "vehicleId": "codd%5Fnew|63029%5F31485"
- },
- {
- "Scheduled": {
- "value": "1568693340",
- "tzOffset": 10800,
- "text": "7:09"
- }
- },
- {
- "Scheduled": {
- "value": "1568696940",
- "tzOffset": 10800,
- "text": "8:09"
- }
- }
- ],
- "departureTime": "21:58"
- }
- }
- ],
- "threadId": "2036928706",
- "EssentialStops": [
- {
- "id": "3163417967",
- "name": "Платформа Дегунино"
- },
- {
- "id": "3163417967",
- "name": "Платформа Дегунино"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568660280",
- "tzOffset": 10800,
- "text": "21:58"
- },
- "Estimated": {
- "value": "1568660255",
- "tzOffset": 10800,
- "text": "21:57"
- },
- "vehicleId": "codd%5Fnew|63029%5F31485"
- },
- {
- "Scheduled": {
- "value": "1568693340",
- "tzOffset": 10800,
- "text": "7:09"
- }
- },
- {
- "Scheduled": {
- "value": "1568696940",
- "tzOffset": 10800,
- "text": "8:09"
- }
- }
- ],
- "departureTime": "21:58"
- }
- },
- {
- "lineId": "213_78_trolleybus_mosgortrans",
- "name": "т78",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "213A_78_trolleybus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9887464",
- "name": "9-я Северная линия"
- },
- {
- "id": "stop__9887464",
- "name": "9-я Северная линия"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659620",
- "tzOffset": 10800,
- "text": "21:47"
- },
- "Estimated": {
- "value": "1568659898",
- "tzOffset": 10800,
- "text": "21:51"
- },
- "vehicleId": "codd%5Fnew|147522%5F31184"
- },
- {
- "Scheduled": {
- "value": "1568660760",
- "tzOffset": 10800,
- "text": "22:06"
- }
- },
- {
- "Scheduled": {
- "value": "1568661900",
- "tzOffset": 10800,
- "text": "22:25"
- }
- }
- ],
- "departureTime": "21:47"
- }
- }
- ],
- "threadId": "213A_78_trolleybus_mosgortrans",
- "EssentialStops": [
- {
- "id": "stop__9887464",
- "name": "9-я Северная линия"
- },
- {
- "id": "stop__9887464",
- "name": "9-я Северная линия"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659620",
- "tzOffset": 10800,
- "text": "21:47"
- },
- "Estimated": {
- "value": "1568659898",
- "tzOffset": 10800,
- "text": "21:51"
- },
- "vehicleId": "codd%5Fnew|147522%5F31184"
- },
- {
- "Scheduled": {
- "value": "1568660760",
- "tzOffset": 10800,
- "text": "22:06"
- }
- },
- {
- "Scheduled": {
- "value": "1568661900",
- "tzOffset": 10800,
- "text": "22:25"
- }
- }
- ],
- "departureTime": "21:47"
- }
- },
- {
- "lineId": "213_82_bus_mosgortrans",
- "name": "82",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "2036925244",
- "EssentialStops": [
- {
- "id": "2310890052",
- "name": "Метро Верхние Лихоборы"
- },
- {
- "id": "2310890052",
- "name": "Метро Верхние Лихоборы"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659680",
- "tzOffset": 10800,
- "text": "21:48"
- }
- },
- {
- "Scheduled": {
- "value": "1568661780",
- "tzOffset": 10800,
- "text": "22:23"
- }
- },
- {
- "Scheduled": {
- "value": "1568663760",
- "tzOffset": 10800,
- "text": "22:56"
- }
- }
- ],
- "departureTime": "21:48"
- }
- }
- ],
- "threadId": "2036925244",
- "EssentialStops": [
- {
- "id": "2310890052",
- "name": "Метро Верхние Лихоборы"
- },
- {
- "id": "2310890052",
- "name": "Метро Верхние Лихоборы"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659680",
- "tzOffset": 10800,
- "text": "21:48"
- }
- },
- {
- "Scheduled": {
- "value": "1568661780",
- "tzOffset": 10800,
- "text": "22:23"
- }
- },
- {
- "Scheduled": {
- "value": "1568663760",
- "tzOffset": 10800,
- "text": "22:56"
- }
- }
- ],
- "departureTime": "21:48"
- }
- },
- {
- "lineId": "2465131598",
- "name": "179к",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "2465131758",
- "EssentialStops": [
- {
- "id": "stop__9640244",
- "name": "Платформа Лианозово"
- },
- {
- "id": "stop__9639480",
- "name": "Платформа Лианозово"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659500",
- "tzOffset": 10800,
- "text": "21:45"
- }
- },
- {
- "Scheduled": {
- "value": "1568659980",
- "tzOffset": 10800,
- "text": "21:53"
- }
- },
- {
- "Scheduled": {
- "value": "1568660880",
- "tzOffset": 10800,
- "text": "22:08"
- }
- }
- ],
- "departureTime": "21:45"
- }
- }
- ],
- "threadId": "2465131758",
- "EssentialStops": [
- {
- "id": "stop__9640244",
- "name": "Платформа Лианозово"
- },
- {
- "id": "stop__9639480",
- "name": "Платформа Лианозово"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659500",
- "tzOffset": 10800,
- "text": "21:45"
- }
- },
- {
- "Scheduled": {
- "value": "1568659980",
- "tzOffset": 10800,
- "text": "21:53"
- }
- },
- {
- "Scheduled": {
- "value": "1568660880",
- "tzOffset": 10800,
- "text": "22:08"
- }
- }
- ],
- "departureTime": "21:45"
- }
- },
- {
- "lineId": "466_bus_default",
- "name": "466",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "466B_bus_default",
- "EssentialStops": [
- {
- "id": "stop__9640546",
- "name": "Станция Бескудниково"
- },
- {
- "id": "stop__9640545",
- "name": "Станция Бескудниково"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "22 мин",
- "value": 1320,
- "begin": {
- "value": "1568604647",
- "tzOffset": 10800,
- "text": "6:30"
- },
- "end": {
- "value": "1568675447",
- "tzOffset": 10800,
- "text": "2:10"
- }
- }
- }
- }
- ],
- "threadId": "466B_bus_default",
- "EssentialStops": [
- {
- "id": "stop__9640546",
- "name": "Станция Бескудниково"
- },
- {
- "id": "stop__9640545",
- "name": "Станция Бескудниково"
- }
- ],
- "BriefSchedule": {
- "Events": [],
- "Frequency": {
- "text": "22 мин",
- "value": 1320,
- "begin": {
- "value": "1568604647",
- "tzOffset": 10800,
- "text": "6:30"
- },
- "end": {
- "value": "1568675447",
- "tzOffset": 10800,
- "text": "2:10"
- }
- }
- }
- },
- {
- "lineId": "677k_bus_default",
- "name": "677к",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "677kA_bus_default",
- "EssentialStops": [
- {
- "id": "stop__9640244",
- "name": "Платформа Лианозово"
- },
- {
- "id": "stop__9639480",
- "name": "Платформа Лианозово"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659920",
- "tzOffset": 10800,
- "text": "21:52"
- },
- "Estimated": {
- "value": "1568660003",
- "tzOffset": 10800,
- "text": "21:53"
- },
- "vehicleId": "codd%5Fnew|130308%5F31319"
- },
- {
- "Scheduled": {
- "value": "1568661240",
- "tzOffset": 10800,
- "text": "22:14"
- }
- },
- {
- "Scheduled": {
- "value": "1568662500",
- "tzOffset": 10800,
- "text": "22:35"
- }
- }
- ],
- "departureTime": "21:52"
- }
- }
- ],
- "threadId": "677kA_bus_default",
- "EssentialStops": [
- {
- "id": "stop__9640244",
- "name": "Платформа Лианозово"
- },
- {
- "id": "stop__9639480",
- "name": "Платформа Лианозово"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Scheduled": {
- "value": "1568659920",
- "tzOffset": 10800,
- "text": "21:52"
- },
- "Estimated": {
- "value": "1568660003",
- "tzOffset": 10800,
- "text": "21:53"
- },
- "vehicleId": "codd%5Fnew|130308%5F31319"
- },
- {
- "Scheduled": {
- "value": "1568661240",
- "tzOffset": 10800,
- "text": "22:14"
- }
- },
- {
- "Scheduled": {
- "value": "1568662500",
- "tzOffset": 10800,
- "text": "22:35"
- }
- }
- ],
- "departureTime": "21:52"
- }
- },
- {
- "lineId": "m10_bus_default",
- "name": "м10",
- "Types": [
- "bus"
- ],
- "type": "bus",
- "threads": [
- {
- "threadId": "2036926048",
- "EssentialStops": [
- {
- "id": "stop__9640554",
- "name": "Лобненская улица"
- },
- {
- "id": "stop__9640553",
- "name": "Лобненская улица"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568659718",
- "tzOffset": 10800,
- "text": "21:48"
- },
- "vehicleId": "codd%5Fnew|146260%5F31212"
- },
- {
- "Estimated": {
- "value": "1568660422",
- "tzOffset": 10800,
- "text": "22:00"
- },
- "vehicleId": "codd%5Fnew|13997%5F31247"
- }
- ],
- "Frequency": {
- "text": "15 мин",
- "value": 900,
- "begin": {
- "value": "1568606903",
- "tzOffset": 10800,
- "text": "7:08"
- },
- "end": {
- "value": "1568675183",
- "tzOffset": 10800,
- "text": "2:06"
- }
- }
- }
- }
- ],
- "threadId": "2036926048",
- "EssentialStops": [
- {
- "id": "stop__9640554",
- "name": "Лобненская улица"
- },
- {
- "id": "stop__9640553",
- "name": "Лобненская улица"
- }
- ],
- "BriefSchedule": {
- "Events": [
- {
- "Estimated": {
- "value": "1568659718",
- "tzOffset": 10800,
- "text": "21:48"
- },
- "vehicleId": "codd%5Fnew|146260%5F31212"
- },
- {
- "Estimated": {
- "value": "1568660422",
- "tzOffset": 10800,
- "text": "22:00"
- },
- "vehicleId": "codd%5Fnew|13997%5F31247"
- }
- ],
- "Frequency": {
- "text": "15 мин",
- "value": 900,
- "begin": {
- "value": "1568606903",
- "tzOffset": 10800,
- "text": "7:08"
- },
- "end": {
- "value": "1568675183",
- "tzOffset": 10800,
- "text": "2:06"
- }
- }
- }
- }
+ ],
+ "names": {
+ "ablative": "",
+ "accusative": "Москву",
+ "dative": "Москве",
+ "directional": "",
+ "genitive": "Москвы",
+ "instrumental": "Москвой",
+ "locative": "",
+ "nominative": "Москва",
+ "preposition": "в",
+ "prepositional": "Москве"
+ },
+ "parent": {
+ "id": 1,
+ "type": 5,
+ "parent_id": 3,
+ "capital_id": 213,
+ "geo_parent_id": 0,
+ "city_id": 213,
+ "name": "moscow-and-moscow-oblast",
+ "native_name": "",
+ "iso_name": "RU-MOS",
+ "is_main": true,
+ "en_name": "Moscow and Moscow Oblast",
+ "short_en_name": "RU-MOS",
+ "phone_code": "495 496 498 499",
+ "phone_code_old": "",
+ "zip_code": "",
+ "population": 7503385,
+ "synonyms": "Московская область, Подмосковье, Podmoskovye",
+ "latitude": 55.815792,
+ "longitude": 37.380031,
+ "latitude_size": 2.705659,
+ "longitude_size": 5.060749,
+ "zoom": 8,
+ "tzname": "Europe/Moscow",
+ "official_languages": "ru",
+ "widespread_languages": "ru",
+ "suggest_list": [
+ 213,
+ 10716,
+ 10747,
+ 10758,
+ 20728,
+ 10740,
+ 10738,
+ 20523,
+ 10735,
+ 10734,
+ 10743,
+ 21622
+ ],
+ "is_eu": false,
+ "services_names": [
+ "bs",
+ "yaca",
+ "ad"
+ ],
+ "ename": "moscow-and-moscow-oblast",
+ "bounds": [
+ [
+ 34.8496565,
+ 54.439456064325434
+ ],
+ [
+ 39.9104055,
+ 57.14511506432543
+ ]
+ ],
+ "names": {
+ "ablative": "",
+ "accusative": "Москву и Московскую область",
+ "dative": "Москве и Московской области",
+ "directional": "",
+ "genitive": "Москвы и Московской области",
+ "instrumental": "Москвой и Московской областью",
+ "locative": "",
+ "nominative": "Москва и Московская область",
+ "preposition": "в",
+ "prepositional": "Москве и Московской области"
+ },
+ "parent": {
+ "id": 225,
+ "type": 3,
+ "parent_id": 10001,
+ "capital_id": 213,
+ "geo_parent_id": 0,
+ "city_id": 213,
+ "name": "russia",
+ "native_name": "",
+ "iso_name": "RU",
+ "is_main": false,
+ "en_name": "Russia",
+ "short_en_name": "RU",
+ "phone_code": "7",
+ "phone_code_old": "",
+ "zip_code": "",
+ "population": 146880432,
+ "synonyms": "Russian Federation,Российская Федерация",
+ "latitude": 61.698653,
+ "longitude": 99.505405,
+ "latitude_size": 40.700127,
+ "longitude_size": 171.643239,
+ "zoom": 3,
+ "tzname": "",
+ "official_languages": "ru",
+ "widespread_languages": "ru",
+ "suggest_list": [
+ 213,
+ 2,
+ 65,
+ 54,
+ 47,
+ 43,
+ 66,
+ 51,
+ 56,
+ 172,
+ 39,
+ 62
+ ],
+ "is_eu": false,
+ "services_names": [
+ "bs",
+ "yaca",
+ "ad"
+ ],
+ "ename": "russia",
+ "bounds": [
+ [
+ 13.683785499999999,
+ 35.290400699917846
+ ],
+ [
+ -174.6729755,
+ 75.99052769991785
]
+ ],
+ "names": {
+ "ablative": "",
+ "accusative": "Россию",
+ "dative": "России",
+ "directional": "",
+ "genitive": "России",
+ "instrumental": "Россией",
+ "locative": "",
+ "nominative": "Россия",
+ "preposition": "в",
+ "prepositional": "России"
+ }
}
+ }
},
- "toponymSeoname": "dmitrovskoye_shosse"
- }
-}
+ "Transport": [
+ {
+ "lineId": "2036924720",
+ "name": "692",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2036928706",
+ "EssentialStops": [
+ {
+ "id": "3163417967",
+ "name": "Платформа Дегунино"
+ },
+ {
+ "id": "3163417967",
+ "name": "Платформа Дегунино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570973441",
+ "tzOffset": 10800,
+ "text": "16:30"
+ },
+ "vehicleId": "codd%5Fnew|144020%5F31402"
+ }
+ ],
+ "Frequency": {
+ "text": "1 ч",
+ "value": 3600,
+ "begin": {
+ "value": "1570938428",
+ "tzOffset": 10800,
+ "text": "6:47"
+ },
+ "end": {
+ "value": "1570990628",
+ "tzOffset": 10800,
+ "text": "21:17"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=2036924720&ll=37.577436%2C55.828981&name=692&r=4037&type=bus",
+ "seoname": "692"
+ },
+ {
+ "lineId": "2036924968",
+ "name": "82",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2036925244",
+ "EssentialStops": [
+ {
+ "id": "2310890052",
+ "name": "Метро Верхние Лихоборы"
+ },
+ {
+ "id": "2310890052",
+ "name": "Метро Верхние Лихоборы"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "34 мин",
+ "value": 2040,
+ "begin": {
+ "value": "1570944072",
+ "tzOffset": 10800,
+ "text": "8:21"
+ },
+ "end": {
+ "value": "1570997592",
+ "tzOffset": 10800,
+ "text": "23:13"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=2036924968&ll=37.571504%2C55.816622&name=82&r=4164&type=bus",
+ "seoname": "82"
+ },
+ {
+ "lineId": "2036925416",
+ "name": "194",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2036927196",
+ "EssentialStops": [
+ {
+ "id": "stop__9711780",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9648742",
+ "name": "Коровино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "12 мин",
+ "value": 720,
+ "begin": {
+ "value": "1570933976",
+ "tzOffset": 10800,
+ "text": "5:32"
+ },
+ "end": {
+ "value": "1571004356",
+ "tzOffset": 10800,
+ "text": "1:05"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=2036925416&ll=37.544800%2C55.865286&name=194&r=3667&type=bus",
+ "seoname": "194"
+ },
+ {
+ "lineId": "2036925728",
+ "name": "282",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_282_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9641102",
+ "name": "Улица Корнейчука"
+ },
+ {
+ "id": "2532226085",
+ "name": "Метро Войковская"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570971861",
+ "tzOffset": 10800,
+ "text": "16:04"
+ },
+ "vehicleId": "codd%5Fnew|34854%5F9345401"
+ },
+ {
+ "Estimated": {
+ "value": "1570973231",
+ "tzOffset": 10800,
+ "text": "16:27"
+ },
+ "vehicleId": "codd%5Fnew|37913%5F9225419"
+ }
+ ],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1570934963",
+ "tzOffset": 10800,
+ "text": "5:49"
+ },
+ "end": {
+ "value": "1571005163",
+ "tzOffset": 10800,
+ "text": "1:19"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=2036925728&ll=37.553526%2C55.860385&name=282&r=5779&type=bus",
+ "seoname": "282"
+ },
+ {
+ "lineId": "2036926781",
+ "name": "154",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_154_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9642548",
+ "name": "ВДНХ (южная)"
+ },
+ {
+ "id": "stop__9711744",
+ "name": "Станция Ховрино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570972424",
+ "tzOffset": 10800,
+ "text": "16:13"
+ },
+ "vehicleId": "codd%5Fnew|1161539%5F191543"
+ },
+ {
+ "Estimated": {
+ "value": "1570973620",
+ "tzOffset": 10800,
+ "text": "16:33"
+ },
+ "vehicleId": "codd%5Fnew|58773%5F190599"
+ }
+ ],
+ "Frequency": {
+ "text": "20 мин",
+ "value": 1200,
+ "begin": {
+ "value": "1570938166",
+ "tzOffset": 10800,
+ "text": "6:42"
+ },
+ "end": {
+ "value": "1571006446",
+ "tzOffset": 10800,
+ "text": "1:40"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=2036926781&ll=37.576158%2C55.846301&name=154&r=4917&type=bus",
+ "seoname": "154"
+ },
+ {
+ "lineId": "2036926818",
+ "name": "994",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_294m_minibus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640756",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9649459",
+ "name": "Метро Алтуфьево"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "30 мин",
+ "value": 1800,
+ "begin": {
+ "value": "1570934327",
+ "tzOffset": 10800,
+ "text": "5:38"
+ },
+ "end": {
+ "value": "1571004527",
+ "tzOffset": 10800,
+ "text": "1:08"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=2036926818&ll=37.560060%2C55.868431&name=994&r=3637&type=bus",
+ "seoname": "994"
+ },
+ {
+ "lineId": "2036926890",
+ "name": "466",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "466B_bus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640546",
+ "name": "Станция Бескудниково"
+ },
+ {
+ "id": "stop__9640545",
+ "name": "Станция Бескудниково"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1570937447",
+ "tzOffset": 10800,
+ "text": "6:30"
+ },
+ "end": {
+ "value": "1571008247",
+ "tzOffset": 10800,
+ "text": "2:10"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=2036926890&ll=37.564238%2C55.845050&name=466&r=4163&type=bus",
+ "seoname": "466"
+ },
+ {
+ "lineId": "213_114_bus_mosgortrans",
+ "name": "114",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_114_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9647199",
+ "name": "Метро Войковская"
+ },
+ {
+ "id": "stop__9639588",
+ "name": "Коровинское шоссе"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570972913",
+ "tzOffset": 10800,
+ "text": "16:21"
+ },
+ "vehicleId": "codd%5Fnew|1092230%5F191422"
+ }
+ ],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1570936205",
+ "tzOffset": 10800,
+ "text": "6:10"
+ },
+ "end": {
+ "value": "1571004965",
+ "tzOffset": 10800,
+ "text": "1:16"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_114_bus_mosgortrans&ll=37.508487%2C55.852137&name=114&r=3544&type=bus",
+ "seoname": "114"
+ },
+ {
+ "lineId": "213_179_bus_mosgortrans",
+ "name": "179",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_179_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9647199",
+ "name": "Метро Войковская"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570971963",
+ "tzOffset": 10800,
+ "text": "16:06"
+ },
+ "vehicleId": "codd%5Fnew|194519%5F31367"
+ },
+ {
+ "Estimated": {
+ "value": "1570973105",
+ "tzOffset": 10800,
+ "text": "16:25"
+ },
+ "vehicleId": "codd%5Fnew|56358%5F31365"
+ }
+ ],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1570936823",
+ "tzOffset": 10800,
+ "text": "6:20"
+ },
+ "end": {
+ "value": "1571005583",
+ "tzOffset": 10800,
+ "text": "1:26"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_179_bus_mosgortrans&ll=37.526151%2C55.858031&name=179&r=4634&type=bus",
+ "seoname": "179"
+ },
+ {
+ "lineId": "213_191m_minibus_default",
+ "name": "591",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_191m_minibus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9647199",
+ "name": "Метро Войковская"
+ },
+ {
+ "id": "stop__9711744",
+ "name": "Станция Ховрино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570972150",
+ "tzOffset": 10800,
+ "text": "16:09"
+ },
+ "vehicleId": "codd%5Fnew|35595%5F9345307"
+ }
+ ],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1570934833",
+ "tzOffset": 10800,
+ "text": "5:47"
+ },
+ "end": {
+ "value": "1571005033",
+ "tzOffset": 10800,
+ "text": "1:17"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_191m_minibus_default&ll=37.510906%2C55.848214&name=591&r=3384&type=bus",
+ "seoname": "591"
+ },
+ {
+ "lineId": "213_206m_minibus_default",
+ "name": "206к",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_206m_minibus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640756",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9640553",
+ "name": "Лобненская улица"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1570934039",
+ "tzOffset": 10800,
+ "text": "5:33"
+ },
+ "end": {
+ "value": "1571004239",
+ "tzOffset": 10800,
+ "text": "1:03"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_206m_minibus_default&ll=37.548997%2C55.864997&name=206%D0%BA&r=3515&type=bus",
+ "seoname": "206k"
+ },
+ {
+ "lineId": "213_215_bus_mosgortrans",
+ "name": "215",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_215_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9711780",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9711744",
+ "name": "Станция Ховрино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "27 мин",
+ "value": 1620,
+ "begin": {
+ "value": "1570934076",
+ "tzOffset": 10800,
+ "text": "5:34"
+ },
+ "end": {
+ "value": "1571004276",
+ "tzOffset": 10800,
+ "text": "1:04"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_215_bus_mosgortrans&ll=37.543701%2C55.854527&name=215&r=2763&type=bus",
+ "seoname": "215"
+ },
+ {
+ "lineId": "213_36_trolleybus_mosgortrans",
+ "name": "т36",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_36_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9642550",
+ "name": "ВДНХ (южная)"
+ },
+ {
+ "id": "stop__9640641",
+ "name": "Дмитровское шоссе, 155"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570972236",
+ "tzOffset": 10800,
+ "text": "16:10"
+ },
+ "vehicleId": "codd%5Fnew|1084830%5F430261"
+ },
+ {
+ "Estimated": {
+ "value": "1570972641",
+ "tzOffset": 10800,
+ "text": "16:17"
+ },
+ "vehicleId": "codd%5Fnew|1084829%5F430260"
+ },
+ {
+ "Estimated": {
+ "value": "1570973178",
+ "tzOffset": 10800,
+ "text": "16:26"
+ },
+ "vehicleId": "codd%5Fnew|1084827%5F430255"
+ }
+ ],
+ "Frequency": {
+ "text": "12 мин",
+ "value": 720,
+ "begin": {
+ "value": "1570932741",
+ "tzOffset": 10800,
+ "text": "5:12"
+ },
+ "end": {
+ "value": "1571003121",
+ "tzOffset": 10800,
+ "text": "0:45"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_36_trolleybus_mosgortrans&ll=37.588604%2C55.859705&name=%D1%8236&r=5104&type=bus",
+ "seoname": "t36"
+ },
+ {
+ "lineId": "213_47_trolleybus_mosgortrans",
+ "name": "т47",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_47_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9639568",
+ "name": "Бескудниковский переулок"
+ },
+ {
+ "id": "stop__9641903",
+ "name": "Бескудниковский переулок"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1570972080",
+ "tzOffset": 10800,
+ "text": "16:08"
+ },
+ "Estimated": {
+ "value": "1570972183",
+ "tzOffset": 10800,
+ "text": "16:09"
+ },
+ "vehicleId": "codd%5Fnew|1132404%5F430361"
+ },
+ {
+ "Scheduled": {
+ "value": "1570972980",
+ "tzOffset": 10800,
+ "text": "16:23"
+ },
+ "Estimated": {
+ "value": "1570972219",
+ "tzOffset": 10800,
+ "text": "16:10"
+ },
+ "vehicleId": "codd%5Fnew|1136132%5F430358"
+ },
+ {
+ "Scheduled": {
+ "value": "1570973940",
+ "tzOffset": 10800,
+ "text": "16:39"
+ }
+ }
+ ],
+ "departureTime": "16:08"
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_47_trolleybus_mosgortrans&ll=37.588308%2C55.818685&name=%D1%8247&r=5359&type=bus",
+ "seoname": "t47"
+ },
+ {
+ "lineId": "213_56_trolleybus_mosgortrans",
+ "name": "т56",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_56_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9639561",
+ "name": "Коровинское шоссе"
+ },
+ {
+ "id": "stop__9639588",
+ "name": "Коровинское шоссе"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1570971900",
+ "tzOffset": 10800,
+ "text": "16:05"
+ },
+ "Estimated": {
+ "value": "1570972560",
+ "tzOffset": 10800,
+ "text": "16:16"
+ },
+ "vehicleId": "codd%5Fnew|1117148%5F430351"
+ },
+ {
+ "Scheduled": {
+ "value": "1570972680",
+ "tzOffset": 10800,
+ "text": "16:18"
+ },
+ "Estimated": {
+ "value": "1570973442",
+ "tzOffset": 10800,
+ "text": "16:30"
+ },
+ "vehicleId": "codd%5Fnew|1080552%5F430302"
+ },
+ {
+ "Scheduled": {
+ "value": "1570973400",
+ "tzOffset": 10800,
+ "text": "16:30"
+ }
+ }
+ ],
+ "departureTime": "16:05"
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_56_trolleybus_mosgortrans&ll=37.551454%2C55.830147&name=%D1%8256&r=6304&type=bus",
+ "seoname": "t56"
+ },
+ {
+ "lineId": "213_63_bus_mosgortrans",
+ "name": "63",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_63_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9640554",
+ "name": "Лобненская улица"
+ },
+ {
+ "id": "stop__9640553",
+ "name": "Лобненская улица"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570972434",
+ "tzOffset": 10800,
+ "text": "16:13"
+ },
+ "vehicleId": "codd%5Fnew|38700%5F9215301"
+ }
+ ],
+ "Frequency": {
+ "text": "17 мин",
+ "value": 1020,
+ "begin": {
+ "value": "1570934207",
+ "tzOffset": 10800,
+ "text": "5:36"
+ },
+ "end": {
+ "value": "1571003507",
+ "tzOffset": 10800,
+ "text": "0:51"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_63_bus_mosgortrans&ll=37.550792%2C55.872690&name=63&r=3057&type=bus",
+ "seoname": "63"
+ },
+ {
+ "lineId": "213_677_bus_mosgortrans",
+ "name": "677",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_677_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9639495",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1570972200",
+ "tzOffset": 10800,
+ "text": "16:10"
+ },
+ "Estimated": {
+ "value": "1570971838",
+ "tzOffset": 10800,
+ "text": "16:03"
+ },
+ "vehicleId": "codd%5Fnew|58581%5F31321"
+ },
+ {
+ "Scheduled": {
+ "value": "1570972560",
+ "tzOffset": 10800,
+ "text": "16:16"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1570972920",
+ "tzOffset": 10800,
+ "text": "16:22"
+ }
+ }
+ ],
+ "departureTime": "16:10"
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_677_bus_mosgortrans&ll=37.564191%2C55.866620&name=677&r=3386&type=bus",
+ "seoname": "677"
+ },
+ {
+ "lineId": "213_78_trolleybus_mosgortrans",
+ "name": "т78",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_78_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9887464",
+ "name": "9-я Северная линия"
+ },
+ {
+ "id": "stop__9887464",
+ "name": "9-я Северная линия"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570971984",
+ "tzOffset": 10800,
+ "text": "16:06"
+ },
+ "vehicleId": "codd%5Fnew|59694%5F31155"
+ },
+ {
+ "Estimated": {
+ "value": "1570972003",
+ "tzOffset": 10800,
+ "text": "16:06"
+ },
+ "vehicleId": "codd%5Fnew|55041%5F31116"
+ },
+ {
+ "Estimated": {
+ "value": "1570972550",
+ "tzOffset": 10800,
+ "text": "16:15"
+ },
+ "vehicleId": "codd%5Fnew|62710%5F31142"
+ },
+ {
+ "Estimated": {
+ "value": "1570973307",
+ "tzOffset": 10800,
+ "text": "16:28"
+ },
+ "vehicleId": "codd%5Fnew|1037437%5F31144"
+ },
+ {
+ "Estimated": {
+ "value": "1570973456",
+ "tzOffset": 10800,
+ "text": "16:30"
+ },
+ "vehicleId": "codd%5Fnew|318517%5F31136"
+ }
+ ],
+ "Frequency": {
+ "text": "11 мин",
+ "value": 660,
+ "begin": {
+ "value": "1570937045",
+ "tzOffset": 10800,
+ "text": "6:24"
+ },
+ "end": {
+ "value": "1571002385",
+ "tzOffset": 10800,
+ "text": "0:33"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=213_78_trolleybus_mosgortrans&ll=37.569453%2C55.855402&name=%D1%8278&r=8810&type=bus",
+ "seoname": "t78"
+ },
+ {
+ "lineId": "2465131598",
+ "name": "179к",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2465131758",
+ "EssentialStops": [
+ {
+ "id": "stop__9640244",
+ "name": "Платформа Лианозово"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1570935230",
+ "tzOffset": 10800,
+ "text": "5:53"
+ },
+ "end": {
+ "value": "1571003030",
+ "tzOffset": 10800,
+ "text": "0:43"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=2465131598&ll=37.561423%2C55.871807&name=179%D0%BA&r=2787&type=bus",
+ "seoname": "179k"
+ },
+ {
+ "lineId": "677k_bus_default",
+ "name": "677к",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "677kA_bus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640244",
+ "name": "Платформа Лианозово"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1570972560",
+ "tzOffset": 10800,
+ "text": "16:16"
+ },
+ "Estimated": {
+ "value": "1570971986",
+ "tzOffset": 10800,
+ "text": "16:06"
+ },
+ "vehicleId": "codd%5Fnew|1038096%5F31398"
+ },
+ {
+ "Scheduled": {
+ "value": "1570973280",
+ "tzOffset": 10800,
+ "text": "16:28"
+ },
+ "Estimated": {
+ "value": "1570972342",
+ "tzOffset": 10800,
+ "text": "16:12"
+ },
+ "vehicleId": "codd%5Fnew|58590%5F31348"
+ },
+ {
+ "Scheduled": {
+ "value": "1570974000",
+ "tzOffset": 10800,
+ "text": "16:40"
+ },
+ "Estimated": {
+ "value": "1570973387",
+ "tzOffset": 10800,
+ "text": "16:29"
+ },
+ "vehicleId": "codd%5Fnew|58902%5F31316"
+ }
+ ],
+ "departureTime": "16:16"
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=677k_bus_default&ll=37.565257%2C55.870397&name=677%D0%BA&r=2987&type=bus",
+ "seoname": "677k"
+ },
+ {
+ "lineId": "m10_bus_default",
+ "name": "м10",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2036926048",
+ "EssentialStops": [
+ {
+ "id": "stop__9640554",
+ "name": "Лобненская улица"
+ },
+ {
+ "id": "stop__9640553",
+ "name": "Лобненская улица"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1570972343",
+ "tzOffset": 10800,
+ "text": "16:12"
+ },
+ "vehicleId": "codd%5Fnew|62922%5F31434"
+ },
+ {
+ "Estimated": {
+ "value": "1570972813",
+ "tzOffset": 10800,
+ "text": "16:20"
+ },
+ "vehicleId": "codd%5Fnew|57281%5F31242"
+ }
+ ],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1570939772",
+ "tzOffset": 10800,
+ "text": "7:09"
+ },
+ "end": {
+ "value": "1571008052",
+ "tzOffset": 10800,
+ "text": "2:07"
+ }
+ }
+ }
+ }
+ ],
+ "uri": "ymapsbm1://transit/line?id=m10_bus_default&ll=37.579221%2C55.823763&name=%D0%BC10&r=8474&type=bus",
+ "seoname": "m10"
+ }
+ ]
+ }
+ },
+ "searchResult": {
+ "requestId": "1570971868582853-530182592-man1-6817",
+ "title": "7-й автобусный парк",
+ "description": "Россия, Москва, Дмитровское шоссе",
+ "address": "Россия, Москва, Дмитровское шоссе",
+ "coordinates": [
+ 37.56528,
+ 55.85196
+ ],
+ "bounds": [
+ [
+ 37.543123,
+ 55.77889866
+ ],
+ [
+ 37.587437,
+ 55.92488366
+ ]
+ ],
+ "displayCoordinates": [
+ 37.56528,
+ 55.85196
+ ],
+ "metro": [
+ {
+ "id": "2244536395",
+ "name": "Верхние Лихоборы",
+ "distance": "510 м",
+ "distanceValue": 509.265,
+ "coordinates": [
+ 37.56121218,
+ 55.854501501
+ ],
+ "type": "metro",
+ "color": "#99cc33"
+ },
+ {
+ "id": "1727539211",
+ "name": "Окружная",
+ "distance": "640 м",
+ "distanceValue": 641.333,
+ "coordinates": [
+ 37.572849014,
+ 55.848814359
+ ],
+ "type": "metro",
+ "color": "#ffa8af"
+ },
+ {
+ "id": "2244535785",
+ "name": "Окружная",
+ "distance": "1,3 км",
+ "distanceValue": 1263.44,
+ "coordinates": [
+ 37.575977155,
+ 55.844377845
+ ],
+ "type": "metro",
+ "color": "#99cc33"
+ }
+ ],
+ "stops": [
+ {
+ "id": "stop__9639579",
+ "name": "7-й автобусный парк",
+ "distance": "0 м",
+ "distanceValue": 0.0383997,
+ "coordinates": [
+ 37.565280044,
+ 55.851959656
+ ],
+ "type": "common"
+ },
+ {
+ "id": "2310890052",
+ "name": "Метро Верхние Лихоборы",
+ "distance": "420 м",
+ "distanceValue": 424.274,
+ "coordinates": [
+ 37.563047501,
+ 55.853727589
+ ],
+ "type": "common"
+ },
+ {
+ "id": "stop__9639678",
+ "name": "Метро Верхние Лихоборы (северный вестибюль)",
+ "distance": "630 м",
+ "distanceValue": 629.689,
+ "coordinates": [
+ 37.562346735,
+ 55.857147019
+ ],
+ "type": "common"
+ },
+ {
+ "id": "station__lh_9601830",
+ "name": "Окружная",
+ "distance": "860 м",
+ "distanceValue": 857.487,
+ "coordinates": [
+ 37.574303,
+ 55.847684
+ ],
+ "type": "common"
+ },
+ {
+ "id": "stop__9639906",
+ "name": "Платформа Окружная",
+ "distance": "930 м",
+ "distanceValue": 926.144,
+ "coordinates": [
+ 37.576123886,
+ 55.847913668
+ ],
+ "type": "common"
+ }
+ ],
+ "logId": "dHlwZT1iaXpmaW5kZXI7aWQ9MjM5MzY2OTUwNjU4",
+ "type": "business",
+ "id": "239366950658",
+ "shortTitle": "7-й автобусный парк",
+ "additionalAddress": "",
+ "fullAddress": "Россия, Москва, Дмитровское шоссе",
+ "postalCode": "",
+ "addressDetails": {
+ "locality": "Москва",
+ "street": "Дмитровское шоссе"
+ },
+ "categories": [
+ {
+ "name": "Остановка общественного транспорта",
+ "class": "bus stop",
+ "seoname": "public_transport_stop",
+ "pluralName": "Остановки общественного транспорта",
+ "id": "223677355200"
+ }
+ ],
+ "status": "open",
+ "businessLinks": [],
+ "businessProperties": {
+ "geoproduct_poi_color": "#ABAEB3",
+ "snippet_show_title": "short_title",
+ "snippet_show_rating": "five_star_rating",
+ "snippet_show_photo": "single_photo",
+ "snippet_show_eta": "show_eta",
+ "snippet_show_category": "single_category",
+ "snippet_show_subline": [
+ "no_subline"
+ ],
+ "snippet_show_geoproduct_offer": "show_geoproduct_offer",
+ "snippet_show_bookmark": "show_bookmark",
+ "detailview_show_claim_organization": "not_show_claim_organization",
+ "detailview_show_reviews": "show_reviews",
+ "detailview_show_add_photo_button": "show_add_photo_button",
+ "detailview_show_taxi_button": "show_taxi_button",
+ "sensitive": "1"
+ },
+ "seoname": "7_y_avtobusny_park",
+ "geoId": 117015,
+ "uri": "ymapsbm1://org?oid=239366950658",
+ "uriList": [
+ "ymapsbm1://org?oid=239366950658",
+ "ymapsbm1://transit/stop?id=stop__9639579"
+ ],
+ "references": [
+ {
+ "id": "2036929560",
+ "scope": "nyak"
+ }
+ ],
+ "ratingData": {
+ "ratingCount": 0,
+ "ratingValue": 0,
+ "reviewCount": 0
+ },
+ "sources": [
+ {
+ "id": "yandex",
+ "name": "Яндекс",
+ "href": "https://www.yandex.ru"
+ }
+ ],
+ "analyticsId": "1"
+ },
+ "toponymSeoname": "dmitrovskoye_shosse"
+ }
+}
\ No newline at end of file
diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py
new file mode 100644
index 00000000000..e47dd834bf7
--- /dev/null
+++ b/tests/helpers/test_config_entry_oauth2_flow.py
@@ -0,0 +1,266 @@
+"""Tests for the Somfy config flow."""
+import asyncio
+import logging
+from unittest.mock import patch
+import time
+
+import pytest
+
+from homeassistant import data_entry_flow, setup, config_entries
+from homeassistant.helpers import config_entry_oauth2_flow
+
+from tests.common import mock_platform, MockConfigEntry
+
+TEST_DOMAIN = "oauth2_test"
+CLIENT_SECRET = "5678"
+CLIENT_ID = "1234"
+REFRESH_TOKEN = "mock-refresh-token"
+ACCESS_TOKEN_1 = "mock-access-token-1"
+ACCESS_TOKEN_2 = "mock-access-token-2"
+AUTHORIZE_URL = "https://example.como/auth/authorize"
+TOKEN_URL = "https://example.como/auth/token"
+
+
+@pytest.fixture
+async def local_impl(hass):
+ """Local implementation."""
+ assert await setup.async_setup_component(hass, "http", {})
+ return config_entry_oauth2_flow.LocalOAuth2Implementation(
+ hass, TEST_DOMAIN, CLIENT_ID, CLIENT_SECRET, AUTHORIZE_URL, TOKEN_URL
+ )
+
+
+@pytest.fixture
+def flow_handler(hass):
+ """Return a registered config flow."""
+
+ mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
+
+ class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
+ """Test flow handler."""
+
+ DOMAIN = TEST_DOMAIN
+
+ @property
+ def logger(self) -> logging.Logger:
+ """Return logger."""
+ return logging.getLogger(__name__)
+
+ @property
+ def extra_authorize_data(self) -> dict:
+ """Extra data that needs to be appended to the authorize url."""
+ return {"scope": "read write"}
+
+ with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}):
+ yield TestFlowHandler
+
+
+class MockOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implementation):
+ """Mock implementation for testing."""
+
+ @property
+ def name(self) -> str:
+ """Name of the implementation."""
+ return "Mock"
+
+ @property
+ def domain(self) -> str:
+ """Domain that is providing the implementation."""
+ return "test"
+
+ async def async_generate_authorize_url(self, flow_id: str) -> str:
+ """Generate a url for the user to authorize."""
+ return "http://example.com/auth"
+
+ async def async_resolve_external_data(self, external_data) -> dict:
+ """Resolve external data to tokens."""
+ return external_data
+
+ async def _async_refresh_token(self, token: dict) -> dict:
+ """Refresh a token."""
+ raise NotImplementedError()
+
+
+def test_inherit_enforces_domain_set():
+ """Test we enforce setting DOMAIN."""
+
+ class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
+ """Test flow handler."""
+
+ @property
+ def logger(self) -> logging.Logger:
+ """Return logger."""
+ return logging.getLogger(__name__)
+
+ with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}):
+ with pytest.raises(TypeError):
+ TestFlowHandler()
+
+
+async def test_abort_if_no_implementation(hass, flow_handler):
+ """Check flow abort when no implementations."""
+ flow = flow_handler()
+ flow.hass = hass
+ result = await flow.async_step_user()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "missing_configuration"
+
+
+async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl):
+ """Check timeout generating authorization url."""
+ flow_handler.async_register_implementation(hass, local_impl)
+
+ flow = flow_handler()
+ flow.hass = hass
+
+ with patch.object(
+ local_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError
+ ):
+ result = await flow.async_step_user()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "authorize_url_timeout"
+
+
+async def test_full_flow(
+ hass, flow_handler, local_impl, aiohttp_client, aioclient_mock
+):
+ """Check full flow."""
+ hass.config.api.base_url = "https://example.com"
+ flow_handler.async_register_implementation(hass, local_impl)
+ config_entry_oauth2_flow.async_register_implementation(
+ hass, TEST_DOMAIN, MockOAuth2Implementation()
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "pick_implementation"
+
+ # Pick implementation
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"implementation": TEST_DOMAIN}
+ )
+
+ state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
+ assert result["url"] == (
+ f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}"
+ "&redirect_uri=https://example.com/auth/external/callback"
+ f"&state={state}&scope=read+write"
+ )
+
+ client = await aiohttp_client(hass.http.app)
+ resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
+ assert resp.status == 200
+ assert resp.headers["content-type"] == "text/html; charset=utf-8"
+
+ aioclient_mock.post(
+ TOKEN_URL,
+ json={
+ "refresh_token": REFRESH_TOKEN,
+ "access_token": ACCESS_TOKEN_1,
+ "type": "bearer",
+ "expires_in": 60,
+ },
+ )
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+
+ assert result["data"]["auth_implementation"] == TEST_DOMAIN
+
+ result["data"]["token"].pop("expires_at")
+ assert result["data"]["token"] == {
+ "refresh_token": REFRESH_TOKEN,
+ "access_token": ACCESS_TOKEN_1,
+ "type": "bearer",
+ "expires_in": 60,
+ }
+
+ entry = hass.config_entries.async_entries(TEST_DOMAIN)[0]
+
+ assert (
+ await config_entry_oauth2_flow.async_get_config_entry_implementation(
+ hass, entry
+ )
+ is local_impl
+ )
+
+
+async def test_local_refresh_token(hass, local_impl, aioclient_mock):
+ """Test we can refresh token."""
+ aioclient_mock.post(
+ TOKEN_URL, json={"access_token": ACCESS_TOKEN_2, "expires_in": 100}
+ )
+
+ new_tokens = await local_impl.async_refresh_token(
+ {
+ "refresh_token": REFRESH_TOKEN,
+ "access_token": ACCESS_TOKEN_1,
+ "type": "bearer",
+ "expires_in": 60,
+ }
+ )
+ new_tokens.pop("expires_at")
+
+ assert new_tokens == {
+ "refresh_token": REFRESH_TOKEN,
+ "access_token": ACCESS_TOKEN_2,
+ "type": "bearer",
+ "expires_in": 100,
+ }
+
+ assert len(aioclient_mock.mock_calls) == 1
+ assert aioclient_mock.mock_calls[0][2] == {
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ "grant_type": "refresh_token",
+ "refresh_token": REFRESH_TOKEN,
+ }
+
+
+async def test_oauth_session(hass, flow_handler, local_impl, aioclient_mock):
+ """Test the OAuth2 session helper."""
+ flow_handler.async_register_implementation(hass, local_impl)
+
+ aioclient_mock.post(
+ TOKEN_URL, json={"access_token": ACCESS_TOKEN_2, "expires_in": 100}
+ )
+
+ aioclient_mock.post("https://example.com", status=201)
+
+ config_entry = MockConfigEntry(
+ domain=TEST_DOMAIN,
+ data={
+ "auth_implementation": TEST_DOMAIN,
+ "token": {
+ "refresh_token": REFRESH_TOKEN,
+ "access_token": ACCESS_TOKEN_1,
+ "expires_in": 10,
+ "expires_at": 0, # Forces a refresh,
+ "token_type": "bearer",
+ "random_other_data": "should_stay",
+ },
+ },
+ )
+
+ now = time.time()
+ session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl)
+ resp = await session.async_request("post", "https://example.com")
+ assert resp.status == 201
+
+ # Refresh token, make request
+ assert len(aioclient_mock.mock_calls) == 2
+
+ assert (
+ aioclient_mock.mock_calls[1][3]["authorization"] == f"Bearer {ACCESS_TOKEN_2}"
+ )
+
+ assert config_entry.data["token"]["refresh_token"] == REFRESH_TOKEN
+ assert config_entry.data["token"]["access_token"] == ACCESS_TOKEN_2
+ assert config_entry.data["token"]["expires_in"] == 100
+ assert config_entry.data["token"]["random_other_data"] == "should_stay"
+ assert round(config_entry.data["token"]["expires_at"] - now) == 100
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index e09f8cf57aa..1f5d6ddfc40 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -494,7 +494,10 @@ def test_deprecated_with_no_optionals(caplog, schema):
test_data = {"mars": True}
output = deprecated_schema(test_data.copy())
assert len(caplog.records) == 1
- assert caplog.records[0].name == __name__
+ assert caplog.records[0].name in [
+ __name__,
+ "homeassistant.helpers.config_validation",
+ ]
assert (
"The 'mars' option (with value 'True') is deprecated, "
"please remove it from your configuration"
diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py
index 18cedf1c46a..9d05920f78b 100644
--- a/tests/helpers/test_entity.py
+++ b/tests/helpers/test_entity.py
@@ -231,6 +231,88 @@ def test_async_schedule_update_ha_state(hass):
assert update_call is True
+async def test_async_async_request_call_without_lock(hass):
+ """Test for async_requests_call works without a lock."""
+ updates = []
+
+ class AsyncEntity(entity.Entity):
+ def __init__(self, entity_id):
+ """Initialize Async test entity."""
+ self.entity_id = entity_id
+ self.hass = hass
+
+ async def testhelper(self, count):
+ """Helper function."""
+ updates.append(count)
+
+ ent_1 = AsyncEntity("light.test_1")
+ ent_2 = AsyncEntity("light.test_2")
+ try:
+ job1 = ent_1.async_request_call(ent_1.testhelper(1))
+ job2 = ent_2.async_request_call(ent_2.testhelper(2))
+
+ await asyncio.wait([job1, job2])
+ while True:
+ if len(updates) >= 2:
+ break
+ await asyncio.sleep(0)
+ finally:
+ pass
+
+ assert len(updates) == 2
+ updates.sort()
+ assert updates == [1, 2]
+
+
+async def test_async_async_request_call_with_lock(hass):
+ """Test for async_requests_call works with a semaphore."""
+ updates = []
+
+ test_semaphore = asyncio.Semaphore(1)
+
+ class AsyncEntity(entity.Entity):
+ def __init__(self, entity_id, lock):
+ """Initialize Async test entity."""
+ self.entity_id = entity_id
+ self.hass = hass
+ self.parallel_updates = lock
+
+ async def testhelper(self, count):
+ """Helper function."""
+ updates.append(count)
+
+ ent_1 = AsyncEntity("light.test_1", test_semaphore)
+ ent_2 = AsyncEntity("light.test_2", test_semaphore)
+
+ try:
+ assert test_semaphore.locked() is False
+ await test_semaphore.acquire()
+ assert test_semaphore.locked()
+
+ job1 = ent_1.async_request_call(ent_1.testhelper(1))
+ job2 = ent_2.async_request_call(ent_2.testhelper(2))
+
+ hass.async_create_task(job1)
+ hass.async_create_task(job2)
+
+ assert len(updates) == 0
+ assert updates == []
+ assert test_semaphore._value == 0
+
+ test_semaphore.release()
+
+ while True:
+ if len(updates) >= 2:
+ break
+ await asyncio.sleep(0)
+ finally:
+ test_semaphore.release()
+
+ assert len(updates) == 2
+ updates.sort()
+ assert updates == [1, 2]
+
+
async def test_async_parallel_updates_with_zero(hass):
"""Test parallel updates with 0 (disabled)."""
updates = []
diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py
index ebc56c111ee..4b8be715f37 100644
--- a/tests/helpers/test_script.py
+++ b/tests/helpers/test_script.py
@@ -9,7 +9,9 @@ import jinja2
import voluptuous as vol
import pytest
+import homeassistant.components.scene as scene
from homeassistant import exceptions
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
from homeassistant.core import Context, callback
# Otherwise can't test just this file (import order issue)
@@ -120,6 +122,31 @@ async def test_calling_service(hass):
assert calls[0].data.get("hello") == "world"
+async def test_activating_scene(hass):
+ """Test the activation of a scene."""
+ calls = []
+ context = Context()
+
+ @callback
+ def record_call(service):
+ """Add recorded event to set."""
+ calls.append(service)
+
+ hass.services.async_register(scene.DOMAIN, SERVICE_TURN_ON, record_call)
+
+ hass.async_add_job(
+ ft.partial(
+ script.call_from_config, hass, {"scene": "scene.hello"}, context=context
+ )
+ )
+
+ await hass.async_block_till_done()
+
+ assert len(calls) == 1
+ assert calls[0].context is context
+ assert calls[0].data.get(ATTR_ENTITY_ID) == "scene.hello"
+
+
async def test_calling_service_template(hass):
"""Test the calling of a service."""
calls = []
diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py
index 7f428c0833d..14bcbde5094 100644
--- a/tests/helpers/test_state.py
+++ b/tests/helpers/test_state.py
@@ -129,7 +129,7 @@ async def test_reproduce_turn_on(hass):
last_call = calls[-1]
assert last_call.domain == "light"
assert SERVICE_TURN_ON == last_call.service
- assert ["light.test"] == last_call.data.get("entity_id")
+ assert "light.test" == last_call.data.get("entity_id")
async def test_reproduce_turn_off(hass):
@@ -146,7 +146,7 @@ async def test_reproduce_turn_off(hass):
last_call = calls[-1]
assert last_call.domain == "light"
assert SERVICE_TURN_OFF == last_call.service
- assert ["light.test"] == last_call.data.get("entity_id")
+ assert "light.test" == last_call.data.get("entity_id")
async def test_reproduce_complex_data(hass):
@@ -155,10 +155,10 @@ async def test_reproduce_complex_data(hass):
hass.states.async_set("light.test", "off")
- complex_data = ["hello", {"11": "22"}]
+ complex_data = [255, 100, 100]
await state.async_reproduce_state(
- hass, ha.State("light.test", "on", {"complex": complex_data})
+ hass, ha.State("light.test", "on", {"rgb_color": complex_data})
)
await hass.async_block_till_done()
@@ -167,7 +167,7 @@ async def test_reproduce_complex_data(hass):
last_call = calls[-1]
assert last_call.domain == "light"
assert SERVICE_TURN_ON == last_call.service
- assert complex_data == last_call.data.get("complex")
+ assert complex_data == last_call.data.get("rgb_color")
async def test_reproduce_bad_state(hass):
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index cc1f7707df6..b69fdb17e35 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -501,6 +501,30 @@ def test_timestamp_local(hass):
)
+def test_to_json(hass):
+ """Test the object to JSON string filter."""
+
+ # Note that we're not testing the actual json.loads and json.dumps methods,
+ # only the filters, so we don't need to be exhaustive with our sample JSON.
+ expected_result = '{"Foo": "Bar"}'
+ actual_result = template.Template(
+ "{{ {'Foo': 'Bar'} | to_json }}", hass
+ ).async_render()
+ assert actual_result == expected_result
+
+
+def test_from_json(hass):
+ """Test the JSON string to object filter."""
+
+ # Note that we're not testing the actual json.loads and json.dumps methods,
+ # only the filters, so we don't need to be exhaustive with our sample JSON.
+ expected_result = "Bar"
+ actual_result = template.Template(
+ '{{ (\'{"Foo": "Bar"}\' | from_json).Foo }}', hass
+ ).async_render()
+ assert actual_result == expected_result
+
+
def test_min(hass):
"""Test the min filter."""
assert template.Template("{{ [1, 2, 3] | min }}", hass).async_render() == "1"
diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py
index 18143c088be..5199f01807f 100644
--- a/tests/scripts/test_check_config.py
+++ b/tests/scripts/test_check_config.py
@@ -92,8 +92,8 @@ def test_secrets(isfile_patch, loop):
files = {
get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG
- + ("http:\n" " api_password: !secret http_pw"),
- secrets_path: ("logger: debug\n" "http_pw: abc123"),
+ + ("http:\n" " cors_allowed_origins: !secret http_pw"),
+ secrets_path: ("logger: debug\n" "http_pw: http://google.com"),
}
with patch_yaml_files(files):
@@ -103,17 +103,15 @@ def test_secrets(isfile_patch, loop):
assert res["except"] == {}
assert res["components"].keys() == {"homeassistant", "http"}
assert res["components"]["http"] == {
- "api_password": "abc123",
- "cors_allowed_origins": ["https://cast.home-assistant.io"],
+ "cors_allowed_origins": ["http://google.com"],
"ip_ban_enabled": True,
"login_attempts_threshold": -1,
"server_host": "0.0.0.0",
"server_port": 8123,
- "trusted_networks": [],
"ssl_profile": "modern",
}
- assert res["secret_cache"] == {secrets_path: {"http_pw": "abc123"}}
- assert res["secrets"] == {"http_pw": "abc123"}
+ assert res["secret_cache"] == {secrets_path: {"http_pw": "http://google.com"}}
+ assert res["secrets"] == {"http_pw": "http://google.com"}
assert normalize_yaml_files(res) == [
".../configuration.yaml",
".../secrets.yaml",
diff --git a/tests/test_config.py b/tests/test_config.py
index a67cd345797..dab51f59176 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -5,7 +5,6 @@ import copy
import os
import unittest.mock as mock
from collections import OrderedDict
-from ipaddress import ip_network
import asynctest
import pytest
@@ -34,11 +33,6 @@ from homeassistant.const import (
from homeassistant.util import dt as dt_util
from homeassistant.util.yaml import SECRET_YAML
from homeassistant.helpers.entity import Entity
-from homeassistant.components.config.group import CONFIG_PATH as GROUP_CONFIG_PATH
-from homeassistant.components.config.automation import (
- CONFIG_PATH as AUTOMATIONS_CONFIG_PATH,
-)
-from homeassistant.components.config.script import CONFIG_PATH as SCRIPTS_CONFIG_PATH
import homeassistant.helpers.check_config as check_config
from tests.common import get_test_config_dir, patch_yaml_files
@@ -47,9 +41,9 @@ CONFIG_DIR = get_test_config_dir()
YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE)
SECRET_PATH = os.path.join(CONFIG_DIR, SECRET_YAML)
VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE)
-GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH)
-AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_CONFIG_PATH)
-SCRIPTS_PATH = os.path.join(CONFIG_DIR, SCRIPTS_CONFIG_PATH)
+GROUP_PATH = os.path.join(CONFIG_DIR, config_util.GROUP_CONFIG_PATH)
+AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, config_util.AUTOMATION_CONFIG_PATH)
+SCRIPTS_PATH = os.path.join(CONFIG_DIR, config_util.SCRIPT_CONFIG_PATH)
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
@@ -346,62 +340,6 @@ def test_config_upgrade_no_file(hass):
assert opened_file.write.call_args == mock.call(__version__)
-@mock.patch("homeassistant.config.shutil")
-@mock.patch("homeassistant.config.os")
-@mock.patch("homeassistant.config.find_config_file", mock.Mock())
-def test_migrate_file_on_upgrade(mock_os, mock_shutil, hass):
- """Test migrate of config files on upgrade."""
- ha_version = "0.7.0"
-
- mock_os.path.isdir = mock.Mock(return_value=True)
-
- mock_open = mock.mock_open()
-
- def _mock_isfile(filename):
- return True
-
- with mock.patch("homeassistant.config.open", mock_open, create=True), mock.patch(
- "homeassistant.config.os.path.isfile", _mock_isfile
- ):
- 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)
-
- assert mock_os.rename.call_count == 1
-
-
-@mock.patch("homeassistant.config.shutil")
-@mock.patch("homeassistant.config.os")
-@mock.patch("homeassistant.config.find_config_file", mock.Mock())
-def test_migrate_no_file_on_upgrade(mock_os, mock_shutil, hass):
- """Test not migrating config files on upgrade."""
- ha_version = "0.7.0"
-
- mock_os.path.isdir = mock.Mock(return_value=True)
-
- mock_open = mock.mock_open()
-
- def _mock_isfile(filename):
- return False
-
- with mock.patch("homeassistant.config.open", mock_open, create=True), mock.patch(
- "homeassistant.config.os.path.isfile", _mock_isfile
- ):
- 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)
-
- assert mock_os.rename.call_count == 0
-
-
async def test_loading_configuration_from_storage(hass, hass_storage):
"""Test loading core config onto hass object."""
hass_storage["core.config"] = {
@@ -876,48 +814,6 @@ async def test_auth_provider_config_default(hass):
assert hass.auth.auth_mfa_modules[0].id == "totp"
-async def test_auth_provider_config_default_api_password(hass):
- """Test loading default auth provider config with api password."""
- core_config = {
- "latitude": 60,
- "longitude": 50,
- "elevation": 25,
- "name": "Huis",
- CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
- "time_zone": "GMT",
- }
- if hasattr(hass, "auth"):
- del hass.auth
- await config_util.async_process_ha_core_config(hass, core_config, "pass")
-
- assert len(hass.auth.auth_providers) == 2
- assert hass.auth.auth_providers[0].type == "homeassistant"
- assert hass.auth.auth_providers[1].type == "legacy_api_password"
- assert hass.auth.auth_providers[1].api_password == "pass"
-
-
-async def test_auth_provider_config_default_trusted_networks(hass):
- """Test loading default auth provider config with trusted networks."""
- core_config = {
- "latitude": 60,
- "longitude": 50,
- "elevation": 25,
- "name": "Huis",
- CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
- "time_zone": "GMT",
- }
- if hasattr(hass, "auth"):
- del hass.auth
- await config_util.async_process_ha_core_config(
- hass, core_config, trusted_networks=["192.168.0.1"]
- )
-
- assert len(hass.auth.auth_providers) == 2
- assert hass.auth.auth_providers[0].type == "homeassistant"
- assert hass.auth.auth_providers[1].type == "trusted_networks"
- assert hass.auth.auth_providers[1].trusted_networks[0] == ip_network("192.168.0.1")
-
-
async def test_disallowed_auth_provider_config(hass):
"""Test loading insecure example auth provider is disallowed."""
core_config = {
diff --git a/tests/test_main.py b/tests/test_main.py
index 509425ce418..29454d269af 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -2,6 +2,7 @@
from unittest.mock import patch, PropertyMock
from homeassistant import __main__ as main
+from homeassistant.const import REQUIRED_PYTHON_VER
@patch("sys.exit")
@@ -31,6 +32,32 @@ def test_validate_python(mock_exit):
mock_exit.reset_mock()
- with patch("sys.version_info", new_callable=PropertyMock(return_value=(3, 6, 0))):
+ with patch(
+ "sys.version_info",
+ new_callable=PropertyMock(
+ return_value=(REQUIRED_PYTHON_VER[0] - 1,) + REQUIRED_PYTHON_VER[1:]
+ ),
+ ):
+ main.validate_python()
+ assert mock_exit.called is True
+
+ mock_exit.reset_mock()
+
+ with patch(
+ "sys.version_info", new_callable=PropertyMock(return_value=REQUIRED_PYTHON_VER)
+ ):
main.validate_python()
assert mock_exit.called is False
+
+ mock_exit.reset_mock()
+
+ with patch(
+ "sys.version_info",
+ new_callable=PropertyMock(
+ return_value=(REQUIRED_PYTHON_VER[:2]) + (REQUIRED_PYTHON_VER[2] + 1,)
+ ),
+ ):
+ main.validate_python()
+ assert mock_exit.called is False
+
+ mock_exit.reset_mock()
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
index b5574fe96fd..780b175778e 100644
--- a/tests/test_requirements.py
+++ b/tests/test_requirements.py
@@ -17,6 +17,13 @@ from homeassistant.requirements import (
from tests.common import get_test_home_assistant, MockModule, mock_integration
+def env_without_wheel_links():
+ """Return env without wheel links."""
+ env = dict(os.environ)
+ env.pop("WHEEL_LINKS", None)
+ return env
+
+
class TestRequirements:
"""Test the requirements module."""
@@ -36,6 +43,7 @@ class TestRequirements:
@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)
+ @patch.dict(os.environ, env_without_wheel_links(), clear=True)
def test_requirement_installed_in_venv(
self, mock_install, mock_denv, mock_venv, mock_dirname
):
@@ -55,6 +63,7 @@ class TestRequirements:
@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)
+ @patch.dict(os.environ, env_without_wheel_links(), clear=True)
def test_requirement_installed_in_deps(
self, mock_install, mock_denv, mock_venv, mock_dirname
):
@@ -136,7 +145,7 @@ async def test_install_with_wheels_index(hass):
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",
@@ -154,11 +163,13 @@ async def test_install_on_docker(hass):
"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:
+ ) as mock_dir, patch.dict(
+ os.environ, env_without_wheel_links(), clear=True
+ ):
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),
diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py
new file mode 100644
index 00000000000..0e2842f8695
--- /dev/null
+++ b/tests/testing_config/custom_components/test/alarm_control_panel.py
@@ -0,0 +1,91 @@
+"""
+Provide a mock alarm_control_panel platform.
+
+Call init before using it in your tests to ensure clean test data.
+"""
+from homeassistant.components.alarm_control_panel import AlarmControlPanel
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
+from tests.common import MockEntity
+
+ENTITIES = {}
+
+
+def init(empty=False):
+ """Initialize the platform with entities."""
+ global ENTITIES
+
+ ENTITIES = (
+ {}
+ if empty
+ else {
+ "arm_code": MockAlarm(
+ name=f"Alarm arm code",
+ code_arm_required=True,
+ unique_id="unique_arm_code",
+ ),
+ "no_arm_code": MockAlarm(
+ name=f"Alarm no arm code",
+ code_arm_required=False,
+ unique_id="unique_no_arm_code",
+ ),
+ }
+ )
+
+
+async def async_setup_platform(
+ hass, config, async_add_entities_callback, discovery_info=None
+):
+ """Return mock entities."""
+ async_add_entities_callback(list(ENTITIES.values()))
+
+
+class MockAlarm(MockEntity, AlarmControlPanel):
+ """Mock Alarm control panel class."""
+
+ def __init__(self, **values):
+ """Init the Mock Alarm Control Panel."""
+ self._state = None
+
+ MockEntity.__init__(self, **values)
+
+ @property
+ def code_arm_required(self):
+ """Whether the code is required for arm actions."""
+ return self._handle("code_arm_required")
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._state
+
+ def alarm_arm_away(self, code=None):
+ """Send arm away command."""
+ self._state = STATE_ALARM_ARMED_AWAY
+ self.async_write_ha_state()
+
+ def alarm_arm_home(self, code=None):
+ """Send arm home command."""
+ self._state = STATE_ALARM_ARMED_HOME
+ self.async_write_ha_state()
+
+ def alarm_arm_night(self, code=None):
+ """Send arm night command."""
+ self._state = STATE_ALARM_ARMED_NIGHT
+ self.async_write_ha_state()
+
+ def alarm_disarm(self, code=None):
+ """Send disarm command."""
+ if code == "1234":
+ self._state = STATE_ALARM_DISARMED
+ self.async_write_ha_state()
+
+ def alarm_trigger(self, code=None):
+ """Send alarm trigger command."""
+ self._state = STATE_ALARM_TRIGGERED
+ self.async_write_ha_state()
diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py
new file mode 100644
index 00000000000..db6ce38b097
--- /dev/null
+++ b/tests/testing_config/custom_components/test/lock.py
@@ -0,0 +1,54 @@
+"""
+Provide a mock lock platform.
+
+Call init before using it in your tests to ensure clean test data.
+"""
+from homeassistant.components.lock import LockDevice, SUPPORT_OPEN
+from tests.common import MockEntity
+
+ENTITIES = {}
+
+
+def init(empty=False):
+ """Initialize the platform with entities."""
+ global ENTITIES
+
+ ENTITIES = (
+ {}
+ if empty
+ else {
+ "support_open": MockLock(
+ name=f"Support open Lock",
+ is_locked=True,
+ supported_features=SUPPORT_OPEN,
+ unique_id="unique_support_open",
+ ),
+ "no_support_open": MockLock(
+ name=f"No support open Lock",
+ is_locked=True,
+ supported_features=0,
+ unique_id="unique_no_support_open",
+ ),
+ }
+ )
+
+
+async def async_setup_platform(
+ hass, config, async_add_entities_callback, discovery_info=None
+):
+ """Return mock entities."""
+ async_add_entities_callback(list(ENTITIES.values()))
+
+
+class MockLock(MockEntity, LockDevice):
+ """Mock Lock class."""
+
+ @property
+ def is_locked(self):
+ """Return true if the lock is locked."""
+ return self._handle("is_locked")
+
+ @property
+ def supported_features(self):
+ """Return the class of this sensor."""
+ return self._handle("supported_features")
diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py
index 414b246466c..d5f8eb4a2c7 100644
--- a/tests/util/test_logging.py
+++ b/tests/util/test_logging.py
@@ -72,12 +72,8 @@ async def test_async_create_catching_coro(hass, caplog):
async def job():
raise Exception("This is a bad coroutine")
- pass
hass.async_create_task(logging_util.async_create_catching_coro(job()))
await hass.async_block_till_done()
assert "This is a bad coroutine" in caplog.text
- assert (
- "hass.async_create_task("
- "logging_util.async_create_catching_coro(job()))" in caplog.text
- )
+ assert "in test_async_create_catching_coro" in caplog.text
diff --git a/tox.ini b/tox.ini
index 2d4cf7c54ba..f6d12fe30f5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,7 +26,7 @@ deps =
-r{toxinidir}/requirements_test.txt
-c{toxinidir}/homeassistant/package_constraints.txt
commands =
- pylint {posargs} homeassistant
+ pylint {env:PYLINT_ARGS} {posargs} homeassistant
[testenv:lint]
deps =
@@ -34,12 +34,11 @@ deps =
commands =
python -m script.gen_requirements_all validate
python -m script.hassfest validate
- flake8 {posargs: homeassistant tests script}
+ pre-commit run flake8 {posargs: --all-files}
[testenv:typing]
-whitelist_externals=/bin/bash
deps =
-r{toxinidir}/requirements_test.txt
-c{toxinidir}/homeassistant/package_constraints.txt
commands =
- /bin/bash -c 'TYPING_FILES=$(cat mypyrc); mypy $TYPING_FILES'
+ pre-commit run mypy {posargs: --all-files}