diff --git a/.coveragerc b/.coveragerc
index 693959684f1..35c47de4160 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -166,7 +166,6 @@ omit =
homeassistant/components/dsmr_reader/*
homeassistant/components/dte_energy_bridge/sensor.py
homeassistant/components/dublin_bus_transport/sensor.py
- homeassistant/components/duke_energy/sensor.py
homeassistant/components/dunehd/media_player.py
homeassistant/components/dwd_weather_warnings/sensor.py
homeassistant/components/dweet/*
@@ -248,7 +247,6 @@ omit =
homeassistant/components/fritzbox/*
homeassistant/components/fritzbox_callmonitor/sensor.py
homeassistant/components/fritzbox_netmonitor/sensor.py
- homeassistant/components/fritzdect/switch.py
homeassistant/components/fronius/sensor.py
homeassistant/components/frontier_silicon/media_player.py
homeassistant/components/futurenow/light.py
@@ -387,7 +385,6 @@ omit =
homeassistant/components/linode/*
homeassistant/components/linux_battery/sensor.py
homeassistant/components/lirc/*
- homeassistant/components/liveboxplaytv/media_player.py
homeassistant/components/llamalab_automate/notify.py
homeassistant/components/lockitron/lock.py
homeassistant/components/logi_circle/__init__.py
@@ -412,9 +409,15 @@ omit =
homeassistant/components/mcp23017/*
homeassistant/components/media_extractor/*
homeassistant/components/mediaroom/media_player.py
+ homeassistant/components/melcloud/__init__.py
+ homeassistant/components/melcloud/climate.py
+ homeassistant/components/melcloud/sensor.py
homeassistant/components/message_bird/notify.py
homeassistant/components/met/weather.py
- homeassistant/components/meteo_france/*
+ homeassistant/components/meteo_france/__init__.py
+ homeassistant/components/meteo_france/const.py
+ homeassistant/components/meteo_france/sensor.py
+ homeassistant/components/meteo_france/weather.py
homeassistant/components/meteoalarm/*
homeassistant/components/metoffice/sensor.py
homeassistant/components/metoffice/weather.py
@@ -424,6 +427,10 @@ omit =
homeassistant/components/mikrotik/device_tracker.py
homeassistant/components/mill/climate.py
homeassistant/components/mill/const.py
+ homeassistant/components/minecraft_server/__init__.py
+ homeassistant/components/minecraft_server/binary_sensor.py
+ homeassistant/components/minecraft_server/const.py
+ homeassistant/components/minecraft_server/sensor.py
homeassistant/components/minio/*
homeassistant/components/mitemp_bt/sensor.py
homeassistant/components/mjpeg/camera.py
@@ -603,6 +610,7 @@ omit =
homeassistant/components/russound_rnet/media_player.py
homeassistant/components/sabnzbd/*
homeassistant/components/saj/sensor.py
+ homeassistant/components/salt/device_tracker.py
homeassistant/components/satel_integra/*
homeassistant/components/scrape/sensor.py
homeassistant/components/scsgate/*
@@ -622,8 +630,6 @@ omit =
homeassistant/components/shodan/sensor.py
homeassistant/components/sht31/sensor.py
homeassistant/components/sigfox/sensor.py
- homeassistant/components/signal_messenger/__init__.py
- homeassistant/components/signal_messenger/notify.py
homeassistant/components/simplepush/notify.py
homeassistant/components/simplisafe/__init__.py
homeassistant/components/simplisafe/alarm_control_panel.py
@@ -750,7 +756,6 @@ omit =
homeassistant/components/twentemilieu/sensor.py
homeassistant/components/twilio_call/notify.py
homeassistant/components/twilio_sms/notify.py
- homeassistant/components/twitch/sensor.py
homeassistant/components/twitter/notify.py
homeassistant/components/ubee/device_tracker.py
homeassistant/components/ubus/device_tracker.py
@@ -781,10 +786,10 @@ omit =
homeassistant/components/vesync/switch.py
homeassistant/components/viaggiatreno/sensor.py
homeassistant/components/vicare/*
+ homeassistant/components/vilfo/__init__.py
+ homeassistant/components/vilfo/sensor.py
+ homeassistant/components/vilfo/const.py
homeassistant/components/vivotek/camera.py
- homeassistant/components/vizio/__init__.py
- homeassistant/components/vizio/const.py
- homeassistant/components/vizio/media_player.py
homeassistant/components/vlc/media_player.py
homeassistant/components/vlc_telnet/media_player.py
homeassistant/components/volkszaehler/sensor.py
diff --git a/.github/stale.yml b/.github/stale.yml
index 44cd95e1f5d..e75d791a57c 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -52,4 +52,14 @@ markComment: >
limitPerRun: 30
# Limit to only `issues` or `pulls`
-only: issues
+# only: issues
+
+# Handle pull requests a little bit faster and with an adjusted comment.
+pulls:
+ daysUntilStale: 30
+ markComment: >
+ There hasn't been any activity on this pull request recently. This pull
+ request has been automatically marked as stale because of that and will
+ be closed if no further activity occurs within 7 days.
+
+ Thank you for your contributions.
diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml
deleted file mode 100644
index a6b882e617b..00000000000
--- a/.pre-commit-config-all.yaml
+++ /dev/null
@@ -1,59 +0,0 @@
-# This configuration includes the full set of hooks we use. In
-# addition to the defaults (see .pre-commit-config.yaml), this
-# includes hooks that require our development and test dependencies
-# installed and the virtualenv containing them active by the time
-# pre-commit runs to produce correct results.
-#
-# If this is not a problem for your workflow, using this config is
-# recommended, install it with
-# pre-commit install --config .pre-commit-config-all.yaml
-# Otherwise, see the default .pre-commit-config.yaml for a lighter one.
-
-repos:
-- repo: https://github.com/psf/black
- rev: 19.10b0
- hooks:
- - id: black
- args:
- - --safe
- - --quiet
- files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
-- repo: https://github.com/PyCQA/flake8
- rev: 3.7.9
- hooks:
- - id: flake8
- additional_dependencies:
- - flake8-docstrings==1.5.0
- - pydocstyle==5.0.2
- files: ^(homeassistant|script|tests)/.+\.py$
-- repo: https://github.com/PyCQA/bandit
- rev: 1.6.2
- hooks:
- - id: bandit
- args:
- - --quiet
- - --format=custom
- - --configfile=tests/bandit.yaml
- files: ^(homeassistant|script|tests)/.+\.py$
-- repo: https://github.com/pre-commit/mirrors-isort
- rev: v4.3.21
- hooks:
- - id: isort
-- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v2.4.0
- hooks:
- - id: check-json
-# 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/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1f27e82b6d9..a340aa7ae67 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,10 +1,3 @@
-# This configuration includes the default, minimal set of hooks to be
-# run on all commits. It requires no specific setup and one can just
-# start using pre-commit with it.
-#
-# See .pre-commit-config-all.yaml for a more complete one that comes
-# with a better coverage at the cost of some specific setup needed.
-
repos:
- repo: https://github.com/psf/black
rev: 19.10b0
@@ -14,6 +7,15 @@ repos:
- --safe
- --quiet
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
+- repo: https://github.com/codespell-project/codespell
+ rev: v1.16.0
+ hooks:
+ - id: codespell
+ args:
+ - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
+ - --skip="./.*,*.json"
+ - --quiet-level=2
+ exclude_types: [json]
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
hooks:
@@ -39,3 +41,16 @@ repos:
rev: v2.4.0
hooks:
- id: check-json
+- repo: local
+ hooks:
+ # Run mypy through our wrapper script in order to get the possible
+ # pyenv and/or virtualenv activated; it may not have been e.g. if
+ # committing from a GUI tool that was not launched from an activated
+ # shell.
+ - id: mypy
+ name: mypy
+ entry: script/run-in-env.sh mypy
+ language: script
+ types: [python]
+ require_serial: true
+ files: ^homeassistant/.+\.py$
diff --git a/CODEOWNERS b/CODEOWNERS
index 6983d13fc8b..a8057197827 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -35,6 +35,7 @@ homeassistant/components/arest/* @fabaff
homeassistant/components/asuswrt/* @kennedyshead
homeassistant/components/aten_pe/* @mtdcr
homeassistant/components/atome/* @baqs
+homeassistant/components/august/* @bdraco
homeassistant/components/aurora_abb_powerone/* @davet2001
homeassistant/components/auth/* @home-assistant/core
homeassistant/components/automatic/* @armills
@@ -83,6 +84,7 @@ homeassistant/components/discogs/* @thibmaek
homeassistant/components/doorbird/* @oblogic7
homeassistant/components/dsmr_reader/* @depl0y
homeassistant/components/dweet/* @fabaff
+homeassistant/components/dynalite/* @ziv1234
homeassistant/components/dyson/* @etheralm
homeassistant/components/ecobee/* @marthoc
homeassistant/components/ecovacs/* @OverloadUT
@@ -117,6 +119,7 @@ homeassistant/components/freebox/* @snoof85
homeassistant/components/fronius/* @nielstron
homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/garmin_connect/* @cyberjunky
+homeassistant/components/gdacs/* @exxamalte
homeassistant/components/gearbest/* @HerrHofrat
homeassistant/components/geniushub/* @zxdavb
homeassistant/components/geo_rss_events/* @exxamalte
@@ -184,14 +187,13 @@ homeassistant/components/kef/* @basnijholt
homeassistant/components/keyboard_remote/* @bendavid
homeassistant/components/knx/* @Julius2342
homeassistant/components/kodi/* @armills
-homeassistant/components/konnected/* @heythisisnate
+homeassistant/components/konnected/* @heythisisnate @kit-klein
homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus
homeassistant/components/lcn/* @alengwenus
homeassistant/components/life360/* @pnbruckner
homeassistant/components/linky/* @Quentame
homeassistant/components/linux_battery/* @fabaff
-homeassistant/components/liveboxplaytv/* @pschmitt
homeassistant/components/local_ip/* @issacg
homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
@@ -204,14 +206,16 @@ homeassistant/components/mastodon/* @fabaff
homeassistant/components/matrix/* @tinloaf
homeassistant/components/mcp23017/* @jardiamj
homeassistant/components/mediaroom/* @dgomes
+homeassistant/components/melcloud/* @vilppuvuorinen
homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen
-homeassistant/components/meteo_france/* @victorcerutti @oncleben31
+homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame
homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
homeassistant/components/mikrotik/* @engrbm87
homeassistant/components/mill/* @danielhiversen
homeassistant/components/min_max/* @fabaff
+homeassistant/components/minecraft_server/* @elmurato
homeassistant/components/minio/* @tkislan
homeassistant/components/mobile_app/* @robbiet480
homeassistant/components/modbus/* @adamchengtkc
@@ -275,7 +279,7 @@ homeassistant/components/quantum_gateway/* @cisasteelersfan
homeassistant/components/qwikswitch/* @kellerza
homeassistant/components/rainbird/* @konikvranik
homeassistant/components/raincloud/* @vanstinator
-homeassistant/components/rainforest_eagle/* @gtdiehl
+homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff
homeassistant/components/repetier/* @MTrab
@@ -285,6 +289,7 @@ homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roomba/* @pschmitt
homeassistant/components/safe_mode/* @home-assistant/core
homeassistant/components/saj/* @fredericvl
+homeassistant/components/salt/* @bjornorri
homeassistant/components/samsungtv/* @escoand
homeassistant/components/scene/* @home-assistant/core
homeassistant/components/scrape/* @fabaff
@@ -354,6 +359,7 @@ homeassistant/components/time_date/* @fabaff
homeassistant/components/tmb/* @alemuro
homeassistant/components/todoist/* @boralyl
homeassistant/components/toon/* @frenck
+homeassistant/components/totalconnect/* @austinmroczek
homeassistant/components/tplink/* @rytilahti
homeassistant/components/traccar/* @ludeeus
homeassistant/components/tradfri/* @ggravlingen
@@ -379,6 +385,7 @@ homeassistant/components/versasense/* @flamm3blemuff1n
homeassistant/components/version/* @fabaff
homeassistant/components/vesync/* @markperdue @webdjoe
homeassistant/components/vicare/* @oischinger
+homeassistant/components/vilfo/* @ManneW
homeassistant/components/vivotek/* @HarlemSquirrel
homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf
diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml
index 546b63950fe..4c6a353d775 100644
--- a/azure-pipelines-ci.yml
+++ b/azure-pipelines-ci.yml
@@ -43,7 +43,11 @@ stages:
. venv/bin/activate
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
- pre-commit install-hooks --config .pre-commit-config-all.yaml
+ pre-commit install-hooks
+ - script: |
+ . venv/bin/activate
+ pre-commit run codespell --all-files
+ displayName: 'Run codespell'
- script: |
. venv/bin/activate
pre-commit run flake8 --all-files
@@ -94,7 +98,7 @@ stages:
. venv/bin/activate
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
- pre-commit install-hooks --config .pre-commit-config-all.yaml
+ pre-commit install-hooks
- script: |
. venv/bin/activate
pre-commit run black --all-files --show-diff-on-failure
@@ -190,8 +194,8 @@ stages:
. venv/bin/activate
pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt
- pre-commit install-hooks --config .pre-commit-config-all.yaml
+ pre-commit install-hooks
- script: |
. venv/bin/activate
- pre-commit run --config .pre-commit-config-all.yaml mypy --all-files
+ pre-commit run mypy --all-files
displayName: 'Run mypy'
diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml
index 135057f2ae4..c98f12dfac6 100644
--- a/azure-pipelines-release.yml
+++ b/azure-pipelines-release.yml
@@ -163,7 +163,7 @@ stages:
git commit -am "Bump Home Assistant $version"
git push
- displayName: 'Update version files'
+ displayName: "Update version files"
- job: 'ReleaseDocker'
pool:
vmImage: 'ubuntu-latest'
diff --git a/.codecov.yml b/codecov.yml
similarity index 92%
rename from .codecov.yml
rename to codecov.yml
index be739b61809..1455c20749a 100644
--- a/.codecov.yml
+++ b/codecov.yml
@@ -13,4 +13,7 @@ coverage:
url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg="
comment:
require_changes: yes
- branches: master
+ layout: reach
+ branches:
+ - master
+ - !dev
diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py
index 9b3cf49fa22..710b4af1cd8 100644
--- a/homeassistant/auth/__init__.py
+++ b/homeassistant/auth/__init__.py
@@ -301,7 +301,7 @@ class AuthManager:
async def async_deactivate_user(self, user: models.User) -> None:
"""Deactivate a user."""
if user.is_owner:
- raise ValueError("Unable to deactive the owner")
+ raise ValueError("Unable to deactivate the owner")
await self._store.async_deactivate_user(user)
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py
index fd9e61b9d17..c2ec2260cf2 100644
--- a/homeassistant/auth/mfa_modules/__init__.py
+++ b/homeassistant/auth/mfa_modules/__init__.py
@@ -1,4 +1,4 @@
-"""Plugable auth modules for Home Assistant."""
+"""Pluggable auth modules for Home Assistant."""
import importlib
import logging
import types
diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py
index 46cc634bcae..8da81a44a61 100644
--- a/homeassistant/auth/mfa_modules/notify.py
+++ b/homeassistant/auth/mfa_modules/notify.py
@@ -317,7 +317,7 @@ class NotifySetupFlow(SetupFlow):
async def async_step_setup(
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
- """Verify user can recevie one-time password."""
+ """Verify user can receive one-time password."""
errors: Dict[str, str] = {}
hass = self._auth_module.hass
diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py
index 08f2f375b41..8b4e6355700 100644
--- a/homeassistant/auth/models.py
+++ b/homeassistant/auth/models.py
@@ -31,22 +31,28 @@ class User:
"""A user."""
name = attr.ib(type=Optional[str])
- perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, cmp=False)
+ perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, eq=False, order=False)
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False)
system_generated = attr.ib(type=bool, default=False)
- groups = attr.ib(type=List[Group], factory=list, cmp=False)
+ groups = attr.ib(type=List[Group], factory=list, eq=False, order=False)
# List of credentials of a user.
- credentials = attr.ib(type=List["Credentials"], factory=list, cmp=False)
+ credentials = attr.ib(type=List["Credentials"], factory=list, eq=False, order=False)
# Tokens associated with a user.
- refresh_tokens = attr.ib(type=Dict[str, "RefreshToken"], factory=dict, cmp=False)
+ refresh_tokens = attr.ib(
+ type=Dict[str, "RefreshToken"], factory=dict, eq=False, order=False
+ )
_permissions = attr.ib(
- type=Optional[perm_mdl.PolicyPermissions], init=False, cmp=False, default=None
+ type=Optional[perm_mdl.PolicyPermissions],
+ init=False,
+ eq=False,
+ order=False,
+ default=None,
)
@property
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 3d8523bf9ac..7d4155257db 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -1,23 +1,26 @@
"""Provide methods to bootstrap a Home Assistant instance."""
import asyncio
+import contextlib
import logging
import logging.handlers
import os
import sys
-from time import time
+from time import monotonic
from typing import Any, Dict, Optional, Set
+from async_timeout import timeout
import voluptuous as vol
from homeassistant import config as conf_util, config_entries, core, loader
from homeassistant.components import http
from homeassistant.const import (
EVENT_HOMEASSISTANT_CLOSE,
+ EVENT_HOMEASSISTANT_STOP,
REQUIRED_NEXT_PYTHON_DATE,
REQUIRED_NEXT_PYTHON_VER,
)
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.setup import async_setup_component
+from homeassistant.setup import DATA_SETUP, async_setup_component
from homeassistant.util.logging import AsyncHandler
from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
@@ -71,6 +74,7 @@ async def async_setup_hass(
_LOGGER.info("Config directory: %s", config_dir)
config_dict = None
+ basic_setup_success = False
if not safe_mode:
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
@@ -79,19 +83,45 @@ async def async_setup_hass(
config_dict = await conf_util.async_hass_config_yaml(hass)
except HomeAssistantError as err:
_LOGGER.error(
- "Failed to parse configuration.yaml: %s. Falling back to safe mode",
- err,
+ "Failed to parse configuration.yaml: %s. Activating safe mode", err,
)
else:
if not is_virtual_env():
await async_mount_local_lib_path(config_dir)
- await async_from_config_dict(config_dict, hass)
+ basic_setup_success = (
+ await async_from_config_dict(config_dict, hass) is not None
+ )
finally:
clear_secret_cache()
- if safe_mode or config_dict is None:
+ if config_dict is None:
+ safe_mode = True
+
+ elif not basic_setup_success:
+ _LOGGER.warning("Unable to set up core integrations. Activating safe mode")
+ safe_mode = True
+
+ elif (
+ "frontend" in hass.data.get(DATA_SETUP, {})
+ and "frontend" not in hass.config.components
+ ):
+ _LOGGER.warning("Detected that frontend did not load. Activating safe mode")
+ # Ask integrations to shut down. It's messy but we can't
+ # do a clean stop without knowing what is broken
+ hass.async_track_tasks()
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
+ with contextlib.suppress(asyncio.TimeoutError):
+ async with timeout(10):
+ await hass.async_block_till_done()
+
+ safe_mode = True
+ hass = core.HomeAssistant()
+ hass.config.config_dir = config_dir
+
+ if safe_mode:
_LOGGER.info("Starting in safe mode")
+ hass.config.safe_mode = True
http_conf = (await http.async_get_last_config(hass)) or {}
@@ -110,7 +140,26 @@ async def async_from_config_dict(
Dynamically loads required components and its dependencies.
This method is a coroutine.
"""
- start = time()
+ start = monotonic()
+
+ hass.config_entries = config_entries.ConfigEntries(hass, config)
+ await hass.config_entries.async_initialize()
+
+ # Set up core.
+ _LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
+
+ if not all(
+ await asyncio.gather(
+ *(
+ async_setup_component(hass, domain, config)
+ for domain in CORE_INTEGRATIONS
+ )
+ )
+ ):
+ _LOGGER.error("Home Assistant core failed to initialize. ")
+ return None
+
+ _LOGGER.debug("Home Assistant core initialized")
core_config = config.get(core.DOMAIN, {})
@@ -126,12 +175,9 @@ async def async_from_config_dict(
)
return None
- hass.config_entries = config_entries.ConfigEntries(hass, config)
- await hass.config_entries.async_initialize()
-
await _async_set_up_integrations(hass, config)
- stop = time()
+ stop = monotonic()
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER:
@@ -193,7 +239,7 @@ def async_enable_logging(
pass
# If the above initialization failed for any reason, setup the default
- # formatting. If the above succeeds, this wil result in a no-op.
+ # formatting. If the above succeeds, this will result in a no-op.
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
# Suppress overly verbose logs from libraries that aren't helpful
@@ -264,7 +310,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
- if "safe_mode" not in config:
+ if not hass.config.safe_mode:
domains.update(hass.config_entries.async_domains())
# Make sure the Hass.io component is loaded
@@ -296,25 +342,6 @@ async def _async_set_up_integrations(
return_exceptions=True,
)
- # Set up core.
- _LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
-
- if not all(
- await asyncio.gather(
- *(
- async_setup_component(hass, domain, config)
- for domain in CORE_INTEGRATIONS
- )
- )
- ):
- _LOGGER.error(
- "Home Assistant core failed to initialize. "
- "Further initialization aborted"
- )
- return
-
- _LOGGER.debug("Home Assistant core initialized")
-
# Finish resolving domains
for dep_domains in await resolved_domains_task:
# Result is either a set or an exception. We ignore exceptions
diff --git a/homeassistant/components/abode/.translations/es-419.json b/homeassistant/components/abode/.translations/es-419.json
new file mode 100644
index 00000000000..f2def50d063
--- /dev/null
+++ b/homeassistant/components/abode/.translations/es-419.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": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/hu.json b/homeassistant/components/abode/.translations/hu.json
new file mode 100644
index 00000000000..385334c8549
--- /dev/null
+++ b/homeassistant/components/abode/.translations/hu.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett."
+ },
+ "error": {
+ "connection_error": "Nem lehet csatlakozni az Abode-hez.",
+ "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van",
+ "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Email c\u00edm"
+ },
+ "title": "T\u00f6ltse ki az Abode bejelentkez\u00e9si adatait"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/.translations/pl.json b/homeassistant/components/abode/.translations/pl.json
index c3f3b8f2c88..d086aaca395 100644
--- a/homeassistant/components/abode/.translations/pl.json
+++ b/homeassistant/components/abode/.translations/pl.json
@@ -5,7 +5,7 @@
},
"error": {
"connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.",
- "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane",
+ "identifier_exists": "Konto jest ju\u017c zarejestrowane.",
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce"
},
"step": {
diff --git a/homeassistant/components/abode/.translations/sv.json b/homeassistant/components/abode/.translations/sv.json
new file mode 100644
index 00000000000..9a59e4c2007
--- /dev/null
+++ b/homeassistant/components/abode/.translations/sv.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten."
+ },
+ "error": {
+ "connection_error": "Det gick inte att ansluta till Abode.",
+ "identifier_exists": "Kontot \u00e4r redan registrerat.",
+ "invalid_credentials": "Ogiltiga autentiseringsuppgifter."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "L\u00f6senord",
+ "username": "E-postadress"
+ },
+ "title": "Fyll i din inloggningsinformation f\u00f6r Abode"
+ }
+ },
+ "title": "Abode"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py
index bbe3f01f488..d6773e10ca1 100644
--- a/homeassistant/components/abode/switch.py
+++ b/homeassistant/components/abode/switch.py
@@ -11,6 +11,8 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
+DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE]
+
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Abode switch devices."""
@@ -18,8 +20,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities = []
- for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH):
- entities.append(AbodeSwitch(data, device))
+ for device_type in DEVICE_TYPES:
+ for device in data.abode.get_devices(generic_type=device_type):
+ entities.append(AbodeSwitch(data, device))
for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION):
entities.append(
diff --git a/homeassistant/components/adguard/.translations/es-419.json b/homeassistant/components/adguard/.translations/es-419.json
index ed8e0c3a358..eb3274f19b6 100644
--- a/homeassistant/components/adguard/.translations/es-419.json
+++ b/homeassistant/components/adguard/.translations/es-419.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}. Actualice su complemento Hass.io AdGuard Home.",
+ "adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}.",
"existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.",
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
},
diff --git a/homeassistant/components/adguard/.translations/sv.json b/homeassistant/components/adguard/.translations/sv.json
index 22bd81e3e97..519ecef52db 100644
--- a/homeassistant/components/adguard/.translations/sv.json
+++ b/homeassistant/components/adguard/.translations/sv.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "adguard_home_addon_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}. Uppdatera ditt Hass.io AdGuard Home-till\u00e4gg.",
+ "adguard_home_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}.",
"existing_instance_updated": "Uppdaterade existerande konfiguration.",
"single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten."
},
diff --git a/homeassistant/components/airly/.translations/es-419.json b/homeassistant/components/airly/.translations/es-419.json
new file mode 100644
index 00000000000..74924493863
--- /dev/null
+++ b/homeassistant/components/airly/.translations/es-419.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "error": {
+ "auth": "La clave API no es correcta.",
+ "name_exists": "El nombre ya existe.",
+ "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clave API de Airly",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Nombre de la integraci\u00f3n"
+ },
+ "description": "Configure la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave API, vaya a https://developer.airly.eu/register",
+ "title": "Airly"
+ }
+ },
+ "title": "Airly"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airly/.translations/hu.json b/homeassistant/components/airly/.translations/hu.json
new file mode 100644
index 00000000000..30898c61abb
--- /dev/null
+++ b/homeassistant/components/airly/.translations/hu.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ezen koordin\u00e1t\u00e1k Airly integr\u00e1ci\u00f3ja m\u00e1r konfigur\u00e1lva van."
+ },
+ "error": {
+ "auth": "Az API kulcs nem megfelel\u0151.",
+ "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik",
+ "wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Airly API kulcs",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g",
+ "name": "Az integr\u00e1ci\u00f3 neve"
+ },
+ "description": "Az Airly leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Api-kulcs l\u00e9trehoz\u00e1s\u00e1hoz nyissa meg a k\u00f6vetkez\u0151 weboldalt: https://developer.airly.eu/register",
+ "title": "Airly"
+ }
+ },
+ "title": "Airly"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airly/.translations/nl.json b/homeassistant/components/airly/.translations/nl.json
index 232d5d54d85..a9c6865ad91 100644
--- a/homeassistant/components/airly/.translations/nl.json
+++ b/homeassistant/components/airly/.translations/nl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Airly-integratie voor deze co\u00f6rdinaten is al geconfigureerd."
+ },
"error": {
"auth": "API-sleutel is niet correct.",
"name_exists": "Naam bestaat al.",
diff --git a/homeassistant/components/airly/.translations/sl.json b/homeassistant/components/airly/.translations/sl.json
index 08f57d88bcb..f8ca4e5b6d5 100644
--- a/homeassistant/components/airly/.translations/sl.json
+++ b/homeassistant/components/airly/.translations/sl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Airly integracija za te koordinate je \u017ee nastavljen."
+ },
"error": {
"auth": "Klju\u010d API ni pravilen.",
"name_exists": "Ime \u017ee obstaja",
diff --git a/homeassistant/components/airly/.translations/sv.json b/homeassistant/components/airly/.translations/sv.json
new file mode 100644
index 00000000000..5b81b4625a2
--- /dev/null
+++ b/homeassistant/components/airly/.translations/sv.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Airly-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad."
+ },
+ "error": {
+ "auth": "API-nyckeln \u00e4r inte korrekt.",
+ "name_exists": "Namnet finns redan.",
+ "wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Airly API-nyckel",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Integrationens namn"
+ },
+ "description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register",
+ "title": "Airly"
+ }
+ },
+ "title": "Airly"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/.translations/sv.json b/homeassistant/components/alarm_control_panel/.translations/sv.json
new file mode 100644
index 00000000000..65e4433f5a3
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/.translations/sv.json
@@ -0,0 +1,18 @@
+{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Larma {entity_name} borta",
+ "arm_home": "Larma {entity_name} hemma",
+ "arm_night": "Larma {entity_name} natt",
+ "disarm": "Avlarma {entity_name}",
+ "trigger": "Utl\u00f6sare {entity_name}"
+ },
+ "trigger_type": {
+ "armed_away": "{entity_name} larmad borta",
+ "armed_home": "{entity_name} larmad hemma",
+ "armed_night": "{entity_name} larmad natt",
+ "disarmed": "{entity_name} bortkopplad",
+ "triggered": "{entity_name} utl\u00f6st"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py
index dc3f16b7d22..13a7913e190 100644
--- a/homeassistant/components/alarmdecoder/binary_sensor.py
+++ b/homeassistant/components/alarmdecoder/binary_sensor.py
@@ -138,7 +138,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
def _restore_callback(self, zone):
"""Update the zone's state, if needed."""
- if zone is None or int(zone) == self._zone_number:
+ if zone is None or (int(zone) == self._zone_number and not self._loop):
self._state = 0
self.schedule_update_ha_state()
diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py
index eb1474aed7e..94cf41d530b 100644
--- a/homeassistant/components/alexa/capabilities.py
+++ b/homeassistant/components/alexa/capabilities.py
@@ -1,5 +1,6 @@
"""Alexa capabilities."""
import logging
+import math
from homeassistant.components import (
cover,
@@ -645,6 +646,43 @@ class AlexaSpeaker(AlexaCapability):
"""Return the Alexa API name of this interface."""
return "Alexa.Speaker"
+ def properties_supported(self):
+ """Return what properties this entity supports."""
+ properties = [{"name": "volume"}]
+
+ supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ if supported & media_player.SUPPORT_VOLUME_MUTE:
+ properties.append({"name": "muted"})
+
+ return properties
+
+ 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 == "volume":
+ current_level = self.entity.attributes.get(
+ media_player.ATTR_MEDIA_VOLUME_LEVEL
+ )
+ try:
+ current = math.floor(int(current_level * 100))
+ except ZeroDivisionError:
+ current = 0
+ return current
+
+ if name == "muted":
+ return bool(
+ self.entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED)
+ )
+
+ return None
+
class AlexaStepSpeaker(AlexaCapability):
"""Implements Alexa.StepSpeaker.
@@ -711,6 +749,13 @@ class AlexaInputController(AlexaCapability):
source_list = self.entity.attributes.get(
media_player.ATTR_INPUT_SOURCE_LIST, []
)
+ input_list = AlexaInputController.get_valid_inputs(source_list)
+
+ return input_list
+
+ @staticmethod
+ def get_valid_inputs(source_list):
+ """Return list of supported inputs."""
input_list = []
for source in source_list:
formatted_source = (
diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py
index 254cec44553..b10f11e2bbc 100644
--- a/homeassistant/components/alexa/entities.py
+++ b/homeassistant/components/alexa/entities.py
@@ -508,12 +508,7 @@ class MediaPlayerCapabilities(AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & media_player.const.SUPPORT_VOLUME_SET:
yield AlexaSpeaker(self.entity)
-
- step_volume_features = (
- media_player.const.SUPPORT_VOLUME_MUTE
- | media_player.const.SUPPORT_VOLUME_STEP
- )
- if supported & step_volume_features:
+ elif supported & media_player.const.SUPPORT_VOLUME_STEP:
yield AlexaStepSpeaker(self.entity)
playback_features = (
@@ -531,7 +526,13 @@ class MediaPlayerCapabilities(AlexaEntity):
yield AlexaSeekController(self.entity)
if supported & media_player.SUPPORT_SELECT_SOURCE:
- yield AlexaInputController(self.entity)
+ inputs = AlexaInputController.get_valid_inputs(
+ self.entity.attributes.get(
+ media_player.const.ATTR_INPUT_SOURCE_LIST, []
+ )
+ )
+ if len(inputs) > 0:
+ yield AlexaInputController(self.entity)
if supported & media_player.const.SUPPORT_PLAY_MEDIA:
yield AlexaChannelController(self.entity)
diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py
index cb78f269f8f..4dd154ea11f 100644
--- a/homeassistant/components/alexa/messages.py
+++ b/homeassistant/components/alexa/messages.py
@@ -43,7 +43,7 @@ class AlexaDirective:
Behavior when self.has_endpoint is False is undefined.
Will raise AlexaInvalidEndpointError if the endpoint in the request is
- malformed or nonexistant.
+ malformed or nonexistent.
"""
_endpoint_id = self._directive[API_ENDPOINT]["endpointId"]
self.entity_id = _endpoint_id.replace("#", ".")
diff --git a/homeassistant/components/almond/.translations/hu.json b/homeassistant/components/almond/.translations/hu.json
new file mode 100644
index 00000000000..2331e57c6eb
--- /dev/null
+++ b/homeassistant/components/almond/.translations/hu.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Csak egy Almond fi\u00f3kot konfigur\u00e1lhat.",
+ "cannot_connect": "Nem lehet csatlakozni az Almond szerverhez.",
+ "missing_configuration": "K\u00e9rj\u00fck, ellen\u0151rizze az Almond be\u00e1ll\u00edt\u00e1s\u00e1nak dokument\u00e1ci\u00f3j\u00e1t."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Hass.io kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?",
+ "title": "Almond a Hass.io kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl"
+ },
+ "pick_implementation": {
+ "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
+ }
+ },
+ "title": "Almond"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/almond/.translations/nl.json b/homeassistant/components/almond/.translations/nl.json
index d77fe69f7fa..939a9a904ad 100644
--- a/homeassistant/components/almond/.translations/nl.json
+++ b/homeassistant/components/almond/.translations/nl.json
@@ -6,6 +6,10 @@
"missing_configuration": "Raadpleeg de documentatie over het instellen van Almond."
},
"step": {
+ "hassio_confirm": {
+ "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de hass.io add-on {addon} ?",
+ "title": "Almond via Hass.io add-on"
+ },
"pick_implementation": {
"title": "Kies de authenticatie methode"
}
diff --git a/homeassistant/components/almond/.translations/sl.json b/homeassistant/components/almond/.translations/sl.json
index 086190590ac..4a593cc5605 100644
--- a/homeassistant/components/almond/.translations/sl.json
+++ b/homeassistant/components/almond/.translations/sl.json
@@ -6,6 +6,10 @@
"missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond."
},
"step": {
+ "hassio_confirm": {
+ "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Hass.io: {addon} ?",
+ "title": "Almond prek dodatka Hass.io"
+ },
"pick_implementation": {
"title": "Izberite na\u010din preverjanja pristnosti"
}
diff --git a/homeassistant/components/almond/.translations/sv.json b/homeassistant/components/almond/.translations/sv.json
index 61af3a04e47..d2630b95c02 100644
--- a/homeassistant/components/almond/.translations/sv.json
+++ b/homeassistant/components/almond/.translations/sv.json
@@ -1,9 +1,19 @@
{
"config": {
+ "abort": {
+ "already_setup": "Du kan bara konfigurera ett Almond-konto.",
+ "cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.",
+ "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond."
+ },
"step": {
"hassio_confirm": {
+ "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Hass.io-till\u00e4gget: {addon} ?",
"title": "Almond via Hass.io-till\u00e4gget"
+ },
+ "pick_implementation": {
+ "title": "V\u00e4lj autentiseringsmetod"
}
- }
+ },
+ "title": "Almond"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json
index 33348c9d7b3..c7220d8e059 100644
--- a/homeassistant/components/alpha_vantage/manifest.json
+++ b/homeassistant/components/alpha_vantage/manifest.json
@@ -2,7 +2,7 @@
"domain": "alpha_vantage",
"name": "Alpha Vantage",
"documentation": "https://www.home-assistant.io/integrations/alpha_vantage",
- "requirements": ["alpha_vantage==2.1.2"],
+ "requirements": ["alpha_vantage==2.1.3"],
"dependencies": [],
"codeowners": ["@fabaff"]
}
diff --git a/homeassistant/components/ambient_station/.translations/ca.json b/homeassistant/components/ambient_station/.translations/ca.json
index d3c451f3e3f..280a90354b0 100644
--- a/homeassistant/components/ambient_station/.translations/ca.json
+++ b/homeassistant/components/ambient_station/.translations/ca.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Aquesta clau d'aplicaci\u00f3 ja est\u00e0 en \u00fas."
+ },
"error": {
"identifier_exists": "Clau d'aplicaci\u00f3 i/o clau API ja registrada",
"invalid_key": "Clau API i/o clau d'aplicaci\u00f3 inv\u00e0lida/es",
diff --git a/homeassistant/components/ambient_station/.translations/de.json b/homeassistant/components/ambient_station/.translations/de.json
index 1431efbf167..451a2e70e68 100644
--- a/homeassistant/components/ambient_station/.translations/de.json
+++ b/homeassistant/components/ambient_station/.translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Dieser App-Schl\u00fcssel wird bereits verwendet."
+ },
"error": {
"identifier_exists": "Anwendungsschl\u00fcssel und / oder API-Schl\u00fcssel bereits registriert",
"invalid_key": "Ung\u00fcltiger API Key und / oder Anwendungsschl\u00fcssel",
diff --git a/homeassistant/components/ambient_station/.translations/en.json b/homeassistant/components/ambient_station/.translations/en.json
index 5bd643da55c..8b8e71d5316 100644
--- a/homeassistant/components/ambient_station/.translations/en.json
+++ b/homeassistant/components/ambient_station/.translations/en.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "This app key is already in use."
+ },
"error": {
"identifier_exists": "Application Key and/or API Key already registered",
"invalid_key": "Invalid API Key and/or Application Key",
diff --git a/homeassistant/components/ambient_station/.translations/ko.json b/homeassistant/components/ambient_station/.translations/ko.json
index eb9209a6c37..3379411678b 100644
--- a/homeassistant/components/ambient_station/.translations/ko.json
+++ b/homeassistant/components/ambient_station/.translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uc774 \uc571 \ud0a4\ub294 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4."
+ },
"error": {
"identifier_exists": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
diff --git a/homeassistant/components/ambient_station/.translations/no.json b/homeassistant/components/ambient_station/.translations/no.json
index 0b9d377718b..4a089eba4c0 100644
--- a/homeassistant/components/ambient_station/.translations/no.json
+++ b/homeassistant/components/ambient_station/.translations/no.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Denne app n\u00f8kkelen er allerede i bruk."
+ },
"error": {
"identifier_exists": "Programn\u00f8kkel og/eller API-n\u00f8kkel er allerede registrert",
"invalid_key": "Ugyldig API-n\u00f8kkel og/eller programn\u00f8kkel",
diff --git a/homeassistant/components/ambient_station/.translations/pl.json b/homeassistant/components/ambient_station/.translations/pl.json
index 6ebd0848a63..5da886f05cd 100644
--- a/homeassistant/components/ambient_station/.translations/pl.json
+++ b/homeassistant/components/ambient_station/.translations/pl.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ten klucz aplikacji jest ju\u017c w u\u017cyciu."
+ },
"error": {
- "identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany",
+ "identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany.",
"invalid_key": "Nieprawid\u0142owy klucz API i/lub klucz aplikacji",
"no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie"
},
diff --git a/homeassistant/components/ambient_station/.translations/zh-Hant.json b/homeassistant/components/ambient_station/.translations/zh-Hant.json
index 6c7c88a8045..6de1579f6ff 100644
--- a/homeassistant/components/ambient_station/.translations/zh-Hant.json
+++ b/homeassistant/components/ambient_station/.translations/zh-Hant.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u6b64\u61c9\u7528\u7a0b\u5f0f\u5bc6\u9470\u5df2\u88ab\u4f7f\u7528\u3002"
+ },
"error": {
"identifier_exists": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u5df2\u8a3b\u518a",
"invalid_key": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u7121\u6548",
diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py
index c61e15dfeb5..0bbb7a760fe 100644
--- a/homeassistant/components/ambient_station/__init__.py
+++ b/homeassistant/components/ambient_station/__init__.py
@@ -378,7 +378,7 @@ class AmbientStation:
if data != self.stations[mac_address][ATTR_LAST_DATA]:
_LOGGER.debug("New data received: %s", data)
self.stations[mac_address][ATTR_LAST_DATA] = data
- async_dispatcher_send(self._hass, TOPIC_UPDATE)
+ async_dispatcher_send(self._hass, TOPIC_UPDATE.format(mac_address))
_LOGGER.debug("Resetting watchdog")
self._watchdog_listener()
@@ -518,7 +518,7 @@ class AmbientWeatherEntity(Entity):
self.async_schedule_update_ha_state(True)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
- self.hass, TOPIC_UPDATE, update
+ self.hass, TOPIC_UPDATE.format(self._mac_address), update
)
async def async_will_remove_from_hass(self):
diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py
index 21a6e514b30..4f94e1cfe88 100644
--- a/homeassistant/components/ambient_station/const.py
+++ b/homeassistant/components/ambient_station/const.py
@@ -8,7 +8,7 @@ CONF_APP_KEY = "app_key"
DATA_CLIENT = "data_client"
-TOPIC_UPDATE = "update"
+TOPIC_UPDATE = "ambient_station_data_update_{0}"
TYPE_BINARY_SENSOR = "binary_sensor"
TYPE_SENSOR = "sensor"
diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py
index f7814939e3a..5578d350e22 100644
--- a/homeassistant/components/amcrest/__init__.py
+++ b/homeassistant/components/amcrest/__init__.py
@@ -23,6 +23,7 @@ from homeassistant.const import (
CONF_SENSORS,
CONF_USERNAME,
ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
HTTP_BASIC_AUTHENTICATION,
)
from homeassistant.exceptions import Unauthorized, UnknownUser
@@ -34,7 +35,15 @@ from homeassistant.helpers.service import async_extract_entity_ids
from .binary_sensor import BINARY_SENSORS
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
-from .const import CAMERAS, DATA_AMCREST, DEVICES, DOMAIN, SERVICE_UPDATE
+from .const import (
+ CAMERAS,
+ COMM_RETRIES,
+ COMM_TIMEOUT,
+ DATA_AMCREST,
+ DEVICES,
+ DOMAIN,
+ SERVICE_UPDATE,
+)
from .helpers import service_signal
from .sensor import SENSORS
@@ -110,38 +119,56 @@ class AmcrestChecker(Http):
self._wrap_name = name
self._wrap_errors = 0
self._wrap_lock = threading.Lock()
+ self._wrap_login_err = False
self._unsub_recheck = None
super().__init__(
- host, port, user, password, retries_connection=1, timeout_protocol=3.05
+ host,
+ port,
+ user,
+ password,
+ retries_connection=COMM_RETRIES,
+ timeout_protocol=COMM_TIMEOUT,
)
@property
def available(self):
"""Return if camera's API is responding."""
- return self._wrap_errors <= MAX_ERRORS
+ return self._wrap_errors <= MAX_ERRORS and not self._wrap_login_err
+
+ def _start_recovery(self):
+ dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name))
+ self._unsub_recheck = track_time_interval(
+ self._hass, self._wrap_test_online, RECHECK_INTERVAL
+ )
def command(self, cmd, retries=None, timeout_cmd=None, stream=False):
"""amcrest.Http.command wrapper to catch errors."""
try:
ret = super().command(cmd, retries, timeout_cmd, stream)
+ except LoginError as ex:
+ with self._wrap_lock:
+ was_online = self.available
+ was_login_err = self._wrap_login_err
+ self._wrap_login_err = True
+ if not was_login_err:
+ _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex)
+ if was_online:
+ self._start_recovery()
+ raise
except AmcrestError:
with self._wrap_lock:
was_online = self.available
- self._wrap_errors += 1
- _LOGGER.debug("%s camera errs: %i", self._wrap_name, self._wrap_errors)
+ errs = self._wrap_errors = self._wrap_errors + 1
offline = not self.available
- if offline and was_online:
+ _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs)
+ if was_online and offline:
_LOGGER.error("%s camera offline: Too many errors", self._wrap_name)
- dispatcher_send(
- self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)
- )
- self._unsub_recheck = track_time_interval(
- self._hass, self._wrap_test_online, RECHECK_INTERVAL
- )
+ self._start_recovery()
raise
with self._wrap_lock:
was_offline = not self.available
self._wrap_errors = 0
+ self._wrap_login_err = False
if was_offline:
self._unsub_recheck()
self._unsub_recheck = None
@@ -151,6 +178,7 @@ class AmcrestChecker(Http):
def _wrap_test_online(self, now):
"""Test if camera is back online."""
+ _LOGGER.debug("Testing if %s back online", self._wrap_name)
try:
self.current_time
except AmcrestError:
@@ -166,14 +194,9 @@ def setup(hass, config):
username = device[CONF_USERNAME]
password = device[CONF_PASSWORD]
- try:
- api = AmcrestChecker(
- hass, name, device[CONF_HOST], device[CONF_PORT], username, password
- )
-
- except LoginError as ex:
- _LOGGER.error("Login error for %s camera: %s", name, ex)
- continue
+ api = AmcrestChecker(
+ hass, name, device[CONF_HOST], device[CONF_PORT], username, password
+ )
ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS]
resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]]
@@ -236,6 +259,9 @@ def setup(hass, config):
if have_permission(user, entity_id)
]
+ if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
+ return []
+
call_ids = await async_extract_entity_ids(hass, call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py
index ac16f0664aa..a99901f54a3 100644
--- a/homeassistant/components/amcrest/binary_sensor.py
+++ b/homeassistant/components/amcrest/binary_sensor.py
@@ -1,4 +1,4 @@
-"""Suppoort for Amcrest IP camera binary sensors."""
+"""Support for Amcrest IP camera binary sensors."""
from datetime import timedelta
import logging
diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py
index e9e1e2b5f84..0e64d4fefc9 100644
--- a/homeassistant/components/amcrest/camera.py
+++ b/homeassistant/components/amcrest/camera.py
@@ -1,11 +1,11 @@
"""Support for Amcrest IP cameras."""
import asyncio
from datetime import timedelta
+from functools import partial
import logging
from amcrest import AmcrestError
from haffmpeg.camera import CameraMjpeg
-from urllib3.exceptions import HTTPError
import voluptuous as vol
from homeassistant.components.camera import (
@@ -26,9 +26,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
CAMERA_WEB_SESSION_TIMEOUT,
CAMERAS,
+ COMM_TIMEOUT,
DATA_AMCREST,
DEVICES,
SERVICE_UPDATE,
+ SNAPSHOT_TIMEOUT,
)
from .helpers import log_update_error, service_signal
@@ -90,6 +92,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
+class CannotSnapshot(Exception):
+ """Conditions are not valid for taking a snapshot."""
+
+
class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera."""
@@ -112,28 +118,58 @@ class AmcrestCam(Camera):
self._motion_recording_enabled = None
self._color_bw = None
self._rtsp_url = None
- self._snapshot_lock = asyncio.Lock()
+ self._snapshot_task = None
self._unsub_dispatcher = []
self._update_succeeded = False
- async def async_camera_image(self):
- """Return a still image response from the camera."""
+ def _check_snapshot_ok(self):
available = self.available
if not available or not self.is_on:
_LOGGER.warning(
- "Attempt to take snaphot when %s camera is %s",
+ "Attempt to take snapshot when %s camera is %s",
self.name,
"offline" if not available else "off",
)
+ raise CannotSnapshot
+
+ async def _async_get_image(self):
+ try:
+ # Send the request to snap a picture and return raw jpg data
+ # Snapshot command needs a much longer read timeout than other commands.
+ return await self.hass.async_add_executor_job(
+ partial(
+ self._api.snapshot,
+ timeout=(COMM_TIMEOUT, SNAPSHOT_TIMEOUT),
+ stream=False,
+ )
+ )
+ except AmcrestError as error:
+ log_update_error(_LOGGER, "get image from", self.name, "camera", error)
+ return None
+ finally:
+ self._snapshot_task = None
+
+ async def async_camera_image(self):
+ """Return a still image response from the camera."""
+ _LOGGER.debug("Take snapshot from %s", self._name)
+ try:
+ # Amcrest cameras only support one snapshot command at a time.
+ # Hence need to wait if a previous snapshot has not yet finished.
+ # Also need to check that camera is online and turned on before each wait
+ # and before initiating shapshot.
+ while self._snapshot_task:
+ self._check_snapshot_ok()
+ _LOGGER.debug("Waiting for previous snapshot from %s ...", self._name)
+ await self._snapshot_task
+ self._check_snapshot_ok()
+ # Run snapshot command in separate Task that can't be cancelled so
+ # 1) it's not possible to send another snapshot command while camera is
+ # still working on a previous one, and
+ # 2) someone will be around to catch any exceptions.
+ self._snapshot_task = self.hass.async_create_task(self._async_get_image())
+ return await asyncio.shield(self._snapshot_task)
+ except CannotSnapshot:
return None
- async with self._snapshot_lock:
- try:
- # Send the request to snap a picture and return raw jpg data
- response = await self.hass.async_add_executor_job(self._api.snapshot)
- return response.data
- except (AmcrestError, HTTPError) as error:
- log_update_error(_LOGGER, "get image from", self.name, "camera", error)
- return None
async def handle_async_mjpeg_stream(self, request):
"""Return an MJPEG stream."""
diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py
index 98d613634b5..38ff8a8894e 100644
--- a/homeassistant/components/amcrest/const.py
+++ b/homeassistant/components/amcrest/const.py
@@ -6,6 +6,9 @@ DEVICES = "devices"
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
CAMERA_WEB_SESSION_TIMEOUT = 10
+COMM_RETRIES = 1
+COMM_TIMEOUT = 6.05
SENSOR_SCAN_INTERVAL_SECS = 10
+SNAPSHOT_TIMEOUT = 20
SERVICE_UPDATE = "update"
diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json
index 8b2d72effa6..38e19e4ec26 100644
--- a/homeassistant/components/amcrest/manifest.json
+++ b/homeassistant/components/amcrest/manifest.json
@@ -2,7 +2,7 @@
"domain": "amcrest",
"name": "Amcrest",
"documentation": "https://www.home-assistant.io/integrations/amcrest",
- "requirements": ["amcrest==1.5.3"],
+ "requirements": ["amcrest==1.5.6"],
"dependencies": ["ffmpeg"],
"codeowners": ["@pnbruckner"]
}
diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py
index be03b3bedff..04436cd95ab 100644
--- a/homeassistant/components/amcrest/sensor.py
+++ b/homeassistant/components/amcrest/sensor.py
@@ -1,4 +1,4 @@
-"""Suppoort for Amcrest IP camera sensors."""
+"""Support for Amcrest IP camera sensors."""
from datetime import timedelta
import logging
diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json
index a8a5506fc0a..5908523e6d8 100644
--- a/homeassistant/components/apcupsd/manifest.json
+++ b/homeassistant/components/apcupsd/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "apcupsd",
- "name": "APCUPSd",
+ "name": "apcupsd",
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
"requirements": ["apcaccess==0.0.13"],
"dependencies": [],
diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py
index febe344a9c4..3cd43ee36ae 100644
--- a/homeassistant/components/apns/notify.py
+++ b/homeassistant/components/apns/notify.py
@@ -177,7 +177,7 @@ class ApnsNotificationService(BaseNotificationService):
def device_state_changed_listener(self, entity_id, from_s, to_s):
"""
- Listen for sate change.
+ Listen for state change.
Track device state change if a device has a tracking id specified.
"""
diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json
index 2f37d941ac8..8ca42beab61 100644
--- a/homeassistant/components/apple_tv/manifest.json
+++ b/homeassistant/components/apple_tv/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"requirements": ["pyatv==0.3.13"],
"dependencies": ["configurator"],
+ "after_dependencies": ["discovery"],
"codeowners": []
}
diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json
index 1f41d5a24e2..0895c2af1f9 100644
--- a/homeassistant/components/apprise/manifest.json
+++ b/homeassistant/components/apprise/manifest.json
@@ -2,7 +2,7 @@
"domain": "apprise",
"name": "Apprise",
"documentation": "https://www.home-assistant.io/integrations/apprise",
- "requirements": ["apprise==0.8.3"],
+ "requirements": ["apprise==0.8.4"],
"dependencies": [],
"codeowners": ["@caronc"]
}
diff --git a/homeassistant/components/arcam_fmj/.translations/sv.json b/homeassistant/components/arcam_fmj/.translations/sv.json
new file mode 100644
index 00000000000..b0ad4660d0f
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/.translations/sv.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Arcam FMJ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py
index 838f319abc1..49a1bced577 100644
--- a/homeassistant/components/arlo/alarm_control_panel.py
+++ b/homeassistant/components/arlo/alarm_control_panel.py
@@ -110,19 +110,19 @@ class ArloBaseStation(AlarmControlPanel):
else:
self._state = None
- async def async_alarm_disarm(self, code=None):
+ def alarm_disarm(self, code=None):
"""Send disarm command."""
self._base_station.mode = DISARMED
- async def async_alarm_arm_away(self, code=None):
+ def alarm_arm_away(self, code=None):
"""Send arm away command. Uses custom mode."""
self._base_station.mode = self._away_mode_name
- async def async_alarm_arm_home(self, code=None):
+ def alarm_arm_home(self, code=None):
"""Send arm home command. Uses custom mode."""
self._base_station.mode = self._home_mode_name
- async def async_alarm_arm_night(self, code=None):
+ def alarm_arm_night(self, code=None):
"""Send arm night command. Uses custom mode."""
self._base_station.mode = self._night_mode_name
diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py
index 958c383765a..22b1bfbe810 100644
--- a/homeassistant/components/arlo/camera.py
+++ b/homeassistant/components/arlo/camera.py
@@ -78,8 +78,10 @@ class ArloCam(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
+ video = await self.hass.async_add_executor_job(
+ getattr, self._camera, "last_video"
+ )
- video = self._camera.last_video
if not video:
error_msg = "Video not found for {0}. Is it older than {1} days?".format(
self.name, self._camera.min_days_vdo_cache
diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py
index 64d2d7c7a4b..897258c6299 100644
--- a/homeassistant/components/asuswrt/__init__.py
+++ b/homeassistant/components/asuswrt/__init__.py
@@ -70,7 +70,7 @@ async def async_setup(hass, config):
await api.connection.async_connect()
if not api.is_connected:
- _LOGGER.error("Unable to setup asuswrt component")
+ _LOGGER.error("Unable to setup component")
return False
hass.data[DATA_ASUSWRT] = api
diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json
index 02999ada68b..416144b450c 100644
--- a/homeassistant/components/asuswrt/manifest.json
+++ b/homeassistant/components/asuswrt/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "asuswrt",
- "name": "Asuswrt",
+ "name": "ASUSWRT",
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
"requirements": ["aioasuswrt==1.1.22"],
"dependencies": [],
diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py
index b5ce8539f44..50100d3625d 100644
--- a/homeassistant/components/asuswrt/sensor.py
+++ b/homeassistant/components/asuswrt/sensor.py
@@ -1,6 +1,7 @@
"""Asuswrt status sensors."""
import logging
+from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND
from homeassistant.helpers.entity import Entity
from . import DATA_ASUSWRT
@@ -61,7 +62,7 @@ class AsuswrtRXSensor(AsuswrtSensor):
"""Representation of a asuswrt download speed sensor."""
_name = "Asuswrt Download Speed"
- _unit = "Mbit/s"
+ _unit = DATA_RATE_MEGABITS_PER_SECOND
@property
def unit_of_measurement(self):
@@ -79,7 +80,7 @@ class AsuswrtTXSensor(AsuswrtSensor):
"""Representation of a asuswrt upload speed sensor."""
_name = "Asuswrt Upload Speed"
- _unit = "Mbit/s"
+ _unit = DATA_RATE_MEGABITS_PER_SECOND
@property
def unit_of_measurement(self):
@@ -97,7 +98,7 @@ class AsuswrtTotalRXSensor(AsuswrtSensor):
"""Representation of a asuswrt total download sensor."""
_name = "Asuswrt Download"
- _unit = "Gigabyte"
+ _unit = DATA_GIGABYTES
@property
def unit_of_measurement(self):
@@ -115,7 +116,7 @@ class AsuswrtTotalTXSensor(AsuswrtSensor):
"""Representation of a asuswrt total upload sensor."""
_name = "Asuswrt Upload"
- _unit = "Gigabyte"
+ _unit = DATA_GIGABYTES
@property
def unit_of_measurement(self):
diff --git a/homeassistant/components/august/.translations/de.json b/homeassistant/components/august/.translations/de.json
new file mode 100644
index 00000000000..dd3b2ea9f44
--- /dev/null
+++ b/homeassistant/components/august/.translations/de.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "login_method": "Anmeldemethode",
+ "password": "Passwort",
+ "timeout": "Zeit\u00fcberschreitung (Sekunden)",
+ "username": "Benutzername"
+ },
+ "title": "Richten Sie ein August-Konto ein"
+ },
+ "validation": {
+ "data": {
+ "code": "Verifizierungs-Code"
+ },
+ "description": "Bitte \u00fcberpr\u00fcfen Sie Ihre {login_method} ({username}) und geben Sie den Best\u00e4tigungscode ein",
+ "title": "Zwei-Faktor-Authentifizierung"
+ }
+ },
+ "title": "August"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/.translations/en.json b/homeassistant/components/august/.translations/en.json
new file mode 100644
index 00000000000..32c628f0b0d
--- /dev/null
+++ b/homeassistant/components/august/.translations/en.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect, please try again",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "login_method": "Login Method",
+ "password": "Password",
+ "timeout": "Timeout (seconds)",
+ "username": "Username"
+ },
+ "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
+ "title": "Setup an August account"
+ },
+ "validation": {
+ "data": {
+ "code": "Verification code"
+ },
+ "description": "Please check your {login_method} ({username}) and enter the verification code below",
+ "title": "Two factor authentication"
+ }
+ },
+ "title": "August"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py
index a52df5e361c..67e177d11d9 100644
--- a/homeassistant/components/august/__init__.py
+++ b/homeassistant/components/august/__init__.py
@@ -1,8 +1,10 @@
"""Support for August devices."""
+import asyncio
from datetime import timedelta
+from functools import partial
import logging
-from august.api import Api
+from august.api import Api, AugustApiHTTPError
from august.authenticator import AuthenticationState, Authenticator, ValidationResult
from requests import RequestException, Session
import voluptuous as vol
@@ -13,9 +15,10 @@ from homeassistant.const import (
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
-from homeassistant.util import Throttle
+from homeassistant.util import Throttle, dt
_LOGGER = logging.getLogger(__name__)
@@ -42,9 +45,22 @@ DEFAULT_ENTITY_NAMESPACE = "august"
# avoid hitting rate limits
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)
-DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
+# Limit locks status check to 900 seconds now that
+# we get the state from the lock and unlock api calls
+# and the lock and unlock activities are now captured
+MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES = timedelta(seconds=900)
+
+# Doorbells need to update more frequently than locks
+# since we get an image from the doorbell api
+MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES = timedelta(seconds=20)
+
+# Activity needs to be checked more frequently as the
+# doorbell motion and rings are included here
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
+DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
+
+
LOGIN_METHODS = ["phone", "email"]
CONFIG_SCHEMA = vol.Schema(
@@ -65,7 +81,7 @@ CONFIG_SCHEMA = vol.Schema(
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"]
-def request_configuration(hass, config, api, authenticator):
+def request_configuration(hass, config, api, authenticator, token_refresh_lock):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
@@ -79,7 +95,7 @@ def request_configuration(hass, config, api, authenticator):
_CONFIGURING[DOMAIN], "Invalid verification code"
)
elif result == ValidationResult.VALIDATED:
- setup_august(hass, config, api, authenticator)
+ setup_august(hass, config, api, authenticator, token_refresh_lock)
if DOMAIN not in _CONFIGURING:
authenticator.send_verification_code()
@@ -100,7 +116,7 @@ def request_configuration(hass, config, api, authenticator):
)
-def setup_august(hass, config, api, authenticator):
+def setup_august(hass, config, api, authenticator, token_refresh_lock):
"""Set up the August component."""
authentication = None
@@ -123,7 +139,9 @@ def setup_august(hass, config, api, authenticator):
if DOMAIN in _CONFIGURING:
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
- hass.data[DATA_AUGUST] = AugustData(hass, api, authentication.access_token)
+ hass.data[DATA_AUGUST] = AugustData(
+ hass, api, authentication, authenticator, token_refresh_lock
+ )
for component in AUGUST_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
@@ -133,13 +151,13 @@ def setup_august(hass, config, api, authenticator):
_LOGGER.error("Invalid password provided")
return False
if state == AuthenticationState.REQUIRES_VALIDATION:
- request_configuration(hass, config, api, authenticator)
+ request_configuration(hass, config, api, authenticator, token_refresh_lock)
return True
return False
-def setup(hass, config):
+async def async_setup(hass, config):
"""Set up the August component."""
conf = config[DOMAIN]
@@ -171,20 +189,28 @@ def setup(hass, config):
_LOGGER.debug("August HTTP session closed.")
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
_LOGGER.debug("Registered for Home Assistant stop event")
- return setup_august(hass, config, api, authenticator)
+ token_refresh_lock = asyncio.Lock()
+
+ return await hass.async_add_executor_job(
+ setup_august, hass, config, api, authenticator, token_refresh_lock
+ )
class AugustData:
"""August data object."""
- def __init__(self, hass, api, access_token):
+ def __init__(self, hass, api, authentication, authenticator, token_refresh_lock):
"""Init August data object."""
self._hass = hass
self._api = api
- self._access_token = access_token
+ self._authenticator = authenticator
+ self._access_token = authentication.access_token
+ self._access_token_expires = authentication.access_token_expires
+
+ self._token_refresh_lock = token_refresh_lock
self._doorbells = self._api.get_doorbells(self._access_token) or []
self._locks = self._api.get_operable_locks(self._access_token) or []
self._house_ids = set()
@@ -192,11 +218,20 @@ class AugustData:
self._house_ids.add(device.house_id)
self._doorbell_detail_by_id = {}
+ self._door_last_state_update_time_utc_by_id = {}
+ self._lock_last_status_update_time_utc_by_id = {}
self._lock_status_by_id = {}
self._lock_detail_by_id = {}
self._door_state_by_id = {}
self._activities_by_id = {}
+ # We check the locks right away so we can
+ # remove inoperative ones
+ self._update_locks_status()
+ self._update_locks_detail()
+
+ self._filter_inoperative_locks()
+
@property
def house_ids(self):
"""Return a list of house_ids."""
@@ -212,24 +247,48 @@ class AugustData:
"""Return a list of locks."""
return self._locks
- def get_device_activities(self, device_id, *activity_types):
+ async def _async_refresh_access_token_if_needed(self):
+ """Refresh the august access token if needed."""
+ if self._authenticator.should_refresh():
+ async with self._token_refresh_lock:
+ await self._hass.async_add_executor_job(self._refresh_access_token)
+
+ def _refresh_access_token(self):
+ refreshed_authentication = self._authenticator.refresh_access_token(force=False)
+ _LOGGER.info(
+ "Refreshed august access token. The old token expired at %s, and the new token expires at %s",
+ self._access_token_expires,
+ refreshed_authentication.access_token_expires,
+ )
+ self._access_token = refreshed_authentication.access_token
+ self._access_token_expires = refreshed_authentication.access_token_expires
+
+ async def async_get_device_activities(self, device_id, *activity_types):
"""Return a list of activities."""
- _LOGGER.debug("Getting device activities")
- self._update_device_activities()
+ _LOGGER.debug("Getting device activities for %s", device_id)
+ await self._async_update_device_activities()
activities = self._activities_by_id.get(device_id, [])
if activity_types:
return [a for a in activities if a.activity_type in activity_types]
return activities
- def get_latest_device_activity(self, device_id, *activity_types):
+ async def async_get_latest_device_activity(self, device_id, *activity_types):
"""Return latest activity."""
- activities = self.get_device_activities(device_id, *activity_types)
+ activities = await self.async_get_device_activities(device_id, *activity_types)
return next(iter(activities or []), None)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
- def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
+ async def _async_update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
"""Update data object with latest from August API."""
+
+ # This is the only place we refresh the api token
+ await self._async_refresh_access_token_if_needed()
+ return await self._hass.async_add_executor_job(
+ partial(self._update_device_activities, limit=ACTIVITY_FETCH_LIMIT)
+ )
+
+ def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
_LOGGER.debug("Start retrieving device activities")
for house_id in self.house_ids:
_LOGGER.debug("Updating device activity for house id %s", house_id)
@@ -243,14 +302,18 @@ class AugustData:
self._activities_by_id[device_id] = [
a for a in activities if a.device_id == device_id
]
+
_LOGGER.debug("Completed retrieving device activities")
- def get_doorbell_detail(self, doorbell_id):
+ async def async_get_doorbell_detail(self, doorbell_id):
"""Return doorbell detail."""
- self._update_doorbells()
+ await self._async_update_doorbells()
return self._doorbell_detail_by_id.get(doorbell_id)
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ @Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES)
+ async def _async_update_doorbells(self):
+ await self._hass.async_add_executor_job(self._update_doorbells)
+
def _update_doorbells(self):
detail_by_id = {}
@@ -275,38 +338,79 @@ class AugustData:
_LOGGER.debug("Completed retrieving doorbell details")
self._doorbell_detail_by_id = detail_by_id
- def get_lock_status(self, lock_id):
+ def update_door_state(self, lock_id, door_state, update_start_time_utc):
+ """Set the door status and last status update time.
+
+ This is called when newer activity is detected on the activity feed
+ in order to keep the internal data in sync
+ """
+ self._door_state_by_id[lock_id] = door_state
+ self._door_last_state_update_time_utc_by_id[lock_id] = update_start_time_utc
+ return True
+
+ def update_lock_status(self, lock_id, lock_status, update_start_time_utc):
+ """Set the lock status and last status update time.
+
+ This is used when the lock, unlock apis are called
+ or newer activity is detected on the activity feed
+ in order to keep the internal data in sync
+ """
+ self._lock_status_by_id[lock_id] = lock_status
+ self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc
+ return True
+
+ def lock_has_doorsense(self, lock_id):
+ """Determine if a lock has doorsense installed and can tell when the door is open or closed."""
+ # We do not update here since this is not expected
+ # to change until restart
+ if self._lock_detail_by_id[lock_id] is None:
+ return False
+ return self._lock_detail_by_id[lock_id].doorsense
+
+ async def async_get_lock_status(self, lock_id):
"""Return status if the door is locked or unlocked.
This is status for the lock itself.
"""
- self._update_locks()
+ await self._async_update_locks()
return self._lock_status_by_id.get(lock_id)
- def get_lock_detail(self, lock_id):
+ async def async_get_lock_detail(self, lock_id):
"""Return lock detail."""
- self._update_locks()
+ await self._async_update_locks()
return self._lock_detail_by_id.get(lock_id)
- def get_door_state(self, lock_id):
+ def get_lock_name(self, device_id):
+ """Return lock name as August has it stored."""
+ for lock in self._locks:
+ if lock.device_id == device_id:
+ return lock.device_name
+
+ async def async_get_door_state(self, lock_id):
"""Return status if the door is open or closed.
This is the status from the door sensor.
"""
- self._update_locks_status()
+ await self._async_update_locks_status()
return self._door_state_by_id.get(lock_id)
- def _update_locks(self):
- self._update_locks_status()
- self._update_locks_detail()
+ async def _async_update_locks(self):
+ await self._async_update_locks_status()
+ await self._async_update_locks_detail()
+
+ @Throttle(MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES)
+ async def _async_update_locks_status(self):
+ await self._hass.async_add_executor_job(self._update_locks_status)
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_locks_status(self):
status_by_id = {}
state_by_id = {}
+ lock_last_status_update_by_id = {}
+ door_last_state_update_by_id = {}
_LOGGER.debug("Start retrieving lock and door status")
for lock in self._locks:
+ update_start_time_utc = dt.utcnow()
_LOGGER.debug("Updating lock and door status for %s", lock.device_name)
try:
(
@@ -315,6 +419,13 @@ class AugustData:
) = self._api.get_lock_status(
self._access_token, lock.device_id, door_status=True
)
+ # Since there is a a race condition between calling the
+ # lock and activity apis, we set the last update time
+ # BEFORE making the api call since we will compare this
+ # to activity later we want activity to win over stale lock/door
+ # state.
+ lock_last_status_update_by_id[lock.device_id] = update_start_time_utc
+ door_last_state_update_by_id[lock.device_id] = update_start_time_utc
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve lock and door status for %s. %s",
@@ -331,8 +442,33 @@ class AugustData:
_LOGGER.debug("Completed retrieving lock and door status")
self._lock_status_by_id = status_by_id
self._door_state_by_id = state_by_id
+ self._door_last_state_update_time_utc_by_id = door_last_state_update_by_id
+ self._lock_last_status_update_time_utc_by_id = lock_last_status_update_by_id
+
+ def get_last_lock_status_update_time_utc(self, lock_id):
+ """Return the last time that a lock status update was seen from the august API."""
+ # Since the activity api is called more frequently than
+ # the lock api it is possible that the lock has not
+ # been updated yet
+ if lock_id not in self._lock_last_status_update_time_utc_by_id:
+ return dt.utc_from_timestamp(0)
+
+ return self._lock_last_status_update_time_utc_by_id[lock_id]
+
+ def get_last_door_state_update_time_utc(self, lock_id):
+ """Return the last time that a door status update was seen from the august API."""
+ # Since the activity api is called more frequently than
+ # the lock api it is possible that the door has not
+ # been updated yet
+ if lock_id not in self._door_last_state_update_time_utc_by_id:
+ return dt.utc_from_timestamp(0)
+
+ return self._door_last_state_update_time_utc_by_id[lock_id]
@Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES)
+ async def _async_update_locks_detail(self):
+ await self._hass.async_add_executor_job(self._update_locks_detail)
+
def _update_locks_detail(self):
detail_by_id = {}
@@ -358,8 +494,60 @@ class AugustData:
def lock(self, device_id):
"""Lock the device."""
- return self._api.lock(self._access_token, device_id)
+ return _call_api_operation_that_requires_bridge(
+ self.get_lock_name(device_id),
+ "lock",
+ self._api.lock,
+ self._access_token,
+ device_id,
+ )
def unlock(self, device_id):
"""Unlock the device."""
- return self._api.unlock(self._access_token, device_id)
+ return _call_api_operation_that_requires_bridge(
+ self.get_lock_name(device_id),
+ "unlock",
+ self._api.unlock,
+ self._access_token,
+ device_id,
+ )
+
+ def _filter_inoperative_locks(self):
+ # Remove non-operative locks as there must
+ # be a bridge (August Connect) for them to
+ # be usable
+ operative_locks = []
+ for lock in self._locks:
+ lock_detail = self._lock_detail_by_id.get(lock.device_id)
+ if lock_detail is None:
+ _LOGGER.info(
+ "The lock %s could not be setup because the system could not fetch details about the lock.",
+ lock.device_name,
+ )
+ elif lock_detail.bridge is None:
+ _LOGGER.info(
+ "The lock %s could not be setup because it does not have a bridge (Connect).",
+ lock.device_name,
+ )
+ elif not lock_detail.bridge.operative:
+ _LOGGER.info(
+ "The lock %s could not be setup because the bridge (Connect) is not operative.",
+ lock.device_name,
+ )
+ else:
+ operative_locks.append(lock)
+
+ self._locks = operative_locks
+
+
+def _call_api_operation_that_requires_bridge(
+ device_name, operation_name, func, *args, **kwargs
+):
+ """Call an API that requires the bridge to be online."""
+ ret = None
+ try:
+ ret = func(*args, **kwargs)
+ except AugustApiHTTPError as err:
+ raise HomeAssistantError(device_name + ": " + str(err))
+
+ return ret
diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py
index f840d3db532..aed1995d592 100644
--- a/homeassistant/components/august/binary_sensor.py
+++ b/homeassistant/components/august/binary_sensor.py
@@ -2,84 +2,92 @@
from datetime import datetime, timedelta
import logging
-from august.activity import ActivityType
+from august.activity import ACTIVITY_ACTION_STATES, ActivityType
from august.lock import LockDoorStatus
from homeassistant.components.binary_sensor import BinarySensorDevice
+from homeassistant.util import dt
from . import DATA_AUGUST
_LOGGER = logging.getLogger(__name__)
-SCAN_INTERVAL = timedelta(seconds=10)
+SCAN_INTERVAL = timedelta(seconds=5)
-def _retrieve_door_state(data, lock):
+async def _async_retrieve_door_state(data, lock):
"""Get the latest state of the DoorSense sensor."""
- return data.get_door_state(lock.device_id)
+ return await data.async_get_door_state(lock.device_id)
-def _retrieve_online_state(data, doorbell):
+async def _async_retrieve_online_state(data, doorbell):
"""Get the latest state of the sensor."""
- detail = data.get_doorbell_detail(doorbell.device_id)
+ detail = await data.async_get_doorbell_detail(doorbell.device_id)
if detail is None:
return None
return detail.is_online
-def _retrieve_motion_state(data, doorbell):
+async def _async_retrieve_motion_state(data, doorbell):
- return _activity_time_based_state(
+ return await _async_activity_time_based_state(
data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
)
-def _retrieve_ding_state(data, doorbell):
+async def _async_retrieve_ding_state(data, doorbell):
- return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING])
+ return await _async_activity_time_based_state(
+ data, doorbell, [ActivityType.DOORBELL_DING]
+ )
-def _activity_time_based_state(data, doorbell, activity_types):
+async def _async_activity_time_based_state(data, doorbell, activity_types):
"""Get the latest state of the sensor."""
- latest = data.get_latest_device_activity(doorbell.device_id, *activity_types)
+ latest = await data.async_get_latest_device_activity(
+ doorbell.device_id, *activity_types
+ )
if latest is not None:
start = latest.activity_start_time
- end = latest.activity_end_time + timedelta(seconds=30)
+ end = latest.activity_end_time + timedelta(seconds=45)
return start <= datetime.now() <= end
return None
-# Sensor types: Name, device_class, state_provider
-SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _retrieve_door_state]}
+SENSOR_NAME = 0
+SENSOR_DEVICE_CLASS = 1
+SENSOR_STATE_PROVIDER = 2
+
+# sensor_type: [name, device_class, async_state_provider]
+SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]}
SENSOR_TYPES_DOORBELL = {
- "doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state],
- "doorbell_motion": ["Motion", "motion", _retrieve_motion_state],
- "doorbell_online": ["Online", "connectivity", _retrieve_online_state],
+ "doorbell_ding": ["Ding", "occupancy", _async_retrieve_ding_state],
+ "doorbell_motion": ["Motion", "motion", _async_retrieve_motion_state],
+ "doorbell_online": ["Online", "connectivity", _async_retrieve_online_state],
}
-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 August binary sensors."""
data = hass.data[DATA_AUGUST]
devices = []
for door in data.locks:
for sensor_type in SENSOR_TYPES_DOOR:
- state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
- if state_provider(data, door) is LockDoorStatus.UNKNOWN:
+ if not data.lock_has_doorsense(door.device_id):
_LOGGER.debug(
"Not adding sensor class %s for lock %s ",
- SENSOR_TYPES_DOOR[sensor_type][1],
+ SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name,
)
continue
_LOGGER.debug(
"Adding sensor class %s for %s",
- SENSOR_TYPES_DOOR[sensor_type][1],
+ SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name,
)
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
@@ -88,12 +96,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for sensor_type in SENSOR_TYPES_DOORBELL:
_LOGGER.debug(
"Adding doorbell sensor class %s for %s",
- SENSOR_TYPES_DOORBELL[sensor_type][1],
+ SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS],
doorbell.device_name,
)
devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell))
- add_entities(devices, True)
+ async_add_entities(devices, True)
class AugustDoorBinarySensor(BinarySensorDevice):
@@ -120,28 +128,79 @@ class AugustDoorBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
- return SENSOR_TYPES_DOOR[self._sensor_type][1]
+ return SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_DEVICE_CLASS]
@property
def name(self):
"""Return the name of the binary sensor."""
return "{} {}".format(
- self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][0]
+ self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME]
)
- def update(self):
- """Get the latest state of the sensor."""
- state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
- self._state = state_provider(self._data, self._door)
- self._available = self._state is not None
+ async def async_update(self):
+ """Get the latest state of the sensor and update activity."""
+ async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][
+ SENSOR_STATE_PROVIDER
+ ]
+ lock_door_state = await async_state_provider(self._data, self._door)
+ self._available = (
+ lock_door_state is not None and lock_door_state != LockDoorStatus.UNKNOWN
+ )
+ self._state = lock_door_state == LockDoorStatus.OPEN
- self._state = self._state == LockDoorStatus.OPEN
+ door_activity = await self._data.async_get_latest_device_activity(
+ self._door.device_id, ActivityType.DOOR_OPERATION
+ )
+
+ if door_activity is not None:
+ self._sync_door_activity(door_activity)
+
+ def _update_door_state(self, door_state, update_start_time):
+ new_state = door_state == LockDoorStatus.OPEN
+ if self._state != new_state:
+ self._state = new_state
+ self._data.update_door_state(
+ self._door.device_id, door_state, update_start_time
+ )
+
+ def _sync_door_activity(self, door_activity):
+ """Check the activity for the latest door open/close activity (events).
+
+ We use this to determine the door state in between calls to the lock
+ api as we update it more frequently
+ """
+ last_door_state_update_time_utc = self._data.get_last_door_state_update_time_utc(
+ self._door.device_id
+ )
+ activity_end_time_utc = dt.as_utc(door_activity.activity_end_time)
+
+ if activity_end_time_utc > last_door_state_update_time_utc:
+ _LOGGER.debug(
+ "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_door_state_update_time_utc=%s]",
+ self.name,
+ door_activity.action,
+ activity_end_time_utc,
+ last_door_state_update_time_utc,
+ )
+ activity_start_time_utc = dt.as_utc(door_activity.activity_start_time)
+ if door_activity.action in ACTIVITY_ACTION_STATES:
+ self._update_door_state(
+ ACTIVITY_ACTION_STATES[door_activity.action],
+ activity_start_time_utc,
+ )
+ else:
+ _LOGGER.info(
+ "Unhandled door activity action %s for %s",
+ door_activity.action,
+ self.name,
+ )
@property
def unique_id(self) -> str:
"""Get the unique of the door open binary sensor."""
return "{:s}_{:s}".format(
- self._door.device_id, SENSOR_TYPES_DOOR[self._sensor_type][0].lower()
+ self._door.device_id,
+ SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME].lower(),
)
@@ -169,25 +228,31 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
- return SENSOR_TYPES_DOORBELL[self._sensor_type][1]
+ return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_DEVICE_CLASS]
@property
def name(self):
"""Return the name of the binary sensor."""
return "{} {}".format(
- self._doorbell.device_name, SENSOR_TYPES_DOORBELL[self._sensor_type][0]
+ self._doorbell.device_name,
+ SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME],
)
- def update(self):
+ async def async_update(self):
"""Get the latest state of the sensor."""
- state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
- self._state = state_provider(self._data, self._doorbell)
- self._available = self._doorbell.is_online
+ async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][
+ SENSOR_STATE_PROVIDER
+ ]
+ self._state = await async_state_provider(self._data, self._doorbell)
+ # The doorbell will go into standby mode when there is no motion
+ # for a short while. It will wake by itself when needed so we need
+ # to consider is available or we will not report motion or dings
+ self._available = self._doorbell.is_online or self._doorbell.status == "standby"
@property
def unique_id(self) -> str:
"""Get the unique id of the doorbell sensor."""
return "{:s}_{:s}".format(
self._doorbell.device_id,
- SENSOR_TYPES_DOORBELL[self._sensor_type][0].lower(),
+ SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower(),
)
diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py
index 885ee444c6b..5426d9574dc 100644
--- a/homeassistant/components/august/camera.py
+++ b/homeassistant/components/august/camera.py
@@ -10,7 +10,7 @@ from . import DATA_AUGUST, DEFAULT_TIMEOUT
SCAN_INTERVAL = timedelta(seconds=10)
-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 August cameras."""
data = hass.data[DATA_AUGUST]
devices = []
@@ -18,14 +18,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for doorbell in data.doorbells:
devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT))
- add_entities(devices, True)
+ async_add_entities(devices, True)
class AugustCamera(Camera):
- """An implementation of a Canary security camera."""
+ """An implementation of a August security camera."""
def __init__(self, data, doorbell, timeout):
- """Initialize a Canary security camera."""
+ """Initialize a August security camera."""
super().__init__()
self._data = data
self._doorbell = doorbell
@@ -58,18 +58,23 @@ class AugustCamera(Camera):
"""Return the camera model."""
return "Doorbell"
- def camera_image(self):
+ async def async_camera_image(self):
"""Return bytes of camera image."""
- latest = self._data.get_doorbell_detail(self._doorbell.device_id)
+ latest = await self._data.async_get_doorbell_detail(self._doorbell.device_id)
if self._image_url is not latest.image_url:
self._image_url = latest.image_url
- self._image_content = requests.get(
- self._image_url, timeout=self._timeout
- ).content
+ self._image_content = await self.hass.async_add_executor_job(
+ self._camera_image
+ )
return self._image_content
+ def _camera_image(self):
+ """Return bytes of camera image via http get."""
+ # Move this to py-august: see issue#32048
+ return requests.get(self._image_url, timeout=self._timeout).content
+
@property
def unique_id(self) -> str:
"""Get the unique id of the camera."""
diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py
index d336e21653b..9d5df1192a7 100644
--- a/homeassistant/components/august/lock.py
+++ b/homeassistant/components/august/lock.py
@@ -2,11 +2,12 @@
from datetime import timedelta
import logging
-from august.activity import ActivityType
+from august.activity import ACTIVITY_ACTION_STATES, ActivityType
from august.lock import LockStatus
from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
+from homeassistant.util import dt
from . import DATA_AUGUST
@@ -15,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
-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 August locks."""
data = hass.data[DATA_AUGUST]
devices = []
@@ -24,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.debug("Adding lock for %s", lock.device_name)
devices.append(AugustLock(data, lock))
- add_entities(devices, True)
+ async_add_entities(devices, True)
class AugustLock(LockDevice):
@@ -39,27 +40,77 @@ class AugustLock(LockDevice):
self._changed_by = None
self._available = False
- def lock(self, **kwargs):
+ async def async_lock(self, **kwargs):
"""Lock the device."""
- self._data.lock(self._lock.device_id)
+ update_start_time_utc = dt.utcnow()
+ lock_status = await self.hass.async_add_executor_job(
+ self._data.lock, self._lock.device_id
+ )
+ self._update_lock_status(lock_status, update_start_time_utc)
- def unlock(self, **kwargs):
+ async def async_unlock(self, **kwargs):
"""Unlock the device."""
- self._data.unlock(self._lock.device_id)
+ update_start_time_utc = dt.utcnow()
+ lock_status = await self.hass.async_add_executor_job(
+ self._data.unlock, self._lock.device_id
+ )
+ self._update_lock_status(lock_status, update_start_time_utc)
- def update(self):
- """Get the latest state of the sensor."""
- self._lock_status = self._data.get_lock_status(self._lock.device_id)
- self._available = self._lock_status is not None
+ def _update_lock_status(self, lock_status, update_start_time_utc):
+ if self._lock_status != lock_status:
+ self._lock_status = lock_status
+ self._data.update_lock_status(
+ self._lock.device_id, lock_status, update_start_time_utc
+ )
+ self.schedule_update_ha_state()
- self._lock_detail = self._data.get_lock_detail(self._lock.device_id)
+ async def async_update(self):
+ """Get the latest state of the sensor and update activity."""
+ self._lock_status = await self._data.async_get_lock_status(self._lock.device_id)
+ self._available = (
+ self._lock_status is not None and self._lock_status != LockStatus.UNKNOWN
+ )
+ self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id)
- activity = self._data.get_latest_device_activity(
+ lock_activity = await self._data.async_get_latest_device_activity(
self._lock.device_id, ActivityType.LOCK_OPERATION
)
- if activity is not None:
- self._changed_by = activity.operated_by
+ if lock_activity is not None:
+ self._changed_by = lock_activity.operated_by
+ self._sync_lock_activity(lock_activity)
+
+ def _sync_lock_activity(self, lock_activity):
+ """Check the activity for the latest lock/unlock activity (events).
+
+ We use this to determine the lock state in between calls to the lock
+ api as we update it more frequently
+ """
+ last_lock_status_update_time_utc = self._data.get_last_lock_status_update_time_utc(
+ self._lock.device_id
+ )
+ activity_end_time_utc = dt.as_utc(lock_activity.activity_end_time)
+
+ if activity_end_time_utc > last_lock_status_update_time_utc:
+ _LOGGER.debug(
+ "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_lock_status_update_time_utc=%s]",
+ self.name,
+ lock_activity.action,
+ activity_end_time_utc,
+ last_lock_status_update_time_utc,
+ )
+ activity_start_time_utc = dt.as_utc(lock_activity.activity_start_time)
+ if lock_activity.action in ACTIVITY_ACTION_STATES:
+ self._update_lock_status(
+ ACTIVITY_ACTION_STATES[lock_activity.action],
+ activity_start_time_utc,
+ )
+ else:
+ _LOGGER.info(
+ "Unhandled lock activity action %s for %s",
+ lock_activity.action,
+ self.name,
+ )
@property
def name(self):
diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json
index bacd7346ca7..7afa742f3ca 100644
--- a/homeassistant/components/august/manifest.json
+++ b/homeassistant/components/august/manifest.json
@@ -2,7 +2,7 @@
"domain": "august",
"name": "August",
"documentation": "https://www.home-assistant.io/integrations/august",
- "requirements": ["py-august==0.8.1"],
+ "requirements": ["py-august==0.14.0"],
"dependencies": ["configurator"],
- "codeowners": []
+ "codeowners": ["@bdraco"]
}
diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py
index fb0073c78d5..046cbba2873 100644
--- a/homeassistant/components/automation/mqtt.py
+++ b/homeassistant/components/automation/mqtt.py
@@ -11,8 +11,10 @@ import homeassistant.helpers.config_validation as cv
# mypy: allow-untyped-defs
CONF_ENCODING = "encoding"
+CONF_QOS = "qos"
CONF_TOPIC = "topic"
DEFAULT_ENCODING = "utf-8"
+DEFAULT_QOS = 0
TRIGGER_SCHEMA = vol.Schema(
{
@@ -20,6 +22,9 @@ TRIGGER_SCHEMA = vol.Schema(
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
+ vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All(
+ vol.Coerce(int), vol.In([0, 1, 2])
+ ),
}
)
@@ -29,6 +34,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
topic = config[CONF_TOPIC]
payload = config.get(CONF_PAYLOAD)
encoding = config[CONF_ENCODING] or None
+ qos = config[CONF_QOS]
@callback
def mqtt_automation_listener(mqttmsg):
@@ -49,6 +55,6 @@ async def async_attach_trigger(hass, config, action, automation_info):
hass.async_run_job(action, {"trigger": data})
remove = await mqtt.async_subscribe(
- hass, topic, mqtt_automation_listener, encoding=encoding
+ hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos
)
return remove
diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json
index ecf7b552bba..58f5c0e4ad2 100644
--- a/homeassistant/components/axis/.translations/ca.json
+++ b/homeassistant/components/axis/.translations/ca.json
@@ -9,7 +9,7 @@
},
"error": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
- "already_in_progress": "El flux de dades pel dispositiu ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
"device_unavailable": "El dispositiu no est\u00e0 disponible",
"faulty_credentials": "Credencials d'usuari incorrectes"
},
diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json
index abc1e2f17ec..1f00800422c 100644
--- a/homeassistant/components/axis/.translations/en.json
+++ b/homeassistant/components/axis/.translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
- "bad_config_file": "Bad data from config file",
+ "bad_config_file": "Bad data from configuration file",
"link_local_address": "Link local addresses are not supported",
"not_axis_device": "Discovered device not an Axis device",
"updated_configuration": "Updated device configuration with new host address"
diff --git a/homeassistant/components/axis/.translations/hu.json b/homeassistant/components/axis/.translations/hu.json
index 41dd3c00d2b..4f05087cad8 100644
--- a/homeassistant/components/axis/.translations/hu.json
+++ b/homeassistant/components/axis/.translations/hu.json
@@ -1,10 +1,14 @@
{
"config": {
+ "abort": {
+ "updated_configuration": "Friss\u00edtett eszk\u00f6zkonfigur\u00e1ci\u00f3 \u00faj \u00e1llom\u00e1sc\u00edmmel"
+ },
"error": {
"already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk",
"device_unavailable": "Az eszk\u00f6z nem \u00e9rhet\u0151 el",
"faulty_credentials": "Rossz felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok"
},
+ "flow_title": "Axis eszk\u00f6z: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/ko.json b/homeassistant/components/axis/.translations/ko.json
index e471ae3ea7a..3f1aa97f266 100644
--- a/homeassistant/components/axis/.translations/ko.json
+++ b/homeassistant/components/axis/.translations/ko.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "bad_config_file": "\uad6c\uc131 \ud30c\uc77c\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "bad_config_file": "\uad6c\uc131 \ud30c\uc77c\uc5d0 \uc798\ubabb\ub41c \ub370\uc774\ud130\uac00 \uc788\uc2b5\ub2c8\ub2e4",
"link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
"not_axis_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Axis \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4",
"updated_configuration": "\uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ub41c \uae30\uae30 \uad6c\uc131"
diff --git a/homeassistant/components/axis/.translations/nl.json b/homeassistant/components/axis/.translations/nl.json
index 10fc8c02d66..b512690e2a3 100644
--- a/homeassistant/components/axis/.translations/nl.json
+++ b/homeassistant/components/axis/.translations/nl.json
@@ -4,7 +4,8 @@
"already_configured": "Apparaat is al geconfigureerd",
"bad_config_file": "Slechte gegevens van het configuratiebestand",
"link_local_address": "Link-lokale adressen worden niet ondersteund",
- "not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat"
+ "not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat",
+ "updated_configuration": "Bijgewerkte apparaatconfiguratie met nieuw hostadres"
},
"error": {
"already_configured": "Apparaat is al geconfigureerd",
diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json
index 60db56146fa..32e1cc2fd40 100644
--- a/homeassistant/components/axis/.translations/no.json
+++ b/homeassistant/components/axis/.translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
- "bad_config_file": "D\u00e5rlig data fra konfigurasjonsfilen",
+ "bad_config_file": "D\u00e5rlige data fra konfigurasjonsfilen",
"link_local_address": "Linking av lokale adresser st\u00f8ttes ikke",
"not_axis_device": "Oppdaget enhet ikke en Axis enhet",
"updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse"
diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json
index d5deb327a75..9f7bb55147d 100644
--- a/homeassistant/components/axis/.translations/pl.json
+++ b/homeassistant/components/axis/.translations/pl.json
@@ -1,15 +1,15 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
"bad_config_file": "B\u0142\u0119dne dane z pliku konfiguracyjnego",
"link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane",
"not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis",
"updated_configuration": "Zaktualizowano konfiguracj\u0119 urz\u0105dzenia o nowy adres hosta"
},
"error": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
- "already_in_progress": "Konfigurowanie urz\u0105dzenia jest ju\u017c w toku.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
"device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne",
"faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce"
},
diff --git a/homeassistant/components/axis/.translations/sv.json b/homeassistant/components/axis/.translations/sv.json
index a38ef2ef745..59761c7202f 100644
--- a/homeassistant/components/axis/.translations/sv.json
+++ b/homeassistant/components/axis/.translations/sv.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
- "bad_config_file": "Felaktig data fr\u00e5n config fil",
+ "bad_config_file": "Felaktig data fr\u00e5n konfigurationsfilen",
"link_local_address": "Link local addresses are not supported",
- "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet"
+ "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet",
+ "updated_configuration": "Uppdaterad enhetskonfiguration med ny v\u00e4rdadress"
},
"error": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
@@ -12,6 +13,7 @@
"device_unavailable": "Enheten \u00e4r inte tillg\u00e4nglig",
"faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter"
},
+ "flow_title": "Axisenhet: {name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json
index 751a7544202..ac552afe583 100644
--- a/homeassistant/components/axis/.translations/zh-Hant.json
+++ b/homeassistant/components/axis/.translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548",
+ "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548\u932f\u8aa4",
"link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740",
"not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099",
"updated_configuration": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0\u88dd\u7f6e\u8a2d\u5b9a"
diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py
index 5c928aa9f31..5294e30ed6f 100644
--- a/homeassistant/components/axis/__init__.py
+++ b/homeassistant/components/axis/__init__.py
@@ -1,15 +1,23 @@
"""Support for Axis devices."""
+import logging
+
from homeassistant.const import (
CONF_DEVICE,
+ CONF_HOST,
CONF_MAC,
+ CONF_PASSWORD,
+ CONF_PORT,
CONF_TRIGGER_TIME,
+ CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN
from .device import AxisNetworkDevice, get_device
+LOGGER = logging.getLogger(__name__)
+
async def async_setup(hass, config):
"""Old way to set up Axis devices."""
@@ -35,7 +43,7 @@ async def async_setup_entry(hass, config_entry):
config_entry, unique_id=device.api.vapix.params.system_serialnumber
)
- hass.data[DOMAIN][device.serial] = device
+ hass.data[DOMAIN][config_entry.unique_id] = device
await device.async_update_device_registry()
@@ -52,7 +60,13 @@ async def async_unload_entry(hass, config_entry):
async def async_populate_options(hass, config_entry):
"""Populate default options for device."""
- device = await get_device(hass, config_entry.data[CONF_DEVICE])
+ device = await get_device(
+ hass,
+ host=config_entry.data[CONF_HOST],
+ port=config_entry.data[CONF_PORT],
+ username=config_entry.data[CONF_USERNAME],
+ password=config_entry.data[CONF_PASSWORD],
+ )
supported_formats = device.vapix.params.image_format
camera = bool(supported_formats)
@@ -64,3 +78,18 @@ async def async_populate_options(hass, config_entry):
}
hass.config_entries.async_update_entry(config_entry, options=options)
+
+
+async def async_migrate_entry(hass, config_entry):
+ """Migrate old entry."""
+ LOGGER.debug("Migrating from version %s", config_entry.version)
+
+ # Flatten configuration but keep old data if user rollbacks HASS
+ if config_entry.version == 1:
+ config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
+
+ config_entry.version = 2
+
+ LOGGER.info("Migration to version %s successful", config_entry.version)
+
+ return True
diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py
index 6b82c938a99..51d6b6805cc 100644
--- a/homeassistant/components/axis/camera.py
+++ b/homeassistant/components/axis/camera.py
@@ -9,7 +9,6 @@ from homeassistant.components.mjpeg.camera import (
)
from homeassistant.const import (
CONF_AUTHENTICATION,
- CONF_DEVICE,
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
@@ -35,15 +34,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
config = {
CONF_NAME: config_entry.data[CONF_NAME],
- CONF_USERNAME: config_entry.data[CONF_DEVICE][CONF_USERNAME],
- CONF_PASSWORD: config_entry.data[CONF_DEVICE][CONF_PASSWORD],
+ CONF_USERNAME: config_entry.data[CONF_USERNAME],
+ CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
CONF_MJPEG_URL: AXIS_VIDEO.format(
- config_entry.data[CONF_DEVICE][CONF_HOST],
- config_entry.data[CONF_DEVICE][CONF_PORT],
+ config_entry.data[CONF_HOST], config_entry.data[CONF_PORT],
),
CONF_STILL_IMAGE_URL: AXIS_IMAGE.format(
- config_entry.data[CONF_DEVICE][CONF_HOST],
- config_entry.data[CONF_DEVICE][CONF_PORT],
+ config_entry.data[CONF_HOST], config_entry.data[CONF_PORT],
),
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
}
@@ -76,14 +73,14 @@ class AxisCamera(AxisEntityBase, MjpegCamera):
async def stream_source(self):
"""Return the stream source."""
return AXIS_STREAM.format(
- self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME],
- self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD],
+ self.device.config_entry.data[CONF_USERNAME],
+ self.device.config_entry.data[CONF_PASSWORD],
self.device.host,
)
def _new_address(self):
"""Set new device address for video stream."""
- port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT]
+ port = self.device.config_entry.data[CONF_PORT]
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port)
self._still_image_url = AXIS_IMAGE.format(self.device.host, port)
diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py
index 88c1cab98c1..29658c19c5b 100644
--- a/homeassistant/components/axis/config_flow.py
+++ b/homeassistant/components/axis/config_flow.py
@@ -4,7 +4,6 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
- CONF_DEVICE,
CONF_HOST,
CONF_MAC,
CONF_NAME,
@@ -33,16 +32,12 @@ DEFAULT_PORT = 80
class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Axis config flow."""
- VERSION = 1
+ VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize the Axis config flow."""
self.device_config = {}
- self.model = None
- self.name = None
- self.serial_number = None
-
self.discovery_schema = {}
self.import_schema = {}
@@ -55,24 +50,32 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
+ device = await get_device(
+ self.hass,
+ host=user_input[CONF_HOST],
+ port=user_input[CONF_PORT],
+ username=user_input[CONF_USERNAME],
+ password=user_input[CONF_PASSWORD],
+ )
+
+ serial_number = device.vapix.params.system_serialnumber
+ await self.async_set_unique_id(serial_number)
+
+ self._abort_if_unique_id_configured(
+ updates={
+ CONF_HOST: user_input[CONF_HOST],
+ CONF_PORT: user_input[CONF_PORT],
+ }
+ )
+
self.device_config = {
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
+ CONF_MAC: serial_number,
+ CONF_MODEL: device.vapix.params.prodnbr,
}
- device = await get_device(self.hass, self.device_config)
-
- self.serial_number = device.vapix.params.system_serialnumber
- config_entry = await self.async_set_unique_id(self.serial_number)
- if config_entry:
- return self._update_entry(
- config_entry,
- host=user_input[CONF_HOST],
- port=user_input[CONF_PORT],
- )
-
- self.model = device.vapix.params.prodnbr
return await self._create_entry()
@@ -101,41 +104,23 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
Generate a name to be used as a prefix for device entities.
"""
+ model = self.device_config[CONF_MODEL]
same_model = [
entry.data[CONF_NAME]
for entry in self.hass.config_entries.async_entries(DOMAIN)
- if entry.data[CONF_MODEL] == self.model
+ if entry.data[CONF_MODEL] == model
]
- self.name = f"{self.model}"
+ name = model
for idx in range(len(same_model) + 1):
- self.name = f"{self.model} {idx}"
- if self.name not in same_model:
+ name = f"{model} {idx}"
+ if name not in same_model:
break
- data = {
- CONF_DEVICE: self.device_config,
- CONF_NAME: self.name,
- CONF_MAC: self.serial_number,
- CONF_MODEL: self.model,
- }
+ self.device_config[CONF_NAME] = name
- title = f"{self.model} - {self.serial_number}"
- return self.async_create_entry(title=title, data=data)
-
- def _update_entry(self, entry, host, port):
- """Update existing entry."""
- if (
- entry.data[CONF_DEVICE][CONF_HOST] == host
- and entry.data[CONF_DEVICE][CONF_PORT] == port
- ):
- return self.async_abort(reason="already_configured")
-
- entry.data[CONF_DEVICE][CONF_HOST] = host
- entry.data[CONF_DEVICE][CONF_PORT] = port
-
- self.hass.config_entries.async_update_entry(entry)
- return self.async_abort(reason="updated_configuration")
+ title = f"{model} - {self.device_config[CONF_MAC]}"
+ return self.async_create_entry(title=title, data=self.device_config)
async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered Axis device."""
@@ -147,18 +132,19 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if discovery_info[CONF_HOST].startswith("169.254"):
return self.async_abort(reason="link_local_address")
- config_entry = await self.async_set_unique_id(serial_number)
- if config_entry:
- return self._update_entry(
- config_entry,
- host=discovery_info[CONF_HOST],
- port=discovery_info[CONF_PORT],
- )
+ await self.async_set_unique_id(serial_number)
+
+ self._abort_if_unique_id_configured(
+ updates={
+ CONF_HOST: discovery_info[CONF_HOST],
+ CONF_PORT: discovery_info[CONF_PORT],
+ }
+ )
# 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],
+ CONF_NAME: discovery_info["hostname"][:-7],
+ CONF_HOST: discovery_info[CONF_HOST],
}
self.discovery_schema = {
diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py
index 85ad59268df..a204136e018 100644
--- a/homeassistant/components/axis/device.py
+++ b/homeassistant/components/axis/device.py
@@ -7,9 +7,7 @@ import axis
from axis.streammanager import SIGNAL_PLAYING
from homeassistant.const import (
- CONF_DEVICE,
CONF_HOST,
- CONF_MAC,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
@@ -42,7 +40,7 @@ class AxisNetworkDevice:
@property
def host(self):
"""Return the host of this device."""
- return self.config_entry.data[CONF_DEVICE][CONF_HOST]
+ return self.config_entry.data[CONF_HOST]
@property
def model(self):
@@ -75,7 +73,13 @@ class AxisNetworkDevice:
async def async_setup(self):
"""Set up the device."""
try:
- self.api = await get_device(self.hass, self.config_entry.data[CONF_DEVICE])
+ self.api = await get_device(
+ self.hass,
+ host=self.config_entry.data[CONF_HOST],
+ port=self.config_entry.data[CONF_PORT],
+ username=self.config_entry.data[CONF_USERNAME],
+ password=self.config_entry.data[CONF_PASSWORD],
+ )
except CannotConnect:
raise ConfigEntryNotReady
@@ -126,7 +130,7 @@ class AxisNetworkDevice:
This is a static method because a class method (bound method),
can not be used with weak references.
"""
- device = hass.data[DOMAIN][entry.data[CONF_MAC]]
+ device = hass.data[DOMAIN][entry.unique_id]
device.api.config.host = device.host
async_dispatcher_send(hass, device.event_new_address)
@@ -197,15 +201,15 @@ class AxisNetworkDevice:
return True
-async def get_device(hass, config):
+async def get_device(hass, host, port, username, password):
"""Create a Axis device."""
device = axis.AxisDevice(
loop=hass.loop,
- host=config[CONF_HOST],
- username=config[CONF_USERNAME],
- password=config[CONF_PASSWORD],
- port=config[CONF_PORT],
+ host=host,
+ port=port,
+ username=username,
+ password=password,
web_proto="http",
)
@@ -224,13 +228,11 @@ async def get_device(hass, config):
return device
except axis.Unauthorized:
- LOGGER.warning(
- "Connected to device at %s but not registered.", config[CONF_HOST]
- )
+ LOGGER.warning("Connected to device at %s but not registered.", host)
raise AuthenticationRequired
except (asyncio.TimeoutError, axis.RequestError):
- LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST])
+ LOGGER.error("Error connecting to the Axis device at %s", host)
raise CannotConnect
except axis.AxisException:
diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json
index 7facd7060ad..04a9f9e388a 100644
--- a/homeassistant/components/axis/strings.json
+++ b/homeassistant/components/axis/strings.json
@@ -1,30 +1,29 @@
{
- "config": {
- "title": "Axis device",
- "flow_title": "Axis device: {name} ({host})",
- "step": {
- "user": {
- "title": "Set up Axis device",
- "data": {
- "host": "Host",
- "username": "Username",
- "password": "Password",
- "port": "Port"
- }
- }
- },
- "error": {
- "already_configured": "Device is already configured",
- "already_in_progress": "Config flow for device is already in progress.",
- "device_unavailable": "Device is not available",
- "faulty_credentials": "Bad user credentials"
- },
- "abort": {
- "already_configured": "Device is already configured",
- "bad_config_file": "Bad data from config file",
- "link_local_address": "Link local addresses are not supported",
- "not_axis_device": "Discovered device not an Axis device",
- "updated_configuration": "Updated device configuration with new host address"
+ "config": {
+ "title": "Axis device",
+ "flow_title": "Axis device: {name} ({host})",
+ "step": {
+ "user": {
+ "title": "Set up Axis device",
+ "data": {
+ "host": "Host",
+ "username": "Username",
+ "password": "Password",
+ "port": "Port"
}
+ }
+ },
+ "error": {
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Config flow for device is already in progress.",
+ "device_unavailable": "Device is not available",
+ "faulty_credentials": "Bad user credentials"
+ },
+ "abort": {
+ "already_configured": "Device is already configured",
+ "bad_config_file": "Bad data from configuration file",
+ "link_local_address": "Link local addresses are not supported",
+ "not_axis_device": "Discovered device not an Axis device"
}
+ }
}
diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py
index f5e5865f6f0..259066d4561 100644
--- a/homeassistant/components/bbox/sensor.py
+++ b/homeassistant/components/bbox/sensor.py
@@ -11,6 +11,7 @@ from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_MONITORED_VARIABLES,
CONF_NAME,
+ DATA_RATE_MEGABITS_PER_SECOND,
DEVICE_CLASS_TIMESTAMP,
)
import homeassistant.helpers.config_validation as cv
@@ -20,8 +21,6 @@ from homeassistant.util.dt import utcnow
_LOGGER = logging.getLogger(__name__)
-BANDWIDTH_MEGABITS_SECONDS = "Mb/s"
-
ATTRIBUTION = "Powered by Bouygues Telecom"
DEFAULT_NAME = "Bbox"
@@ -32,22 +31,22 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
SENSOR_TYPES = {
"down_max_bandwidth": [
"Maximum Download Bandwidth",
- BANDWIDTH_MEGABITS_SECONDS,
+ DATA_RATE_MEGABITS_PER_SECOND,
"mdi:download",
],
"up_max_bandwidth": [
"Maximum Upload Bandwidth",
- BANDWIDTH_MEGABITS_SECONDS,
+ DATA_RATE_MEGABITS_PER_SECOND,
"mdi:upload",
],
"current_down_bandwidth": [
"Currently Used Download Bandwidth",
- BANDWIDTH_MEGABITS_SECONDS,
+ DATA_RATE_MEGABITS_PER_SECOND,
"mdi:download",
],
"current_up_bandwidth": [
"Currently Used Upload Bandwidth",
- BANDWIDTH_MEGABITS_SECONDS,
+ DATA_RATE_MEGABITS_PER_SECOND,
"mdi:upload",
],
"uptime": ["Uptime", None, "mdi:clock"],
diff --git a/homeassistant/components/binary_sensor/.translations/es-419.json b/homeassistant/components/binary_sensor/.translations/es-419.json
index f1c20e5346b..e727e18775a 100644
--- a/homeassistant/components/binary_sensor/.translations/es-419.json
+++ b/homeassistant/components/binary_sensor/.translations/es-419.json
@@ -28,6 +28,12 @@
"is_not_occupied": "{entity_name} no est\u00e1 ocupado",
"is_not_open": "{entity_name} est\u00e1 cerrado",
"is_not_plugged_in": "{entity_name} est\u00e1 desconectado",
+ "is_not_unsafe": "{entity_name} es seguro",
+ "is_occupied": "{entity_name} est\u00e1 ocupado",
+ "is_off": "{entity_name} est\u00e1 apagado",
+ "is_on": "{entity_name} est\u00e1 encendido",
+ "is_open": "{entity_name} est\u00e1 abierto",
+ "is_plugged_in": "{entity_name} est\u00e1 enchufado",
"is_powered": "{entity_name} est\u00e1 encendido",
"is_present": "{entity_name} est\u00e1 presente",
"is_problem": "{entity_name} est\u00e1 detectando un problema",
@@ -45,6 +51,7 @@
"hot": "{entity_name} se calent\u00f3",
"light": "{entity_name} comenz\u00f3 a detectar luz",
"locked": "{entity_name} bloqueado",
+ "moist": "{entity_name} se humedeci\u00f3",
"moist\u00a7": "{entity_name} se humedeci\u00f3",
"motion": "{entity_name} comenz\u00f3 a detectar movimiento",
"moving": "{entity_name} comenz\u00f3 a moverse",
@@ -59,7 +66,22 @@
"not_cold": "{entity_name} no se enfri\u00f3",
"not_connected": "{entity_name} desconectado",
"not_hot": "{entity_name} no se calent\u00f3",
- "not_locked": "{entity_name} desbloqueado"
+ "not_locked": "{entity_name} desbloqueado",
+ "not_moist": "{entity_name} se sec\u00f3",
+ "not_moving": "{entity_name} dej\u00f3 de moverse",
+ "not_opened": "{entity_name} cerrado",
+ "not_plugged_in": "{entity_name} desconectado",
+ "not_present": "{entity_name} no presente",
+ "not_unsafe": "{entity_name} se volvi\u00f3 seguro",
+ "occupied": "{entity_name} se ocup\u00f3",
+ "opened": "{entity_name} abierto",
+ "plugged_in": "{entity_name} enchufado",
+ "present": "{entity_name} presente",
+ "problem": "{entity_name} comenz\u00f3 a detectar problemas",
+ "smoke": "{entity_name} comenz\u00f3 a detectar humo",
+ "sound": "{entity_name} comenz\u00f3 a detectar sonido",
+ "turned_off": "{entity_name} apagado",
+ "turned_on": "{entity_name} encendido"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/.translations/sv.json b/homeassistant/components/binary_sensor/.translations/sv.json
new file mode 100644
index 00000000000..5df2ce17c92
--- /dev/null
+++ b/homeassistant/components/binary_sensor/.translations/sv.json
@@ -0,0 +1,94 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "{entity_name}-batteriet \u00e4r l\u00e5gt",
+ "is_cold": "{entity_name} \u00e4r kall",
+ "is_connected": "{entity_name} \u00e4r ansluten",
+ "is_gas": "{entity_name} detekterar gas",
+ "is_hot": "{entity_name} \u00e4r varm",
+ "is_light": "{entity_name} uppt\u00e4cker ljus",
+ "is_locked": "{entity_name} \u00e4r l\u00e5st",
+ "is_moist": "{entity_name} \u00e4r fuktig",
+ "is_motion": "{entity_name} detekterar r\u00f6relse",
+ "is_moving": "{entity_name} r\u00f6r sig",
+ "is_no_gas": "{entity_name} uppt\u00e4cker inte gas",
+ "is_no_light": "{entity_name} uppt\u00e4cker inte ljus",
+ "is_no_motion": "{entity_name} detekterar inte r\u00f6relse",
+ "is_no_problem": "{entity_name} uppt\u00e4cker inte problem",
+ "is_no_smoke": "{entity_name} detekterar inte r\u00f6k",
+ "is_no_sound": "{entity_name} uppt\u00e4cker inte ljud",
+ "is_no_vibration": "{entity_name} uppt\u00e4cker inte vibrationer",
+ "is_not_bat_low": "{entity_name} batteri \u00e4r normalt",
+ "is_not_cold": "{entity_name} \u00e4r inte kall",
+ "is_not_connected": "{entity_name} \u00e4r fr\u00e5nkopplad",
+ "is_not_hot": "{entity_name} \u00e4r inte varm",
+ "is_not_locked": "{entity_name} \u00e4r ol\u00e5st",
+ "is_not_moist": "{entity_name} \u00e4r torr",
+ "is_not_moving": "{entity_name} r\u00f6r sig inte",
+ "is_not_occupied": "{entity_name} \u00e4r inte upptagen",
+ "is_not_open": "{entity_name} \u00e4r st\u00e4ngd",
+ "is_not_plugged_in": "{entity_name} \u00e4r urkopplad",
+ "is_not_powered": "{entity_name} \u00e4r inte str\u00f6mf\u00f6rd",
+ "is_not_present": "{entity_name} finns inte",
+ "is_not_unsafe": "{entity_name} \u00e4r s\u00e4ker",
+ "is_occupied": "{entity_name} \u00e4r upptagen",
+ "is_off": "{entity_name} \u00e4r avst\u00e4ngd",
+ "is_on": "{entity_name} \u00e4r p\u00e5",
+ "is_open": "{entity_name} \u00e4r \u00f6ppen",
+ "is_plugged_in": "{entity_name} \u00e4r ansluten",
+ "is_powered": "{entity_name} \u00e4r str\u00f6mf\u00f6rd",
+ "is_present": "{entity_name} \u00e4r n\u00e4rvarande",
+ "is_problem": "{entity_name} uppt\u00e4cker problem",
+ "is_smoke": "{entity_name} detekterar r\u00f6k",
+ "is_sound": "{entity_name} uppt\u00e4cker ljud",
+ "is_unsafe": "{entity_name} \u00e4r os\u00e4ker",
+ "is_vibration": "{entity_name} uppt\u00e4cker vibrationer"
+ },
+ "trigger_type": {
+ "bat_low": "{entity_name} batteri l\u00e5gt",
+ "closed": "{entity_name} st\u00e4ngd",
+ "cold": "{entity_name} blev kall",
+ "connected": "{entity_name} ansluten",
+ "gas": "{entity_name} b\u00f6rjade detektera gas",
+ "hot": "{entity_name} blev varm",
+ "light": "{entity_name} b\u00f6rjade uppt\u00e4cka ljus",
+ "locked": "{entity_name} l\u00e5st",
+ "moist": "{entity_name} blev fuktig",
+ "moist\u00a7": "{entity_name} blev fuktig",
+ "motion": "{entity_name} b\u00f6rjade detektera r\u00f6relse",
+ "moving": "{entity_name} b\u00f6rjade r\u00f6ra sig",
+ "no_gas": "{entity_name} slutade uppt\u00e4cka gas",
+ "no_light": "{entity_name} slutade uppt\u00e4cka ljus",
+ "no_motion": "{entity_name} slutade uppt\u00e4cka r\u00f6relse",
+ "no_problem": "{entity_name} slutade uppt\u00e4cka problem",
+ "no_smoke": "{entity_name} slutade detektera r\u00f6k",
+ "no_sound": "{entity_name} slutade uppt\u00e4cka ljud",
+ "no_vibration": "{entity_name} slutade uppt\u00e4cka vibrationer",
+ "not_bat_low": "{entity_name} batteri normalt",
+ "not_cold": "{entity_name} blev inte kall",
+ "not_connected": "{entity_name} fr\u00e5nkopplad",
+ "not_hot": "{entity_name} blev inte varm",
+ "not_locked": "{entity_name} ol\u00e5st",
+ "not_moist": "{entity_name} blev torr",
+ "not_moving": "{entity_name} slutade r\u00f6ra sig",
+ "not_occupied": "{entity_name} blev inte upptagen",
+ "not_opened": "{entity_name} st\u00e4ngd",
+ "not_plugged_in": "{entity_name} urkopplad",
+ "not_powered": "{entity_name} inte str\u00f6mf\u00f6rd",
+ "not_present": "{entity_name} inte n\u00e4rvarande",
+ "not_unsafe": "{entity_name} blev s\u00e4ker",
+ "occupied": "{entity_name} blev upptagen",
+ "opened": "{entity_name} \u00f6ppnades",
+ "plugged_in": "{entity_name} ansluten",
+ "powered": "{entity_name} str\u00f6mf\u00f6rd",
+ "present": "{entity_name} n\u00e4rvarande",
+ "problem": "{entity_name} b\u00f6rjade uppt\u00e4cka problem",
+ "smoke": "{entity_name} b\u00f6rjade detektera r\u00f6k",
+ "sound": "{entity_name} b\u00f6rjade uppt\u00e4cka ljud",
+ "turned_off": "{entity_name} st\u00e4ngdes av",
+ "turned_on": "{entity_name} slogs p\u00e5",
+ "unsafe": "{entity_name} blev os\u00e4ker",
+ "vibration": "{entity_name} b\u00f6rjade detektera vibrationer"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py
index 63f84b657c1..cb98ec90b5d 100644
--- a/homeassistant/components/binary_sensor/device_condition.py
+++ b/homeassistant/components/binary_sensor/device_condition.py
@@ -1,4 +1,4 @@
-"""Implemenet device conditions for binary sensor."""
+"""Implement device conditions for binary sensor."""
from typing import Dict, List
import voluptuous as vol
diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py
index bc8394d51a5..6a8651be6dc 100644
--- a/homeassistant/components/bitcoin/sensor.py
+++ b/homeassistant/components/bitcoin/sensor.py
@@ -1,4 +1,4 @@
-"""Bitcoin information service that uses blockchain.info."""
+"""Bitcoin information service that uses blockchain.com."""
from datetime import timedelta
import logging
@@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
-ATTRIBUTION = "Data provided by blockchain.info"
+ATTRIBUTION = "Data provided by blockchain.com"
DEFAULT_CURRENCY = "USD"
@@ -168,7 +168,7 @@ class BitcoinData:
self.ticker = None
def update(self):
- """Get the latest data from blockchain.info."""
+ """Get the latest data from blockchain.com."""
self.stats = statistics.get()
self.ticker = exchangerates.get_ticker()
diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json
index f57e91a9262..324abf792df 100644
--- a/homeassistant/components/blockchain/manifest.json
+++ b/homeassistant/components/blockchain/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "blockchain",
- "name": "Blockchain.info",
+ "name": "Blockchain.com",
"documentation": "https://www.home-assistant.io/integrations/blockchain",
"requirements": ["python-blockchain-api==0.0.2"],
"dependencies": [],
diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py
index 6d17484bdd7..acf86957957 100644
--- a/homeassistant/components/blockchain/sensor.py
+++ b/homeassistant/components/blockchain/sensor.py
@@ -1,4 +1,4 @@
-"""Support for Blockchain.info sensors."""
+"""Support for Blockchain.com sensors."""
from datetime import timedelta
import logging
@@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
-ATTRIBUTION = "Data provided by blockchain.info"
+ATTRIBUTION = "Data provided by blockchain.com"
CONF_ADDRESSES = "addresses"
@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Blockchain.info sensors."""
+ """Set up the Blockchain.com sensors."""
addresses = config.get(CONF_ADDRESSES)
name = config.get(CONF_NAME)
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class BlockchainSensor(Entity):
- """Representation of a Blockchain.info sensor."""
+ """Representation of a Blockchain.com sensor."""
def __init__(self, name, addresses):
"""Initialize the sensor."""
diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py
index 04ba21555d4..db5c65eab8b 100644
--- a/homeassistant/components/bluesound/media_player.py
+++ b/homeassistant/components/bluesound/media_player.py
@@ -421,7 +421,7 @@ class BluesoundPlayer(MediaPlayerDevice):
# sync_status. We will force an update if the player is
# grouped this isn't a foolproof solution. A better
# solution would be to fetch sync_status more often when
- # the device is playing. This would solve alot of
+ # the device is playing. This would solve a lot of
# problems. This change will be done when the
# communication is moved to a separate library
await self.force_update_sync_status()
diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py
index 65c87890242..43430f724bb 100644
--- a/homeassistant/components/bme680/sensor.py
+++ b/homeassistant/components/bme680/sensor.py
@@ -1,7 +1,7 @@
"""Support for BME680 Sensor over SMBus."""
import logging
import threading
-from time import sleep, time
+from time import monotonic, sleep
import bme680 # pylint: disable=import-error
from smbus import SMBus # pylint: disable=import-error
@@ -240,15 +240,15 @@ class BME680Handler:
# Pause to allow initial data read for device validation.
sleep(1)
- start_time = time()
- curr_time = time()
+ start_time = monotonic()
+ curr_time = monotonic()
burn_in_data = []
_LOGGER.info(
"Beginning %d second gas sensor burn in for Air Quality", burn_in_time
)
while curr_time - start_time < burn_in_time:
- curr_time = time()
+ curr_time = monotonic()
if self._sensor.get_sensor_data() and self._sensor.data.heat_stable:
gas_resistance = self._sensor.data.gas_resistance
burn_in_data.append(gas_resistance)
diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json
index 9e507a212b9..6b6311c6b0d 100644
--- a/homeassistant/components/bmw_connected_drive/manifest.json
+++ b/homeassistant/components/bmw_connected_drive/manifest.json
@@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
- "requirements": ["bimmer_connected==0.7.0"],
+ "requirements": ["bimmer_connected==0.7.1"],
"dependencies": [],
"codeowners": ["@gerard33"]
}
diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py
index 7d951968cb2..bd57e20edaa 100644
--- a/homeassistant/components/bom/sensor.py
+++ b/homeassistant/components/bom/sensor.py
@@ -112,7 +112,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if station is not None:
if zone_id and wmo_id:
_LOGGER.warning(
- "Using config %s, not %s and %s for BOM sensor",
+ "Using configuration %s, not %s and %s for BOM sensor",
CONF_STATION,
CONF_ZONE_ID,
CONF_WMO_ID,
@@ -281,7 +281,7 @@ def _get_bom_stations():
"""Return {CONF_STATION: (lat, lon)} for all stations, for auto-config.
This function does several MB of internet requests, so please use the
- caching version to minimise latency and hit-count.
+ caching version to minimize latency and hit-count.
"""
latlon = {}
with io.BytesIO() as file_obj:
diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json
index 1cb3efdd2cc..8bfa48b9195 100644
--- a/homeassistant/components/braviatv/manifest.json
+++ b/homeassistant/components/braviatv/manifest.json
@@ -2,7 +2,7 @@
"domain": "braviatv",
"name": "Sony Bravia TV",
"documentation": "https://www.home-assistant.io/integrations/braviatv",
- "requirements": ["braviarc-homeassistant==0.3.7.dev0", "getmac==0.8.1"],
+ "requirements": ["bravia-tv==1.0", "getmac==0.8.1"],
"dependencies": ["configurator"],
"codeowners": ["@robbiet480"]
}
diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py
index ef0640c8e87..67feb8bfc48 100644
--- a/homeassistant/components/braviatv/media_player.py
+++ b/homeassistant/components/braviatv/media_player.py
@@ -2,7 +2,7 @@
import ipaddress
import logging
-from braviarc.braviarc import BraviaRC
+from bravia_tv import BraviaRC
from getmac import get_mac_address
import voluptuous as vol
diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py
index dd7c02b82ad..be6aa266491 100644
--- a/homeassistant/components/broadlink/__init__.py
+++ b/homeassistant/components/broadlink/__init__.py
@@ -75,18 +75,20 @@ def async_setup_service(hass, host, device):
async def _learn_command(call):
"""Learn a packet from remote."""
+
device = hass.data[DOMAIN][call.data[CONF_HOST]]
- try:
- auth = await hass.async_add_executor_job(device.auth)
- except socket.timeout:
- _LOGGER.error("Failed to connect to device, timeout")
- return
- if not auth:
- _LOGGER.error("Failed to connect to device")
- return
-
- await hass.async_add_executor_job(device.enter_learning)
+ for retry in range(DEFAULT_RETRY):
+ try:
+ await hass.async_add_executor_job(device.enter_learning)
+ break
+ except (socket.timeout, ValueError):
+ try:
+ await hass.async_add_executor_job(device.auth)
+ except socket.timeout:
+ if retry == DEFAULT_RETRY - 1:
+ _LOGGER.error("Failed to enter learning mode")
+ return
_LOGGER.info("Press the key you want Home Assistant to learn")
start_time = utcnow()
diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py
index 0b9d10b1e74..96698e5b02f 100644
--- a/homeassistant/components/broadlink/remote.py
+++ b/homeassistant/components/broadlink/remote.py
@@ -270,7 +270,7 @@ class BroadlinkRemote(RemoteDevice):
async def _async_learn_code(self, command, device, toggle, timeout):
"""Learn a code from a remote.
- Capture an aditional code for toggle commands.
+ Capture an additional code for toggle commands.
"""
try:
if not toggle:
diff --git a/homeassistant/components/brother/.translations/es-419.json b/homeassistant/components/brother/.translations/es-419.json
new file mode 100644
index 00000000000..49b77b829b5
--- /dev/null
+++ b/homeassistant/components/brother/.translations/es-419.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Impresora Brother"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/brother/.translations/fr.json b/homeassistant/components/brother/.translations/fr.json
index 99d49cc3bd8..788d0c74003 100644
--- a/homeassistant/components/brother/.translations/fr.json
+++ b/homeassistant/components/brother/.translations/fr.json
@@ -9,6 +9,7 @@
"snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.",
"wrong_host": "Nom d'h\u00f4te ou adresse IP invalide."
},
+ "flow_title": "Imprimante Brother: {model} {serial_number}",
"step": {
"user": {
"data": {
@@ -21,7 +22,9 @@
"zeroconf_confirm": {
"data": {
"type": "Type d'imprimante"
- }
+ },
+ "description": "Voulez-vous ajouter l'imprimante Brother {model} avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant ?",
+ "title": "Imprimante Brother d\u00e9couverte"
}
},
"title": "Imprimante Brother"
diff --git a/homeassistant/components/brother/.translations/hu.json b/homeassistant/components/brother/.translations/hu.json
index a0e83450b37..1907d65f289 100644
--- a/homeassistant/components/brother/.translations/hu.json
+++ b/homeassistant/components/brother/.translations/hu.json
@@ -1,7 +1,24 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ez a nyomtat\u00f3 m\u00e1r konfigur\u00e1lva van.",
+ "unsupported_model": "Ez a nyomtat\u00f3modell nem t\u00e1mogatott."
+ },
+ "error": {
+ "connection_error": "Csatlakoz\u00e1si hiba.",
+ "snmp_error": "Az SNMP szerver ki van kapcsolva, vagy a nyomtat\u00f3 nem t\u00e1mogatott.",
+ "wrong_host": "\u00c9rv\u00e9nytelen \u00e1llom\u00e1sn\u00e9v vagy IP-c\u00edm."
+ },
"flow_title": "Brother nyomtat\u00f3: {model} {serial_number}",
"step": {
+ "user": {
+ "data": {
+ "host": "Nyomtat\u00f3 \u00e1llom\u00e1sneve vagy IP-c\u00edme",
+ "type": "A nyomtat\u00f3 t\u00edpusa"
+ },
+ "description": "A Brother nyomtat\u00f3 integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha probl\u00e9m\u00e1id vannak a konfigur\u00e1ci\u00f3val, l\u00e1togass el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/brother",
+ "title": "Brother nyomtat\u00f3"
+ },
"zeroconf_confirm": {
"data": {
"type": "A nyomtat\u00f3 t\u00edpusa"
diff --git a/homeassistant/components/brother/.translations/nl.json b/homeassistant/components/brother/.translations/nl.json
index ed7d3980f47..c72aab46801 100644
--- a/homeassistant/components/brother/.translations/nl.json
+++ b/homeassistant/components/brother/.translations/nl.json
@@ -1,18 +1,32 @@
{
"config": {
"abort": {
+ "already_configured": "Deze printer is al geconfigureerd.",
"unsupported_model": "Dit printermodel wordt niet ondersteund."
},
"error": {
"connection_error": "Verbindingsfout.",
+ "snmp_error": "SNMP-server uitgeschakeld of printer wordt niet ondersteund.",
"wrong_host": "Ongeldige hostnaam of IP-adres."
},
+ "flow_title": "Brother Printer: {model} {serial_number}",
"step": {
"user": {
"data": {
- "host": "Printerhostnaam of IP-adres"
- }
+ "host": "Printerhostnaam of IP-adres",
+ "type": "Type printer"
+ },
+ "description": "Zet Brother printerintegratie op. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/brother",
+ "title": "Brother Printer"
+ },
+ "zeroconf_confirm": {
+ "data": {
+ "type": "Type printer"
+ },
+ "description": "Wilt u het Brother Printer {model} met serienummer {serial_number}' toevoegen aan Home Assistant?",
+ "title": "Ontdekte Brother Printer"
}
- }
+ },
+ "title": "Brother Printer"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/brother/.translations/sl.json b/homeassistant/components/brother/.translations/sl.json
index 99caf69a86f..d22f128ffbe 100644
--- a/homeassistant/components/brother/.translations/sl.json
+++ b/homeassistant/components/brother/.translations/sl.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_configured": "Ta tiskalnik je \u017ee konfiguriran.",
"unsupported_model": "Ta model tiskalnika ni podprt."
},
"error": {
@@ -8,6 +9,7 @@
"snmp_error": "Stre\u017enik SNMP je izklopljen ali tiskalnik ni podprt.",
"wrong_host": "Neveljavno ime gostitelja ali IP naslov."
},
+ "flow_title": "Tiskalnik Brother: {model} {serial_number}",
"step": {
"user": {
"data": {
@@ -16,6 +18,13 @@
},
"description": "Nastavite integracijo tiskalnika Brother. \u010ce imate te\u017eave s konfiguracijo, pojdite na: https://www.home-assistant.io/integrations/brother",
"title": "Brother Tiskalnik"
+ },
+ "zeroconf_confirm": {
+ "data": {
+ "type": "Vrsta tiskalnika"
+ },
+ "description": "Ali \u017eelite dodati Brother tiskalnik {model} s serijsko \u0161tevilko ' {serial_number} ' v Home Assistant?",
+ "title": "Odkriti Brother tiskalniki"
}
},
"title": "Brother Tiskalnik"
diff --git a/homeassistant/components/brother/.translations/sv.json b/homeassistant/components/brother/.translations/sv.json
index 8661c3278bc..774863d4f08 100644
--- a/homeassistant/components/brother/.translations/sv.json
+++ b/homeassistant/components/brother/.translations/sv.json
@@ -1,11 +1,32 @@
{
"config": {
+ "abort": {
+ "already_configured": "Den h\u00e4r skrivaren \u00e4r redan konfigurerad.",
+ "unsupported_model": "Den h\u00e4r skrivarmodellen st\u00f6ds inte."
+ },
+ "error": {
+ "connection_error": "Anslutningsfel.",
+ "snmp_error": "SNMP-servern har st\u00e4ngts av eller s\u00e5 st\u00f6ds inte skrivaren.",
+ "wrong_host": "Ogiltigt v\u00e4rdnamn eller IP-adress."
+ },
+ "flow_title": "Brother-skrivare: {model} {serial_number}",
"step": {
+ "user": {
+ "data": {
+ "host": "Skrivarens v\u00e4rdnamn eller IP-adress",
+ "type": "Typ av skrivare"
+ },
+ "description": "St\u00e4ll in Brother-skrivarintegration. Om du har problem med konfigurationen g\u00e5r du till: https://www.home-assistant.io/integrations/brother",
+ "title": "Brother-skrivare"
+ },
"zeroconf_confirm": {
"data": {
"type": "Typ av skrivare"
- }
+ },
+ "description": "Vill du l\u00e4gga till Brother-skrivaren {model} med serienumret {serial_number} i Home Assistant?",
+ "title": "Uppt\u00e4ckte Brother-skrivare"
}
- }
+ },
+ "title": "Brother-skrivare"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json
index e63fb9b0d7c..51e6c3284ff 100644
--- a/homeassistant/components/brother/manifest.json
+++ b/homeassistant/components/brother/manifest.json
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/brother",
"dependencies": [],
"codeowners": ["@bieniu"],
- "requirements": ["brother==0.1.4"],
+ "requirements": ["brother==0.1.6"],
"zeroconf": ["_printer._tcp.local."],
"config_flow": true
}
diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py
index ad9dac1f727..7cdf69a0c33 100644
--- a/homeassistant/components/caldav/calendar.py
+++ b/homeassistant/components/caldav/calendar.py
@@ -193,27 +193,50 @@ class WebDavCalendarData:
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
+ start_of_today = dt.start_of_local_day()
+ start_of_tomorrow = dt.start_of_local_day() + timedelta(days=1)
+
# We have to retrieve the results for the whole day as the server
# won't return events that have already started
- results = self.calendar.date_search(
- dt.start_of_local_day(), dt.start_of_local_day() + timedelta(days=1)
- )
+ results = self.calendar.date_search(start_of_today, start_of_tomorrow)
+
+ # Create new events for each recurrence of an event that happens today.
+ # For recurring events, some servers return the original event with recurrence rules
+ # and they would not be properly parsed using their original start/end dates.
+ new_events = []
+ for event in results:
+ vevent = event.instance.vevent
+ for start_dt in vevent.getrruleset() or []:
+ _start_of_today = start_of_today
+ _start_of_tomorrow = start_of_tomorrow
+ if self.is_all_day(vevent):
+ start_dt = start_dt.date()
+ _start_of_today = _start_of_today.date()
+ _start_of_tomorrow = _start_of_tomorrow.date()
+ if _start_of_today <= start_dt < _start_of_tomorrow:
+ new_event = event.copy()
+ new_vevent = new_event.instance.vevent
+ if hasattr(new_vevent, "dtend"):
+ dur = new_vevent.dtend.value - new_vevent.dtstart.value
+ new_vevent.dtend.value = start_dt + dur
+ new_vevent.dtstart.value = start_dt
+ new_events.append(new_event)
+ elif _start_of_tomorrow <= start_dt:
+ break
+ vevents = [event.instance.vevent for event in results + new_events]
# dtstart can be a date or datetime depending if the event lasts a
# whole day. Convert everything to datetime to be able to sort it
- results.sort(key=lambda x: self.to_datetime(x.instance.vevent.dtstart.value))
+ vevents.sort(key=lambda x: self.to_datetime(x.dtstart.value))
vevent = next(
(
- event.instance.vevent
- for event in results
+ vevent
+ for vevent in vevents
if (
- self.is_matching(event.instance.vevent, self.search)
- and (
- not self.is_all_day(event.instance.vevent)
- or self.include_all_day
- )
- and not self.is_over(event.instance.vevent)
+ self.is_matching(vevent, self.search)
+ and (not self.is_all_day(vevent) or self.include_all_day)
+ and not self.is_over(vevent)
)
),
None,
@@ -223,7 +246,7 @@ class WebDavCalendarData:
if vevent is None:
_LOGGER.debug(
"No matching event found in the %d results for %s",
- len(results),
+ len(vevents),
self.calendar.name,
)
self.event = None
diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json
index 2f48fb5fc27..85dc005a6a8 100644
--- a/homeassistant/components/caldav/manifest.json
+++ b/homeassistant/components/caldav/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "caldav",
- "name": "CalDav",
+ "name": "CalDAV",
"documentation": "https://www.home-assistant.io/integrations/caldav",
"requirements": ["caldav==0.6.1"],
"dependencies": [],
diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py
index b02874780e5..647e54556c4 100644
--- a/homeassistant/components/camera/__init__.py
+++ b/homeassistant/components/camera/__init__.py
@@ -377,7 +377,7 @@ class Camera(Entity):
async def handle_async_mjpeg_stream(self, request):
"""Serve an HTTP MJPEG stream from the camera.
- This method can be overridden by camera plaforms to proxy
+ This method can be overridden by camera platforms to proxy
a direct stream from the camera.
"""
return await self.handle_async_still_stream(request, self.frame_interval)
diff --git a/homeassistant/components/cert_expiry/.translations/es-419.json b/homeassistant/components/cert_expiry/.translations/es-419.json
index 392dbf35f5a..e350faffcb3 100644
--- a/homeassistant/components/cert_expiry/.translations/es-419.json
+++ b/homeassistant/components/cert_expiry/.translations/es-419.json
@@ -1,5 +1,26 @@
{
"config": {
+ "abort": {
+ "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada"
+ },
+ "error": {
+ "certificate_error": "El certificado no pudo ser validado",
+ "certificate_fetch_failed": "No se puede recuperar el certificado de esta combinaci\u00f3n de host y puerto",
+ "connection_timeout": "Tiempo de espera al conectarse a este host",
+ "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada",
+ "resolve_failed": "Este host no puede resolverse",
+ "wrong_host": "El certificado no coincide con el nombre de host"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "El nombre de host del certificado",
+ "name": "El nombre del certificado",
+ "port": "El puerto del certificado"
+ },
+ "title": "Definir el certificado para probar"
+ }
+ },
"title": "Expiraci\u00f3n del certificado"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json
index 671cbfcd1ff..2e50a9f8cbc 100644
--- a/homeassistant/components/cert_expiry/.translations/pl.json
+++ b/homeassistant/components/cert_expiry/.translations/pl.json
@@ -1,13 +1,13 @@
{
"config": {
"abort": {
- "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana"
+ "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany."
},
"error": {
"certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu",
"certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu",
"connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z hostem.",
- "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana",
+ "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany.",
"resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107",
"wrong_host": "Certyfikat nie pasuje do nazwy hosta"
},
diff --git a/homeassistant/components/cert_expiry/.translations/sv.json b/homeassistant/components/cert_expiry/.translations/sv.json
new file mode 100644
index 00000000000..bdccf51b2cd
--- /dev/null
+++ b/homeassistant/components/cert_expiry/.translations/sv.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "host_port_exists": "Denna v\u00e4rd- och portkombination \u00e4r redan konfigurerad"
+ },
+ "error": {
+ "certificate_error": "Certifikatet kunde inte valideras",
+ "certificate_fetch_failed": "Kan inte h\u00e4mta certifikat fr\u00e5n denna v\u00e4rd- och portkombination",
+ "connection_timeout": "Timeout vid anslutning till den h\u00e4r v\u00e4rden",
+ "host_port_exists": "Denna v\u00e4rd- och portkombination \u00e4r redan konfigurerad",
+ "resolve_failed": "Denna v\u00e4rd kan inte resolveras",
+ "wrong_host": "Certifikatet matchar inte v\u00e4rdnamnet"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Certifikatets v\u00e4rdnamn",
+ "name": "Certifikatets namn",
+ "port": "Certifikatets port"
+ },
+ "title": "Definiera certifikatet som ska testas"
+ }
+ },
+ "title": "Certifikatets utg\u00e5ng"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py
index 14532aea65f..f3bd2f07d63 100644
--- a/homeassistant/components/cert_expiry/config_flow.py
+++ b/homeassistant/components/cert_expiry/config_flow.py
@@ -69,7 +69,7 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return False
async def async_step_user(self, user_input=None):
- """Step when user intializes a integration."""
+ """Step when user initializes a integration."""
self._errors = {}
if user_input is not None:
# set some defaults in case we need to return to the form
diff --git a/homeassistant/components/climate/.translations/es-419.json b/homeassistant/components/climate/.translations/es-419.json
new file mode 100644
index 00000000000..f3b861b9195
--- /dev/null
+++ b/homeassistant/components/climate/.translations/es-419.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_hvac_mode": "Cambiar el modo HVAC en {entity_name}"
+ },
+ "condition_type": {
+ "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/climate/.translations/hu.json b/homeassistant/components/climate/.translations/hu.json
new file mode 100644
index 00000000000..38d6cc68822
--- /dev/null
+++ b/homeassistant/components/climate/.translations/hu.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_hvac_mode": "F\u0171t\u00e9s- \u00e9s l\u00e9gtechnikai (HVAC) \u00fczemm\u00f3d m\u00f3dos\u00edt\u00e1sa a k\u00f6vetkez\u0151n: {entity_name}",
+ "set_preset_mode": "A(z) {entity_name} be\u00e1ll\u00edt\u00e1s\u00e1nak v\u00e1lt\u00e1sa"
+ },
+ "condition_type": {
+ "is_hvac_mode": "{entity_name} speci\u00e1lis f\u0171t\u00e9s, szell\u0151z\u00e9s \u00e9s l\u00e9gkondicion\u00e1l\u00e1s (HVAC) \u00fczemm\u00f3dra van be\u00e1ll\u00edtva",
+ "is_preset_mode": "A(z) {entity_name} el\u0151re be\u00e1ll\u00edtott m\u00f3dja van kiv\u00e1lasztva"
+ },
+ "trigger_type": {
+ "current_humidity_changed": "{entity_name} m\u00e9rt p\u00e1ratartalma megv\u00e1ltozott",
+ "current_temperature_changed": "{entity_name} m\u00e9rt h\u0151m\u00e9rs\u00e9klete megv\u00e1ltozott",
+ "hvac_mode_changed": "{entity_name} f\u0171t\u00e9s, szell\u0151z\u00e9s \u00e9s l\u00e9gkondicion\u00e1l\u00e1s (HVAC) \u00fczemm\u00f3d megv\u00e1ltozott"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/climate/.translations/sv.json b/homeassistant/components/climate/.translations/sv.json
new file mode 100644
index 00000000000..51fe0540549
--- /dev/null
+++ b/homeassistant/components/climate/.translations/sv.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_hvac_mode": "\u00c4ndra HVAC-l\u00e4ge p\u00e5 {entity_name}",
+ "set_preset_mode": "\u00c4ndra f\u00f6rinst\u00e4llning p\u00e5 {entity_name}"
+ },
+ "condition_type": {
+ "is_hvac_mode": "{entity_name} \u00e4r inst\u00e4lld p\u00e5 ett specifikt HVAC-l\u00e4ge",
+ "is_preset_mode": "{entity_name} \u00e4r inst\u00e4lld p\u00e5 ett specifikt f\u00f6rinst\u00e4llt l\u00e4ge"
+ },
+ "trigger_type": {
+ "current_humidity_changed": "{entity_name} uppm\u00e4tt fuktighet har \u00e4ndrats",
+ "current_temperature_changed": "{entity_name} uppm\u00e4tt temperatur har \u00e4ndrats",
+ "hvac_mode_changed": "{entity_name} HVAC-l\u00e4ge har \u00e4ndrats"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml
index 34e89d57346..815df57f342 100644
--- a/homeassistant/components/climate/services.yaml
+++ b/homeassistant/components/climate/services.yaml
@@ -30,7 +30,7 @@ set_temperature:
description: New target temperature for HVAC.
example: 25
target_temp_high:
- description: New target high tempereature for HVAC.
+ description: New target high temperature for HVAC.
example: 26
target_temp_low:
description: New target low temperature for HVAC.
diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
index 22df26cce4e..c69a3ed5739 100644
--- a/homeassistant/components/config/config_entries.py
+++ b/homeassistant/components/config/config_entries.py
@@ -8,6 +8,7 @@ from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES
from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView
from homeassistant.exceptions import Unauthorized
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView,
FlowManagerResourceView,
@@ -45,7 +46,9 @@ def _prepare_json(result):
if schema is None:
data["data_schema"] = []
else:
- data["data_schema"] = voluptuous_serialize.convert(schema)
+ data["data_schema"] = voluptuous_serialize.convert(
+ schema, custom_serializer=cv.custom_serializer
+ )
return data
diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py
index 458a9dd3ecb..a7993017116 100644
--- a/homeassistant/components/config/entity_registry.py
+++ b/homeassistant/components/config/entity_registry.py
@@ -68,6 +68,7 @@ async def websocket_get_entity(hass, connection, msg):
vol.Required("entity_id"): cv.entity_id,
# If passed in, we update value. Passing None will remove old value.
vol.Optional("name"): vol.Any(str, None),
+ vol.Optional("icon"): vol.Any(str, None),
vol.Optional("new_entity_id"): str,
# We only allow setting disabled_by user via API.
vol.Optional("disabled_by"): vol.Any("user", None),
@@ -88,11 +89,9 @@ async def websocket_update_entity(hass, connection, msg):
changes = {}
- if "name" in msg:
- changes["name"] = msg["name"]
-
- if "disabled_by" in msg:
- changes["disabled_by"] = msg["disabled_by"]
+ for key in ("name", "icon", "disabled_by"):
+ if key in msg:
+ changes[key] = msg[key]
if "new_entity_id" in msg and msg["new_entity_id"] != msg["entity_id"]:
changes["new_entity_id"] = msg["new_entity_id"]
@@ -151,5 +150,8 @@ def _entry_dict(entry):
"disabled_by": entry.disabled_by,
"entity_id": entry.entity_id,
"name": entry.name,
+ "icon": entry.icon,
"platform": entry.platform,
+ "original_name": entry.original_name,
+ "original_icon": entry.original_icon,
}
diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json
index 809db4ffecc..5d5db4b0741 100644
--- a/homeassistant/components/config/manifest.json
+++ b/homeassistant/components/config/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "config",
- "name": "Config",
+ "name": "Configuration",
"documentation": "https://www.home-assistant.io/integrations/config",
"requirements": [],
"dependencies": ["http"],
diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py
index 78333d96355..4d79b13355c 100644
--- a/homeassistant/components/configurator/__init__.py
+++ b/homeassistant/components/configurator/__init__.py
@@ -209,7 +209,7 @@ class Configurator:
entity_id = self._requests.pop(request_id)[0]
# If we remove the state right away, it will not be included with
- # the result fo the service call (current design limitation).
+ # the result of the service call (current design limitation).
# Instead, we will set it to configured to give as feedback but delete
# it shortly after so that it is deleted when the client updates.
self.hass.states.async_set(entity_id, STATE_CONFIGURED)
diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py
index 158a365981b..91031c141dd 100644
--- a/homeassistant/components/conversation/__init__.py
+++ b/homeassistant/components/conversation/__init__.py
@@ -131,9 +131,22 @@ class ConversationProcessView(http.HomeAssistantView):
"""Send a request for processing."""
hass = request.app["hass"]
- intent_result = await _async_converse(
- hass, data["text"], data.get("conversation_id"), self.context(request)
- )
+ try:
+ intent_result = await _async_converse(
+ hass, data["text"], data.get("conversation_id"), self.context(request)
+ )
+ except intent.IntentError as err:
+ _LOGGER.error("Error handling intent: %s", err)
+ return self.json(
+ {
+ "success": False,
+ "error": {
+ "code": str(err.__class__.__name__).lower(),
+ "message": str(err),
+ },
+ },
+ status_code=500,
+ )
return self.json(intent_result)
diff --git a/homeassistant/components/coolmaster/.translations/es-419.json b/homeassistant/components/coolmaster/.translations/es-419.json
new file mode 100644
index 00000000000..2bcdecb2aec
--- /dev/null
+++ b/homeassistant/components/coolmaster/.translations/es-419.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "off": "Puede ser apagado"
+ },
+ "title": "Configure los detalles de su conexi\u00f3n CoolMasterNet."
+ }
+ },
+ "title": "CoolMasterNet"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coolmaster/.translations/hu.json b/homeassistant/components/coolmaster/.translations/hu.json
new file mode 100644
index 00000000000..cbf055e2fba
--- /dev/null
+++ b/homeassistant/components/coolmaster/.translations/hu.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coolmaster/.translations/sv.json b/homeassistant/components/coolmaster/.translations/sv.json
new file mode 100644
index 00000000000..89e2ab32863
--- /dev/null
+++ b/homeassistant/components/coolmaster/.translations/sv.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "error": {
+ "connection_error": "Det gick inte att ansluta till CoolMasterNet-instansen. Kontrollera din v\u00e4rd.",
+ "no_units": "Det gick inte att hitta n\u00e5gra HVAC-enheter i CoolMasterNet-v\u00e4rden."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "cool": "St\u00f6d svalt l\u00e4ge",
+ "dry": "St\u00f6d torrl\u00e4ge",
+ "fan_only": "St\u00f6d endast fl\u00e4ktl\u00e4ge",
+ "heat": "St\u00f6d v\u00e4rmel\u00e4ge",
+ "heat_cool": "St\u00f6d automatiskt v\u00e4rme/kyl-l\u00e4ge",
+ "host": "V\u00e4rd",
+ "off": "Kan st\u00e4ngas av"
+ },
+ "title": "St\u00e4ll in dina CoolMasterNet-anslutningsdetaljer."
+ }
+ },
+ "title": "CoolMasterNet"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py
index e9cef562647..c267b283118 100644
--- a/homeassistant/components/coolmaster/config_flow.py
+++ b/homeassistant/components/coolmaster/config_flow.py
@@ -26,6 +26,7 @@ class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+ @core.callback
def _async_get_entry(self, data):
supported_modes = [
key for (key, value) in data.items() if key in AVAILABLE_MODES and value
diff --git a/homeassistant/components/cover/.translations/sv.json b/homeassistant/components/cover/.translations/sv.json
new file mode 100644
index 00000000000..906768d3eb3
--- /dev/null
+++ b/homeassistant/components/cover/.translations/sv.json
@@ -0,0 +1,20 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_closed": "{entity_name} \u00e4r st\u00e4ngd",
+ "is_closing": "{entity_name} st\u00e4ngs",
+ "is_open": "{entity_name} \u00e4r \u00f6ppen",
+ "is_opening": "{entity_name} \u00f6ppnas",
+ "is_position": "Aktuell position f\u00f6r {entity_name} \u00e4r",
+ "is_tilt_position": "Aktuell {entity_name} lutningsposition \u00e4r"
+ },
+ "trigger_type": {
+ "closed": "{entity_name} st\u00e4ngd",
+ "closing": "{entity_name} st\u00e4nger",
+ "opened": "{entity_name} \u00f6ppnades",
+ "opening": "{entity_name} \u00f6ppnas",
+ "position": "{entity_name} position \u00e4ndras",
+ "tilt_position": "{entity_name} lutningsposition \u00e4ndras"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/daikin/.translations/pl.json b/homeassistant/components/daikin/.translations/pl.json
index 5d5448a93db..3caea70c4de 100644
--- a/homeassistant/components/daikin/.translations/pl.json
+++ b/homeassistant/components/daikin/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
"device_fail": "Nieoczekiwany b\u0142\u0105d tworzenia urz\u0105dzenia.",
"device_timeout": "Przekroczono limit czasu \u0142\u0105czenia z urz\u0105dzeniem."
},
diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py
index ea0002d0ac3..247eb955154 100644
--- a/homeassistant/components/danfoss_air/sensor.py
+++ b/homeassistant/components/danfoss_air/sensor.py
@@ -48,10 +48,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
["Danfoss Air Remaining Filter", "%", ReadCommand.filterPercent, None],
["Danfoss Air Humidity", "%", ReadCommand.humidity, DEVICE_CLASS_HUMIDITY],
["Danfoss Air Fan Step", "%", ReadCommand.fan_step, None],
- ["Dandoss Air Exhaust Fan Speed", "RPM", ReadCommand.exhaust_fan_speed, None],
- ["Dandoss Air Supply Fan Speed", "RPM", ReadCommand.supply_fan_speed, None],
+ ["Danfoss Air Exhaust Fan Speed", "RPM", ReadCommand.exhaust_fan_speed, None],
+ ["Danfoss Air Supply Fan Speed", "RPM", ReadCommand.supply_fan_speed, None],
[
- "Dandoss Air Dial Battery",
+ "Danfoss Air Dial Battery",
"%",
ReadCommand.battery_percent,
DEVICE_CLASS_BATTERY,
diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json
index 8a9ae15a7c1..e690d597dce 100644
--- a/homeassistant/components/deconz/.translations/ca.json
+++ b/homeassistant/components/deconz/.translations/ca.json
@@ -66,16 +66,16 @@
},
"trigger_type": {
"remote_awakened": "Dispositiu despertat",
- "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives",
- "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament",
+ "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades",
+ "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament",
"remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut",
- "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives",
- "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives",
+ "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades",
+ "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades",
"remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat",
"remote_button_rotation_stopped": "La rotaci\u00f3 del bot\u00f3 \"{subtype}\" s'ha aturat",
"remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut",
"remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat",
- "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives",
+ "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades",
"remote_double_tap": "Dispositiu \"{subtype}\" tocat dues vegades",
"remote_double_tap_any_side": "Dispositiu tocat dues vegades a alguna cara",
"remote_falling": "Dispositiu en caiguda lliure",
@@ -108,7 +108,8 @@
"allow_clip_sensor": "Permet sensors deCONZ CLIP",
"allow_deconz_groups": "Permet grups de llums deCONZ"
},
- "description": "Configura la visibilitat dels tipus dels dispositius deCONZ"
+ "description": "Configura la visibilitat dels tipus dels dispositius deCONZ",
+ "title": "Opcions de deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json
index ed1f0b06e64..d1af7e1f4ba 100644
--- a/homeassistant/components/deconz/.translations/da.json
+++ b/homeassistant/components/deconz/.translations/da.json
@@ -108,7 +108,8 @@
"allow_clip_sensor": "Tillad deCONZ CLIP-sensorer",
"allow_deconz_groups": "Tillad deCONZ-lysgrupper"
},
- "description": "Konfigurer synligheden af deCONZ-enhedstyper"
+ "description": "Konfigurer synligheden af deCONZ-enhedstyper",
+ "title": "deCONZ-indstillinger"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json
index 479e645173b..c3ad3cd24c8 100644
--- a/homeassistant/components/deconz/.translations/de.json
+++ b/homeassistant/components/deconz/.translations/de.json
@@ -108,7 +108,8 @@
"allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen",
"allow_deconz_groups": "deCONZ-Lichtgruppen zulassen"
},
- "description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren"
+ "description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren",
+ "title": "deCONZ-Optionen"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json
index b3d9e00bfe6..756636ad90a 100644
--- a/homeassistant/components/deconz/.translations/en.json
+++ b/homeassistant/components/deconz/.translations/en.json
@@ -108,7 +108,8 @@
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
"allow_deconz_groups": "Allow deCONZ light groups"
},
- "description": "Configure visibility of deCONZ device types"
+ "description": "Configure visibility of deCONZ device types",
+ "title": "deCONZ options"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json
index 6f5513d9729..cfff05b1e02 100644
--- a/homeassistant/components/deconz/.translations/es.json
+++ b/homeassistant/components/deconz/.translations/es.json
@@ -108,7 +108,8 @@
"allow_clip_sensor": "Permitir sensores deCONZ CLIP",
"allow_deconz_groups": "Permitir grupos de luz deCONZ"
},
- "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ"
+ "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ",
+ "title": "Opciones deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json
index c900bdab6ab..214c887cc34 100644
--- a/homeassistant/components/deconz/.translations/fr.json
+++ b/homeassistant/components/deconz/.translations/fr.json
@@ -77,6 +77,7 @@
"remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9",
"remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9",
"remote_double_tap": "Appareil \"{subtype}\" tapot\u00e9 deux fois",
+ "remote_double_tap_any_side": "Appareil double tap\u00e9 de n\u2019importe quel c\u00f4t\u00e9",
"remote_falling": "Appareil en chute libre",
"remote_flip_180_degrees": "Dispositif retourn\u00e9 \u00e0 180 degr\u00e9s",
"remote_flip_90_degrees": "Dispositif retourn\u00e9 \u00e0 90 degr\u00e9s",
diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json
index f162130680c..c5bf9718127 100644
--- a/homeassistant/components/deconz/.translations/hu.json
+++ b/homeassistant/components/deconz/.translations/hu.json
@@ -2,13 +2,23 @@
"config": {
"abort": {
"already_configured": "A bridge m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "Az \u00e1tj\u00e1r\u00f3 konfigur\u00e1ci\u00f3s folyamata m\u00e1r folyamatban van.",
"no_bridges": "Nem tal\u00e1ltam deCONZ bridget",
- "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat"
+ "not_deconz_bridge": "Nem egy deCONZ \u00e1tj\u00e1r\u00f3",
+ "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat",
+ "updated_instance": "A deCONZ-p\u00e9ld\u00e1ny \u00faj \u00e1llom\u00e1sc\u00edmmel friss\u00edtve"
},
"error": {
"no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt"
},
+ "flow_title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 ({host})",
"step": {
+ "hassio_confirm": {
+ "data": {
+ "allow_clip_sensor": "Virtu\u00e1lis \u00e9rz\u00e9kel\u0151k import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se"
+ },
+ "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Hass.io kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel"
+ },
"init": {
"data": {
"host": "Hoszt",
@@ -32,12 +42,72 @@
},
"device_automation": {
"trigger_subtype": {
- "close": "Bez\u00e1r\u00e1s"
+ "both_buttons": "Mindk\u00e9t gomb",
+ "button_1": "Els\u0151 gomb",
+ "button_2": "M\u00e1sodik gomb",
+ "button_3": "Harmadik gomb",
+ "button_4": "Negyedik gomb",
+ "close": "Bez\u00e1r\u00e1s",
+ "dim_down": "S\u00f6t\u00e9t\u00edt",
+ "dim_up": "Vil\u00e1gos\u00edt",
+ "left": "Balra",
+ "open": "Nyitva",
+ "right": "Jobbra",
+ "side_1": "1. oldal",
+ "side_2": "2. oldal",
+ "side_3": "3. oldal",
+ "side_4": "4. oldal",
+ "side_5": "5. oldal",
+ "side_6": "6. oldal",
+ "turn_off": "Kikapcsolva",
+ "turn_on": "Bekapcsolva"
},
"trigger_type": {
+ "remote_awakened": "A k\u00e9sz\u00fcl\u00e9k fel\u00e9bredt",
+ "remote_button_double_press": "\" {subtype} \" gombra k\u00e9tszer kattintottak",
+ "remote_button_long_press": "A \" {subtype} \" gomb folyamatosan lenyomva",
+ "remote_button_long_release": "A \" {subtype} \" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve",
+ "remote_button_quadruple_press": "\" {subtype} \" gombra n\u00e9gyszer kattintottak",
+ "remote_button_quintuple_press": "\" {subtype} \" gombra \u00f6tsz\u00f6r kattintottak",
+ "remote_button_rotated": "A gomb elforgatva: \" {subtype} \"",
+ "remote_button_rotation_stopped": "A (z) \" {subtype} \" gomb forg\u00e1sa le\u00e1llt",
+ "remote_button_short_press": "\" {subtype} \" gomb lenyomva",
+ "remote_button_short_release": "\"{alt\u00edpus}\" gomb elengedve",
+ "remote_button_triple_press": "\" {subtype} \" gombra h\u00e1romszor kattintottak",
+ "remote_double_tap": "Az \" {subtype} \" eszk\u00f6z dupla kattint\u00e1sa",
"remote_double_tap_any_side": "A k\u00e9sz\u00fcl\u00e9k b\u00e1rmelyik oldal\u00e1n dupl\u00e1n koppint.",
+ "remote_falling": "K\u00e9sz\u00fcl\u00e9k szabades\u00e9sben",
"remote_flip_180_degrees": "180 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z",
- "remote_flip_90_degrees": "90 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z"
+ "remote_flip_90_degrees": "90 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z",
+ "remote_gyro_activated": "A k\u00e9sz\u00fcl\u00e9k meg lett r\u00e1zva",
+ "remote_moved": "Az eszk\u00f6z a \" {subtype} \"-lal felfel\u00e9 mozgatva",
+ "remote_moved_any_side": "A k\u00e9sz\u00fcl\u00e9k valamelyik oldal\u00e1val felfel\u00e9 mozogott",
+ "remote_rotate_from_side_1": "Az eszk\u00f6z a \"1. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
+ "remote_rotate_from_side_2": "Az eszk\u00f6z a \"2. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
+ "remote_rotate_from_side_3": "Az eszk\u00f6z a \"3. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
+ "remote_rotate_from_side_4": "Az eszk\u00f6z a \"4. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
+ "remote_rotate_from_side_5": "Az eszk\u00f6z a \"5. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
+ "remote_rotate_from_side_6": "Az eszk\u00f6z a \"6. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
+ "remote_turned_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val megegyez\u0151en fordult",
+ "remote_turned_counter_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val ellent\u00e9tes ir\u00e1nyban fordult"
+ }
+ },
+ "options": {
+ "step": {
+ "async_step_deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Enged\u00e9lyezze a deCONZ CLIP \u00e9rz\u00e9kel\u0151ket",
+ "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se"
+ },
+ "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Enged\u00e9lyezze a deCONZ CLIP \u00e9rz\u00e9kel\u0151ket",
+ "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se"
+ },
+ "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json
index 980409d6987..f6223cec6c1 100644
--- a/homeassistant/components/deconz/.translations/it.json
+++ b/homeassistant/components/deconz/.translations/it.json
@@ -108,7 +108,8 @@
"allow_clip_sensor": "Consentire sensori CLIP deCONZ",
"allow_deconz_groups": "Consentire gruppi luce deCONZ"
},
- "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ"
+ "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ",
+ "title": "Opzioni deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json
index d526d706a8b..1b72545bc09 100644
--- a/homeassistant/components/deconz/.translations/ko.json
+++ b/homeassistant/components/deconz/.translations/ko.json
@@ -108,7 +108,8 @@
"allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9",
"allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9"
},
- "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131"
+ "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131",
+ "title": "deCONZ \uc635\uc158"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json
index 4b04cfa03ce..42fd840524f 100644
--- a/homeassistant/components/deconz/.translations/lb.json
+++ b/homeassistant/components/deconz/.translations/lb.json
@@ -108,7 +108,8 @@
"allow_clip_sensor": "deCONZ Clip Sensoren erlaben",
"allow_deconz_groups": "deCONZ Luucht Gruppen erlaben"
},
- "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren"
+ "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren",
+ "title": "deCONZ Optiounen"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json
index c0ee391b0c7..585c09c5339 100644
--- a/homeassistant/components/deconz/.translations/nl.json
+++ b/homeassistant/components/deconz/.translations/nl.json
@@ -77,15 +77,21 @@
"remote_button_short_release": "\"{subtype}\" knop losgelaten",
"remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt",
"remote_double_tap": "Apparaat \"{subtype}\" dubbel getikt",
+ "remote_double_tap_any_side": "Apparaat dubbel getikt aan elke kant",
"remote_falling": "Apparaat in vrije val",
+ "remote_flip_180_degrees": "Apparaat 180 graden omgedraaid",
+ "remote_flip_90_degrees": "Apparaat 90 graden omgedraaid",
"remote_gyro_activated": "Apparaat geschud",
"remote_moved": "Apparaat verplaatst met \"{subtype}\" omhoog",
+ "remote_moved_any_side": "Apparaat gedraaid met elke kant naar boven",
"remote_rotate_from_side_1": "Apparaat gedraaid van \"zijde 1\" naar \"{subtype}\"\".",
"remote_rotate_from_side_2": "Apparaat gedraaid van \"zijde 2\" naar \"{subtype}\"\".",
"remote_rotate_from_side_3": "Apparaat gedraaid van \"zijde 3\" naar \" {subtype} \"",
"remote_rotate_from_side_4": "Apparaat gedraaid van \"zijde 4\" naar \" {subtype} \"",
"remote_rotate_from_side_5": "Apparaat gedraaid van \"zijde 5\" naar \" {subtype} \"",
- "remote_rotate_from_side_6": "Apparaat gedraaid van \"zijde 6\" naar \" {subtype} \""
+ "remote_rotate_from_side_6": "Apparaat gedraaid van \"zijde 6\" naar \" {subtype} \"",
+ "remote_turned_clockwise": "Apparaat met de klok mee gedraaid",
+ "remote_turned_counter_clockwise": "Apparaat tegen de klok in gedraaid"
}
},
"options": {
diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json
index d6133542c64..3387c993ae0 100644
--- a/homeassistant/components/deconz/.translations/no.json
+++ b/homeassistant/components/deconz/.translations/no.json
@@ -108,7 +108,8 @@
"allow_clip_sensor": "Tillat deCONZ CLIP-sensorer",
"allow_deconz_groups": "Tillat deCONZ lys grupper"
},
- "description": "Konfigurere synlighet av deCONZ enhetstyper"
+ "description": "Konfigurere synlighet av deCONZ enhetstyper",
+ "title": "deCONZ alternativer"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json
index df85e7b8d1d..d12e633bf23 100644
--- a/homeassistant/components/deconz/.translations/pl.json
+++ b/homeassistant/components/deconz/.translations/pl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Mostek jest ju\u017c skonfigurowany",
- "already_in_progress": "Konfigurowanie mostka jest ju\u017c w toku.",
+ "already_configured": "Mostek jest ju\u017c skonfigurowany.",
+ "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.",
"no_bridges": "Nie odkryto mostk\u00f3w deCONZ",
"not_deconz_bridge": "To nie jest mostek deCONZ",
"one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ",
@@ -108,7 +108,8 @@
"allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP",
"allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ"
},
- "description": "Skonfiguruj widoczno\u015b\u0107 typ\u00f3w urz\u0105dze\u0144 deCONZ"
+ "description": "Skonfiguruj widoczno\u015b\u0107 typ\u00f3w urz\u0105dze\u0144 deCONZ",
+ "title": "Opcje deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json
index 3c61e447bca..054c85f595a 100644
--- a/homeassistant/components/deconz/.translations/ru.json
+++ b/homeassistant/components/deconz/.translations/ru.json
@@ -15,7 +15,7 @@
"step": {
"hassio_confirm": {
"data": {
- "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432",
+ "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432",
"allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ"
},
"description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?",
@@ -34,7 +34,7 @@
},
"options": {
"data": {
- "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432",
+ "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432",
"allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ"
},
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 deCONZ"
@@ -98,17 +98,18 @@
"step": {
"async_step_deconz_devices": {
"data": {
- "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 deCONZ CLIP",
+ "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b deCONZ CLIP",
"allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ"
},
"deconz_devices": {
"data": {
- "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 deCONZ CLIP",
+ "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b deCONZ CLIP",
"allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ"
},
- "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ"
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json
index 02869dcf76e..3d74d6cb944 100644
--- a/homeassistant/components/deconz/.translations/sv.json
+++ b/homeassistant/components/deconz/.translations/sv.json
@@ -11,6 +11,7 @@
"error": {
"no_key": "Det gick inte att ta emot en API-nyckel"
},
+ "flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"hassio_confirm": {
"data": {
@@ -40,5 +41,75 @@
}
},
"title": "deCONZ Zigbee Gateway"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "B\u00e5da knapparna",
+ "button_1": "F\u00f6rsta knappen",
+ "button_2": "Andra knappen",
+ "button_3": "Tredje knappen",
+ "button_4": "Fj\u00e4rde knappen",
+ "close": "St\u00e4ng",
+ "dim_down": "Dimma ned",
+ "dim_up": "Dimma upp",
+ "left": "V\u00e4nster",
+ "open": "\u00d6ppen",
+ "right": "H\u00f6ger",
+ "side_1": "Sida 1",
+ "side_2": "Sida 2",
+ "side_3": "Sida 3",
+ "side_4": "Sida 4",
+ "side_5": "Sida 5",
+ "side_6": "Sida 6",
+ "turn_off": "St\u00e4ng av",
+ "turn_on": "Starta"
+ },
+ "trigger_type": {
+ "remote_awakened": "Enheten v\u00e4cktes",
+ "remote_button_double_press": "\"{subtype}\"-knappen dubbelklickades",
+ "remote_button_long_press": "\"{subtype}\"-knappen kontinuerligt nedtryckt",
+ "remote_button_long_release": "\"{subtype}\"-knappen sl\u00e4pptes efter ett l\u00e5ngttryck",
+ "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt",
+ "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt",
+ "remote_button_rotated": "Knappen roterade \"{subtype}\"",
+ "remote_button_rotation_stopped": "Knapprotationen \"{subtype}\" stoppades",
+ "remote_button_short_press": "\"{subtype}\"-knappen trycktes in",
+ "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt",
+ "remote_button_triple_press": "\"{subtype}\"-knappen trippelklickad",
+ "remote_double_tap": "Enheten \"{subtype}\" dubbeltryckt",
+ "remote_double_tap_any_side": "Enheten dubbeltryckt p\u00e5 valfri sida",
+ "remote_falling": "Enhet i fritt fall",
+ "remote_flip_180_degrees": "Enheten v\u00e4nd 180 grader",
+ "remote_flip_90_degrees": "Enheten v\u00e4nd 90 grader",
+ "remote_gyro_activated": "Enhet skakad",
+ "remote_moved": "Enheten flyttades med \"{subtype}\" upp",
+ "remote_moved_any_side": "Enheten flyttades med valfri sida upp\u00e5t",
+ "remote_rotate_from_side_1": "Enheten roterades fr\u00e5n \"sida 1\" till \"{subtype}\"",
+ "remote_rotate_from_side_2": "Enheten roterades fr\u00e5n \"sida 2\" till \"{subtype}\"",
+ "remote_rotate_from_side_3": "Enheten roterades fr\u00e5n \"sida 3\" till \"{subtype}\"",
+ "remote_rotate_from_side_4": "Enheten roterades fr\u00e5n \"sida 4\" till \"{subtype}\"",
+ "remote_rotate_from_side_5": "Enheten roterades fr\u00e5n \"sida 5\" till \"{subtype}\"",
+ "remote_rotate_from_side_6": "Enheten roterades fr\u00e5n \"sida 6\" till \"{subtype}\"",
+ "remote_turned_clockwise": "Enheten vriden medurs",
+ "remote_turned_counter_clockwise": "Enheten v\u00e4nde moturs"
+ }
+ },
+ "options": {
+ "step": {
+ "async_step_deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer",
+ "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper"
+ },
+ "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer",
+ "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper"
+ },
+ "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json
index 37b82cff29c..ce51a54ac77 100644
--- a/homeassistant/components/deconz/.translations/zh-Hans.json
+++ b/homeassistant/components/deconz/.translations/zh-Hans.json
@@ -52,5 +52,12 @@
"remote_rotate_from_side_5": "\u8bbe\u5907\u4ece\u201c\u7b2c 5 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d",
"remote_rotate_from_side_6": "\u8bbe\u5907\u4ece\u201c\u7b2c 6 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d"
}
+ },
+ "options": {
+ "step": {
+ "deconz_devices": {
+ "title": "deCONZ \u9009\u9879"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json
index 96ab68a8dbb..073ebd784c6 100644
--- a/homeassistant/components/deconz/.translations/zh-Hant.json
+++ b/homeassistant/components/deconz/.translations/zh-Hant.json
@@ -108,7 +108,8 @@
"allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668",
"allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44"
},
- "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b"
+ "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b",
+ "title": "deCONZ \u9078\u9805"
}
}
}
diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py
index 667eb6db075..2514a49f23c 100644
--- a/homeassistant/components/deconz/binary_sensor.py
+++ b/homeassistant/components/deconz/binary_sensor.py
@@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR
from .deconz_device import DeconzDevice
-from .gateway import DeconzEntityHandler, get_gateway_from_config_entry
+from .gateway import get_gateway_from_config_entry
ATTR_ORIENTATION = "orientation"
ATTR_TILTANGLE = "tiltangle"
@@ -23,8 +23,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor."""
gateway = get_gateway_from_config_entry(hass, config_entry)
- entity_handler = DeconzEntityHandler(gateway)
-
@callback
def async_add_sensor(sensors, new=True):
"""Add binary sensor from deCONZ."""
@@ -32,10 +30,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for sensor in sensors:
- if new and sensor.BINARY:
- new_sensor = DeconzBinarySensor(sensor, gateway)
- entity_handler.add_entity(new_sensor)
- entities.append(new_sensor)
+ if (
+ new
+ and sensor.BINARY
+ and (
+ gateway.option_allow_clip_sensor
+ or not sensor.type.startswith("CLIP")
+ )
+ and sensor.deconz_id not in gateway.deconz_ids.values()
+ ):
+ entities.append(DeconzBinarySensor(sensor, gateway))
async_add_entities(entities, True)
diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py
index ba1f1ce846a..34cc0e0b832 100644
--- a/homeassistant/components/deconz/climate.py
+++ b/homeassistant/components/deconz/climate.py
@@ -37,7 +37,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for sensor in sensors:
- if new and sensor.type in Thermostat.ZHATYPE:
+ if (
+ new
+ and sensor.type in Thermostat.ZHATYPE
+ and (
+ gateway.option_allow_clip_sensor
+ or not sensor.type.startswith("CLIP")
+ )
+ and sensor.deconz_id not in gateway.deconz_ids.values()
+ ):
entities.append(DeconzThermostat(sensor, gateway))
async_add_entities(entities, True)
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index 614d2378c88..3a38a67f0c6 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -1,5 +1,6 @@
"""Config flow to configure deCONZ component."""
import asyncio
+from pprint import pformat
from urllib.parse import urlparse
import async_timeout
@@ -26,6 +27,7 @@ from .const import (
DEFAULT_ALLOW_DECONZ_GROUPS,
DEFAULT_PORT,
DOMAIN,
+ LOGGER,
)
DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de"
@@ -93,6 +95,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
except (asyncio.TimeoutError, ResponseError):
self.bridges = []
+ LOGGER.debug("Discovered deCONZ gateways %s", pformat(self.bridges))
+
if len(self.bridges) == 1:
return await self.async_step_user(self.bridges[0])
@@ -121,6 +125,10 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Attempt to link with the deCONZ bridge."""
errors = {}
+ LOGGER.debug(
+ "Preparing linking with deCONZ gateway %s", pformat(self.deconz_config)
+ )
+
if user_input is not None:
session = aiohttp_client.async_get_clientsession(self.hass)
@@ -170,6 +178,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
):
return self.async_abort(reason="not_deconz_bridge")
+ LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info))
+
self.bridge_id = normalize_bridge_id(discovery_info[ssdp.ATTR_UPNP_SERIAL])
parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
@@ -196,6 +206,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
This flow is triggered by the discovery component.
"""
+ LOGGER.debug("deCONZ HASSIO discovery %s", pformat(user_input))
+
self.bridge_id = normalize_bridge_id(user_input[CONF_SERIAL])
await self.async_set_unique_id(self.bridge_id)
diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py
index 293e0d9719c..11dbd07e86a 100644
--- a/homeassistant/components/deconz/const.py
+++ b/homeassistant/components/deconz/const.py
@@ -1,7 +1,7 @@
"""Constants for the deCONZ component."""
import logging
-_LOGGER = logging.getLogger(__package__)
+LOGGER = logging.getLogger(__package__)
DOMAIN = "deconz"
diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py
index 06756bb49f6..b3dedf6cf00 100644
--- a/homeassistant/components/deconz/deconz_device.py
+++ b/homeassistant/components/deconz/deconz_device.py
@@ -63,17 +63,6 @@ class DeconzDevice(DeconzBase, Entity):
Daylight is a virtual sensor from deCONZ that should never be enabled by default.
"""
- if not self.gateway.option_allow_clip_sensor and self._device.type.startswith(
- "CLIP"
- ):
- return False
-
- if (
- not self.gateway.option_allow_deconz_groups
- and self._device.type == "LightGroup"
- ):
- return False
-
if self._device.type == "Daylight":
return False
@@ -81,13 +70,18 @@ class DeconzDevice(DeconzBase, Entity):
async def async_added_to_hass(self):
"""Subscribe to device events."""
- self._device.register_async_callback(self.async_update_callback)
+ self._device.register_callback(self.async_update_callback)
self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id
self.listeners.append(
async_dispatcher_connect(
self.hass, self.gateway.signal_reachable, self.async_update_callback
)
)
+ self.listeners.append(
+ async_dispatcher_connect(
+ self.hass, self.gateway.signal_remove_entity, self.async_remove_self
+ )
+ )
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
@@ -96,6 +90,15 @@ class DeconzDevice(DeconzBase, Entity):
for unsub_dispatcher in self.listeners:
unsub_dispatcher()
+ async def async_remove_self(self, deconz_ids: list) -> None:
+ """Schedule removal of this entity.
+
+ Called by signal_remove_entity scheduled by async_added_to_hass.
+ """
+ if self._device.deconz_id not in deconz_ids:
+ return
+ await self.async_remove()
+
@callback
def async_update_callback(self, force_update=False, ignore_update=False):
"""Update the device's state."""
diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py
index 98a85a707bd..1009ae4e54c 100644
--- a/homeassistant/components/deconz/deconz_event.py
+++ b/homeassistant/components/deconz/deconz_event.py
@@ -3,7 +3,7 @@ from homeassistant.const import CONF_EVENT, CONF_ID
from homeassistant.core import callback
from homeassistant.util import slugify
-from .const import _LOGGER, CONF_GESTURE
+from .const import CONF_GESTURE, LOGGER
from .deconz_device import DeconzBase
CONF_DECONZ_EVENT = "deconz_event"
@@ -21,11 +21,11 @@ class DeconzEvent(DeconzBase):
"""Register callback that will be used for signals."""
super().__init__(device, gateway)
- self._device.register_async_callback(self.async_update_callback)
+ self._device.register_callback(self.async_update_callback)
self.device_id = None
self.event_id = slugify(self._device.name)
- _LOGGER.debug("deCONZ event created: %s", self.event_id)
+ LOGGER.debug("deCONZ event created: %s", self.event_id)
@property
def device(self):
diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py
index 04452cc313c..0b69b82463c 100644
--- a/homeassistant/components/deconz/gateway.py
+++ b/homeassistant/components/deconz/gateway.py
@@ -9,24 +9,19 @@ from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
-from homeassistant.helpers.dispatcher import (
- async_dispatcher_connect,
- async_dispatcher_send,
-)
-from homeassistant.helpers.entity_registry import (
- DISABLED_CONFIG_ENTRY,
- async_get_registry,
-)
+from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
- _LOGGER,
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_DECONZ_GROUPS,
CONF_MASTER_GATEWAY,
DEFAULT_ALLOW_CLIP_SENSOR,
DEFAULT_ALLOW_DECONZ_GROUPS,
DOMAIN,
+ LOGGER,
NEW_DEVICE,
+ NEW_GROUP,
+ NEW_SENSOR,
SUPPORTED_PLATFORMS,
)
from .errors import AuthenticationRequired, CannotConnect
@@ -52,6 +47,9 @@ class DeconzGateway:
self.events = []
self.listeners = []
+ self._current_option_allow_clip_sensor = self.option_allow_clip_sensor
+ self._current_option_allow_deconz_groups = self.option_allow_deconz_groups
+
@property
def bridgeid(self) -> str:
"""Return the unique identifier of the gateway."""
@@ -103,7 +101,7 @@ class DeconzGateway:
raise ConfigEntryNotReady
except Exception as err: # pylint: disable=broad-except
- _LOGGER.error("Error connecting with deCONZ gateway: %s", err)
+ LOGGER.error("Error connecting with deCONZ gateway: %s", err)
return False
for component in SUPPORTED_PLATFORMS:
@@ -115,23 +113,64 @@ class DeconzGateway:
self.api.start()
- self.config_entry.add_update_listener(self.async_new_address)
- self.config_entry.add_update_listener(self.async_options_updated)
+ self.config_entry.add_update_listener(self.async_config_entry_updated)
return True
@staticmethod
- async def async_new_address(hass, entry) -> None:
- """Handle signals of gateway getting new address.
+ async def async_config_entry_updated(hass, entry) -> None:
+ """Handle signals of config entry being updated.
- This is a static method because a class method (bound method),
- can not be used with weak references.
+ This is a static method because a class method (bound method), can not be used with weak references.
+ Causes for this is either discovery updating host address or config entry options changing.
"""
gateway = get_gateway_from_config_entry(hass, entry)
if gateway.api.host != entry.data[CONF_HOST]:
gateway.api.close()
gateway.api.host = entry.data[CONF_HOST]
gateway.api.start()
+ return
+
+ await gateway.options_updated()
+
+ async def options_updated(self):
+ """Manage entities affected by config entry options."""
+ deconz_ids = []
+
+ if self._current_option_allow_clip_sensor != self.option_allow_clip_sensor:
+ self._current_option_allow_clip_sensor = self.option_allow_clip_sensor
+
+ sensors = [
+ sensor
+ for sensor in self.api.sensors.values()
+ if sensor.type.startswith("CLIP")
+ ]
+
+ if self.option_allow_clip_sensor:
+ self.async_add_device_callback(NEW_SENSOR, sensors)
+ else:
+ deconz_ids += [sensor.deconz_id for sensor in sensors]
+
+ if self._current_option_allow_deconz_groups != self.option_allow_deconz_groups:
+ self._current_option_allow_deconz_groups = self.option_allow_deconz_groups
+
+ groups = list(self.api.groups.values())
+
+ if self.option_allow_deconz_groups:
+ self.async_add_device_callback(NEW_GROUP, groups)
+ else:
+ deconz_ids += [group.deconz_id for group in groups]
+
+ if deconz_ids:
+ async_dispatcher_send(self.hass, self.signal_remove_entity, deconz_ids)
+
+ entity_registry = await self.hass.helpers.entity_registry.async_get_registry()
+
+ for entity_id, deconz_id in self.deconz_ids.items():
+ if deconz_id in deconz_ids and entity_registry.async_is_registered(
+ entity_id
+ ):
+ entity_registry.async_remove(entity_id)
@property
def signal_reachable(self) -> str:
@@ -144,24 +183,16 @@ class DeconzGateway:
self.available = available
async_dispatcher_send(self.hass, self.signal_reachable, True)
- @property
- def signal_options_update(self) -> str:
- """Event specific per deCONZ entry to signal new options."""
- return f"deconz-options-{self.bridgeid}"
-
- @staticmethod
- async def async_options_updated(hass, entry) -> None:
- """Triggered by config entry options updates."""
- gateway = get_gateway_from_config_entry(hass, entry)
-
- registry = await async_get_registry(hass)
- async_dispatcher_send(hass, gateway.signal_options_update, registry)
-
@callback
def async_signal_new_device(self, device_type) -> str:
"""Gateway specific event to signal new device."""
return NEW_DEVICE[device_type].format(self.bridgeid)
+ @property
+ def signal_remove_entity(self) -> str:
+ """Gateway specific event to signal removal of entity."""
+ return f"deconz-remove-{self.bridgeid}"
+
@callback
def async_add_device_callback(self, device_type, device) -> None:
"""Handle event of new device creation in deCONZ."""
@@ -221,44 +252,9 @@ async def get_gateway(
return deconz
except errors.Unauthorized:
- _LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST])
+ LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST])
raise AuthenticationRequired
except (asyncio.TimeoutError, errors.RequestError):
- _LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST])
+ LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST])
raise CannotConnect
-
-
-class DeconzEntityHandler:
- """Platform entity handler to help with updating disabled by."""
-
- def __init__(self, gateway) -> None:
- """Create an entity handler."""
- self.gateway = gateway
- self._entities = []
-
- gateway.listeners.append(
- async_dispatcher_connect(
- gateway.hass, gateway.signal_options_update, self.update_entity_registry
- )
- )
-
- @callback
- def add_entity(self, entity) -> None:
- """Add a new entity to handler."""
- self._entities.append(entity)
-
- @callback
- def update_entity_registry(self, entity_registry) -> None:
- """Update entity registry disabled by status."""
- for entity in self._entities:
-
- if entity.entity_registry_enabled_default != entity.enabled:
- disabled_by = None
-
- if entity.enabled:
- disabled_by = DISABLED_CONFIG_ENTRY
-
- entity_registry.async_update_entity(
- entity.registry_entry.entity_id, disabled_by=disabled_by
- )
diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py
index ee22c86c44a..f62f9315c49 100644
--- a/homeassistant/components/deconz/light.py
+++ b/homeassistant/components/deconz/light.py
@@ -30,7 +30,7 @@ from .const import (
SWITCH_TYPES,
)
from .deconz_device import DeconzDevice
-from .gateway import DeconzEntityHandler, get_gateway_from_config_entry
+from .gateway import get_gateway_from_config_entry
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -41,8 +41,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ lights and groups from a config entry."""
gateway = get_gateway_from_config_entry(hass, config_entry)
- entity_handler = DeconzEntityHandler(gateway)
-
@callback
def async_add_light(lights):
"""Add light from deCONZ."""
@@ -63,13 +61,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_group(groups):
"""Add group from deCONZ."""
+ if not gateway.option_allow_deconz_groups:
+ return
+
entities = []
for group in groups:
- if group.lights:
- new_group = DeconzGroup(group, gateway)
- entity_handler.add_entity(new_group)
- entities.append(new_group)
+ if group.lights and group.deconz_id not in gateway.deconz_ids.values():
+ entities.append(DeconzGroup(group, gateway))
async_add_entities(entities, True)
@@ -156,7 +155,7 @@ class DeconzLight(DeconzDevice, Light):
if ATTR_TRANSITION in kwargs:
data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10)
- elif "IKEA" in (self._device.manufacturer or ""):
+ elif "IKEA" in self._device.manufacturer:
data["transitiontime"] = 0
if ATTR_FLASH in kwargs:
diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index adac6f54493..425a44bf042 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==69"
+ "pydeconz==70"
],
"ssdp": [
{
diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py
index 81804dfb9f6..6b88c414243 100644
--- a/homeassistant/components/deconz/sensor.py
+++ b/homeassistant/components/deconz/sensor.py
@@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import (
from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR
from .deconz_device import DeconzDevice
from .deconz_event import DeconzEvent
-from .gateway import DeconzEntityHandler, get_gateway_from_config_entry
+from .gateway import get_gateway_from_config_entry
ATTR_CURRENT = "current"
ATTR_POWER = "power"
@@ -37,7 +37,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
batteries = set()
battery_handler = DeconzBatteryHandler(gateway)
- entity_handler = DeconzEntityHandler(gateway)
@callback
def async_add_sensor(sensors, new=True):
@@ -65,11 +64,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
new
and sensor.BINARY is False
and sensor.type not in Battery.ZHATYPE + Thermostat.ZHATYPE
+ and (
+ gateway.option_allow_clip_sensor
+ or not sensor.type.startswith("CLIP")
+ )
+ and sensor.deconz_id not in gateway.deconz_ids.values()
):
-
- new_sensor = DeconzSensor(sensor, gateway)
- entity_handler.add_entity(new_sensor)
- entities.append(new_sensor)
+ entities.append(DeconzSensor(sensor, gateway))
if sensor.battery is not None:
new_battery = DeconzBattery(sensor, gateway)
@@ -216,7 +217,7 @@ class DeconzSensorStateTracker:
"""Set up tracker."""
self.sensor = sensor
self.gateway = gateway
- sensor.register_async_callback(self.async_update_callback)
+ sensor.register_callback(self.async_update_callback)
@callback
def close(self):
diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py
index f1b19c79fce..c85fa8073a3 100644
--- a/homeassistant/components/deconz/services.py
+++ b/homeassistant/components/deconz/services.py
@@ -6,9 +6,9 @@ from homeassistant.helpers import config_validation as cv
from .config_flow import get_master_gateway
from .const import (
- _LOGGER,
CONF_BRIDGE_ID,
DOMAIN,
+ LOGGER,
NEW_GROUP,
NEW_LIGHT,
NEW_SCENE,
@@ -110,7 +110,7 @@ async def async_configure_service(hass, data):
try:
field = gateway.deconz_ids[entity_id] + field
except KeyError:
- _LOGGER.error("Could not find the entity %s", entity_id)
+ LOGGER.error("Could not find the entity %s", entity_id)
return
await gateway.api.request("put", field, json=data)
diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json
index b61ea6236da..52cd90e54a1 100644
--- a/homeassistant/components/deconz/strings.json
+++ b/homeassistant/components/deconz/strings.json
@@ -14,20 +14,9 @@
"title": "Link with deCONZ",
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button"
},
- "options": {
- "title": "Extra configuration options for deCONZ",
- "data": {
- "allow_clip_sensor": "Allow importing virtual sensors",
- "allow_deconz_groups": "Allow importing deCONZ groups"
- }
- },
"hassio_confirm": {
"title": "deCONZ Zigbee gateway via Hass.io add-on",
- "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?",
- "data": {
- "allow_clip_sensor": "Allow importing virtual sensors",
- "allow_deconz_groups": "Allow importing deCONZ groups"
- }
+ "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?"
}
},
"error": {
@@ -45,11 +34,12 @@
"options": {
"step": {
"deconz_devices": {
- "description": "Configure visibility of deCONZ device types",
"data": {
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
"allow_deconz_groups": "Allow deCONZ light groups"
- }
+ },
+ "description": "Configure visibility of deCONZ device types",
+ "title": "deCONZ options"
}
}
},
@@ -105,4 +95,4 @@
"side_6": "Side 6"
}
}
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py
index 7df87490c60..55309ea8b31 100644
--- a/homeassistant/components/deluge/sensor.py
+++ b/homeassistant/components/deluge/sensor.py
@@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
+ DATA_RATE_KILOBYTES_PER_SECOND,
STATE_IDLE,
)
from homeassistant.exceptions import PlatformNotReady
@@ -27,8 +28,8 @@ DHT_UPLOAD = 1000
DHT_DOWNLOAD = 1000
SENSOR_TYPES = {
"current_status": ["Status", None],
- "download_speed": ["Down Speed", "kB/s"],
- "upload_speed": ["Up Speed", "kB/s"],
+ "download_speed": ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND],
+ "upload_speed": ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/demo/.translations/ca.json b/homeassistant/components/demo/.translations/ca.json
index 944d358e739..a29718fea7a 100644
--- a/homeassistant/components/demo/.translations/ca.json
+++ b/homeassistant/components/demo/.translations/ca.json
@@ -1,5 +1,22 @@
{
"config": {
"title": "Demostraci\u00f3"
+ },
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "Entrada booleana opcional",
+ "int": "Entrada num\u00e8rica"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Selecci\u00f3 m\u00faltiple",
+ "select": "Selecciona una opci\u00f3",
+ "string": "Valor de cadena (string)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/da.json b/homeassistant/components/demo/.translations/da.json
index ef01fcb4f3c..fd2764e5ec9 100644
--- a/homeassistant/components/demo/.translations/da.json
+++ b/homeassistant/components/demo/.translations/da.json
@@ -1,5 +1,28 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "one": "en",
+ "other": "anden"
+ }
+ },
+ "options_1": {
+ "data": {
+ "bool": "Valgfri boolsk",
+ "int": "Numerisk input"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Multimarkering",
+ "select": "V\u00e6lg en mulighed",
+ "string": "Strengv\u00e6rdi"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/de.json b/homeassistant/components/demo/.translations/de.json
index ef01fcb4f3c..a600790d2fc 100644
--- a/homeassistant/components/demo/.translations/de.json
+++ b/homeassistant/components/demo/.translations/de.json
@@ -1,5 +1,22 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "Optionaler Boolescher Wert",
+ "int": "Numerische Eingabe"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Mehrfachauswahl",
+ "select": "W\u00e4hlen Sie eine Option",
+ "string": "String-Wert"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/en.json b/homeassistant/components/demo/.translations/en.json
index ef01fcb4f3c..e49671c88c8 100644
--- a/homeassistant/components/demo/.translations/en.json
+++ b/homeassistant/components/demo/.translations/en.json
@@ -1,5 +1,22 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "Optional boolean",
+ "int": "Numeric input"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Multiselect",
+ "select": "Select an option",
+ "string": "String value"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/es-419.json b/homeassistant/components/demo/.translations/es-419.json
new file mode 100644
index 00000000000..ef01fcb4f3c
--- /dev/null
+++ b/homeassistant/components/demo/.translations/es-419.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Demo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/es.json b/homeassistant/components/demo/.translations/es.json
index ef01fcb4f3c..73ed9809d65 100644
--- a/homeassistant/components/demo/.translations/es.json
+++ b/homeassistant/components/demo/.translations/es.json
@@ -1,5 +1,22 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "Booleano opcional",
+ "int": "Entrada num\u00e9rica"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Multiselecci\u00f3n",
+ "select": "Selecciona una opci\u00f3n",
+ "string": "Valor de cadena"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/it.json b/homeassistant/components/demo/.translations/it.json
index ef01fcb4f3c..7b299913c8e 100644
--- a/homeassistant/components/demo/.translations/it.json
+++ b/homeassistant/components/demo/.translations/it.json
@@ -1,5 +1,28 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "one": "uno",
+ "other": "altro"
+ }
+ },
+ "options_1": {
+ "data": {
+ "bool": "Valore booleano facoltativo",
+ "int": "Input numerico"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Selezione multipla",
+ "select": "Selezionare un'opzione",
+ "string": "Valore stringa"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/ko.json b/homeassistant/components/demo/.translations/ko.json
index d20943c7b36..efe69b575fb 100644
--- a/homeassistant/components/demo/.translations/ko.json
+++ b/homeassistant/components/demo/.translations/ko.json
@@ -1,5 +1,22 @@
{
"config": {
"title": "\ub370\ubaa8"
+ },
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "\ub17c\ub9ac \uc120\ud0dd",
+ "int": "\uc22b\uc790 \uc785\ub825"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "\ub2e4\uc911 \uc120\ud0dd",
+ "select": "\uc635\uc158 \uc120\ud0dd",
+ "string": "\ubb38\uc790\uc5f4 \uac12"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/lb.json b/homeassistant/components/demo/.translations/lb.json
index ef01fcb4f3c..d968b43af8b 100644
--- a/homeassistant/components/demo/.translations/lb.json
+++ b/homeassistant/components/demo/.translations/lb.json
@@ -1,5 +1,14 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "options_2": {
+ "data": {
+ "select": "Eng Optioun auswielen"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/nl.json b/homeassistant/components/demo/.translations/nl.json
index ef01fcb4f3c..cb932a0d9d6 100644
--- a/homeassistant/components/demo/.translations/nl.json
+++ b/homeassistant/components/demo/.translations/nl.json
@@ -1,5 +1,28 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "one": "Empty",
+ "other": ""
+ }
+ },
+ "options_1": {
+ "data": {
+ "bool": "Optioneel Boolean",
+ "int": "Numerieke invoer"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Meerkeuze selectie",
+ "select": "Kies een optie",
+ "string": "String waarde"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/no.json b/homeassistant/components/demo/.translations/no.json
index ef01fcb4f3c..a46606621b9 100644
--- a/homeassistant/components/demo/.translations/no.json
+++ b/homeassistant/components/demo/.translations/no.json
@@ -1,5 +1,22 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "Valgfri boolean",
+ "int": "Numerisk inndata"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Flervalg",
+ "select": "Velg et alternativ",
+ "string": "Strengverdi"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/pl.json b/homeassistant/components/demo/.translations/pl.json
index ef01fcb4f3c..f224d100929 100644
--- a/homeassistant/components/demo/.translations/pl.json
+++ b/homeassistant/components/demo/.translations/pl.json
@@ -1,5 +1,30 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "few": "kilka",
+ "many": "wiele",
+ "one": "jedena",
+ "other": "inne"
+ }
+ },
+ "options_1": {
+ "data": {
+ "bool": "Warto\u015b\u0107 logiczna",
+ "int": "Warto\u015b\u0107 numeryczna"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Wielokrotny wyb\u00f3r",
+ "select": "Wybierz opcj\u0119",
+ "string": "Warto\u015b\u0107 tekstowa"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/ru.json b/homeassistant/components/demo/.translations/ru.json
index 0438252a429..22ea3d2e196 100644
--- a/homeassistant/components/demo/.translations/ru.json
+++ b/homeassistant/components/demo/.translations/ru.json
@@ -1,5 +1,22 @@
{
"config": {
"title": "\u0414\u0435\u043c\u043e"
+ },
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "\u041b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439",
+ "int": "\u0427\u0438\u0441\u043b\u043e\u0432\u043e\u0439"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e",
+ "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u043e\u043f\u0446\u0438\u044e",
+ "string": "\u0421\u0442\u0440\u043e\u043a\u043e\u0432\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/sl.json b/homeassistant/components/demo/.translations/sl.json
index ef01fcb4f3c..b67d4d56fb1 100644
--- a/homeassistant/components/demo/.translations/sl.json
+++ b/homeassistant/components/demo/.translations/sl.json
@@ -1,5 +1,30 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "few": "prazni",
+ "one": "prazen",
+ "other": "prazni",
+ "two": "prazna"
+ }
+ },
+ "options_1": {
+ "data": {
+ "bool": "Izbirna logi\u010dna vrednost",
+ "int": "\u0160tevil\u010dni vnos"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Multiselect",
+ "select": "Izberite mo\u017enost",
+ "string": "Vrednost niza"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/sv.json b/homeassistant/components/demo/.translations/sv.json
new file mode 100644
index 00000000000..4c5f477cc1c
--- /dev/null
+++ b/homeassistant/components/demo/.translations/sv.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "title": "Demo"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "one": "Tom",
+ "other": "Tomma"
+ }
+ },
+ "options_1": {
+ "data": {
+ "bool": "Valfritt boolesk",
+ "int": "Numerisk inmatning"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Flera val",
+ "select": "V\u00e4lj ett alternativ",
+ "string": "Str\u00e4ngv\u00e4rde"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/zh-Hans.json b/homeassistant/components/demo/.translations/zh-Hans.json
new file mode 100644
index 00000000000..9155b5066c5
--- /dev/null
+++ b/homeassistant/components/demo/.translations/zh-Hans.json
@@ -0,0 +1,19 @@
+{
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "\u5e03\u5c14\u9009\u9879",
+ "int": "\u6570\u503c\u8f93\u5165"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "\u591a\u91cd\u9009\u62e9",
+ "select": "\u9009\u62e9\u9009\u9879",
+ "string": "\u5b57\u7b26\u4e32\u503c"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/.translations/zh-Hant.json b/homeassistant/components/demo/.translations/zh-Hant.json
index cfb0fced0c2..7f6ac42d609 100644
--- a/homeassistant/components/demo/.translations/zh-Hant.json
+++ b/homeassistant/components/demo/.translations/zh-Hant.json
@@ -1,5 +1,22 @@
{
"config": {
"title": "\u5c55\u793a"
+ },
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "\u9078\u9805\u5e03\u6797",
+ "int": "\u6578\u503c\u8f38\u5165"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "\u591a\u91cd\u9078\u64c7",
+ "select": "\u9078\u64c7\u9078\u9805",
+ "string": "\u5b57\u4e32\u503c"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py
index e6b275920c8..1f3975d0241 100644
--- a/homeassistant/components/demo/config_flow.py
+++ b/homeassistant/components/demo/config_flow.py
@@ -1,16 +1,97 @@
"""Config flow to configure demo component."""
+import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
# pylint: disable=unused-import
from . import DOMAIN
+CONF_STRING = "string"
+CONF_BOOLEAN = "bool"
+CONF_INT = "int"
+CONF_SELECT = "select"
+CONF_MULTISELECT = "multi"
+
class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Demo configuration flow."""
VERSION = 1
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return OptionsFlowHandler(config_entry)
+
async def async_step_import(self, import_info):
"""Set the config entry up from yaml."""
return self.async_create_entry(title="Demo", data={})
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle options."""
+
+ def __init__(self, config_entry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+ self.options = dict(config_entry.options)
+
+ async def async_step_init(self, user_input=None):
+ """Manage the options."""
+ return await self.async_step_options_1()
+
+ async def async_step_options_1(self, user_input=None):
+ """Manage the options."""
+ if user_input is not None:
+ self.options.update(user_input)
+ return await self.async_step_options_2()
+
+ return self.async_show_form(
+ step_id="options_1",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_BOOLEAN,
+ default=self.config_entry.options.get(CONF_BOOLEAN, False),
+ ): bool,
+ vol.Optional(
+ CONF_INT, default=self.config_entry.options.get(CONF_INT, 10),
+ ): int,
+ }
+ ),
+ )
+
+ async def async_step_options_2(self, user_input=None):
+ """Manage the options 2."""
+ if user_input is not None:
+ self.options.update(user_input)
+ return await self._update_options()
+
+ return self.async_show_form(
+ step_id="options_2",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_STRING,
+ default=self.config_entry.options.get(CONF_STRING, "Default",),
+ ): str,
+ vol.Optional(
+ CONF_SELECT,
+ default=self.config_entry.options.get(CONF_SELECT, "default"),
+ ): vol.In(["default", "other"]),
+ vol.Optional(
+ CONF_MULTISELECT,
+ default=self.config_entry.options.get(
+ CONF_MULTISELECT, ["default"]
+ ),
+ ): cv.multi_select({"default": "Default", "other": "Other"}),
+ }
+ ),
+ )
+
+ async def _update_options(self):
+ """Update config entry options."""
+ return self.async_create_entry(title="", data=self.options)
diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json
index a2c0103280b..33f3b4229dc 100644
--- a/homeassistant/components/demo/strings.json
+++ b/homeassistant/components/demo/strings.json
@@ -1,5 +1,25 @@
{
"config": {
"title": "Demo"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {}
+ },
+ "options_1": {
+ "data": {
+ "bool": "Optional boolean",
+ "int": "Numeric input"
+ }
+ },
+ "options_2": {
+ "data": {
+ "string": "String value",
+ "select": "Select an option",
+ "multi": "Multiselect"
+ }
+ }
+ }
}
}
diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json
index 1ecbe5b939f..1387875c02d 100644
--- a/homeassistant/components/denonavr/manifest.json
+++ b/homeassistant/components/denonavr/manifest.json
@@ -2,7 +2,7 @@
"domain": "denonavr",
"name": "Denon AVR Network Receivers",
"documentation": "https://www.home-assistant.io/integrations/denonavr",
- "requirements": ["denonavr==0.7.11"],
+ "requirements": ["denonavr==0.7.12"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py
index 350d065f9d9..b14592d1b78 100644
--- a/homeassistant/components/denonavr/media_player.py
+++ b/homeassistant/components/denonavr/media_player.py
@@ -30,6 +30,7 @@ from homeassistant.const import (
CONF_TIMEOUT,
CONF_ZONE,
ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
STATE_OFF,
STATE_ON,
STATE_PAUSED,
@@ -201,6 +202,10 @@ class DenonDevice(MediaPlayerDevice):
def signal_handler(self, data):
"""Handle domain-specific signal by calling appropriate method."""
entity_ids = data[ATTR_ENTITY_ID]
+
+ if entity_ids == ENTITY_MATCH_NONE:
+ return
+
if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids:
params = {
key: value
diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py
index 56e087f0e5f..95b3fc9fdb3 100644
--- a/homeassistant/components/device_automation/__init__.py
+++ b/homeassistant/components/device_automation/__init__.py
@@ -1,5 +1,6 @@
"""Helpers for device automations."""
import asyncio
+from functools import wraps
import logging
from types import ModuleType
from typing import Any, List, MutableMapping
@@ -14,7 +15,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.loader import IntegrationNotFound, async_get_integration
-from .exceptions import InvalidDeviceAutomationConfig
+from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig
# mypy: allow-untyped-calls, allow-untyped-defs
@@ -117,6 +118,10 @@ async def _async_get_device_automations(hass, automation_type, device_id):
domains = set()
automations: List[MutableMapping[str, Any]] = []
device = device_registry.async_get(device_id)
+
+ if device is None:
+ raise DeviceNotFound
+
for entry_id in device.config_entries:
config_entry = hass.config_entries.async_get_entry(entry_id)
domains.add(config_entry.domain)
@@ -173,6 +178,21 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom
return capabilities
+def handle_device_errors(func):
+ """Handle device automation errors."""
+
+ @wraps(func)
+ async def with_error_handling(hass, connection, msg):
+ try:
+ await func(hass, connection, msg)
+ except DeviceNotFound:
+ connection.send_error(
+ msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found"
+ )
+
+ return with_error_handling
+
+
@websocket_api.websocket_command(
{
vol.Required("type"): "device_automation/action/list",
@@ -180,6 +200,7 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom
}
)
@websocket_api.async_response
+@handle_device_errors
async def websocket_device_automation_list_actions(hass, connection, msg):
"""Handle request for device actions."""
device_id = msg["device_id"]
@@ -194,6 +215,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg):
}
)
@websocket_api.async_response
+@handle_device_errors
async def websocket_device_automation_list_conditions(hass, connection, msg):
"""Handle request for device conditions."""
device_id = msg["device_id"]
@@ -208,6 +230,7 @@ async def websocket_device_automation_list_conditions(hass, connection, msg):
}
)
@websocket_api.async_response
+@handle_device_errors
async def websocket_device_automation_list_triggers(hass, connection, msg):
"""Handle request for device triggers."""
device_id = msg["device_id"]
@@ -222,6 +245,7 @@ async def websocket_device_automation_list_triggers(hass, connection, msg):
}
)
@websocket_api.async_response
+@handle_device_errors
async def websocket_device_automation_get_action_capabilities(hass, connection, msg):
"""Handle request for device action capabilities."""
action = msg["action"]
@@ -238,6 +262,7 @@ async def websocket_device_automation_get_action_capabilities(hass, connection,
}
)
@websocket_api.async_response
+@handle_device_errors
async def websocket_device_automation_get_condition_capabilities(hass, connection, msg):
"""Handle request for device condition capabilities."""
condition = msg["condition"]
@@ -254,6 +279,7 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio
}
)
@websocket_api.async_response
+@handle_device_errors
async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg):
"""Handle request for device trigger capabilities."""
trigger = msg["trigger"]
diff --git a/homeassistant/components/device_automation/exceptions.py b/homeassistant/components/device_automation/exceptions.py
index 2f7c0df0187..ad92696cb94 100644
--- a/homeassistant/components/device_automation/exceptions.py
+++ b/homeassistant/components/device_automation/exceptions.py
@@ -4,3 +4,7 @@ from homeassistant.exceptions import HomeAssistantError
class InvalidDeviceAutomationConfig(HomeAssistantError):
"""When device automation config is invalid."""
+
+
+class DeviceNotFound(HomeAssistantError):
+ """When referenced device not found."""
diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py
index f6bb74edbec..a2dcd62db8c 100644
--- a/homeassistant/components/device_automation/toggle_entity.py
+++ b/homeassistant/components/device_automation/toggle_entity.py
@@ -74,10 +74,12 @@ ENTITY_TRIGGERS = [
},
]
+DEVICE_ACTION_TYPES = [CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]
+
ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
- vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]),
+ vol.Required(CONF_TYPE): vol.In(DEVICE_ACTION_TYPES),
}
)
diff --git a/homeassistant/components/device_tracker/.translations/es-419.json b/homeassistant/components/device_tracker/.translations/es-419.json
new file mode 100644
index 00000000000..cfbf7bcfe3e
--- /dev/null
+++ b/homeassistant/components/device_tracker/.translations/es-419.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_home": "{entity_name} est\u00e1 en casa",
+ "is_not_home": "{entity_name} no est\u00e1 en casa"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/device_tracker/.translations/hu.json b/homeassistant/components/device_tracker/.translations/hu.json
new file mode 100644
index 00000000000..7302f40df9e
--- /dev/null
+++ b/homeassistant/components/device_tracker/.translations/hu.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_home": "{entity_name} otthon van",
+ "is_not_home": "{entity_name} nincs otthon"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/device_tracker/.translations/sv.json b/homeassistant/components/device_tracker/.translations/sv.json
new file mode 100644
index 00000000000..70287ad318a
--- /dev/null
+++ b/homeassistant/components/device_tracker/.translations/sv.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_home": "{entity_name} \u00e4r hemma",
+ "is_not_home": "{entity_name} \u00e4r inte hemma"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py
index 6c5cacac591..059c51989fe 100644
--- a/homeassistant/components/device_tracker/config_entry.py
+++ b/homeassistant/components/device_tracker/config_entry.py
@@ -61,6 +61,11 @@ class BaseTrackerEntity(Entity):
class TrackerEntity(BaseTrackerEntity):
"""Represent a tracked device."""
+ @property
+ def force_update(self):
+ """All updates need to be written to the state machine."""
+ return True
+
@property
def location_accuracy(self):
"""Return the location accuracy of the device.
diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py
index da3c945bc86..08bbed12519 100644
--- a/homeassistant/components/device_tracker/legacy.py
+++ b/homeassistant/components/device_tracker/legacy.py
@@ -197,7 +197,6 @@ class DeviceTracker:
self.track_new,
dev_id,
mac,
- (host_name or dev_id).replace("_", " "),
picture=picture,
icon=icon,
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE),
@@ -342,7 +341,7 @@ class Device(RestoreEntity):
@property
def name(self):
"""Return the name of the entity."""
- return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
+ return self.config_name or self.host_name or self.dev_id or DEVICE_DEFAULT_NAME
@property
def state(self):
@@ -393,7 +392,7 @@ class Device(RestoreEntity):
"""Mark the device as seen."""
self.source_type = source_type
self.last_seen = dt_util.utcnow()
- self.host_name = host_name
+ self.host_name = host_name or self.host_name
self.location_name = location_name
self.consider_home = consider_home or self.consider_home
diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json
index adf05621a2c..b0f0f8bb5eb 100644
--- a/homeassistant/components/directv/manifest.json
+++ b/homeassistant/components/directv/manifest.json
@@ -2,7 +2,7 @@
"domain": "directv",
"name": "DirecTV",
"documentation": "https://www.home-assistant.io/integrations/directv",
- "requirements": ["directpy==0.5"],
+ "requirements": ["directpy==0.6"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json
index a9aeea27aef..e496ad0d532 100644
--- a/homeassistant/components/discord/manifest.json
+++ b/homeassistant/components/discord/manifest.json
@@ -2,7 +2,7 @@
"domain": "discord",
"name": "Discord",
"documentation": "https://www.home-assistant.io/integrations/discord",
- "requirements": ["discord.py==1.2.5"],
+ "requirements": ["discord.py==1.3.1"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py
index fa6b60d0c19..1e3ba840d6f 100644
--- a/homeassistant/components/dlna_dmr/media_player.py
+++ b/homeassistant/components/dlna_dmr/media_player.py
@@ -99,10 +99,10 @@ def catch_request_errors():
"""Call wrapper for decorator."""
@functools.wraps(func)
- def wrapper(self, *args, **kwargs):
+ async def wrapper(self, *args, **kwargs):
"""Catch asyncio.TimeoutError, aiohttp.ClientError errors."""
try:
- return func(self, *args, **kwargs)
+ return await func(self, *args, **kwargs)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error during call %s", func.__name__)
diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py
index 9525f9e8ddf..65a32938140 100644
--- a/homeassistant/components/doods/image_processing.py
+++ b/homeassistant/components/doods/image_processing.py
@@ -285,7 +285,7 @@ class Doods(ImageProcessingEntity):
)
# Run detection
- start = time.time()
+ start = time.monotonic()
response = self._doods.detect(
image, dconfig=self._dconfig, detector_name=self._detector_name
)
@@ -293,7 +293,7 @@ class Doods(ImageProcessingEntity):
"doods detect: %s response: %s duration: %s",
self._dconfig,
response,
- time.time() - start,
+ time.monotonic() - start,
)
matches = {}
diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json
index 551af839b5c..1ac905feac2 100644
--- a/homeassistant/components/doods/manifest.json
+++ b/homeassistant/components/doods/manifest.json
@@ -2,7 +2,10 @@
"domain": "doods",
"name": "DOODS - Distributed Outside Object Detection Service",
"documentation": "https://www.home-assistant.io/integrations/doods",
- "requirements": ["pydoods==1.0.2", "pillow==6.2.1"],
+ "requirements": [
+ "pydoods==1.0.2",
+ "pillow==7.0.0"
+ ],
"dependencies": [],
"codeowners": []
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py
index d3374c8d02a..ab85c376469 100644
--- a/homeassistant/components/dovado/sensor.py
+++ b/homeassistant/components/dovado/sensor.py
@@ -6,7 +6,7 @@ import re
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_SENSORS
+from homeassistant.const import CONF_SENSORS, DATA_GIGABYTES
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -26,8 +26,13 @@ SENSORS = {
SENSOR_NETWORK: ("signal strength", "Network", None, "mdi:access-point-network"),
SENSOR_SIGNAL: ("signal strength", "Signal Strength", "%", "mdi:signal"),
SENSOR_SMS_UNREAD: ("sms unread", "SMS unread", "", "mdi:message-text-outline"),
- SENSOR_UPLOAD: ("traffic modem tx", "Sent", "GB", "mdi:cloud-upload"),
- SENSOR_DOWNLOAD: ("traffic modem rx", "Received", "GB", "mdi:cloud-download"),
+ SENSOR_UPLOAD: ("traffic modem tx", "Sent", DATA_GIGABYTES, "mdi:cloud-upload"),
+ SENSOR_DOWNLOAD: (
+ "traffic modem rx",
+ "Received",
+ DATA_GIGABYTES,
+ "mdi:cloud-download",
+ ),
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py
index 253e8409f1b..54c8e3e29b2 100644
--- a/homeassistant/components/dsmr/sensor.py
+++ b/homeassistant/components/dsmr/sensor.py
@@ -1,6 +1,5 @@
"""Support for Dutch Smart Meter (also known as Smartmeter or P1 port)."""
import asyncio
-from datetime import timedelta
from functools import partial
import logging
@@ -32,9 +31,6 @@ ICON_POWER = "mdi:flash"
ICON_POWER_FAILURE = "mdi:flash-off"
ICON_SWELL_SAG = "mdi:pulse"
-# Smart meter sends telegram every 10 seconds
-MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
-
RECONNECT_INTERVAL = 5
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -42,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All(
- cv.string, vol.In(["5", "4", "2.2"])
+ cv.string, vol.In(["5B", "5", "4", "2.2"])
),
vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int),
@@ -62,17 +58,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE],
["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY],
["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF],
- ["Power Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL],
- ["Power Consumption (low)", obis_ref.ELECTRICITY_USED_TARIFF_1],
- ["Power Consumption (normal)", obis_ref.ELECTRICITY_USED_TARIFF_2],
- ["Power Production (low)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1],
- ["Power Production (normal)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2],
+ ["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL],
+ ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1],
+ ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2],
+ ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1],
+ ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2],
["Power Consumption Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE],
["Power Consumption Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE],
["Power Consumption Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE],
["Power Production Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE],
["Power Production Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE],
["Power Production Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE],
+ ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT],
["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT],
["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT],
["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT],
@@ -83,6 +80,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1],
["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2],
["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3],
+ ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1],
+ ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2],
+ ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3],
]
# Generate device entities
@@ -91,6 +91,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
# Protocol version specific obis
if dsmr_version in ("4", "5"):
gas_obis = obis_ref.HOURLY_GAS_METER_READING
+ elif dsmr_version in ("5B"):
+ gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING
else:
gas_obis = obis_ref.GAS_METER_READING
@@ -214,7 +216,7 @@ class DSMREntity(Entity):
value = self.get_dsmr_object_attr("value")
if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF:
- return self.translate_tariff(value)
+ return self.translate_tariff(value, self._config[CONF_DSMR_VERSION])
try:
value = round(float(value), self._config[CONF_PRECISION])
@@ -232,8 +234,15 @@ class DSMREntity(Entity):
return self.get_dsmr_object_attr("unit")
@staticmethod
- def translate_tariff(value):
- """Convert 2/1 to normal/low."""
+ def translate_tariff(value, dsmr_version):
+ """Convert 2/1 to normal/low depending on DSMR version."""
+ # DSMR V5B: Note: In Belgium values are swapped:
+ # Rate code 2 is used for low rate and rate code 1 is used for normal rate.
+ if dsmr_version in ("5B"):
+ if value == "0001":
+ value = "0002"
+ elif value == "0002":
+ value = "0001"
# DSMR V2.2: Note: Rate code 1 is used for low rate and rate code 2 is
# used for normal rate.
if value == "0002":
diff --git a/homeassistant/components/duke_energy/__init__.py b/homeassistant/components/duke_energy/__init__.py
deleted file mode 100644
index 5a1f29add43..00000000000
--- a/homeassistant/components/duke_energy/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The duke_energy component."""
diff --git a/homeassistant/components/duke_energy/manifest.json b/homeassistant/components/duke_energy/manifest.json
deleted file mode 100644
index cebbf45df11..00000000000
--- a/homeassistant/components/duke_energy/manifest.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "domain": "duke_energy",
- "name": "Duke Energy",
- "documentation": "https://www.home-assistant.io/integrations/duke_energy",
- "requirements": ["pydukeenergy==0.0.6"],
- "dependencies": [],
- "codeowners": []
-}
diff --git a/homeassistant/components/duke_energy/sensor.py b/homeassistant/components/duke_energy/sensor.py
deleted file mode 100644
index cd30ae96caf..00000000000
--- a/homeassistant/components/duke_energy/sensor.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""Support for Duke Energy Gas and Electric meters."""
-import logging
-
-from pydukeenergy.api import DukeEnergy, DukeEnergyException
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-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__)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
-)
-
-LAST_BILL_USAGE = "last_bills_usage"
-LAST_BILL_AVERAGE_USAGE = "last_bills_average_usage"
-LAST_BILL_DAYS_BILLED = "last_bills_days_billed"
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up all Duke Energy meters."""
-
- try:
- duke = DukeEnergy(
- config[CONF_USERNAME], config[CONF_PASSWORD], update_interval=120
- )
- except DukeEnergyException:
- _LOGGER.error("Failed to set up Duke Energy")
- return
-
- add_entities([DukeEnergyMeter(meter) for meter in duke.get_meters()])
-
-
-class DukeEnergyMeter(Entity):
- """Representation of a Duke Energy meter."""
-
- def __init__(self, meter):
- """Initialize the meter."""
- self.duke_meter = meter
-
- @property
- def name(self):
- """Return the name."""
- return f"duke_energy_{self.duke_meter.id}"
-
- @property
- def unique_id(self):
- """Return the unique ID."""
- return self.duke_meter.id
-
- @property
- def state(self):
- """Return yesterdays usage."""
- return self.duke_meter.get_usage()
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement this sensor expresses itself in."""
- return self.duke_meter.get_unit()
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- attributes = {
- LAST_BILL_USAGE: self.duke_meter.get_total(),
- LAST_BILL_AVERAGE_USAGE: self.duke_meter.get_average(),
- LAST_BILL_DAYS_BILLED: self.duke_meter.get_days_billed(),
- }
- return attributes
-
- def update(self):
- """Update meter."""
- self.duke_meter.update()
diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py
new file mode 100755
index 00000000000..f4fc65b8261
--- /dev/null
+++ b/homeassistant/components/dynalite/__init__.py
@@ -0,0 +1,138 @@
+"""Support for the Dynalite networks."""
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_HOST
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import config_validation as cv
+
+# Loading the config flow file will register the flow
+from .bridge import DynaliteBridge
+from .const import (
+ CONF_ACTIVE,
+ CONF_AREA,
+ CONF_AUTO_DISCOVER,
+ CONF_BRIDGES,
+ CONF_CHANNEL,
+ CONF_DEFAULT,
+ CONF_FADE,
+ CONF_NAME,
+ CONF_POLLTIMER,
+ CONF_PORT,
+ DEFAULT_NAME,
+ DEFAULT_PORT,
+ DOMAIN,
+ LOGGER,
+)
+
+
+def num_string(value):
+ """Test if value is a string of digits, aka an integer."""
+ new_value = str(value)
+ if new_value.isdigit():
+ return new_value
+ raise vol.Invalid("Not a string with numbers")
+
+
+CHANNEL_DATA_SCHEMA = vol.Schema(
+ {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_FADE): vol.Coerce(float)}
+)
+
+CHANNEL_SCHEMA = vol.Schema({num_string: CHANNEL_DATA_SCHEMA})
+
+AREA_DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_NAME): cv.string,
+ vol.Optional(CONF_FADE): vol.Coerce(float),
+ vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA,
+ },
+)
+
+AREA_SCHEMA = vol.Schema({num_string: vol.Any(AREA_DATA_SCHEMA, None)})
+
+PLATFORM_DEFAULTS_SCHEMA = vol.Schema({vol.Optional(CONF_FADE): vol.Coerce(float)})
+
+
+BRIDGE_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
+ vol.Optional(CONF_AUTO_DISCOVER, default=False): vol.Coerce(bool),
+ vol.Optional(CONF_POLLTIMER, default=1.0): vol.Coerce(float),
+ vol.Optional(CONF_AREA): AREA_SCHEMA,
+ vol.Optional(CONF_DEFAULT): PLATFORM_DEFAULTS_SCHEMA,
+ vol.Optional(CONF_ACTIVE, default=False): vol.Coerce(bool),
+ }
+)
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {vol.Optional(CONF_BRIDGES): vol.All(cv.ensure_list, [BRIDGE_SCHEMA])}
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass, config):
+ """Set up the Dynalite platform."""
+
+ conf = config.get(DOMAIN)
+ LOGGER.debug("Setting up dynalite component config = %s", conf)
+
+ if conf is None:
+ conf = {}
+
+ hass.data[DOMAIN] = {}
+
+ # User has configured bridges
+ if CONF_BRIDGES not in conf:
+ return True
+
+ bridges = conf[CONF_BRIDGES]
+
+ for bridge_conf in bridges:
+ host = bridge_conf[CONF_HOST]
+ LOGGER.debug("Starting config entry flow host=%s conf=%s", host, bridge_conf)
+
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=bridge_conf,
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up a bridge from a config entry."""
+ LOGGER.debug("Setting up entry %s", entry.data)
+
+ bridge = DynaliteBridge(hass, entry.data)
+
+ if not await bridge.async_setup():
+ LOGGER.error("Could not set up bridge for entry %s", entry.data)
+ return False
+
+ if not await bridge.try_connection():
+ LOGGER.errot("Could not connect with entry %s", entry)
+ raise ConfigEntryNotReady
+
+ hass.data[DOMAIN][entry.entry_id] = bridge
+
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, "light")
+ )
+ return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ LOGGER.debug("Unloading entry %s", entry.data)
+ hass.data[DOMAIN].pop(entry.entry_id)
+ result = await hass.config_entries.async_forward_entry_unload(entry, "light")
+ return result
diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py
new file mode 100755
index 00000000000..cbe08fdadb5
--- /dev/null
+++ b/homeassistant/components/dynalite/bridge.py
@@ -0,0 +1,82 @@
+"""Code to handle a Dynalite bridge."""
+
+import asyncio
+
+from dynalite_devices_lib import DynaliteDevices
+
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+from .const import CONF_ALL, CONF_HOST, LOGGER
+
+CONNECT_TIMEOUT = 30
+CONNECT_INTERVAL = 1
+
+
+class DynaliteBridge:
+ """Manages a single Dynalite bridge."""
+
+ def __init__(self, hass, config):
+ """Initialize the system based on host parameter."""
+ self.hass = hass
+ self.area = {}
+ self.async_add_devices = None
+ self.waiting_devices = []
+ self.host = config[CONF_HOST]
+ # Configure the dynalite devices
+ self.dynalite_devices = DynaliteDevices(
+ config=config,
+ newDeviceFunc=self.add_devices_when_registered,
+ updateDeviceFunc=self.update_device,
+ )
+
+ async def async_setup(self):
+ """Set up a Dynalite bridge."""
+ # Configure the dynalite devices
+ return await self.dynalite_devices.async_setup()
+
+ def update_signal(self, device=None):
+ """Create signal to use to trigger entity update."""
+ if device:
+ signal = f"dynalite-update-{self.host}-{device.unique_id}"
+ else:
+ signal = f"dynalite-update-{self.host}"
+ return signal
+
+ @callback
+ def update_device(self, device):
+ """Call when a device or all devices should be updated."""
+ if device == CONF_ALL:
+ # This is used to signal connection or disconnection, so all devices may become available or not.
+ log_string = (
+ "Connected" if self.dynalite_devices.available else "Disconnected"
+ )
+ LOGGER.info("%s to dynalite host", log_string)
+ async_dispatcher_send(self.hass, self.update_signal())
+ else:
+ async_dispatcher_send(self.hass, self.update_signal(device))
+
+ async def try_connection(self):
+ """Try to connect to dynalite with timeout."""
+ # Currently by polling. Future - will need to change the library to be proactive
+ for _ in range(0, CONNECT_TIMEOUT):
+ if self.dynalite_devices.available:
+ return True
+ await asyncio.sleep(CONNECT_INTERVAL)
+ return False
+
+ @callback
+ def register_add_devices(self, async_add_devices):
+ """Add an async_add_entities for a category."""
+ self.async_add_devices = async_add_devices
+ if self.waiting_devices:
+ self.async_add_devices(self.waiting_devices)
+
+ def add_devices_when_registered(self, devices):
+ """Add the devices to HA if the add devices callback was registered, otherwise queue until it is."""
+ if not devices:
+ return
+ if self.async_add_devices:
+ self.async_add_devices(devices)
+ else: # handle it later when it is registered
+ self.waiting_devices.extend(devices)
diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py
new file mode 100755
index 00000000000..aac42172181
--- /dev/null
+++ b/homeassistant/components/dynalite/config_flow.py
@@ -0,0 +1,35 @@
+"""Config flow to configure Dynalite hub."""
+from homeassistant import config_entries
+from homeassistant.const import CONF_HOST
+
+from .bridge import DynaliteBridge
+from .const import DOMAIN, LOGGER # pylint: disable=unused-import
+
+
+class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a Dynalite config flow."""
+
+ 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 Dynalite flow."""
+ self.host = None
+
+ async def async_step_import(self, import_info):
+ """Import a new bridge as a config entry."""
+ LOGGER.debug("Starting async_step_import - %s", import_info)
+ host = import_info[CONF_HOST]
+ await self.async_set_unique_id(host)
+ self._abort_if_unique_id_configured(import_info)
+ # New entry
+ bridge = DynaliteBridge(self.hass, import_info)
+ if not await bridge.async_setup():
+ LOGGER.error("Unable to setup bridge - import info=%s", import_info)
+ return self.async_abort(reason="bridge_setup_failed")
+ if not await bridge.try_connection():
+ return self.async_abort(reason="no_connection")
+ LOGGER.debug("Creating entry for the bridge - %s", import_info)
+ return self.async_create_entry(title=host, data=import_info)
diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py
new file mode 100755
index 00000000000..f7795554465
--- /dev/null
+++ b/homeassistant/components/dynalite/const.py
@@ -0,0 +1,21 @@
+"""Constants for the Dynalite component."""
+import logging
+
+LOGGER = logging.getLogger(__package__)
+DOMAIN = "dynalite"
+
+CONF_ACTIVE = "active"
+CONF_ALL = "ALL"
+CONF_AREA = "area"
+CONF_AUTO_DISCOVER = "autodiscover"
+CONF_BRIDGES = "bridges"
+CONF_CHANNEL = "channel"
+CONF_DEFAULT = "default"
+CONF_FADE = "fade"
+CONF_HOST = "host"
+CONF_NAME = "name"
+CONF_POLLTIMER = "polltimer"
+CONF_PORT = "port"
+
+DEFAULT_NAME = "dynalite"
+DEFAULT_PORT = 12345
diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py
new file mode 100755
index 00000000000..652a6178705
--- /dev/null
+++ b/homeassistant/components/dynalite/light.py
@@ -0,0 +1,96 @@
+"""Support for Dynalite channels as lights."""
+from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import DOMAIN, LOGGER
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Record the async_add_entities function to add them later when received from Dynalite."""
+ LOGGER.debug("Setting up light entry = %s", config_entry.data)
+ bridge = hass.data[DOMAIN][config_entry.entry_id]
+
+ @callback
+ def async_add_lights(devices):
+ added_lights = []
+ for device in devices:
+ if device.category == "light":
+ added_lights.append(DynaliteLight(device, bridge))
+ if added_lights:
+ async_add_entities(added_lights)
+
+ bridge.register_add_devices(async_add_lights)
+
+
+class DynaliteLight(Light):
+ """Representation of a Dynalite Channel as a Home Assistant Light."""
+
+ def __init__(self, device, bridge):
+ """Initialize the base class."""
+ self._device = device
+ self._bridge = bridge
+
+ @property
+ def name(self):
+ """Return the name of the entity."""
+ return self._device.name
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of the entity."""
+ return self._device.unique_id
+
+ @property
+ def available(self):
+ """Return if entity is available."""
+ return self._device.available
+
+ async def async_update(self):
+ """Update the entity."""
+ return
+
+ @property
+ def device_info(self):
+ """Device info for this entity."""
+ return {
+ "identifiers": {(DOMAIN, self.unique_id)},
+ "name": self.name,
+ "manufacturer": "Dynalite",
+ }
+
+ @property
+ def brightness(self):
+ """Return the brightness of this light between 0..255."""
+ return self._device.brightness
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self._device.is_on
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the light on."""
+ await self._device.async_turn_on(**kwargs)
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the light off."""
+ await self._device.async_turn_off(**kwargs)
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_BRIGHTNESS
+
+ async def async_added_to_hass(self):
+ """Added to hass so need to register to dispatch."""
+ # register for device specific update
+ async_dispatcher_connect(
+ self.hass,
+ self._bridge.update_signal(self._device),
+ self.async_schedule_update_ha_state,
+ )
+ # register for wide update
+ async_dispatcher_connect(
+ self.hass, self._bridge.update_signal(), self.async_schedule_update_ha_state
+ )
diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json
new file mode 100755
index 00000000000..95667733d38
--- /dev/null
+++ b/homeassistant/components/dynalite/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "dynalite",
+ "name": "Philips Dynalite",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/dynalite",
+ "dependencies": [],
+ "codeowners": ["@ziv1234"],
+ "requirements": ["dynalite_devices==0.1.22"]
+}
diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py
index 55504e8edf7..54355ed3bb8 100644
--- a/homeassistant/components/ebox/sensor.py
+++ b/homeassistant/components/ebox/sensor.py
@@ -19,6 +19,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
+ DATA_GIGABITS,
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
@@ -27,7 +28,6 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
-GIGABITS = "Gb"
PRICE = "CAD"
DAYS = "days"
PERCENT = "%"
@@ -41,17 +41,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
SENSOR_TYPES = {
"usage": ["Usage", PERCENT, "mdi:percent"],
"balance": ["Balance", PRICE, "mdi:square-inc-cash"],
- "limit": ["Data limit", GIGABITS, "mdi:download"],
+ "limit": ["Data limit", DATA_GIGABITS, "mdi:download"],
"days_left": ["Days left", DAYS, "mdi:calendar-today"],
- "before_offpeak_download": ["Download before offpeak", GIGABITS, "mdi:download"],
- "before_offpeak_upload": ["Upload before offpeak", GIGABITS, "mdi:upload"],
- "before_offpeak_total": ["Total before offpeak", GIGABITS, "mdi:download"],
- "offpeak_download": ["Offpeak download", GIGABITS, "mdi:download"],
- "offpeak_upload": ["Offpeak Upload", GIGABITS, "mdi:upload"],
- "offpeak_total": ["Offpeak Total", GIGABITS, "mdi:download"],
- "download": ["Download", GIGABITS, "mdi:download"],
- "upload": ["Upload", GIGABITS, "mdi:upload"],
- "total": ["Total", GIGABITS, "mdi:download"],
+ "before_offpeak_download": [
+ "Download before offpeak",
+ DATA_GIGABITS,
+ "mdi:download",
+ ],
+ "before_offpeak_upload": ["Upload before offpeak", DATA_GIGABITS, "mdi:upload"],
+ "before_offpeak_total": ["Total before offpeak", DATA_GIGABITS, "mdi:download"],
+ "offpeak_download": ["Offpeak download", DATA_GIGABITS, "mdi:download"],
+ "offpeak_upload": ["Offpeak Upload", DATA_GIGABITS, "mdi:upload"],
+ "offpeak_total": ["Offpeak Total", DATA_GIGABITS, "mdi:download"],
+ "download": ["Download", DATA_GIGABITS, "mdi:download"],
+ "upload": ["Upload", DATA_GIGABITS, "mdi:upload"],
+ "total": ["Total", DATA_GIGABITS, "mdi:download"],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/ecobee/.translations/es-419.json b/homeassistant/components/ecobee/.translations/es-419.json
new file mode 100644
index 00000000000..3e19977f10f
--- /dev/null
+++ b/homeassistant/components/ecobee/.translations/es-419.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_only": "Esta integraci\u00f3n actualmente solo admite una instancia de ecobee."
+ },
+ "error": {
+ "pin_request_failed": "Error al solicitar PIN de ecobee; verifique que la clave API sea correcta.",
+ "token_request_failed": "Error al solicitar tokens de ecobee; Int\u00e9ntelo de nuevo."
+ },
+ "step": {
+ "authorize": {
+ "description": "Autorice esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo PIN: \n\n {pin} \n \n Luego, presione Enviar."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/.translations/hu.json b/homeassistant/components/ecobee/.translations/hu.json
new file mode 100644
index 00000000000..0950d52bd0e
--- /dev/null
+++ b/homeassistant/components/ecobee/.translations/hu.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_only": "Ez az integr\u00e1ci\u00f3 jelenleg csak egy ecobee p\u00e9ld\u00e1nyt t\u00e1mogat."
+ },
+ "error": {
+ "pin_request_failed": "Hiba t\u00f6rt\u00e9nt a PIN-k\u00f3d ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 k\u00e9r\u00e9sekor; ellen\u0151rizze, hogy az API-kulcs helyes-e.",
+ "token_request_failed": "Hiba t\u00f6rt\u00e9nt a tokenek ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 ig\u00e9nyl\u00e9se k\u00f6zben; pr\u00f3b\u00e1lkozzon \u00fajra."
+ },
+ "step": {
+ "authorize": {
+ "description": "K\u00e9rj\u00fck, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n {pin} \n \n Ezut\u00e1n nyomja meg a K\u00fcld\u00e9s gombot.",
+ "title": "Alkalmaz\u00e1s enged\u00e9lyez\u00e9se ecobee.com-on"
+ },
+ "user": {
+ "data": {
+ "api_key": "API kulcs"
+ },
+ "description": "Adja meg az ecobee.com webhelyr\u0151l beszerzett API-kulcsot.",
+ "title": "ecobee API kulcs"
+ }
+ },
+ "title": "ecobee"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/.translations/sv.json b/homeassistant/components/ecobee/.translations/sv.json
index f4a63bb449d..da62172dc10 100644
--- a/homeassistant/components/ecobee/.translations/sv.json
+++ b/homeassistant/components/ecobee/.translations/sv.json
@@ -1,11 +1,25 @@
{
"config": {
+ "abort": {
+ "one_instance_only": "Denna integration st\u00f6der f\u00f6r n\u00e4rvarande endast en ecobee-instans."
+ },
+ "error": {
+ "pin_request_failed": "Fel vid beg\u00e4ran av PIN-kod fr\u00e5n ecobee. kontrollera API-nyckeln \u00e4r korrekt.",
+ "token_request_failed": "Fel vid beg\u00e4ran av tokens fr\u00e5n ecobee; v\u00e4nligen f\u00f6rs\u00f6k igen."
+ },
"step": {
+ "authorize": {
+ "description": "V\u00e4nligen auktorisera denna app p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kod:\n\n{pin}\n\nTryck sedan p\u00e5 Skicka.",
+ "title": "Auktorisera app p\u00e5 ecobee.com"
+ },
"user": {
"data": {
"api_key": "API-nyckel"
- }
+ },
+ "description": "V\u00e4nligen ange API-nyckeln som erh\u00e5llits fr\u00e5n ecobee.com.",
+ "title": "ecobee API-nyckel"
}
- }
+ },
+ "title": "ecobee"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py
index 80c3be7954b..26bfbe5b3da 100644
--- a/homeassistant/components/ecobee/__init__.py
+++ b/homeassistant/components/ecobee/__init__.py
@@ -96,9 +96,7 @@ class EcobeeData:
await self._hass.async_add_executor_job(self.ecobee.update)
_LOGGER.debug("Updating ecobee")
except ExpiredTokenError:
- _LOGGER.warning(
- "Ecobee update failed; attempting to refresh expired tokens"
- )
+ _LOGGER.debug("Refreshing expired ecobee tokens")
await self.refresh()
async def refresh(self) -> bool:
@@ -113,7 +111,7 @@ class EcobeeData:
},
)
return True
- _LOGGER.error("Error updating ecobee tokens")
+ _LOGGER.error("Error refreshing ecobee tokens")
return False
diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py
index bb406d81e3a..cbe16832a34 100644
--- a/homeassistant/components/ecobee/config_flow.py
+++ b/homeassistant/components/ecobee/config_flow.py
@@ -107,7 +107,7 @@ class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if await self.hass.async_add_executor_job(ecobee.refresh_tokens):
# Credentials found and validated; create the entry.
_LOGGER.debug(
- "Valid ecobee configuration found for import, creating config entry"
+ "Valid ecobee configuration found for import, creating configuration entry"
)
return self.async_create_entry(
title=DOMAIN,
diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json
index 32b58964926..8e21b9931cd 100644
--- a/homeassistant/components/ecobee/manifest.json
+++ b/homeassistant/components/ecobee/manifest.json
@@ -1,9 +1,9 @@
{
"domain": "ecobee",
- "name": "Ecobee",
+ "name": "ecobee",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecobee",
"dependencies": [],
- "requirements": ["python-ecobee-api==0.1.4"],
+ "requirements": ["python-ecobee-api==0.2.1"],
"codeowners": ["@marthoc"]
}
diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py
index 22d3533d32f..3b5aa95701f 100644
--- a/homeassistant/components/eddystone_temperature/sensor.py
+++ b/homeassistant/components/eddystone_temperature/sensor.py
@@ -91,7 +91,7 @@ def get_from_conf(config, config_key, length):
string = config.get(config_key)
if len(string) != length:
_LOGGER.error(
- "Error in config parameter %s: Must be exactly %d "
+ "Error in configuration parameter %s: Must be exactly %d "
"bytes. Device will not be added",
config_key,
length / 2,
diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json
index 20036311592..de8b978b9f9 100644
--- a/homeassistant/components/edimax/manifest.json
+++ b/homeassistant/components/edimax/manifest.json
@@ -2,7 +2,7 @@
"domain": "edimax",
"name": "Edimax",
"documentation": "https://www.home-assistant.io/integrations/edimax",
- "requirements": ["pyedimax==0.1"],
+ "requirements": ["pyedimax==0.2.1"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py
index 3d558f6c770..e44ec23bca7 100644
--- a/homeassistant/components/edimax/switch.py
+++ b/homeassistant/components/edimax/switch.py
@@ -10,6 +10,8 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
+DOMAIN = "edimax"
+
DEFAULT_NAME = "Edimax Smart Plug"
DEFAULT_PASSWORD = "1234"
DEFAULT_USERNAME = "admin"
@@ -30,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
auth = (config.get(CONF_USERNAME), config.get(CONF_PASSWORD))
name = config.get(CONF_NAME)
- add_entities([SmartPlugSwitch(SmartPlug(host, auth), name)])
+ add_entities([SmartPlugSwitch(SmartPlug(host, auth), name)], True)
class SmartPlugSwitch(SwitchDevice):
@@ -43,6 +45,14 @@ class SmartPlugSwitch(SwitchDevice):
self._now_power = None
self._now_energy_day = None
self._state = False
+ self._supports_power_monitoring = False
+ self._info = None
+ self._mac = None
+
+ @property
+ def unique_id(self):
+ """Return the device's MAC address."""
+ return self._mac
@property
def name(self):
@@ -74,14 +84,20 @@ class SmartPlugSwitch(SwitchDevice):
def update(self):
"""Update edimax switch."""
- try:
- self._now_power = float(self.smartplug.now_power)
- except (TypeError, ValueError):
- self._now_power = None
+ if not self._info:
+ self._info = self.smartplug.info
+ self._mac = self._info["mac"]
+ self._supports_power_monitoring = self._info["model"] != "SP1101W"
- try:
- self._now_energy_day = float(self.smartplug.now_energy_day)
- except (TypeError, ValueError):
- self._now_energy_day = None
+ if self._supports_power_monitoring:
+ try:
+ self._now_power = float(self.smartplug.now_power)
+ except (TypeError, ValueError):
+ self._now_power = None
+
+ try:
+ self._now_energy_day = float(self.smartplug.now_energy_day)
+ except (TypeError, ValueError):
+ self._now_energy_day = None
self._state = self.smartplug.state == "ON"
diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py
index a8a5a6e1fcc..595144013b6 100644
--- a/homeassistant/components/eight_sleep/__init__.py
+++ b/homeassistant/components/eight_sleep/__init__.py
@@ -43,12 +43,14 @@ SIGNAL_UPDATE_USER = "eight_user_update"
NAME_MAP = {
"left_current_sleep": "Left Sleep Session",
+ "left_current_sleep_fitness": "Left Sleep Fitness",
"left_last_sleep": "Left Previous Sleep Session",
"left_bed_state": "Left Bed State",
"left_presence": "Left Bed Presence",
"left_bed_temp": "Left Bed Temperature",
"left_sleep_stage": "Left Sleep Stage",
"right_current_sleep": "Right Sleep Session",
+ "right_current_sleep_fitness": "Right Sleep Fitness",
"right_last_sleep": "Right Previous Sleep Session",
"right_bed_state": "Right Bed State",
"right_presence": "Right Bed Presence",
@@ -57,14 +59,21 @@ NAME_MAP = {
"room_temp": "Room Temperature",
}
-SENSORS = ["current_sleep", "last_sleep", "bed_state", "bed_temp", "sleep_stage"]
+SENSORS = [
+ "current_sleep",
+ "current_sleep_fitness",
+ "last_sleep",
+ "bed_state",
+ "bed_temp",
+ "sleep_stage",
+]
SERVICE_HEAT_SET = "heat_set"
ATTR_TARGET_HEAT = "target"
ATTR_HEAT_DURATION = "duration"
-VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100))
+VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=-100, max=100))
VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800))
SERVICE_EIGHT_SCHEMA = vol.Schema(
@@ -101,7 +110,7 @@ async def async_setup(hass, config):
_LOGGER.error("Timezone is not set in Home Assistant.")
return False
- timezone = hass.config.time_zone
+ timezone = str(hass.config.time_zone)
eight = EightSleep(user, password, timezone, partner, None, hass.loop)
diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json
index 75998e71e5f..6372967b42b 100644
--- a/homeassistant/components/eight_sleep/manifest.json
+++ b/homeassistant/components/eight_sleep/manifest.json
@@ -2,7 +2,7 @@
"domain": "eight_sleep",
"name": "Eight Sleep",
"documentation": "https://www.home-assistant.io/integrations/eight_sleep",
- "requirements": ["pyeight==0.1.2"],
+ "requirements": ["pyeight==0.1.3"],
"dependencies": [],
"codeowners": ["@mezz64"]
}
diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py
index d3d54fd58ca..af6de2657ce 100644
--- a/homeassistant/components/eight_sleep/sensor.py
+++ b/homeassistant/components/eight_sleep/sensor.py
@@ -28,6 +28,11 @@ ATTR_ACTIVE_HEAT = "Heating Active"
ATTR_DURATION_HEAT = "Heating Time Remaining"
ATTR_PROCESSING = "Processing"
ATTR_SESSION_START = "Session Start"
+ATTR_FIT_DATE = "Fitness Date"
+ATTR_FIT_DURATION_SCORE = "Fitness Duration Score"
+ATTR_FIT_ASLEEP_SCORE = "Fitness Asleep Score"
+ATTR_FIT_OUT_SCORE = "Fitness Out-of-Bed Score"
+ATTR_FIT_WAKEUP_SCORE = "Fitness Wakeup Score"
_LOGGER = logging.getLogger(__name__)
@@ -151,7 +156,11 @@ class EightUserSensor(EightSleepUserEntity):
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
- if "current_sleep" in self._sensor or "last_sleep" in self._sensor:
+ if (
+ "current_sleep" in self._sensor
+ or "last_sleep" in self._sensor
+ or "current_sleep_fitness" in self._sensor
+ ):
return "Score"
if "bed_temp" in self._sensor:
if self._units == "si":
@@ -169,8 +178,12 @@ class EightUserSensor(EightSleepUserEntity):
"""Retrieve latest state."""
_LOGGER.debug("Updating User sensor: %s", self._sensor)
if "current" in self._sensor:
- self._state = self._usrobj.current_sleep_score
- self._attr = self._usrobj.current_values
+ if "fitness" in self._sensor:
+ self._state = self._usrobj.current_sleep_fitness_score
+ self._attr = self._usrobj.current_fitness_values
+ else:
+ self._state = self._usrobj.current_sleep_score
+ self._attr = self._usrobj.current_values
elif "last" in self._sensor:
self._state = self._usrobj.last_sleep_score
self._attr = self._usrobj.last_values
@@ -193,6 +206,16 @@ class EightUserSensor(EightSleepUserEntity):
# Skip attributes if sensor type doesn't support
return None
+ if "fitness" in self._sensor_root:
+ state_attr = {
+ ATTR_FIT_DATE: self._attr["date"],
+ ATTR_FIT_DURATION_SCORE: self._attr["duration"],
+ ATTR_FIT_ASLEEP_SCORE: self._attr["asleep"],
+ ATTR_FIT_OUT_SCORE: self._attr["out"],
+ ATTR_FIT_WAKEUP_SCORE: self._attr["wakeup"],
+ }
+ return state_attr
+
state_attr = {ATTR_SESSION_START: self._attr["date"]}
state_attr[ATTR_TNT] = self._attr["tnt"]
state_attr[ATTR_PROCESSING] = self._attr["processing"]
diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml
index db7690730dd..49a3c67d604 100644
--- a/homeassistant/components/eight_sleep/services.yaml
+++ b/homeassistant/components/eight_sleep/services.yaml
@@ -1,6 +1,6 @@
heat_set:
- description: Set heating level for eight sleep.
+ description: Set heating/cooling level for eight sleep.
fields:
- duration: {description: Duration to heat at the target level in seconds., example: 3600}
+ duration: {description: Duration to heat/cool at the target level in seconds., example: 3600}
entity_id: {description: Entity id of the bed state to adjust., example: sensor.eight_left_bed_state}
- target: {description: Target heating level from 0-100., example: 35}
+ target: {description: Target cooling/heating level from -100 to 100., example: 35}
diff --git a/homeassistant/components/elgato/.translations/es-419.json b/homeassistant/components/elgato/.translations/es-419.json
new file mode 100644
index 00000000000..2653060030a
--- /dev/null
+++ b/homeassistant/components/elgato/.translations/es-419.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host o direcci\u00f3n IP",
+ "port": "N\u00famero de puerto"
+ },
+ "description": "Configure su Elgato Key Light para integrarse con Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "title": "Dispositivo Elgato Key Light descubierto"
+ }
+ },
+ "title": "Elgato Key Light"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elgato/.translations/hu.json b/homeassistant/components/elgato/.translations/hu.json
new file mode 100644
index 00000000000..d3618d0039d
--- /dev/null
+++ b/homeassistant/components/elgato/.translations/hu.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ez az Elgato Key Light eszk\u00f6z m\u00e1r konfigur\u00e1lva van.",
+ "connection_error": "Nem siker\u00fclt csatlakozni az Elgato Key Light eszk\u00f6zh\u00f6z."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hosztn\u00e9v vagy IP c\u00edm",
+ "port": "Portsz\u00e1m"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elgato/.translations/sv.json b/homeassistant/components/elgato/.translations/sv.json
new file mode 100644
index 00000000000..83850c186c7
--- /dev/null
+++ b/homeassistant/components/elgato/.translations/sv.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Den h\u00e4r Elgato Key Light-enheten \u00e4r redan konfigurerad.",
+ "connection_error": "Det gick inte att ansluta till Elgato Key Light-enheten."
+ },
+ "error": {
+ "connection_error": "Det gick inte att ansluta till Elgato Key Light-enheten."
+ },
+ "flow_title": "Elgato Key Light: {serial_number}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e4rd eller IP-adress",
+ "port": "Portnummer"
+ },
+ "description": "St\u00e4ll in ditt Elgato Key Light f\u00f6r att integrera med Home Assistant.",
+ "title": "L\u00e4nk din Elgato Key Light"
+ },
+ "zeroconf_confirm": {
+ "description": "Vill du l\u00e4gga till Elgato Key Light med serienummer `{serial_number}` till Home Assistant?",
+ "title": "Uppt\u00e4ckte Elgato Key Light-enhet"
+ }
+ },
+ "title": "Elgato Key Light"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py
index a8a81734999..2f3e05fd720 100644
--- a/homeassistant/components/elgato/config_flow.py
+++ b/homeassistant/components/elgato/config_flow.py
@@ -7,8 +7,8 @@ import voluptuous as vol
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_PORT
-from homeassistant.helpers import ConfigType
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.typing import ConfigType
from .const import CONF_SERIAL_NUMBER, DOMAIN # pylint: disable=unused-import
diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py
index 34063e4c253..f956d3a7295 100644
--- a/homeassistant/components/emoncms/sensor.py
+++ b/homeassistant/components/emoncms/sensor.py
@@ -246,7 +246,7 @@ class EmonCmsData:
self.data = req.json()
else:
_LOGGER.error(
- "Please verify if the specified config value "
+ "Please verify if the specified configuration value "
"'%s' is correct! (HTTP Status_code = %d)",
CONF_URL,
req.status_code,
diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py
index 56e76b1d499..e9e7114074a 100644
--- a/homeassistant/components/emulated_hue/hue_api.py
+++ b/homeassistant/components/emulated_hue/hue_api.py
@@ -718,7 +718,7 @@ def create_hue_success_response(entity_id, attr, value):
def create_list_of_entities(config, request):
- """Create a list of all entites."""
+ """Create a list of all entities."""
hass = request.app["hass"]
json_response = {}
diff --git a/homeassistant/components/emulated_roku/.translations/pl.json b/homeassistant/components/emulated_roku/.translations/pl.json
index 0ed3cc3d14a..0dd32f66c9f 100644
--- a/homeassistant/components/emulated_roku/.translations/pl.json
+++ b/homeassistant/components/emulated_roku/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "name_exists": "Nazwa ju\u017c istnieje"
+ "name_exists": "Nazwa ju\u017c istnieje."
},
"step": {
"user": {
diff --git a/homeassistant/components/emulated_roku/binding.py b/homeassistant/components/emulated_roku/binding.py
index a44effff55a..1d233c9ed81 100644
--- a/homeassistant/components/emulated_roku/binding.py
+++ b/homeassistant/components/emulated_roku/binding.py
@@ -109,7 +109,7 @@ class EmulatedRoku:
)
LOGGER.debug(
- "Intializing emulated_roku %s on %s:%s",
+ "Initializing emulated_roku %s on %s:%s",
self.roku_usn,
self.host_ip,
self.listen_port,
diff --git a/homeassistant/components/esphome/.translations/pl.json b/homeassistant/components/esphome/.translations/pl.json
index 9394b5af543..ebd201b5550 100644
--- a/homeassistant/components/esphome/.translations/pl.json
+++ b/homeassistant/components/esphome/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "ESP jest ju\u017c skonfigurowane"
+ "already_configured": "ESP jest ju\u017c skonfigurowane."
},
"error": {
"connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.",
diff --git a/homeassistant/components/esphome/.translations/zh-Hans.json b/homeassistant/components/esphome/.translations/zh-Hans.json
index 46790868aba..4839167785d 100644
--- a/homeassistant/components/esphome/.translations/zh-Hans.json
+++ b/homeassistant/components/esphome/.translations/zh-Hans.json
@@ -8,6 +8,7 @@
"invalid_password": "\u65e0\u6548\u7684\u5bc6\u7801\uff01",
"resolve_error": "\u65e0\u6cd5\u89e3\u6790 ESP \u7684\u5730\u5740\u3002\u5982\u679c\u6b64\u9519\u8bef\u6301\u7eed\u5b58\u5728\uff0c\u8bf7\u8bbe\u7f6e\u9759\u6001IP\u5730\u5740\uff1ahttps://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py
index 53289799b43..17d3ed5f659 100644
--- a/homeassistant/components/esphome/config_flow.py
+++ b/homeassistant/components/esphome/config_flow.py
@@ -5,8 +5,8 @@ from typing import Optional
from aioesphomeapi import APIClient, APIConnectionError
import voluptuous as vol
-from homeassistant import config_entries
-from homeassistant.helpers import ConfigType
+from homeassistant import config_entries, core
+from homeassistant.helpers.typing import ConfigType
from .entry_data import DATA_KEY, RuntimeEntryData
@@ -115,6 +115,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow):
return await self.async_step_discovery_confirm()
+ @core.callback
def _async_get_entry(self):
return self.async_create_entry(
title=self._name,
diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py
index b106d9d2ae6..e3ce1ccaafa 100644
--- a/homeassistant/components/essent/sensor.py
+++ b/homeassistant/components/essent/sensor.py
@@ -1,5 +1,6 @@
"""Support for Essent API."""
from datetime import timedelta
+from typing import Optional
from pyessent import PyEssent
import voluptuous as vol
@@ -73,7 +74,7 @@ class EssentBase:
def update(self):
"""Retrieve the latest meter data from Essent."""
essent = PyEssent(self._username, self._password)
- eans = essent.get_EANs()
+ eans = set(essent.get_EANs())
for possible_meter in eans:
meter_data = essent.read_meter(possible_meter, only_last_meter_reading=True)
if meter_data:
@@ -92,6 +93,11 @@ class EssentMeter(Entity):
self._tariff = tariff
self._unit = unit
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Return a unique ID."""
+ return f"{self._meter}-{self._type}-{self._tariff}"
+
@property
def name(self):
"""Return the name of the sensor."""
diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py
index 949471d64d0..1a408c0a660 100644
--- a/homeassistant/components/evohome/__init__.py
+++ b/homeassistant/components/evohome/__init__.py
@@ -148,7 +148,7 @@ def _handle_exception(err) -> bool:
return False
except aiohttp.ClientConnectionError:
- # this appears to be a common occurance with the vendor's servers
+ # this appears to be a common occurrence with the vendor's servers
_LOGGER.warning(
"Unable to connect with the vendor's server. "
"Check your network and the vendor's service status page. "
@@ -184,7 +184,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
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
+ # any tokens won't be valid, and store might be be corrupt
await store.async_save({})
return ({}, None)
@@ -266,7 +266,7 @@ def setup_service_functions(hass: HomeAssistantType, broker):
Not all Honeywell TCC-compatible systems support all operating modes. In addition,
each mode will require any of four distinct service schemas. This has to be
- enumerated before registering the approperiate handlers.
+ enumerated before registering the appropriate handlers.
It appears that all TCC-compatible systems support the same three zones modes.
"""
diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py
index b7f6e965a8f..aece0f0ec0d 100644
--- a/homeassistant/components/evohome/climate.py
+++ b/homeassistant/components/evohome/climate.py
@@ -175,7 +175,7 @@ class EvoZone(EvoChild, EvoClimateDevice):
if ATTR_DURATION_UNTIL in data:
duration = data[ATTR_DURATION_UNTIL]
- if duration == 0:
+ if duration.total_seconds() == 0:
await self._update_schedule()
until = parse_datetime(str(self.setpoints.get("next_sp_from")))
else:
diff --git a/homeassistant/components/fan/.translations/es-419.json b/homeassistant/components/fan/.translations/es-419.json
new file mode 100644
index 00000000000..dd0c006d760
--- /dev/null
+++ b/homeassistant/components/fan/.translations/es-419.json
@@ -0,0 +1,12 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_off": "{entity_name} est\u00e1 apagado",
+ "is_on": "{entity_name} est\u00e1 encendido"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} se apag\u00f3",
+ "turned_on": "{entity_name} se encendi\u00f3"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fan/.translations/sv.json b/homeassistant/components/fan/.translations/sv.json
new file mode 100644
index 00000000000..c080d1b364b
--- /dev/null
+++ b/homeassistant/components/fan/.translations/sv.json
@@ -0,0 +1,16 @@
+{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "St\u00e4ng av {entity_name}",
+ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u00e4r avst\u00e4ngd",
+ "is_on": "{entity_name} \u00e4r p\u00e5"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} st\u00e4ngdes av",
+ "turned_on": "{entity_name} aktiverades"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py
index 38234a8f832..76bd16a6363 100644
--- a/homeassistant/components/fan/__init__.py
+++ b/homeassistant/components/fan/__init__.py
@@ -169,7 +169,7 @@ class FanEntity(ToggleEntity):
@property
def capability_attributes(self):
- """Return capabilitiy attributes."""
+ """Return capability attributes."""
return {ATTR_SPEED_LIST: self.speed_list}
@property
diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py
index 6d9445ce159..a6eaa21ae35 100644
--- a/homeassistant/components/fastdotcom/sensor.py
+++ b/homeassistant/components/fastdotcom/sensor.py
@@ -1,6 +1,7 @@
"""Support for Fast.com internet speed testing sensor."""
import logging
+from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity
@@ -11,8 +12,6 @@ _LOGGER = logging.getLogger(__name__)
ICON = "mdi:speedometer"
-UNIT_OF_MEASUREMENT = "Mbit/s"
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Fast.com sensor."""
@@ -41,7 +40,7 @@ class SpeedtestSensor(RestoreEntity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
- return UNIT_OF_MEASUREMENT
+ return DATA_RATE_MEGABITS_PER_SECOND
@property
def icon(self):
diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py
index ba77942a448..38779a05cb0 100644
--- a/homeassistant/components/fibaro/light.py
+++ b/homeassistant/components/fibaro/light.py
@@ -68,7 +68,7 @@ class FibaroLight(FibaroDevice, Light):
supports_dimming = "levelChange" in fibaro_device.interfaces
supports_white_v = "setW" in fibaro_device.actions
- # Configuration can overrride default capability detection
+ # Configuration can override default capability detection
if devconf.get(CONF_DIMMING, supports_dimming):
self._supported_flags |= SUPPORT_BRIGHTNESS
if devconf.get(CONF_COLOR, supports_color):
diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py
index 086ae87a529..f444abd25ee 100644
--- a/homeassistant/components/fido/sensor.py
+++ b/homeassistant/components/fido/sensor.py
@@ -20,6 +20,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
+ DATA_KILOBITS,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -27,7 +28,6 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
-KILOBITS = "Kb"
PRICE = "CAD"
MESSAGES = "messages"
MINUTES = "minutes"
@@ -40,9 +40,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
SENSOR_TYPES = {
"fido_dollar": ["Fido dollar", PRICE, "mdi:square-inc-cash"],
"balance": ["Balance", PRICE, "mdi:square-inc-cash"],
- "data_used": ["Data used", KILOBITS, "mdi:download"],
- "data_limit": ["Data limit", KILOBITS, "mdi:download"],
- "data_remaining": ["Data remaining", KILOBITS, "mdi:download"],
+ "data_used": ["Data used", DATA_KILOBITS, "mdi:download"],
+ "data_limit": ["Data limit", DATA_KILOBITS, "mdi:download"],
+ "data_remaining": ["Data remaining", DATA_KILOBITS, "mdi:download"],
"text_used": ["Text used", MESSAGES, "mdi:message-text"],
"text_limit": ["Text limit", MESSAGES, "mdi:message-text"],
"text_remaining": ["Text remaining", MESSAGES, "mdi:message-text"],
diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py
index 8c6cd30b118..3d96aab04e9 100644
--- a/homeassistant/components/filesize/sensor.py
+++ b/homeassistant/components/filesize/sensor.py
@@ -6,6 +6,7 @@ import os
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import DATA_MEGABYTES
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -42,7 +43,7 @@ class Filesize(Entity):
self._size = None
self._last_updated = None
self._name = path.split("/")[-1]
- self._unit_of_measurement = "MB"
+ self._unit_of_measurement = DATA_MEGABYTES
def update(self):
"""Update the sensor."""
diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py
index baa4f90af3f..77622f62b1d 100644
--- a/homeassistant/components/filter/sensor.py
+++ b/homeassistant/components/filter/sensor.py
@@ -377,7 +377,7 @@ class Filter:
@property
def skip_processing(self):
- """Return wether the current filter_state should be skipped."""
+ """Return whether the current filter_state should be skipped."""
return self._skip_processing
def _filter_state(self, new_state):
diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py
index a706ab2a0b5..19a5791d7cb 100644
--- a/homeassistant/components/folder/sensor.py
+++ b/homeassistant/components/folder/sensor.py
@@ -7,6 +7,7 @@ import os
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import DATA_MEGABYTES
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -63,7 +64,7 @@ class Folder(Entity):
self._number_of_files = None
self._size = None
self._name = os.path.split(os.path.split(folder_path)[0])[1]
- self._unit_of_measurement = "MB"
+ self._unit_of_measurement = DATA_MEGABYTES
self._file_list = None
def update(self):
diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json
index 5a29a619a33..7a66490c90d 100644
--- a/homeassistant/components/freebox/manifest.json
+++ b/homeassistant/components/freebox/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/freebox",
"requirements": ["aiofreepybox==0.0.8"],
"dependencies": [],
+ "after_dependencies": ["discovery"],
"codeowners": ["@snoof85"]
}
diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py
index 61ec670d217..0653120b49c 100644
--- a/homeassistant/components/freebox/sensor.py
+++ b/homeassistant/components/freebox/sensor.py
@@ -1,6 +1,7 @@
"""Support for Freebox devices (Freebox v6 and Freebox mini 4K)."""
import logging
+from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND
from homeassistant.helpers.entity import Entity
from . import DATA_FREEBOX
@@ -56,7 +57,7 @@ class FbxRXSensor(FbxSensor):
"""Update the Freebox RxSensor."""
_name = "Freebox download speed"
- _unit = "KB/s"
+ _unit = DATA_RATE_KILOBYTES_PER_SECOND
_icon = "mdi:download-network"
async def async_update(self):
@@ -70,7 +71,7 @@ class FbxTXSensor(FbxSensor):
"""Update the Freebox TxSensor."""
_name = "Freebox upload speed"
- _unit = "KB/s"
+ _unit = DATA_RATE_KILOBYTES_PER_SECOND
_icon = "mdi:upload-network"
async def async_update(self):
diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py
index b6655c9634f..062d6a699fe 100644
--- a/homeassistant/components/freebox/switch.py
+++ b/homeassistant/components/freebox/switch.py
@@ -18,7 +18,7 @@ class FbxWifiSwitch(SwitchDevice):
"""Representation of a freebox wifi switch."""
def __init__(self, fbx):
- """Initilize the Wifi switch."""
+ """Initialize the Wifi switch."""
self._name = "Freebox WiFi"
self._state = None
self._fbx = fbx
diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json
index 21b86e26af1..5536e8fada3 100644
--- a/homeassistant/components/fritz/manifest.json
+++ b/homeassistant/components/fritz/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "fritz",
- "name": "AVM Fritzbox",
+ "name": "AVM FRITZ!Box",
"documentation": "https://www.home-assistant.io/integrations/fritz",
"requirements": ["fritzconnection==1.2.0"],
"dependencies": [],
diff --git a/homeassistant/components/fritzdect/__init__.py b/homeassistant/components/fritzdect/__init__.py
deleted file mode 100644
index d64990bc3f0..00000000000
--- a/homeassistant/components/fritzdect/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The fritzdect component."""
diff --git a/homeassistant/components/fritzdect/manifest.json b/homeassistant/components/fritzdect/manifest.json
deleted file mode 100644
index 9fc91293608..00000000000
--- a/homeassistant/components/fritzdect/manifest.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "domain": "fritzdect",
- "name": "AVM FRITZ!DECT",
- "documentation": "https://www.home-assistant.io/integrations/fritzdect",
- "requirements": ["fritzhome==1.0.4"],
- "dependencies": [],
- "codeowners": []
-}
diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py
deleted file mode 100644
index f67c84ae552..00000000000
--- a/homeassistant/components/fritzdect/switch.py
+++ /dev/null
@@ -1,224 +0,0 @@
-"""Support for FRITZ!DECT Switches."""
-import logging
-
-from fritzhome.fritz import FritzBox
-from requests.exceptions import HTTPError, RequestException
-import voluptuous as vol
-
-from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
-from homeassistant.const import (
- ATTR_TEMPERATURE,
- CONF_HOST,
- CONF_PASSWORD,
- CONF_USERNAME,
- ENERGY_KILO_WATT_HOUR,
- POWER_WATT,
- TEMP_CELSIUS,
-)
-import homeassistant.helpers.config_validation as cv
-
-_LOGGER = logging.getLogger(__name__)
-
-# Standard Fritz Box IP
-DEFAULT_HOST = "fritz.box"
-
-ATTR_CURRENT_CONSUMPTION = "current_consumption"
-ATTR_CURRENT_CONSUMPTION_UNIT = "current_consumption_unit"
-ATTR_CURRENT_CONSUMPTION_UNIT_VALUE = POWER_WATT
-
-ATTR_TOTAL_CONSUMPTION = "total_consumption"
-ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit"
-ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR
-
-ATTR_TEMPERATURE_UNIT = "temperature_unit"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Add all switches connected to Fritz Box."""
-
- host = config.get(CONF_HOST)
- username = config.get(CONF_USERNAME)
- password = config.get(CONF_PASSWORD)
-
- # Log into Fritz Box
- fritz = FritzBox(host, username, password)
- try:
- fritz.login()
- except Exception: # pylint: disable=broad-except
- _LOGGER.error("Login to Fritz!Box failed")
- return
-
- # Add all actors to hass
- for actor in fritz.get_actors():
- # Only add devices that support switching
- if actor.has_switch:
- data = FritzDectSwitchData(fritz, actor.actor_id)
- data.is_online = True
- add_entities([FritzDectSwitch(hass, data, actor.name)], True)
-
-
-class FritzDectSwitch(SwitchDevice):
- """Representation of a FRITZ!DECT switch."""
-
- def __init__(self, hass, data, name):
- """Initialize the switch."""
- self.units = hass.config.units
- self.data = data
- self._name = name
-
- @property
- def name(self):
- """Return the name of the FRITZ!DECT switch, if any."""
- return self._name
-
- @property
- def device_state_attributes(self):
- """Return the state attributes of the device."""
- attrs = {}
-
- if (
- self.data.has_powermeter
- and self.data.current_consumption is not None
- and self.data.total_consumption is not None
- ):
- attrs[ATTR_CURRENT_CONSUMPTION] = "{:.1f}".format(
- self.data.current_consumption
- )
- attrs[ATTR_CURRENT_CONSUMPTION_UNIT] = "{}".format(
- ATTR_CURRENT_CONSUMPTION_UNIT_VALUE
- )
- attrs[ATTR_TOTAL_CONSUMPTION] = f"{self.data.total_consumption:.3f}"
- attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = "{}".format(
- ATTR_TOTAL_CONSUMPTION_UNIT_VALUE
- )
-
- if self.data.has_temperature and self.data.temperature is not None:
- attrs[ATTR_TEMPERATURE] = "{}".format(
- self.units.temperature(self.data.temperature, TEMP_CELSIUS)
- )
- attrs[ATTR_TEMPERATURE_UNIT] = f"{self.units.temperature_unit}"
- return attrs
-
- @property
- def current_power_w(self):
- """Return the current power usage in Watt."""
- try:
- return float(self.data.current_consumption)
- except ValueError:
- return None
-
- @property
- def is_on(self):
- """Return true if switch is on."""
- return self.data.state
-
- def turn_on(self, **kwargs):
- """Turn the switch on."""
- if not self.data.is_online:
- _LOGGER.error("turn_on: Not online skipping request")
- return
-
- try:
- actor = self.data.fritz.get_actor_by_ain(self.data.ain)
- actor.switch_on()
- except (RequestException, HTTPError):
- _LOGGER.error("Fritz!Box query failed, triggering relogin")
- self.data.is_online = False
-
- def turn_off(self, **kwargs):
- """Turn the switch off."""
- if not self.data.is_online:
- _LOGGER.error("turn_off: Not online skipping request")
- return
-
- try:
- actor = self.data.fritz.get_actor_by_ain(self.data.ain)
- actor.switch_off()
- except (RequestException, HTTPError):
- _LOGGER.error("Fritz!Box query failed, triggering relogin")
- self.data.is_online = False
-
- def update(self):
- """Get the latest data from the fritz box and updates the states."""
- if not self.data.is_online:
- _LOGGER.error("update: Not online, logging back in")
-
- try:
- self.data.fritz.login()
- except Exception: # pylint: disable=broad-except
- _LOGGER.error("Login to Fritz!Box failed")
- return
-
- self.data.is_online = True
-
- try:
- self.data.update()
- except Exception: # pylint: disable=broad-except
- _LOGGER.error("Fritz!Box query failed, triggering relogin")
- self.data.is_online = False
-
-
-class FritzDectSwitchData:
- """Get the latest data from the fritz box."""
-
- def __init__(self, fritz, ain):
- """Initialize the data object."""
- self.fritz = fritz
- self.ain = ain
- self.state = None
- self.temperature = None
- self.current_consumption = None
- self.total_consumption = None
- self.has_switch = False
- self.has_temperature = False
- self.has_powermeter = False
- self.is_online = False
-
- def update(self):
- """Get the latest data from the fritz box."""
- if not self.is_online:
- _LOGGER.error("Not online skipping request")
- return
-
- try:
- actor = self.fritz.get_actor_by_ain(self.ain)
- except (RequestException, HTTPError):
- _LOGGER.error("Request to actor registry failed")
- self.state = None
- self.temperature = None
- self.current_consumption = None
- self.total_consumption = None
- raise Exception("Request to actor registry failed")
-
- if actor is None:
- _LOGGER.error("Actor could not be found")
- self.state = None
- self.temperature = None
- self.current_consumption = None
- self.total_consumption = None
- raise Exception("Actor could not be found")
-
- try:
- self.state = actor.get_state()
- self.current_consumption = (actor.get_power() or 0.0) / 1000
- self.total_consumption = (actor.get_energy() or 0.0) / 1000
- except (RequestException, HTTPError):
- _LOGGER.error("Request to actor failed")
- self.state = None
- self.temperature = None
- self.current_consumption = None
- self.total_consumption = None
- raise Exception("Request to actor failed")
-
- self.temperature = actor.temperature
- self.has_switch = actor.has_switch
- self.has_temperature = actor.has_temperature
- self.has_powermeter = actor.has_powermeter
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index fdea21fe91e..a6f531b6dd5 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -16,6 +16,7 @@ from homeassistant.components.http.view import HomeAssistantView
from homeassistant.config import async_hass_config_yaml
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
+from homeassistant.helpers import service
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
@@ -61,6 +62,10 @@ MANIFEST_JSON = {
"short_name": "Assistant",
"start_url": "/?homescreen=1",
"theme_color": DEFAULT_THEME_COLOR,
+ "prefer_related_applications": True,
+ "related_applications": [
+ {"platform": "play", "id": "io.homeassistant.companion.android"}
+ ],
}
DATA_PANELS = "frontend_panels"
@@ -103,19 +108,6 @@ CONFIG_SCHEMA = vol.Schema(
SERVICE_SET_THEME = "set_theme"
SERVICE_RELOAD_THEMES = "reload_themes"
-SERVICE_SET_THEME_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string})
-WS_TYPE_GET_PANELS = "get_panels"
-SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
- {vol.Required("type"): WS_TYPE_GET_PANELS}
-)
-WS_TYPE_GET_THEMES = "frontend/get_themes"
-SCHEMA_GET_THEMES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
- {vol.Required("type"): WS_TYPE_GET_THEMES}
-)
-WS_TYPE_GET_TRANSLATIONS = "frontend/get_translations"
-SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
- {vol.Required("type"): WS_TYPE_GET_TRANSLATIONS, vol.Required("language"): str}
-)
class Panel:
@@ -251,15 +243,9 @@ def _frontend_root(dev_repo_path):
async def async_setup(hass, config):
"""Set up the serving of the frontend."""
await async_setup_frontend_storage(hass)
- hass.components.websocket_api.async_register_command(
- WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS
- )
- hass.components.websocket_api.async_register_command(
- WS_TYPE_GET_THEMES, websocket_get_themes, SCHEMA_GET_THEMES
- )
- hass.components.websocket_api.async_register_command(
- WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS
- )
+ hass.components.websocket_api.async_register_command(websocket_get_panels)
+ hass.components.websocket_api.async_register_command(websocket_get_themes)
+ hass.components.websocket_api.async_register_command(websocket_get_translations)
hass.http.register_view(ManifestJSONView)
conf = config.get(DOMAIN, {})
@@ -331,11 +317,7 @@ async def async_setup(hass, config):
def _async_setup_themes(hass, themes):
"""Set up themes data and services."""
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
- if themes is None:
- hass.data[DATA_THEMES] = {}
- return
-
- hass.data[DATA_THEMES] = themes
+ hass.data[DATA_THEMES] = themes or {}
@callback
def update_theme_and_fire_event():
@@ -348,9 +330,7 @@ def _async_setup_themes(hass, themes):
"app-header-background-color",
themes[name].get(PRIMARY_COLOR, DEFAULT_THEME_COLOR),
)
- hass.bus.async_fire(
- EVENT_THEMES_UPDATED, {"themes": themes, "default_theme": name}
- )
+ hass.bus.async_fire(EVENT_THEMES_UPDATED)
@callback
def set_theme(call):
@@ -373,10 +353,17 @@ def _async_setup_themes(hass, themes):
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
update_theme_and_fire_event()
- hass.services.async_register(
- DOMAIN, SERVICE_SET_THEME, set_theme, schema=SERVICE_SET_THEME_SCHEMA
+ service.async_register_admin_service(
+ hass,
+ DOMAIN,
+ SERVICE_SET_THEME,
+ set_theme,
+ vol.Schema({vol.Required(CONF_NAME): cv.string}),
+ )
+
+ service.async_register_admin_service(
+ hass, DOMAIN, SERVICE_RELOAD_THEMES, reload_themes
)
- hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes)
class IndexView(web_urldispatcher.AbstractResource):
@@ -498,6 +485,7 @@ class ManifestJSONView(HomeAssistantView):
@callback
+@websocket_api.websocket_command({"type": "get_panels"})
def websocket_get_panels(hass, connection, msg):
"""Handle get panels command.
@@ -514,11 +502,29 @@ def websocket_get_panels(hass, connection, msg):
@callback
+@websocket_api.websocket_command({"type": "frontend/get_themes"})
def websocket_get_themes(hass, connection, msg):
"""Handle get themes command.
Async friendly.
"""
+ if hass.config.safe_mode:
+ connection.send_message(
+ websocket_api.result_message(
+ msg["id"],
+ {
+ "themes": {
+ "safe_mode": {
+ "primary-color": "#db4437",
+ "accent-color": "#eeee02",
+ }
+ },
+ "default_theme": "safe_mode",
+ },
+ )
+ )
+ return
+
connection.send_message(
websocket_api.result_message(
msg["id"],
@@ -530,6 +536,9 @@ def websocket_get_themes(hass, connection, msg):
)
+@websocket_api.websocket_command(
+ {"type": "frontend/get_translations", vol.Required("language"): str}
+)
@websocket_api.async_response
async def websocket_get_translations(hass, connection, msg):
"""Handle get translations command.
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index 10d2884f182..b9575b7f21a 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==20200130.3"
+ "home-assistant-frontend==20200220.4"
],
"dependencies": [
"api",
diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py
index 010420d0f98..627c3c079b9 100644
--- a/homeassistant/components/frontier_silicon/media_player.py
+++ b/homeassistant/components/frontier_silicon/media_player.py
@@ -275,7 +275,7 @@ class AFSAPIDevice(MediaPlayerDevice):
async def async_set_volume_level(self, volume):
"""Set volume command."""
- await self.fs_device.set_volume(volume)
+ await self.fs_device.set_volume(int(volume * 20))
async def async_select_source(self, source):
"""Select input source."""
diff --git a/homeassistant/components/garmin_connect/.translations/es-419.json b/homeassistant/components/garmin_connect/.translations/es-419.json
new file mode 100644
index 00000000000..6e20b4cd2cc
--- /dev/null
+++ b/homeassistant/components/garmin_connect/.translations/es-419.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Esta cuenta ya est\u00e1 configurada."
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar, intente nuevamente.",
+ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida",
+ "too_many_requests": "Demasiadas solicitudes, vuelva a intentarlo m\u00e1s tarde.",
+ "unknown": "Error inesperado."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Nombre de usuario"
+ },
+ "description": "Ingrese sus credenciales."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garmin_connect/.translations/fr.json b/homeassistant/components/garmin_connect/.translations/fr.json
new file mode 100644
index 00000000000..f0dd8a79e5b
--- /dev/null
+++ b/homeassistant/components/garmin_connect/.translations/fr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9."
+ },
+ "error": {
+ "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer.",
+ "invalid_auth": "Authentification non valide.",
+ "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.",
+ "unknown": "Erreur inattendue."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ },
+ "description": "Entrez vos informations d'identification.",
+ "title": "Garmin Connect"
+ }
+ },
+ "title": "Garmin Connect"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garmin_connect/.translations/hu.json b/homeassistant/components/garmin_connect/.translations/hu.json
index de4dea29166..931fa295962 100644
--- a/homeassistant/components/garmin_connect/.translations/hu.json
+++ b/homeassistant/components/garmin_connect/.translations/hu.json
@@ -2,6 +2,23 @@
"config": {
"abort": {
"already_configured": "Ez a fi\u00f3k m\u00e1r konfigur\u00e1lva van."
- }
+ },
+ "error": {
+ "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.",
+ "too_many_requests": "T\u00fal sok k\u00e9r\u00e9s, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra.",
+ "unknown": "V\u00e1ratlan hiba."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "description": "Adja meg a hiteles\u00edt\u0151 adatait.",
+ "title": "Garmin Csatlakoz\u00e1s"
+ }
+ },
+ "title": "Garmin Csatlakoz\u00e1s"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/garmin_connect/.translations/nl.json b/homeassistant/components/garmin_connect/.translations/nl.json
new file mode 100644
index 00000000000..c7a690de6e2
--- /dev/null
+++ b/homeassistant/components/garmin_connect/.translations/nl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Dit account is al geconfigureerd."
+ },
+ "error": {
+ "cannot_connect": "Verbinding mislukt, probeer het opnieuw.",
+ "invalid_auth": "Ongeldige authenticatie",
+ "too_many_requests": "Te veel aanvragen, probeer het later opnieuw.",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ },
+ "description": "Voer uw gegevens in",
+ "title": "Garmin Connect"
+ }
+ },
+ "title": "Garmin Connect"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garmin_connect/.translations/sl.json b/homeassistant/components/garmin_connect/.translations/sl.json
new file mode 100644
index 00000000000..5b85611d5b7
--- /dev/null
+++ b/homeassistant/components/garmin_connect/.translations/sl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ta ra\u010dun je \u017ee konfiguriran."
+ },
+ "error": {
+ "cannot_connect": "Povezava ni uspela, poskusite znova.",
+ "invalid_auth": "Neveljavna avtentikacija.",
+ "too_many_requests": "Preve\u010d zahtev, poskusite pozneje.",
+ "unknown": "Nepri\u010dakovana napaka."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Geslo",
+ "username": "Uporabni\u0161ko ime"
+ },
+ "description": "Vnesite svoje poverilnice.",
+ "title": "Garmin Connect"
+ }
+ },
+ "title": "Garmin Connect"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py
index 5336394f671..d63d82d1284 100644
--- a/homeassistant/components/garmin_connect/__init__.py
+++ b/homeassistant/components/garmin_connect/__init__.py
@@ -43,13 +43,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError,
) as err:
- _LOGGER.error("Error occured during Garmin Connect login: %s", err)
+ _LOGGER.error("Error occurred during Garmin Connect login: %s", err)
return False
except (GarminConnectConnectionError) as err:
- _LOGGER.error("Error occured during Garmin Connect login: %s", err)
+ _LOGGER.error("Error occurred during Garmin Connect login: %s", err)
raise ConfigEntryNotReady
except Exception: # pylint: disable=broad-except
- _LOGGER.error("Unknown error occured during Garmin Connect login")
+ _LOGGER.error("Unknown error occurred during Garmin Connect login")
return False
garmin_data = GarminConnectData(hass, garmin_client)
@@ -98,11 +98,11 @@ class GarminConnectData:
GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError,
) as err:
- _LOGGER.error("Error occured during Garmin Connect get stats: %s", err)
+ _LOGGER.error("Error occurred during Garmin Connect get stats: %s", err)
return
except (GarminConnectConnectionError) as err:
- _LOGGER.error("Error occured during Garmin Connect get stats: %s", err)
+ _LOGGER.error("Error occurred during Garmin Connect get stats: %s", err)
return
except Exception: # pylint: disable=broad-except
- _LOGGER.error("Unknown error occured during Garmin Connect get stats")
+ _LOGGER.error("Unknown error occurred during Garmin Connect get stats")
return
diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py
index 0ebd39e78d4..5edf54d95dc 100644
--- a/homeassistant/components/garmin_connect/sensor.py
+++ b/homeassistant/components/garmin_connect/sensor.py
@@ -32,9 +32,9 @@ async def async_setup_entry(
GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError,
) as err:
- _LOGGER.error("Error occured during Garmin Connect Client update: %s", err)
+ _LOGGER.error("Error occurred during Garmin Connect Client update: %s", err)
except Exception: # pylint: disable=broad-except
- _LOGGER.error("Unknown error occured during Garmin Connect Client update.")
+ _LOGGER.error("Unknown error occurred during Garmin Connect Client update.")
entities = []
for (
@@ -160,21 +160,20 @@ class GarminConnectSensor(Entity):
return
await self._data.async_update()
- if not self._data.data:
+ data = self._data.data
+ if not data:
_LOGGER.error("Didn't receive data from Garmin Connect")
return
-
- data = self._data.data
- try:
- if "Duration" in self._type and data[self._type]:
- self._state = data[self._type] // 60
- elif "Seconds" in self._type and data[self._type]:
- self._state = data[self._type] // 60
- else:
- self._state = data[self._type]
- except KeyError:
- _LOGGER.debug("Entity type %s not found in fetched data", self._type)
+ if data.get(self._type) is None:
+ _LOGGER.debug("Entity type %s not set in fetched data", self._type)
+ self._available = False
return
+ self._available = True
+
+ if "Duration" in self._type or "Seconds" in self._type:
+ self._state = data[self._type] // 60
+ else:
+ self._state = data[self._type]
_LOGGER.debug(
"Entity %s set to state %s %s", self._type, self._state, self._unit
diff --git a/homeassistant/components/gdacs/.translations/ca.json b/homeassistant/components/gdacs/.translations/ca.json
new file mode 100644
index 00000000000..5f5acfe7ccf
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/ca.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radi"
+ },
+ "title": "Introducci\u00f3 dels detalls del filtre."
+ }
+ },
+ "title": "Sistema Global de Coordinaci\u00f3 i Alerta per Desastres (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/da.json b/homeassistant/components/gdacs/.translations/da.json
new file mode 100644
index 00000000000..64f3dd000c4
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/da.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokaliteten er allerede konfigureret."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radius"
+ },
+ "title": "Udfyld dine filteroplysninger."
+ }
+ },
+ "title": "Globalt katastrofevarslings- og koordineringssystem (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/de.json b/homeassistant/components/gdacs/.translations/de.json
new file mode 100644
index 00000000000..12f94250402
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/de.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Der Standort ist bereits konfiguriert."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radius"
+ },
+ "title": "F\u00fclle deine Filterangaben aus."
+ }
+ },
+ "title": "Globales Katastrophenalarm- und Koordinierungssystem (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/en.json b/homeassistant/components/gdacs/.translations/en.json
new file mode 100644
index 00000000000..4e7ceb3846c
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/en.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Location is already configured."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radius"
+ },
+ "title": "Fill in your filter details."
+ }
+ },
+ "title": "Global Disaster Alert and Coordination System (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/es.json b/homeassistant/components/gdacs/.translations/es.json
new file mode 100644
index 00000000000..6c02b339541
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/es.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radio"
+ },
+ "title": "Rellena los datos de tu filtro."
+ }
+ },
+ "title": "Sistema Mundial de Alerta y Coordinaci\u00f3n de Desastres (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/fr.json b/homeassistant/components/gdacs/.translations/fr.json
new file mode 100644
index 00000000000..a4366cb5dc7
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/fr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Rayon"
+ },
+ "title": "Remplissez les d\u00e9tails de votre filtre."
+ }
+ },
+ "title": "Syst\u00e8me mondial d'alerte et de coordination en cas de catastrophe (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/hu.json b/homeassistant/components/gdacs/.translations/hu.json
new file mode 100644
index 00000000000..79bcba3388f
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/hu.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Sug\u00e1r"
+ },
+ "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait."
+ }
+ },
+ "title": "Glob\u00e1lis katasztr\u00f3fariaszt\u00e1si \u00e9s koordin\u00e1ci\u00f3s rendszer (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/it.json b/homeassistant/components/gdacs/.translations/it.json
new file mode 100644
index 00000000000..249b47f9f59
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/it.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La posizione \u00e8 gi\u00e0 configurata."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Raggio"
+ },
+ "title": "Inserisci i tuoi dettagli del filtro."
+ }
+ },
+ "title": "Sistema globale di allerta e coordinamento delle catastrofi (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/ko.json b/homeassistant/components/gdacs/.translations/ko.json
new file mode 100644
index 00000000000..10d6f73e56f
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/ko.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "\ubc18\uacbd"
+ },
+ "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694."
+ }
+ },
+ "title": "\uad6d\uc81c \uc7ac\ub09c \uacbd\ubcf4 \ubc0f \uc870\uc815 \uae30\uad6c (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/lb.json b/homeassistant/components/gdacs/.translations/lb.json
new file mode 100644
index 00000000000..a4077ed630e
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/lb.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Standuert ass scho konfigu\u00e9iert."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radius"
+ },
+ "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus."
+ }
+ },
+ "title": "Globale D\u00e9saster Alerte a Koordinatioun System (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/nl.json b/homeassistant/components/gdacs/.translations/nl.json
new file mode 100644
index 00000000000..62383e43e36
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/nl.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Locatie is al geconfigureerd."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radius"
+ },
+ "title": "Vul uw filtergegevens in."
+ }
+ },
+ "title": "Wereldwijd rampenwaarschuwings- en co\u00f6rdinatiesysteem (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/no.json b/homeassistant/components/gdacs/.translations/no.json
new file mode 100644
index 00000000000..54b3ca68451
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/no.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Plasseringen er allerede konfigurert."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radius"
+ },
+ "title": "Fyll ut filterdetaljene."
+ }
+ },
+ "title": "Globalt katastrofevarslings- og koordineringssystem (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/pl.json b/homeassistant/components/gdacs/.translations/pl.json
new file mode 100644
index 00000000000..f4b90cc35c7
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/pl.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokalizacja jest ju\u017c skonfigurowana."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Promie\u0144"
+ },
+ "title": "Wprowad\u017a szczeg\u00f3\u0142owe dane filtra."
+ }
+ },
+ "title": "Globalny system ostrzegania i koordynacji w przypadku katastrof (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/ru.json b/homeassistant/components/gdacs/.translations/ru.json
new file mode 100644
index 00000000000..f006832b5be
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/ru.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "\u0420\u0430\u0434\u0438\u0443\u0441"
+ },
+ "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435"
+ }
+ },
+ "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f \u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0446\u0438\u0438 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0442\u0438\u0445\u0438\u0439\u043d\u044b\u0445 \u0431\u0435\u0434\u0441\u0442\u0432\u0438\u0439 (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/sl.json b/homeassistant/components/gdacs/.translations/sl.json
new file mode 100644
index 00000000000..fc522a1a263
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/sl.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokacija je \u017ee nastavljena."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radij"
+ },
+ "title": "Izpolnite podrobnosti filtra."
+ }
+ },
+ "title": "Globalni sistem opozarjanja in koordinacije nesre\u010d (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/sv.json b/homeassistant/components/gdacs/.translations/sv.json
new file mode 100644
index 00000000000..3c7fb00056e
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/sv.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Plats \u00e4r redan konfigurerad."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radie"
+ },
+ "title": "Fyll i filterinformation."
+ }
+ },
+ "title": "Globalt katastrofvarnings- och samordningssystem (GDACS)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/.translations/zh-Hant.json b/homeassistant/components/gdacs/.translations/zh-Hant.json
new file mode 100644
index 00000000000..59f9b7be031
--- /dev/null
+++ b/homeassistant/components/gdacs/.translations/zh-Hant.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u4f4d\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "\u534a\u5f91"
+ },
+ "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002"
+ }
+ },
+ "title": "\u5168\u7403\u707d\u96e3\u9810\u8b66\u548c\u5354\u8abf\u7cfb\u7d71\uff08GDACS\uff09"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py
new file mode 100644
index 00000000000..34f1bdc88d8
--- /dev/null
+++ b/homeassistant/components/gdacs/__init__.py
@@ -0,0 +1,212 @@
+"""The Global Disaster Alert and Coordination System (GDACS) integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+from aio_georss_gdacs import GdacsFeedManager
+import voluptuous as vol
+
+from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.const import (
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_RADIUS,
+ CONF_SCAN_INTERVAL,
+ CONF_UNIT_SYSTEM_IMPERIAL,
+ LENGTH_MILES,
+)
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client, config_validation as cv
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.util.unit_system import METRIC_SYSTEM
+
+from .const import (
+ CONF_CATEGORIES,
+ DEFAULT_RADIUS,
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+ FEED,
+ PLATFORMS,
+ SIGNAL_DELETE_ENTITY,
+ SIGNAL_NEW_GEOLOCATION,
+ SIGNAL_STATUS,
+ SIGNAL_UPDATE_ENTITY,
+ VALID_CATEGORIES,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude,
+ vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude,
+ vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float),
+ vol.Optional(
+ CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
+ ): cv.time_period,
+ vol.Optional(CONF_CATEGORIES, default=[]): vol.All(
+ cv.ensure_list, [vol.In(VALID_CATEGORIES)]
+ ),
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass, config):
+ """Set up the GDACS component."""
+ if DOMAIN not in config:
+ return True
+
+ conf = config[DOMAIN]
+ latitude = conf.get(CONF_LATITUDE, hass.config.latitude)
+ longitude = conf.get(CONF_LONGITUDE, hass.config.longitude)
+ scan_interval = conf[CONF_SCAN_INTERVAL]
+ categories = conf[CONF_CATEGORIES]
+
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={
+ CONF_LATITUDE: latitude,
+ CONF_LONGITUDE: longitude,
+ CONF_RADIUS: conf[CONF_RADIUS],
+ CONF_SCAN_INTERVAL: scan_interval,
+ CONF_CATEGORIES: categories,
+ },
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up the GDACS component as config entry."""
+ hass.data.setdefault(DOMAIN, {})
+ feeds = hass.data[DOMAIN].setdefault(FEED, {})
+
+ radius = config_entry.data[CONF_RADIUS]
+ if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+ radius = METRIC_SYSTEM.length(radius, LENGTH_MILES)
+ # Create feed entity manager for all platforms.
+ manager = GdacsFeedEntityManager(hass, config_entry, radius)
+ feeds[config_entry.entry_id] = manager
+ _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id)
+ await manager.async_init()
+ return True
+
+
+async def async_unload_entry(hass, config_entry):
+ """Unload an GDACS component config entry."""
+ manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id)
+ await manager.async_stop()
+ await asyncio.wait(
+ [
+ hass.config_entries.async_forward_entry_unload(config_entry, domain)
+ for domain in PLATFORMS
+ ]
+ )
+ return True
+
+
+class GdacsFeedEntityManager:
+ """Feed Entity Manager for GDACS feed."""
+
+ def __init__(self, hass, config_entry, radius_in_km):
+ """Initialize the Feed Entity Manager."""
+ self._hass = hass
+ self._config_entry = config_entry
+ coordinates = (
+ config_entry.data[CONF_LATITUDE],
+ config_entry.data[CONF_LONGITUDE],
+ )
+ categories = config_entry.data[CONF_CATEGORIES]
+ websession = aiohttp_client.async_get_clientsession(hass)
+ self._feed_manager = GdacsFeedManager(
+ websession,
+ self._generate_entity,
+ self._update_entity,
+ self._remove_entity,
+ coordinates,
+ filter_radius=radius_in_km,
+ filter_categories=categories,
+ status_async_callback=self._status_update,
+ )
+ self._config_entry_id = config_entry.entry_id
+ self._scan_interval = timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL])
+ self._track_time_remove_callback = None
+ self._status_info = None
+ self.listeners = []
+
+ async def async_init(self):
+ """Schedule initial and regular updates based on configured time interval."""
+
+ for domain in PLATFORMS:
+ self._hass.async_create_task(
+ self._hass.config_entries.async_forward_entry_setup(
+ self._config_entry, domain
+ )
+ )
+
+ async def update(event_time):
+ """Update."""
+ await self.async_update()
+
+ # Trigger updates at regular intervals.
+ self._track_time_remove_callback = async_track_time_interval(
+ self._hass, update, self._scan_interval
+ )
+
+ _LOGGER.debug("Feed entity manager initialized")
+
+ async def async_update(self):
+ """Refresh data."""
+ await self._feed_manager.update()
+ _LOGGER.debug("Feed entity manager updated")
+
+ async def async_stop(self):
+ """Stop this feed entity manager from refreshing."""
+ for unsub_dispatcher in self.listeners:
+ unsub_dispatcher()
+ self.listeners = []
+ if self._track_time_remove_callback:
+ self._track_time_remove_callback()
+ _LOGGER.debug("Feed entity manager stopped")
+
+ @callback
+ def async_event_new_entity(self):
+ """Return manager specific event to signal new entity."""
+ return SIGNAL_NEW_GEOLOCATION.format(self._config_entry_id)
+
+ def get_entry(self, external_id):
+ """Get feed entry by external id."""
+ return self._feed_manager.feed_entries.get(external_id)
+
+ def status_info(self):
+ """Return latest status update info received."""
+ return self._status_info
+
+ async def _generate_entity(self, external_id):
+ """Generate new entity."""
+ async_dispatcher_send(
+ self._hass, self.async_event_new_entity(), self, external_id
+ )
+
+ async def _update_entity(self, external_id):
+ """Update entity."""
+ async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
+
+ async def _remove_entity(self, external_id):
+ """Remove entity."""
+ async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
+
+ async def _status_update(self, status_info):
+ """Propagate status update."""
+ _LOGGER.debug("Status update received: %s", status_info)
+ self._status_info = status_info
+ async_dispatcher_send(self._hass, SIGNAL_STATUS.format(self._config_entry_id))
diff --git a/homeassistant/components/gdacs/config_flow.py b/homeassistant/components/gdacs/config_flow.py
new file mode 100644
index 00000000000..1e12a116ed5
--- /dev/null
+++ b/homeassistant/components/gdacs/config_flow.py
@@ -0,0 +1,66 @@
+"""Config flow to configure the GDACS integration."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import (
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_RADIUS,
+ CONF_SCAN_INTERVAL,
+)
+from homeassistant.helpers import config_validation as cv
+
+from .const import ( # pylint: disable=unused-import
+ CONF_CATEGORIES,
+ DEFAULT_RADIUS,
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+)
+
+DATA_SCHEMA = vol.Schema(
+ {vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int}
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class GdacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a GDACS config flow."""
+
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def _show_form(self, errors=None):
+ """Show the form to the user."""
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors or {}
+ )
+
+ async def async_step_import(self, import_config):
+ """Import a config entry from configuration.yaml."""
+ return await self.async_step_user(import_config)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the start of the config flow."""
+ _LOGGER.debug("User input: %s", user_input)
+ if not user_input:
+ return await self._show_form()
+
+ latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude)
+ user_input[CONF_LATITUDE] = latitude
+ longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude)
+ user_input[CONF_LONGITUDE] = longitude
+
+ identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}"
+
+ await self.async_set_unique_id(identifier)
+ self._abort_if_unique_id_configured()
+
+ scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds
+
+ categories = user_input.get(CONF_CATEGORIES, [])
+ user_input[CONF_CATEGORIES] = categories
+
+ return self.async_create_entry(title=identifier, data=user_input)
diff --git a/homeassistant/components/gdacs/const.py b/homeassistant/components/gdacs/const.py
new file mode 100644
index 00000000000..4579304f30d
--- /dev/null
+++ b/homeassistant/components/gdacs/const.py
@@ -0,0 +1,25 @@
+"""Define constants for the GDACS integration."""
+from datetime import timedelta
+
+from aio_georss_gdacs.consts import EVENT_TYPE_MAP
+
+DOMAIN = "gdacs"
+
+PLATFORMS = ("sensor", "geo_location")
+
+FEED = "feed"
+
+CONF_CATEGORIES = "categories"
+
+DEFAULT_ICON = "mdi:alert"
+DEFAULT_RADIUS = 500.0
+DEFAULT_SCAN_INTERVAL = timedelta(minutes=5)
+
+SIGNAL_DELETE_ENTITY = "gdacs_delete_{}"
+SIGNAL_UPDATE_ENTITY = "gdacs_update_{}"
+SIGNAL_STATUS = "gdacs_status_{}"
+
+SIGNAL_NEW_GEOLOCATION = "gdacs_new_geolocation_{}"
+
+# Fetch valid categories from integration library.
+VALID_CATEGORIES = list(EVENT_TYPE_MAP.values())
diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py
new file mode 100644
index 00000000000..34da104e093
--- /dev/null
+++ b/homeassistant/components/gdacs/geo_location.py
@@ -0,0 +1,234 @@
+"""Geolocation support for GDACS Feed."""
+import logging
+from typing import Optional
+
+from homeassistant.components.geo_location import GeolocationEvent
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ CONF_UNIT_SYSTEM_IMPERIAL,
+ LENGTH_KILOMETERS,
+ LENGTH_MILES,
+)
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+
+from .const import (
+ DEFAULT_ICON,
+ DOMAIN,
+ FEED,
+ SIGNAL_DELETE_ENTITY,
+ SIGNAL_UPDATE_ENTITY,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_ALERT_LEVEL = "alert_level"
+ATTR_COUNTRY = "country"
+ATTR_DESCRIPTION = "description"
+ATTR_DURATION_IN_WEEK = "duration_in_week"
+ATTR_EVENT_TYPE = "event_type"
+ATTR_EXTERNAL_ID = "external_id"
+ATTR_FROM_DATE = "from_date"
+ATTR_POPULATION = "population"
+ATTR_SEVERITY = "severity"
+ATTR_TO_DATE = "to_date"
+ATTR_VULNERABILITY = "vulnerability"
+
+ICONS = {
+ "DR": "mdi:water-off",
+ "EQ": "mdi:pulse",
+ "FL": "mdi:home-flood",
+ "TC": "mdi:weather-hurricane",
+ "TS": "mdi:waves",
+ "VO": "mdi:image-filter-hdr",
+}
+
+# An update of this entity is not making a web request, but uses internal data only.
+PARALLEL_UPDATES = 0
+
+SOURCE = "gdacs"
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the GDACS Feed platform."""
+ manager = hass.data[DOMAIN][FEED][entry.entry_id]
+
+ @callback
+ def async_add_geolocation(feed_manager, external_id):
+ """Add gelocation entity from feed."""
+ new_entity = GdacsEvent(feed_manager, external_id)
+ _LOGGER.debug("Adding geolocation %s", new_entity)
+ async_add_entities([new_entity], True)
+
+ manager.listeners.append(
+ async_dispatcher_connect(
+ hass, manager.async_event_new_entity(), async_add_geolocation
+ )
+ )
+ # Do not wait for update here so that the setup can be completed and because an
+ # update will fetch data from the feed via HTTP and then process that data.
+ hass.async_create_task(manager.async_update())
+ _LOGGER.debug("Geolocation setup done")
+
+
+class GdacsEvent(GeolocationEvent):
+ """This represents an external event with GDACS feed data."""
+
+ def __init__(self, feed_manager, external_id):
+ """Initialize entity with data from feed entry."""
+ self._feed_manager = feed_manager
+ self._external_id = external_id
+ self._title = None
+ self._distance = None
+ self._latitude = None
+ self._longitude = None
+ self._attribution = None
+ self._alert_level = None
+ self._country = None
+ self._description = None
+ self._duration_in_week = None
+ self._event_type_short = None
+ self._event_type = None
+ self._from_date = None
+ self._to_date = None
+ self._population = None
+ self._severity = None
+ self._vulnerability = None
+ self._version = None
+ self._remove_signal_delete = None
+ self._remove_signal_update = None
+
+ async def async_added_to_hass(self):
+ """Call when entity is added to hass."""
+ self._remove_signal_delete = async_dispatcher_connect(
+ self.hass,
+ SIGNAL_DELETE_ENTITY.format(self._external_id),
+ self._delete_callback,
+ )
+ self._remove_signal_update = async_dispatcher_connect(
+ self.hass,
+ SIGNAL_UPDATE_ENTITY.format(self._external_id),
+ self._update_callback,
+ )
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Call when entity will be removed from hass."""
+ self._remove_signal_delete()
+ self._remove_signal_update()
+
+ @callback
+ def _delete_callback(self):
+ """Remove this entity."""
+ self.hass.async_create_task(self.async_remove())
+
+ @callback
+ def _update_callback(self):
+ """Call update method."""
+ self.async_schedule_update_ha_state(True)
+
+ @property
+ def should_poll(self):
+ """No polling needed for GDACS feed location events."""
+ return False
+
+ async def async_update(self):
+ """Update this entity from the data held in the feed manager."""
+ _LOGGER.debug("Updating %s", self._external_id)
+ feed_entry = self._feed_manager.get_entry(self._external_id)
+ if feed_entry:
+ self._update_from_feed(feed_entry)
+
+ def _update_from_feed(self, feed_entry):
+ """Update the internal state from the provided feed entry."""
+ event_name = feed_entry.event_name
+ if not event_name:
+ # Earthquakes usually don't have an event name.
+ event_name = f"{feed_entry.country} ({feed_entry.event_id})"
+ self._title = f"{feed_entry.event_type}: {event_name}"
+ # Convert distance if not metric system.
+ if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+ self._distance = IMPERIAL_SYSTEM.length(
+ feed_entry.distance_to_home, LENGTH_KILOMETERS
+ )
+ else:
+ self._distance = feed_entry.distance_to_home
+ self._latitude = feed_entry.coordinates[0]
+ self._longitude = feed_entry.coordinates[1]
+ self._attribution = feed_entry.attribution
+ self._alert_level = feed_entry.alert_level
+ self._country = feed_entry.country
+ self._description = feed_entry.title
+ self._duration_in_week = feed_entry.duration_in_week
+ self._event_type_short = feed_entry.event_type_short
+ self._event_type = feed_entry.event_type
+ self._from_date = feed_entry.from_date
+ self._to_date = feed_entry.to_date
+ self._population = feed_entry.population
+ self._severity = feed_entry.severity
+ self._vulnerability = feed_entry.vulnerability
+ # Round vulnerability value if presented as float.
+ if isinstance(self._vulnerability, float):
+ self._vulnerability = round(self._vulnerability, 1)
+ self._version = feed_entry.version
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ if self._event_type_short and self._event_type_short in ICONS:
+ return ICONS[self._event_type_short]
+ return DEFAULT_ICON
+
+ @property
+ def source(self) -> str:
+ """Return source value of this external event."""
+ return SOURCE
+
+ @property
+ def name(self) -> Optional[str]:
+ """Return the name of the entity."""
+ return self._title
+
+ @property
+ def distance(self) -> Optional[float]:
+ """Return distance value of this external event."""
+ return self._distance
+
+ @property
+ def latitude(self) -> Optional[float]:
+ """Return latitude value of this external event."""
+ return self._latitude
+
+ @property
+ def longitude(self) -> Optional[float]:
+ """Return longitude value of this external event."""
+ return self._longitude
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+ return LENGTH_MILES
+ return LENGTH_KILOMETERS
+
+ @property
+ def device_state_attributes(self):
+ """Return the device state attributes."""
+ attributes = {}
+ for key, value in (
+ (ATTR_EXTERNAL_ID, self._external_id),
+ (ATTR_DESCRIPTION, self._description),
+ (ATTR_ATTRIBUTION, self._attribution),
+ (ATTR_EVENT_TYPE, self._event_type),
+ (ATTR_ALERT_LEVEL, self._alert_level),
+ (ATTR_COUNTRY, self._country),
+ (ATTR_DURATION_IN_WEEK, self._duration_in_week),
+ (ATTR_FROM_DATE, self._from_date),
+ (ATTR_TO_DATE, self._to_date),
+ (ATTR_POPULATION, self._population),
+ (ATTR_SEVERITY, self._severity),
+ (ATTR_VULNERABILITY, self._vulnerability),
+ ):
+ if value or isinstance(value, bool):
+ attributes[key] = value
+ return attributes
diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json
new file mode 100644
index 00000000000..45105b21ab4
--- /dev/null
+++ b/homeassistant/components/gdacs/manifest.json
@@ -0,0 +1,14 @@
+{
+ "domain": "gdacs",
+ "name": "Global Disaster Alert and Coordination System (GDACS)",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/gdacs",
+ "requirements": [
+ "aio_georss_gdacs==0.3"
+ ],
+ "dependencies": [],
+ "codeowners": [
+ "@exxamalte"
+ ],
+ "quality_scale": "platinum"
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py
new file mode 100644
index 00000000000..e58090fd165
--- /dev/null
+++ b/homeassistant/components/gdacs/sensor.py
@@ -0,0 +1,140 @@
+"""Feed Entity Manager Sensor support for GDACS Feed."""
+import logging
+from typing import Optional
+
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import dt
+
+from .const import DEFAULT_ICON, DOMAIN, FEED, SIGNAL_STATUS
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_STATUS = "status"
+ATTR_LAST_UPDATE = "last_update"
+ATTR_LAST_UPDATE_SUCCESSFUL = "last_update_successful"
+ATTR_LAST_TIMESTAMP = "last_timestamp"
+ATTR_CREATED = "created"
+ATTR_UPDATED = "updated"
+ATTR_REMOVED = "removed"
+
+DEFAULT_UNIT_OF_MEASUREMENT = "alerts"
+
+# An update of this entity is not making a web request, but uses internal data only.
+PARALLEL_UPDATES = 0
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the GDACS Feed platform."""
+ manager = hass.data[DOMAIN][FEED][entry.entry_id]
+ sensor = GdacsSensor(entry.entry_id, entry.title, manager)
+ async_add_entities([sensor])
+ _LOGGER.debug("Sensor setup done")
+
+
+class GdacsSensor(Entity):
+ """This is a status sensor for the GDACS integration."""
+
+ def __init__(self, config_entry_id, config_title, manager):
+ """Initialize entity."""
+ self._config_entry_id = config_entry_id
+ self._config_title = config_title
+ self._manager = manager
+ self._status = None
+ self._last_update = None
+ self._last_update_successful = None
+ self._last_timestamp = None
+ self._total = None
+ self._created = None
+ self._updated = None
+ self._removed = None
+ self._remove_signal_status = None
+
+ async def async_added_to_hass(self):
+ """Call when entity is added to hass."""
+ self._remove_signal_status = async_dispatcher_connect(
+ self.hass,
+ SIGNAL_STATUS.format(self._config_entry_id),
+ self._update_status_callback,
+ )
+ _LOGGER.debug("Waiting for updates %s", self._config_entry_id)
+ # First update is manual because of how the feed entity manager is updated.
+ await self.async_update()
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Call when entity will be removed from hass."""
+ if self._remove_signal_status:
+ self._remove_signal_status()
+
+ @callback
+ def _update_status_callback(self):
+ """Call status update method."""
+ _LOGGER.debug("Received status update for %s", self._config_entry_id)
+ self.async_schedule_update_ha_state(True)
+
+ @property
+ def should_poll(self):
+ """No polling needed for GDACS status sensor."""
+ return False
+
+ async def async_update(self):
+ """Update this entity from the data held in the feed manager."""
+ _LOGGER.debug("Updating %s", self._config_entry_id)
+ if self._manager:
+ status_info = self._manager.status_info()
+ if status_info:
+ self._update_from_status_info(status_info)
+
+ def _update_from_status_info(self, status_info):
+ """Update the internal state from the provided information."""
+ self._status = status_info.status
+ self._last_update = (
+ dt.as_utc(status_info.last_update) if status_info.last_update else None
+ )
+ if status_info.last_update_successful:
+ self._last_update_successful = dt.as_utc(status_info.last_update_successful)
+ else:
+ self._last_update_successful = None
+ self._last_timestamp = status_info.last_timestamp
+ self._total = status_info.total
+ self._created = status_info.created
+ self._updated = status_info.updated
+ self._removed = status_info.removed
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._total
+
+ @property
+ def name(self) -> Optional[str]:
+ """Return the name of the entity."""
+ return f"GDACS ({self._config_title})"
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return DEFAULT_ICON
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return DEFAULT_UNIT_OF_MEASUREMENT
+
+ @property
+ def device_state_attributes(self):
+ """Return the device state attributes."""
+ attributes = {}
+ for key, value in (
+ (ATTR_STATUS, self._status),
+ (ATTR_LAST_UPDATE, self._last_update),
+ (ATTR_LAST_UPDATE_SUCCESSFUL, self._last_update_successful),
+ (ATTR_LAST_TIMESTAMP, self._last_timestamp),
+ (ATTR_CREATED, self._created),
+ (ATTR_UPDATED, self._updated),
+ (ATTR_REMOVED, self._removed),
+ ):
+ if value or isinstance(value, bool):
+ attributes[key] = value
+ return attributes
diff --git a/homeassistant/components/gdacs/strings.json b/homeassistant/components/gdacs/strings.json
new file mode 100644
index 00000000000..353b1b85634
--- /dev/null
+++ b/homeassistant/components/gdacs/strings.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "title": "Global Disaster Alert and Coordination System (GDACS)",
+ "step": {
+ "user": {
+ "title": "Fill in your filter details.",
+ "data": {
+ "radius": "Radius"
+ }
+ }
+ },
+ "abort": {
+ "already_configured": "Location is already configured."
+ }
+ }
+}
diff --git a/homeassistant/components/geonetnz_quakes/.translations/ko.json b/homeassistant/components/geonetnz_quakes/.translations/ko.json
index 26caa2ebe54..66a216149dd 100644
--- a/homeassistant/components/geonetnz_quakes/.translations/ko.json
+++ b/homeassistant/components/geonetnz_quakes/.translations/ko.json
@@ -9,7 +9,7 @@
"mmi": "MMI",
"radius": "\ubc18\uacbd"
},
- "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694."
}
},
"title": "GeoNet NZ Quakes"
diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json
index fd82bba43b5..bdd8f152d39 100644
--- a/homeassistant/components/geonetnz_quakes/.translations/pl.json
+++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "identifier_exists": "Lokalizacja ju\u017c zarejestrowana"
+ "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana."
},
"step": {
"user": {
diff --git a/homeassistant/components/geonetnz_quakes/.translations/sv.json b/homeassistant/components/geonetnz_quakes/.translations/sv.json
new file mode 100644
index 00000000000..13058ad3ad2
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/sv.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Plats redan registrerad"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "Radie"
+ },
+ "title": "Fyll i dina filterdetaljer."
+ }
+ },
+ "title": "GeoNet NZ Quakes"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json
index 775ca8760bc..50813b062f0 100644
--- a/homeassistant/components/geonetnz_quakes/manifest.json
+++ b/homeassistant/components/geonetnz_quakes/manifest.json
@@ -3,7 +3,7 @@
"name": "GeoNet NZ Quakes",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes",
- "requirements": ["aio_geojson_geonetnz_quakes==0.11"],
+ "requirements": ["aio_geojson_geonetnz_quakes==0.12"],
"dependencies": [],
"codeowners": ["@exxamalte"]
}
diff --git a/homeassistant/components/geonetnz_volcano/.translations/hu.json b/homeassistant/components/geonetnz_volcano/.translations/hu.json
index 875a8330f76..e53a91bcb03 100644
--- a/homeassistant/components/geonetnz_volcano/.translations/hu.json
+++ b/homeassistant/components/geonetnz_volcano/.translations/hu.json
@@ -1,9 +1,16 @@
{
"config": {
+ "error": {
+ "identifier_exists": "A hely m\u00e1r regisztr\u00e1lt"
+ },
"step": {
"user": {
+ "data": {
+ "radius": "Sug\u00e1r"
+ },
"title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait."
}
- }
+ },
+ "title": "GeoNet NZ vulk\u00e1n"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_volcano/.translations/ko.json b/homeassistant/components/geonetnz_volcano/.translations/ko.json
index 5d393fef4c4..d19091e75e8 100644
--- a/homeassistant/components/geonetnz_volcano/.translations/ko.json
+++ b/homeassistant/components/geonetnz_volcano/.translations/ko.json
@@ -8,7 +8,7 @@
"data": {
"radius": "\ubc18\uacbd"
},
- "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694."
}
},
"title": "GeoNet NZ Volcano"
diff --git a/homeassistant/components/geonetnz_volcano/.translations/pl.json b/homeassistant/components/geonetnz_volcano/.translations/pl.json
index 7d329815f3f..c51a69356a1 100644
--- a/homeassistant/components/geonetnz_volcano/.translations/pl.json
+++ b/homeassistant/components/geonetnz_volcano/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "identifier_exists": "Lokalizacja ju\u017c zarejestrowana"
+ "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana."
},
"step": {
"user": {
diff --git a/homeassistant/components/geonetnz_volcano/.translations/sv.json b/homeassistant/components/geonetnz_volcano/.translations/sv.json
new file mode 100644
index 00000000000..35e7e24c926
--- /dev/null
+++ b/homeassistant/components/geonetnz_volcano/.translations/sv.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Plats redan registrerad"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radie"
+ },
+ "title": "Fyll i dina filterdetaljer."
+ }
+ },
+ "title": "GeoNet NZ Volcano"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gios/.translations/es-419.json b/homeassistant/components/gios/.translations/es-419.json
new file mode 100644
index 00000000000..53439a7ab7b
--- /dev/null
+++ b/homeassistant/components/gios/.translations/es-419.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "Nombre de la integraci\u00f3n"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gios/.translations/hu.json b/homeassistant/components/gios/.translations/hu.json
new file mode 100644
index 00000000000..75fcb2088a5
--- /dev/null
+++ b/homeassistant/components/gios/.translations/hu.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A GIO\u015a integr\u00e1ci\u00f3 ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz m\u00e1r konfigur\u00e1lva van."
+ },
+ "error": {
+ "cannot_connect": "Nem lehet csatlakozni a GIO\u015a szerverhez.",
+ "invalid_sensors_data": "\u00c9rv\u00e9nytelen \u00e9rz\u00e9kel\u0151k adatai ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz.",
+ "wrong_station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja nem megfelel\u0151."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Az integr\u00e1ci\u00f3 neve",
+ "station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja"
+ },
+ "description": "A GIO\u015a (lengyel k\u00f6rnyezetv\u00e9delmi f\u0151fel\u00fcgyel\u0151) leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ged a konfigur\u00e1ci\u00f3val kapcsolatban, l\u00e1togass ide: https://www.home-assistant.io/integrations/gios",
+ "title": "GIO\u015a (Lengyel K\u00f6rnyezetv\u00e9delmi F\u0151fel\u00fcgyel\u0151s\u00e9g)"
+ }
+ },
+ "title": "GIO\u015a"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gios/.translations/nl.json b/homeassistant/components/gios/.translations/nl.json
new file mode 100644
index 00000000000..eb487681838
--- /dev/null
+++ b/homeassistant/components/gios/.translations/nl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "GIO\u015a-integratie voor dit meetstation is al geconfigureerd."
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken met de GIO\u015a-server.",
+ "invalid_sensors_data": "Ongeldige sensorgegevens voor dit meetstation.",
+ "wrong_station_id": "ID van het meetstation is niet correct."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Naam van de integratie",
+ "station_id": "ID van het meetstation"
+ },
+ "description": "GIO\u015a (Poolse hoofdinspectie van milieubescherming) luchtkwaliteitintegratie instellen. Als u hulp nodig hebt bij de configuratie, kijk dan hier: https://www.home-assistant.io/integrations/gios",
+ "title": "GIO\u015a (Poolse hoofdinspectie van milieubescherming)"
+ }
+ },
+ "title": "GIO\u015a"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gios/.translations/ru.json b/homeassistant/components/gios/.translations/ru.json
index 69ffff98517..0045b08cec8 100644
--- a/homeassistant/components/gios/.translations/ru.json
+++ b/homeassistant/components/gios/.translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 GIO\u015a.",
- "invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.",
+ "invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.",
"wrong_station_id": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438."
},
"step": {
diff --git a/homeassistant/components/gios/.translations/sl.json b/homeassistant/components/gios/.translations/sl.json
index da3995dd0b3..089435dee3f 100644
--- a/homeassistant/components/gios/.translations/sl.json
+++ b/homeassistant/components/gios/.translations/sl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "GIO\u015a integracija za to merilno postajo je \u017ee nastavljena."
+ },
"error": {
"cannot_connect": "Ne morem se povezati s stre\u017enikom GIO\u015a.",
"invalid_sensors_data": "Neveljavni podatki senzorjev za to merilno postajo.",
diff --git a/homeassistant/components/gios/.translations/sv.json b/homeassistant/components/gios/.translations/sv.json
new file mode 100644
index 00000000000..b5a865b5ccd
--- /dev/null
+++ b/homeassistant/components/gios/.translations/sv.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "GIO\u015a-integration f\u00f6r denna m\u00e4tstation \u00e4r redan konfigurerad."
+ },
+ "error": {
+ "cannot_connect": "Det g\u00e5r inte att ansluta till GIO\u015a-servern.",
+ "invalid_sensors_data": "Ogiltig sensordata f\u00f6r denna m\u00e4tstation.",
+ "wrong_station_id": "M\u00e4tstationens ID \u00e4r inte korrekt."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Integrationens namn",
+ "station_id": "M\u00e4tstationens ID"
+ },
+ "description": "St\u00e4ll in luftkvalitetintegration f\u00f6r GIO\u015a (polsk chefinspektorat f\u00f6r milj\u00f6skydd). Om du beh\u00f6ver hj\u00e4lp med konfigurationen titta h\u00e4r: https://www.home-assistant.io/integrations/gios",
+ "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)"
+ }
+ },
+ "title": "GIO\u015a"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/.translations/es-419.json b/homeassistant/components/glances/.translations/es-419.json
new file mode 100644
index 00000000000..6debc6da6c1
--- /dev/null
+++ b/homeassistant/components/glances/.translations/es-419.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "No se puede conectar al host",
+ "wrong_version": "Versi\u00f3n no compatible (2 o 3 solamente)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Nombre",
+ "password": "Contrase\u00f1a",
+ "port": "Puerto",
+ "username": "Nombre de usuario",
+ "verify_ssl": "Verificar la certificaci\u00f3n del sistema",
+ "version": "Versi\u00f3n de API de Glances (2 o 3)"
+ }
+ }
+ }
+ },
+ "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/sv.json b/homeassistant/components/glances/.translations/sv.json
new file mode 100644
index 00000000000..f4b95081a10
--- /dev/null
+++ b/homeassistant/components/glances/.translations/sv.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "V\u00e4rden \u00e4r redan konfigurerad."
+ },
+ "error": {
+ "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden",
+ "wrong_version": "Version st\u00f6ds inte (endast 2 eller 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e4rd",
+ "name": "Namn",
+ "password": "L\u00f6senord",
+ "port": "Port",
+ "ssl": "Anv\u00e4nd SSL / TLS f\u00f6r att ansluta till Glances-systemet",
+ "username": "Anv\u00e4ndarnamn",
+ "verify_ssl": "Verifiera certifieringen av systemet",
+ "version": "Glances API-version (2 eller 3)"
+ },
+ "title": "St\u00e4ll in Glances"
+ }
+ },
+ "title": "Glances"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Uppdateringsfrekvens"
+ },
+ "description": "Konfigurera alternativ f\u00f6r Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py
index e47586ea245..31a3f0f69e4 100644
--- a/homeassistant/components/glances/const.py
+++ b/homeassistant/components/glances/const.py
@@ -1,5 +1,5 @@
"""Constants for Glances component."""
-from homeassistant.const import TEMP_CELSIUS
+from homeassistant.const import DATA_GIBIBYTES, DATA_MEBIBYTES, TEMP_CELSIUS
DOMAIN = "glances"
CONF_VERSION = "version"
@@ -14,23 +14,28 @@ 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"],
+ "disk_use_percent": ["fs", "used percent", "%", "mdi:harddisk"],
+ "disk_use": ["fs", "used", DATA_GIBIBYTES, "mdi:harddisk"],
+ "disk_free": ["fs", "free", DATA_GIBIBYTES, "mdi:harddisk"],
+ "memory_use_percent": ["mem", "RAM used percent", "%", "mdi:memory"],
+ "memory_use": ["mem", "RAM used", DATA_MEBIBYTES, "mdi:memory"],
+ "memory_free": ["mem", "RAM free", DATA_MEBIBYTES, "mdi:memory"],
+ "swap_use_percent": ["memswap", "Swap used percent", "%", "mdi:memory"],
+ "swap_use": ["memswap", "Swap used", DATA_GIBIBYTES, "mdi:memory"],
+ "swap_free": ["memswap", "Swap free", DATA_GIBIBYTES, "mdi:memory"],
+ "processor_load": ["load", "CPU load", "15 min", "mdi:memory"],
+ "process_running": ["processcount", "Running", "Count", "mdi:memory"],
+ "process_total": ["processcount", "Total", "Count", "mdi:memory"],
+ "process_thread": ["processcount", "Thread", "Count", "mdi:memory"],
+ "process_sleeping": ["processcount", "Sleeping", "Count", "mdi:memory"],
+ "cpu_use_percent": ["cpu", "CPU used", "%", "mdi:memory"],
+ "sensor_temp": ["sensors", "Temp", TEMP_CELSIUS, "mdi:thermometer"],
+ "docker_active": ["docker", "Containers active", "", "mdi:docker"],
+ "docker_cpu_use": ["docker", "Containers CPU used", "%", "mdi:docker"],
+ "docker_memory_use": [
+ "docker",
+ "Containers RAM used",
+ DATA_MEBIBYTES,
+ "mdi:docker",
+ ],
}
diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py
index 968081cfc43..f701dfdb741 100644
--- a/homeassistant/components/glances/sensor.py
+++ b/homeassistant/components/glances/sensor.py
@@ -14,13 +14,51 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Glances sensors."""
- glances_data = hass.data[DOMAIN][config_entry.entry_id]
+ client = hass.data[DOMAIN][config_entry.entry_id]
name = config_entry.data[CONF_NAME]
dev = []
- for sensor_type in SENSOR_TYPES:
- dev.append(
- GlancesSensor(glances_data, name, SENSOR_TYPES[sensor_type][0], sensor_type)
- )
+
+ for sensor_type, sensor_details in SENSOR_TYPES.items():
+ if not sensor_details[0] in client.api.data:
+ continue
+ if sensor_details[0] in client.api.data:
+ if sensor_details[0] == "fs":
+ # fs will provide a list of disks attached
+ for disk in client.api.data[sensor_details[0]]:
+ dev.append(
+ GlancesSensor(
+ client,
+ name,
+ disk["mnt_point"],
+ SENSOR_TYPES[sensor_type][1],
+ sensor_type,
+ SENSOR_TYPES[sensor_type],
+ )
+ )
+ elif sensor_details[0] == "sensors":
+ # sensors will provide temp for different devices
+ for sensor in client.api.data[sensor_details[0]]:
+ dev.append(
+ GlancesSensor(
+ client,
+ name,
+ sensor["label"],
+ SENSOR_TYPES[sensor_type][1],
+ sensor_type,
+ SENSOR_TYPES[sensor_type],
+ )
+ )
+ elif client.api.data[sensor_details[0]]:
+ dev.append(
+ GlancesSensor(
+ client,
+ name,
+ "",
+ SENSOR_TYPES[sensor_type][1],
+ sensor_type,
+ SENSOR_TYPES[sensor_type],
+ )
+ )
async_add_entities(dev, True)
@@ -28,19 +66,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class GlancesSensor(Entity):
"""Implementation of a Glances sensor."""
- def __init__(self, glances_data, name, sensor_name, sensor_type):
+ def __init__(
+ self,
+ glances_data,
+ name,
+ sensor_name_prefix,
+ sensor_name_suffix,
+ sensor_type,
+ sensor_details,
+ ):
"""Initialize the sensor."""
self.glances_data = glances_data
- self._sensor_name = sensor_name
+ self._sensor_name_prefix = sensor_name_prefix
+ self._sensor_name_suffix = sensor_name_suffix
self._name = name
self.type = sensor_type
self._state = None
- self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
+ self.sensor_details = sensor_details
+ self.unsub_update = None
@property
def name(self):
"""Return the name of the sensor."""
- return f"{self._name} {self._sensor_name}"
+ return f"{self._name} {self._sensor_name_prefix} {self._sensor_name_suffix}"
@property
def unique_id(self):
@@ -50,12 +98,12 @@ class GlancesSensor(Entity):
@property
def icon(self):
"""Icon to use in the frontend, if any."""
- return SENSOR_TYPES[self.type][2]
+ return self.sensor_details[3]
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
- return self._unit_of_measurement
+ return self.sensor_details[2]
@property
def available(self):
@@ -74,7 +122,7 @@ class GlancesSensor(Entity):
async def async_added_to_hass(self):
"""Handle entity which will be added."""
- async_dispatcher_connect(
+ self.unsub_update = async_dispatcher_connect(
self.hass, DATA_UPDATED, self._schedule_immediate_update
)
@@ -82,22 +130,40 @@ class GlancesSensor(Entity):
def _schedule_immediate_update(self):
self.async_schedule_update_ha_state(True)
+ async def will_remove_from_hass(self):
+ """Unsubscribe from update dispatcher."""
+ if self.unsub_update:
+ self.unsub_update()
+ self.unsub_update = None
+
async def async_update(self):
"""Get the latest data from REST API."""
value = self.glances_data.api.data
+ if value is None:
+ return
if value is not None:
- if self.type == "disk_use_percent":
- self._state = value["fs"][0]["percent"]
- elif self.type == "disk_use":
- self._state = round(value["fs"][0]["used"] / 1024 ** 3, 1)
- elif self.type == "disk_free":
- try:
- self._state = round(value["fs"][0]["free"] / 1024 ** 3, 1)
- except KeyError:
- self._state = round(
- (value["fs"][0]["size"] - value["fs"][0]["used"]) / 1024 ** 3, 1
- )
+ if self.sensor_details[0] == "fs":
+ for var in value["fs"]:
+ if var["mnt_point"] == self._sensor_name_prefix:
+ disk = var
+ break
+ if self.type == "disk_use_percent":
+ self._state = disk["percent"]
+ elif self.type == "disk_use":
+ self._state = round(disk["used"] / 1024 ** 3, 1)
+ elif self.type == "disk_free":
+ try:
+ self._state = round(disk["free"] / 1024 ** 3, 1)
+ except KeyError:
+ self._state = round(
+ (disk["size"] - disk["used"]) / 1024 ** 3, 1,
+ )
+ elif self.type == "sensor_temp":
+ for sensor in value["sensors"]:
+ if sensor["label"] == self._sensor_name_prefix:
+ self._state = sensor["value"]
+ break
elif self.type == "memory_use_percent":
self._state = value["mem"]["percent"]
elif self.type == "memory_use":
@@ -126,25 +192,6 @@ class GlancesSensor(Entity):
self._state = value["processcount"]["sleeping"]
elif self.type == "cpu_use_percent":
self._state = value["quicklook"]["cpu"]
- elif self.type == "cpu_temp":
- for sensor in value["sensors"]:
- if sensor["label"] in [
- "amdgpu 1",
- "aml_thermal",
- "Core 0",
- "Core 1",
- "CPU Temperature",
- "CPU",
- "cpu-thermal 1",
- "cpu_thermal 1",
- "exynos-therm 1",
- "Package id 0",
- "Physical id 0",
- "radeon 1",
- "soc-thermal 1",
- "soc_thermal 1",
- ]:
- self._state = sensor["value"]
elif self.type == "docker_active":
count = 0
try:
diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py
index 7bd3583e5c2..60e4cdae6a5 100644
--- a/homeassistant/components/google_assistant/http.py
+++ b/homeassistant/components/google_assistant/http.py
@@ -180,7 +180,7 @@ class GoogleConfig(AbstractConfig):
return 500
async def async_call_homegraph_api(self, url, data):
- """Call a homegraph api with authenticaiton."""
+ """Call a homegraph api with authentication."""
session = async_get_clientsession(self.hass)
async def _call():
diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json
index 88183acf918..b10c4ad01a0 100644
--- a/homeassistant/components/greeneye_monitor/manifest.json
+++ b/homeassistant/components/greeneye_monitor/manifest.json
@@ -2,7 +2,7 @@
"domain": "greeneye_monitor",
"name": "GreenEye Monitor (GEM)",
"documentation": "https://www.home-assistant.io/integrations/greeneye_monitor",
- "requirements": ["greeneye_monitor==1.0.1"],
+ "requirements": ["greeneye_monitor==2.0"],
"dependencies": [],
"codeowners": ["@jkeljo"]
}
diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py
index 19ef7529b0a..2640c701f92 100644
--- a/homeassistant/components/greeneye_monitor/sensor.py
+++ b/homeassistant/components/greeneye_monitor/sensor.py
@@ -294,12 +294,10 @@ class VoltageSensor(GEMSensor):
def __init__(self, monitor_serial_number, number, name):
"""Construct the entity."""
super().__init__(monitor_serial_number, name, "volts", number)
- self._monitor = None
def _get_sensor(self, monitor):
- """Wire the updates to a current channel."""
- self._monitor = monitor
- return monitor.channels[self._number - 1]
+ """Wire the updates to the monitor itself, since there is no voltage element in the API."""
+ return monitor
@property
def icon(self):
@@ -309,10 +307,10 @@ class VoltageSensor(GEMSensor):
@property
def state(self):
"""Return the current voltage being reported by this sensor."""
- if not self._monitor.voltage:
+ if not self._sensor:
return None
- return self._monitor.voltage
+ return self._sensor.voltage
@property
def unit_of_measurement(self):
diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py
index fc37f904e0d..7257959700f 100644
--- a/homeassistant/components/group/__init__.py
+++ b/homeassistant/components/group/__init__.py
@@ -13,6 +13,8 @@ from homeassistant.const import (
ATTR_NAME,
CONF_ICON,
CONF_NAME,
+ ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
SERVICE_RELOAD,
STATE_CLOSED,
STATE_HOME,
@@ -134,7 +136,10 @@ def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> Lis
"""
found_ids: List[str] = []
for entity_id in entity_ids:
- if not isinstance(entity_id, str):
+ if not isinstance(entity_id, str) or entity_id in (
+ ENTITY_MATCH_NONE,
+ ENTITY_MATCH_ALL,
+ ):
continue
entity_id = entity_id.lower()
diff --git a/homeassistant/components/hangouts/.translations/es-419.json b/homeassistant/components/hangouts/.translations/es-419.json
index 3a297eb15ea..011060694a7 100644
--- a/homeassistant/components/hangouts/.translations/es-419.json
+++ b/homeassistant/components/hangouts/.translations/es-419.json
@@ -5,6 +5,7 @@
"unknown": "Se produjo un error desconocido."
},
"error": {
+ "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, intente nuevamente.",
"invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo."
},
"step": {
diff --git a/homeassistant/components/hangouts/.translations/pl.json b/homeassistant/components/hangouts/.translations/pl.json
index 5da1e219799..1d08296007a 100644
--- a/homeassistant/components/hangouts/.translations/pl.json
+++ b/homeassistant/components/hangouts/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts jest ju\u017c skonfigurowany",
+ "already_configured": "Google Hangouts jest ju\u017c skonfigurowany.",
"unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d."
},
"error": {
diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py
index c48d5fb00b0..bcc9d72ad08 100644
--- a/homeassistant/components/harmony/remote.py
+++ b/homeassistant/components/harmony/remote.py
@@ -397,7 +397,7 @@ class HarmonyRemote(remote.RemoteDevice):
def write_config_file(self):
"""Write Harmony configuration file."""
_LOGGER.debug(
- "%s: Writing hub config to file: %s", self.name, self._config_path
+ "%s: Writing hub configuration to file: %s", self.name, self._config_path
)
if self._client.config is None:
_LOGGER.warning("%s: No configuration received from hub", self.name)
diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml
index f2e5f0b837a..63aee668062 100644
--- a/homeassistant/components/hdmi_cec/services.yaml
+++ b/homeassistant/components/hdmi_cec/services.yaml
@@ -3,7 +3,7 @@ select_device:
description: Select HDMI device.
fields:
device: {description: 'Address of device to select. Can be entity_id, physical
- address or alias from confuguration.', example: '"switch.hdmi_1" or "1.1.0.0"
+ address or alias from configuration.', example: '"switch.hdmi_1" or "1.1.0.0"
or "01:10"'}
send_command:
description: Sends CEC command into HDMI CEC capable adapter.
diff --git a/homeassistant/components/heos/.translations/es-419.json b/homeassistant/components/heos/.translations/es-419.json
index 4d442a4543b..b0d1d7dc3fb 100644
--- a/homeassistant/components/heos/.translations/es-419.json
+++ b/homeassistant/components/heos/.translations/es-419.json
@@ -3,8 +3,12 @@
"abort": {
"already_setup": "Solo puede configurar una sola conexi\u00f3n Heos, ya que ser\u00e1 compatible con todos los dispositivos de la red."
},
+ "error": {
+ "connection_failure": "No se puede conectar con el host especificado."
+ },
"step": {
"user": {
+ "description": "Ingrese el nombre de host o la direcci\u00f3n IP de un dispositivo Heos (preferiblemente uno conectado por cable a la red).",
"title": "Con\u00e9ctate a Heos"
}
},
diff --git a/homeassistant/components/hisense_aehw4a1/.translations/hu.json b/homeassistant/components/hisense_aehw4a1/.translations/hu.json
new file mode 100644
index 00000000000..02716a96a73
--- /dev/null
+++ b/homeassistant/components/hisense_aehw4a1/.translations/hu.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 Hisense AEH-W4A1 eszk\u00f6z.",
+ "single_instance_allowed": "Csak egy konfigur\u00e1ci\u00f3 lehet Hisense AEH-W4A1 eset\u00e9n."
+ },
+ "step": {
+ "confirm": {
+ "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Hisense AEH-W4A1-et?",
+ "title": "Hisense AEH-W4A1"
+ }
+ },
+ "title": "Hisense AEH-W4A1"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hisense_aehw4a1/.translations/sv.json b/homeassistant/components/hisense_aehw4a1/.translations/sv.json
new file mode 100644
index 00000000000..6ec35452e8b
--- /dev/null
+++ b/homeassistant/components/hisense_aehw4a1/.translations/sv.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Inga Hisense AEH-W4A1-enheter hittades i n\u00e4tverket.",
+ "single_instance_allowed": "Endast en enda konfiguration av Hisense AEH-W4A1 \u00e4r m\u00f6jlig."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vill du konfigurera Hisense AEH-W4A1?",
+ "title": "Hisense AEH-W4A1"
+ }
+ },
+ "title": "Hisense AEH-W4A1"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py
index 8aa1d7e020a..7a0ae33345a 100644
--- a/homeassistant/components/homeassistant/__init__.py
+++ b/homeassistant/components/homeassistant/__init__.py
@@ -6,6 +6,7 @@ from typing import Awaitable
import voluptuous as vol
+from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL
import homeassistant.config as conf_util
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -19,8 +20,8 @@ from homeassistant.const import (
SERVICE_TURN_ON,
)
import homeassistant.core as ha
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import config_validation as cv, intent
+from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_extract_entity_ids
_LOGGER = logging.getLogger(__name__)
@@ -54,6 +55,15 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
tasks = []
for domain, ent_ids in by_domain:
+ # This leads to endless loop.
+ if domain == DOMAIN:
+ _LOGGER.warning(
+ "Called service homeassistant.%s with invalid entity IDs %s",
+ service.service,
+ ", ".join(ent_ids),
+ )
+ continue
+
# We want to block for all calls and only return when all calls
# have been processed. If a service does not exist it causes a 10
# second delay while we're blocking waiting for a response.
@@ -72,25 +82,19 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
hass.services.async_call(domain, service.service, data, blocking)
)
- await asyncio.wait(tasks)
+ if tasks:
+ await asyncio.gather(*tasks)
- hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
- hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
- hass.services.async_register(ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
- hass.helpers.intent.async_register(
- intent.ServiceIntentHandler(
- intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"
- )
+ service_schema = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}, extra=vol.ALLOW_EXTRA)
+
+ hass.services.async_register(
+ ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service, schema=service_schema
)
- hass.helpers.intent.async_register(
- intent.ServiceIntentHandler(
- intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned {} off"
- )
+ hass.services.async_register(
+ ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service, schema=service_schema
)
- hass.helpers.intent.async_register(
- intent.ServiceIntentHandler(
- intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"
- )
+ hass.services.async_register(
+ ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service, schema=service_schema
)
async def async_handle_core_service(call):
@@ -118,6 +122,25 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
async def async_handle_update_service(call):
"""Service handler for updating an entity."""
+ if call.context.user_id:
+ user = await hass.auth.async_get_user(call.context.user_id)
+
+ if user is None:
+ raise UnknownUser(
+ context=call.context,
+ permission=POLICY_CONTROL,
+ user_id=call.context.user_id,
+ )
+
+ for entity in call.data[ATTR_ENTITY_ID]:
+ if not user.permissions.check_entity(entity, POLICY_CONTROL):
+ raise Unauthorized(
+ context=call.context,
+ permission=POLICY_CONTROL,
+ user_id=call.context.user_id,
+ perm_category=CAT_ENTITIES,
+ )
+
tasks = [
hass.helpers.entity_component.async_update_entity(entity)
for entity in call.data[ATTR_ENTITY_ID]
@@ -126,13 +149,13 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
if tasks:
await asyncio.wait(tasks)
- hass.services.async_register(
+ hass.helpers.service.async_register_admin_service(
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service
)
- hass.services.async_register(
+ hass.helpers.service.async_register_admin_service(
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service
)
- hass.services.async_register(
+ hass.helpers.service.async_register_admin_service(
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service
)
hass.services.async_register(
diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py
index af5f4cea828..9dad912886d 100644
--- a/homeassistant/components/homeassistant/scene.py
+++ b/homeassistant/components/homeassistant/scene.py
@@ -11,6 +11,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_STATE,
CONF_ENTITIES,
+ CONF_ICON,
CONF_ID,
CONF_NAME,
CONF_PLATFORM,
@@ -75,16 +76,21 @@ CONF_SNAPSHOT = "snapshot_entities"
DATA_PLATFORM = f"homeassistant_scene"
STATES_SCHEMA = vol.All(dict, _convert_states)
+
PLATFORM_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): HA_DOMAIN,
vol.Required(STATES): vol.All(
cv.ensure_list,
[
- {
- vol.Required(CONF_NAME): cv.string,
- vol.Required(CONF_ENTITIES): STATES_SCHEMA,
- }
+ vol.Schema(
+ {
+ vol.Optional(CONF_ID): cv.string,
+ vol.Required(CONF_NAME): cv.string,
+ vol.Optional(CONF_ICON): cv.icon,
+ vol.Required(CONF_ENTITIES): STATES_SCHEMA,
+ }
+ )
],
),
},
@@ -105,7 +111,7 @@ CREATE_SCENE_SCHEMA = vol.All(
SERVICE_APPLY = "apply"
SERVICE_CREATE = "create"
-SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES])
+SCENECONFIG = namedtuple("SceneConfig", [CONF_ID, CONF_NAME, CONF_ICON, STATES])
_LOGGER = logging.getLogger(__name__)
@@ -213,7 +219,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
_LOGGER.warning("Empty scenes are not allowed")
return
- scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], entities)
+ scene_config = SCENECONFIG(None, call.data[CONF_SCENE_ID], None, entities)
entity_id = f"{SCENE_DOMAIN}.{scene_config.name}"
old = platform.entities.get(entity_id)
if old is not None:
@@ -239,8 +245,12 @@ def _process_scenes_config(hass, async_add_entities, config):
async_add_entities(
HomeAssistantScene(
hass,
- SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES]),
- scene.get(CONF_ID),
+ SCENECONFIG(
+ scene.get(CONF_ID),
+ scene[CONF_NAME],
+ scene.get(CONF_ICON),
+ scene[CONF_ENTITIES],
+ ),
)
for scene in scene_config
)
@@ -249,9 +259,8 @@ def _process_scenes_config(hass, async_add_entities, config):
class HomeAssistantScene(Scene):
"""A scene is a group of entities and the states we want them to be."""
- def __init__(self, hass, scene_config, scene_id=None, from_service=False):
+ def __init__(self, hass, scene_config, from_service=False):
"""Initialize the scene."""
- self._id = scene_id
self.hass = hass
self.scene_config = scene_config
self.from_service = from_service
@@ -261,17 +270,23 @@ class HomeAssistantScene(Scene):
"""Return the name of the scene."""
return self.scene_config.name
+ @property
+ def icon(self):
+ """Return the icon of the scene."""
+ return self.scene_config.icon
+
@property
def unique_id(self):
"""Return unique ID."""
- return self._id
+ return self.scene_config.id
@property
def device_state_attributes(self):
"""Return the scene state attributes."""
attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)}
- if self._id is not None:
- attributes[CONF_ID] = self._id
+ unique_id = self.unique_id
+ if unique_id is not None:
+ attributes[CONF_ID] = unique_id
return attributes
async def async_activate(self):
diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py
index 3fc6a0628ff..734568606b2 100644
--- a/homeassistant/components/homekit/type_lights.py
+++ b/homeassistant/components/homekit/type_lights.py
@@ -201,7 +201,7 @@ class Light(HomeAccessory):
# But if it is set to 0, HomeKit will update the brightness to 100 as
# it thinks 0 is off.
#
- # Therefore, if the the brighness is 0 and the device is still on,
+ # Therefore, if the the brightness is 0 and the device is still on,
# the brightness is mapped to 1 otherwise the update is ignored in
# order to avoid this incorrect behavior.
if brightness == 0:
diff --git a/homeassistant/components/homekit_controller/.translations/ca.json b/homeassistant/components/homekit_controller/.translations/ca.json
index f2ed4bd0c21..1d2331870e1 100644
--- a/homeassistant/components/homekit_controller/.translations/ca.json
+++ b/homeassistant/components/homekit_controller/.translations/ca.json
@@ -3,7 +3,7 @@
"abort": {
"accessory_not_found_error": "No s'ha pogut vincular, no s'ha trobat el dispositiu.",
"already_configured": "Accessori ja configurat amb aquest controlador.",
- "already_in_progress": "El flux de dades pel dispositiu ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
"already_paired": "Aquest accessori ja est\u00e0 vinculat amb un altre dispositiu. Reinicia l'accessori i torna-ho a provar.",
"ignored_model": "La disponibilitat de HomeKit per aquest model est\u00e0 bloquejada ja que, de moment, no hi ha una integraci\u00f3 nativa completa.",
"invalid_config_entry": "Aquest dispositiu s'est\u00e0 mostrant com a llest per a ser vinculat per\u00f2, hi ha una entrada de configuraci\u00f3 conflictiva a Home Assistant que s'ha d'eliminar primer.",
diff --git a/homeassistant/components/homekit_controller/.translations/de.json b/homeassistant/components/homekit_controller/.translations/de.json
index e6942a125cd..8223616f11e 100644
--- a/homeassistant/components/homekit_controller/.translations/de.json
+++ b/homeassistant/components/homekit_controller/.translations/de.json
@@ -24,7 +24,7 @@
"data": {
"pairing_code": "Kopplungscode"
},
- "description": "Gebe deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden",
+ "description": "Gib deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden",
"title": "Mit HomeKit Zubeh\u00f6r koppeln"
},
"user": {
diff --git a/homeassistant/components/homekit_controller/.translations/en.json b/homeassistant/components/homekit_controller/.translations/en.json
index 31731a52203..72aa720b449 100644
--- a/homeassistant/components/homekit_controller/.translations/en.json
+++ b/homeassistant/components/homekit_controller/.translations/en.json
@@ -6,7 +6,7 @@
"already_in_progress": "Config flow for device is already in progress.",
"already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.",
"ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.",
- "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.",
+ "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.",
"no_devices": "No unpaired devices could be found"
},
"error": {
diff --git a/homeassistant/components/homekit_controller/.translations/es-419.json b/homeassistant/components/homekit_controller/.translations/es-419.json
index 67a65f752b4..a99011cf8b1 100644
--- a/homeassistant/components/homekit_controller/.translations/es-419.json
+++ b/homeassistant/components/homekit_controller/.translations/es-419.json
@@ -4,17 +4,26 @@
"already_configured": "El accesorio ya est\u00e1 configurado con este controlador.",
"already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicie el accesorio y vuelva a intentarlo."
},
+ "error": {
+ "pairing_failed": "Se produjo un error no controlado al intentar vincularse con este dispositivo. Esto puede ser una falla temporal o su dispositivo puede no ser compatible actualmente.",
+ "unable_to_pair": "No se puede vincular, por favor intente nuevamente.",
+ "unknown_error": "El dispositivo inform\u00f3 un error desconocido. Vinculaci\u00f3n fallida."
+ },
"flow_title": "Accesorio HomeKit: {name}",
"step": {
"pair": {
"data": {
"pairing_code": "C\u00f3digo de emparejamiento"
- }
+ },
+ "description": "Ingrese su c\u00f3digo de emparejamiento de HomeKit (en el formato XXX-XX-XXX) para usar este accesorio",
+ "title": "Vincular con el accesorio HomeKit"
},
"user": {
"data": {
"device": "Dispositivo"
- }
+ },
+ "description": "Seleccione el dispositivo con el que desea vincular",
+ "title": "Vincular con el accesorio HomeKit"
}
},
"title": "Accesorio HomeKit"
diff --git a/homeassistant/components/homekit_controller/.translations/hu.json b/homeassistant/components/homekit_controller/.translations/hu.json
index 60bd173dc8e..264e635d7f4 100644
--- a/homeassistant/components/homekit_controller/.translations/hu.json
+++ b/homeassistant/components/homekit_controller/.translations/hu.json
@@ -1,11 +1,40 @@
{
"config": {
+ "abort": {
+ "accessory_not_found_error": "Nem adhat\u00f3 hozz\u00e1 p\u00e1ros\u00edt\u00e1s, mert az eszk\u00f6z m\u00e1r nem tal\u00e1lhat\u00f3.",
+ "already_configured": "A tartoz\u00e9k m\u00e1r konfigur\u00e1lva van ezzel a vez\u00e9rl\u0151vel.",
+ "already_in_progress": "Az eszk\u00f6z konfigur\u00e1ci\u00f3ja m\u00e1r folyamatban van.",
+ "already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.",
+ "ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.",
+ "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s a Home Assistant-ben, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.",
+ "no_devices": "Nem tal\u00e1lhat\u00f3 nem p\u00e1ros\u00edtott eszk\u00f6z"
+ },
+ "error": {
+ "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rj\u00fck, ellen\u0151rizze, \u00e9s pr\u00f3b\u00e1lja \u00fajra.",
+ "busy_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik vez\u00e9rl\u0151vel.",
+ "max_peers_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel nincs ingyenes p\u00e1ros\u00edt\u00e1si t\u00e1rhely.",
+ "max_tries_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel t\u00f6bb mint 100 sikertelen hiteles\u00edt\u00e9si k\u00eds\u00e9rletet kapott.",
+ "pairing_failed": "Nem kezelt hiba t\u00f6rt\u00e9nt az eszk\u00f6zzel val\u00f3 p\u00e1ros\u00edt\u00e1s sor\u00e1n. Lehet, hogy ez \u00e1tmeneti hiba, vagy jelenleg nem t\u00e1mogatja az eszk\u00f6zt.",
+ "unable_to_pair": "Nem siker\u00fclt p\u00e1ros\u00edtani, pr\u00f3b\u00e1ld \u00fajra.",
+ "unknown_error": "Az eszk\u00f6z ismeretlen hib\u00e1t jelentett. A p\u00e1ros\u00edt\u00e1s sikertelen."
+ },
+ "flow_title": "HomeKit tartoz\u00e9k: {name}",
"step": {
+ "pair": {
+ "data": {
+ "pairing_code": "P\u00e1ros\u00edt\u00e1si k\u00f3d"
+ },
+ "description": "\u00cdrja be a HomeKit p\u00e1ros\u00edt\u00e1si k\u00f3dj\u00e1t (XXX-XX-XXX form\u00e1tumban) a kieg\u00e9sz\u00edt\u0151 haszn\u00e1lat\u00e1hoz",
+ "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa"
+ },
"user": {
"data": {
"device": "Eszk\u00f6z"
- }
+ },
+ "description": "V\u00e1lassza ki azt az eszk\u00f6zt, amelyet p\u00e1ros\u00edtani szeretne",
+ "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa"
}
- }
+ },
+ "title": "HomeKit tartoz\u00e9k"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/.translations/it.json b/homeassistant/components/homekit_controller/.translations/it.json
index 7ed026a529c..69bb1f13c84 100644
--- a/homeassistant/components/homekit_controller/.translations/it.json
+++ b/homeassistant/components/homekit_controller/.translations/it.json
@@ -6,7 +6,7 @@
"already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.",
"already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Si prega di resettare l'accessorio e riprovare.",
"ignored_model": "Il supporto di HomeKit per questo modello \u00e8 bloccato poich\u00e9 \u00e8 disponibile un'integrazione nativa con pi\u00f9 funzionalit\u00e0.",
- "invalid_config_entry": "Questo dispositivo viene visualizzato come pronto per l'associazione, ma c'\u00e8 gi\u00e0 una voce di configurazione in conflitto in Home Assistant che deve prima essere rimossa.",
+ "invalid_config_entry": "Questo dispositivo viene visualizzato come pronto per l'associazione, ma c'\u00e8 gi\u00e0 una voce di configurazione in conflitto in Home Assistant che prima deve essere rimossa.",
"no_devices": "Non \u00e8 stato possibile trovare dispositivi non associati"
},
"error": {
diff --git a/homeassistant/components/homekit_controller/.translations/no.json b/homeassistant/components/homekit_controller/.translations/no.json
index db8b8b035e0..a2816fa92f0 100644
--- a/homeassistant/components/homekit_controller/.translations/no.json
+++ b/homeassistant/components/homekit_controller/.translations/no.json
@@ -6,7 +6,7 @@
"already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
"already_paired": "Dette tilbeh\u00f8ret er allerede sammenkoblet med en annen enhet. Vennligst tilbakestill tilbeh\u00f8ret og pr\u00f8v igjen.",
"ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrering er tilgjengelig.",
- "invalid_config_entry": "Denne enheten vises som klar til \u00e5 sammenkoble, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Home Assistant som m\u00e5 fjernes f\u00f8rst.",
+ "invalid_config_entry": "Denne enheten vises som klar til sammenkobling, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Hjelpeassistenten som f\u00f8rst m\u00e5 fjernes.",
"no_devices": "Ingen ukoblede enheter ble funnet"
},
"error": {
diff --git a/homeassistant/components/homekit_controller/.translations/pl.json b/homeassistant/components/homekit_controller/.translations/pl.json
index e66353c5000..33cd20dc9c9 100644
--- a/homeassistant/components/homekit_controller/.translations/pl.json
+++ b/homeassistant/components/homekit_controller/.translations/pl.json
@@ -3,7 +3,7 @@
"abort": {
"accessory_not_found_error": "Nie mo\u017cna rozpocz\u0105\u0107 parowania, poniewa\u017c nie znaleziono urz\u0105dzenia.",
"already_configured": "Akcesorium jest ju\u017c skonfigurowane z tym kontrolerem.",
- "already_in_progress": "Konfigurowanie urz\u0105dzenia jest ju\u017c w toku.",
+ "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
"already_paired": "To akcesorium jest ju\u017c sparowane z innym urz\u0105dzeniem. Zresetuj akcesorium i spr\u00f3buj ponownie.",
"ignored_model": "Obs\u0142uga HomeKit dla tego modelu jest zablokowana, poniewa\u017c dost\u0119pna jest pe\u0142niejsza integracja natywna.",
"invalid_config_entry": "To urz\u0105dzenie jest wy\u015bwietlane jako gotowe do sparowania, ale istnieje ju\u017c konfliktowy wpis konfiguracyjny dla niego w Home Assistant, kt\u00f3ry musi zosta\u0107 najpierw usuni\u0119ty.",
diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hant.json b/homeassistant/components/homekit_controller/.translations/zh-Hant.json
index 68e87e9aea8..c2092c2016b 100644
--- a/homeassistant/components/homekit_controller/.translations/zh-Hant.json
+++ b/homeassistant/components/homekit_controller/.translations/zh-Hant.json
@@ -6,7 +6,7 @@
"already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
"already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u8a2d\u5099\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002",
"ignored_model": "\u7531\u65bc\u6b64\u578b\u865f\u53ef\u539f\u751f\u652f\u63f4\u66f4\u5b8c\u6574\u529f\u80fd\uff0c\u56e0\u6b64 Homekit \u652f\u63f4\u5df2\u88ab\u7981\u6b62\u3002",
- "invalid_config_entry": "\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002",
+ "invalid_config_entry": "\u6b64\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002",
"no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099"
},
"error": {
diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py
index dc65796a569..b2275282293 100644
--- a/homeassistant/components/homekit_controller/__init__.py
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -63,7 +63,7 @@ class HomeKitEntity(Entity):
return False
def setup(self):
- """Configure an entity baed on its HomeKit characterstics metadata."""
+ """Configure an entity baed on its HomeKit characteristics metadata."""
accessories = self._accessory.accessories
get_uuid = CharacteristicsTypes.get_uuid
@@ -124,7 +124,7 @@ class HomeKitEntity(Entity):
"""Collect new data from bridge and update the entity state in hass."""
accessory_state = self._accessory.current_state.get(self._aid, {})
for iid, result in accessory_state.items():
- # No value so dont process this result
+ # No value so don't process this result
if "value" not in result:
continue
diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py
index 507a5cbb70a..559e0b4a997 100644
--- a/homeassistant/components/homekit_controller/config_flow.py
+++ b/homeassistant/components/homekit_controller/config_flow.py
@@ -262,7 +262,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
_LOGGER.info(
(
"Legacy configuration %s for homekit"
- "accessory migrated to config entries"
+ "accessory migrated to configuration entries"
),
hkid,
)
@@ -352,6 +352,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
return self._async_step_pair_show_form(errors)
+ @callback
def _async_step_pair_show_form(self, errors=None):
return self.async_show_form(
step_id="pair",
diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py
index 64581da45b1..11cb607842a 100644
--- a/homeassistant/components/homekit_controller/connection.py
+++ b/homeassistant/components/homekit_controller/connection.py
@@ -77,7 +77,7 @@ class HKDevice:
# The platorms we have forwarded the config entry so far. If a new
# accessory is added to a bridge we may have to load additional
# platforms. We don't want to load all platforms up front if its just
- # a lightbulb. And we dont want to forward a config entry twice
+ # a lightbulb. And we don't want to forward a config entry twice
# (triggers a Config entry already set up error)
self.platforms = set()
@@ -331,7 +331,7 @@ class HKDevice:
key = (row["aid"], row["iid"])
# If the key was returned by put_characteristics() then the
- # change didnt work
+ # change didn't work
if key in results:
continue
diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json
index b51dcb1f6d8..55718e35b59 100644
--- a/homeassistant/components/homekit_controller/strings.json
+++ b/homeassistant/components/homekit_controller/strings.json
@@ -32,7 +32,7 @@
"already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.",
"ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.",
"already_configured": "Accessory is already configured with this controller.",
- "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.",
+ "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.",
"accessory_not_found_error": "Cannot add pairing as device can no longer be found.",
"already_in_progress": "Config flow for device is already in progress."
}
diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json
index c4e09c36b8e..edf07c3e4d7 100644
--- a/homeassistant/components/homematic/manifest.json
+++ b/homeassistant/components/homematic/manifest.json
@@ -2,7 +2,7 @@
"domain": "homematic",
"name": "Homematic",
"documentation": "https://www.home-assistant.io/integrations/homematic",
- "requirements": ["pyhomematic==0.1.63"],
+ "requirements": ["pyhomematic==0.1.64"],
"dependencies": [],
"codeowners": ["@pvizeli", "@danielperna84"]
}
diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json
index 5102b25aaee..0919e211617 100644
--- a/homeassistant/components/homematicip_cloud/.translations/es-419.json
+++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json
@@ -16,7 +16,8 @@
"hapid": "ID de punto de acceso (SGTIN)",
"name": "Nombre (opcional, usado como prefijo de nombre para todos los dispositivos)",
"pin": "C\u00f3digo PIN (opcional)"
- }
+ },
+ "title": "Elija el punto de acceso HomematicIP"
},
"link": {
"description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_flows/config_homematicip_cloud.png)"
diff --git a/homeassistant/components/homematicip_cloud/.translations/pl.json b/homeassistant/components/homematicip_cloud/.translations/pl.json
index 7c8714c2c11..78905da208e 100644
--- a/homeassistant/components/homematicip_cloud/.translations/pl.json
+++ b/homeassistant/components/homematicip_cloud/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany",
+ "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany.",
"connection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP",
"unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d"
},
diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py
index 46bf300753f..d1982e289a3 100644
--- a/homeassistant/components/homematicip_cloud/__init__.py
+++ b/homeassistant/components/homematicip_cloud/__init__.py
@@ -1,20 +1,13 @@
"""Support for HomematicIP Cloud devices."""
import logging
-from pathlib import Path
-from typing import Optional
-from homematicip.aio.device import AsyncSwitchMeasuring
-from homematicip.aio.group import AsyncHeatingGroup
-from homematicip.aio.home import AsyncHome
-from homematicip.base.helpers import handle_config
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME
+from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.config_validation import comp_entity_ids
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import (
@@ -27,29 +20,10 @@ from .const import (
)
from .device import HomematicipGenericDevice # noqa: F401
from .hap import HomematicipAuth, HomematicipHAP # noqa: F401
+from .services import async_setup_services, async_unload_services
_LOGGER = logging.getLogger(__name__)
-ATTR_ACCESSPOINT_ID = "accesspoint_id"
-ATTR_ANONYMIZE = "anonymize"
-ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index"
-ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix"
-ATTR_CONFIG_OUTPUT_PATH = "config_output_path"
-ATTR_DURATION = "duration"
-ATTR_ENDTIME = "endtime"
-ATTR_TEMPERATURE = "temperature"
-
-DEFAULT_CONFIG_FILE_PREFIX = "hmip-config"
-
-SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = "activate_eco_mode_with_duration"
-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_DUMP_HAP_CONFIG = "dump_hap_config"
-SERVICE_RESET_ENERGY_COUNTER = "reset_energy_counter"
-SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile"
-
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(DOMAIN, default=[]): vol.All(
@@ -68,59 +42,6 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
-SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema(
- {
- vol.Required(ATTR_DURATION): cv.positive_int,
- vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)),
- }
-)
-
-SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD = vol.Schema(
- {
- vol.Required(ATTR_ENDTIME): cv.datetime,
- vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)),
- }
-)
-
-SCHEMA_ACTIVATE_VACATION = vol.Schema(
- {
- vol.Required(ATTR_ENDTIME): cv.datetime,
- vol.Required(ATTR_TEMPERATURE, default=18.0): vol.All(
- vol.Coerce(float), vol.Range(min=0, max=55)
- ),
- vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)),
- }
-)
-
-SCHEMA_DEACTIVATE_ECO_MODE = vol.Schema(
- {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))}
-)
-
-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,
- }
-)
-
-SCHEMA_DUMP_HAP_CONFIG = vol.Schema(
- {
- vol.Optional(ATTR_CONFIG_OUTPUT_PATH): cv.string,
- vol.Optional(
- ATTR_CONFIG_OUTPUT_FILE_PREFIX, default=DEFAULT_CONFIG_FILE_PREFIX
- ): cv.string,
- vol.Optional(ATTR_ANONYMIZE, default=True): cv.boolean,
- }
-)
-
-SCHEMA_RESET_ENERGY_COUNTER = vol.Schema(
- {vol.Required(ATTR_ENTITY_ID): comp_entity_ids}
-)
-
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Set up the HomematicIP Cloud component."""
@@ -145,189 +66,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
)
)
- async def _async_activate_eco_mode_with_duration(service) -> None:
- """Service to activate eco mode with duration."""
- duration = service.data[ATTR_DURATION]
- hapid = service.data.get(ATTR_ACCESSPOINT_ID)
-
- if hapid:
- home = _get_home(hapid)
- if home:
- await home.activate_absence_with_duration(duration)
- else:
- for hap in hass.data[DOMAIN].values():
- await hap.home.activate_absence_with_duration(duration)
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION,
- _async_activate_eco_mode_with_duration,
- schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION,
- )
-
- async def _async_activate_eco_mode_with_period(service) -> None:
- """Service to activate eco mode with period."""
- endtime = service.data[ATTR_ENDTIME]
- hapid = service.data.get(ATTR_ACCESSPOINT_ID)
-
- if hapid:
- home = _get_home(hapid)
- if home:
- await home.activate_absence_with_period(endtime)
- else:
- for hap in hass.data[DOMAIN].values():
- await hap.home.activate_absence_with_period(endtime)
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD,
- _async_activate_eco_mode_with_period,
- schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD,
- )
-
- async def _async_activate_vacation(service) -> None:
- """Service to activate vacation."""
- endtime = service.data[ATTR_ENDTIME]
- temperature = service.data[ATTR_TEMPERATURE]
- hapid = service.data.get(ATTR_ACCESSPOINT_ID)
-
- if hapid:
- home = _get_home(hapid)
- if home:
- await home.activate_vacation(endtime, temperature)
- else:
- for hap in hass.data[DOMAIN].values():
- await hap.home.activate_vacation(endtime, temperature)
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_ACTIVATE_VACATION,
- _async_activate_vacation,
- schema=SCHEMA_ACTIVATE_VACATION,
- )
-
- async def _async_deactivate_eco_mode(service) -> None:
- """Service to deactivate eco mode."""
- hapid = service.data.get(ATTR_ACCESSPOINT_ID)
-
- if hapid:
- home = _get_home(hapid)
- if home:
- await home.deactivate_absence()
- else:
- for hap in hass.data[DOMAIN].values():
- await hap.home.deactivate_absence()
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_DEACTIVATE_ECO_MODE,
- _async_deactivate_eco_mode,
- schema=SCHEMA_DEACTIVATE_ECO_MODE,
- )
-
- async def _async_deactivate_vacation(service) -> None:
- """Service to deactivate vacation."""
- hapid = service.data.get(ATTR_ACCESSPOINT_ID)
-
- if hapid:
- home = _get_home(hapid)
- if home:
- await home.deactivate_vacation()
- else:
- for hap in hass.data[DOMAIN].values():
- await hap.home.deactivate_vacation()
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_DEACTIVATE_VACATION,
- _async_deactivate_vacation,
- schema=SCHEMA_DEACTIVATE_VACATION,
- )
-
- async def _set_active_climate_profile(service) -> None:
- """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 and isinstance(group, AsyncHeatingGroup):
- 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,
- )
-
- async def _async_dump_hap_config(service) -> None:
- """Service to dump the configuration of a Homematic IP Access Point."""
- config_path = (
- service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir
- )
- config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX]
- anonymize = service.data[ATTR_ANONYMIZE]
-
- for hap in hass.data[DOMAIN].values():
- hap_sgtin = hap.config_entry.unique_id
-
- if anonymize:
- hap_sgtin = hap_sgtin[-4:]
-
- file_name = f"{config_file_prefix}_{hap_sgtin}.json"
- path = Path(config_path)
- config_file = path / file_name
-
- json_state = await hap.home.download_configuration()
- json_state = handle_config(json_state, anonymize)
-
- config_file.write_text(json_state, encoding="utf8")
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_DUMP_HAP_CONFIG,
- _async_dump_hap_config,
- schema=SCHEMA_DUMP_HAP_CONFIG,
- )
-
- async def _async_reset_energy_counter(service):
- """Service to reset the energy counter."""
- entity_id_list = service.data[ATTR_ENTITY_ID]
-
- for hap in hass.data[DOMAIN].values():
- if entity_id_list != "all":
- for entity_id in entity_id_list:
- device = hap.hmip_device_by_entity_id.get(entity_id)
- if device and isinstance(device, AsyncSwitchMeasuring):
- await device.reset_energy_counter()
- else:
- for device in hap.home.devices:
- if isinstance(device, AsyncSwitchMeasuring):
- await device.reset_energy_counter()
-
- hass.helpers.service.async_register_admin_service(
- DOMAIN,
- SERVICE_RESET_ENERGY_COUNTER,
- _async_reset_energy_counter,
- schema=SCHEMA_RESET_ENERGY_COUNTER,
- )
-
- def _get_home(hapid: str) -> Optional[AsyncHome]:
- """Return a HmIP home."""
- hap = hass.data[DOMAIN].get(hapid)
- if hap:
- return hap.home
-
- _LOGGER.info("No matching access point found for access point id %s", hapid)
- return None
-
return True
@@ -348,6 +86,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
if not await hap.async_setup():
return False
+ await async_setup_services(hass)
+
+ # Register on HA stop event to gracefully shutdown HomematicIP Cloud connection
+ hap.reset_connection_listener = hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, hap.shutdown
+ )
+
# Register hap as device in registry.
device_registry = await dr.async_get_registry(hass)
home = hap.home
@@ -367,4 +112,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hap = hass.data[DOMAIN].pop(entry.unique_id)
+ hap.reset_connection_listener()
+
+ await async_unload_services(hass)
+
return await hap.async_reset()
diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
index c2f4d833a35..f5316350091 100644
--- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py
+++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
@@ -16,6 +16,7 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
+from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN
@@ -95,10 +96,18 @@ class HomematicipAlarmControlPanel(AlarmControlPanel):
"""Register callbacks."""
self._home.on_update(self._async_device_changed)
+ @callback
def _async_device_changed(self, *args, **kwargs) -> None:
"""Handle device state changes."""
- _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME)
- self.async_schedule_update_ha_state()
+ # Don't update disabled entities
+ if self.enabled:
+ _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME)
+ self.async_schedule_update_ha_state()
+ else:
+ _LOGGER.debug(
+ "Device Changed Event for %s (Alarm Control Panel) not fired. Entity is disabled.",
+ self.name,
+ )
@property
def name(self) -> str:
diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py
index 0d2131f9cb3..768c893a100 100644
--- a/homeassistant/components/homematicip_cloud/cover.py
+++ b/homeassistant/components/homematicip_cloud/cover.py
@@ -7,6 +7,7 @@ from homematicip.aio.device import (
AsyncFullFlushShutter,
AsyncGarageDoorModuleTormatic,
)
+from homematicip.aio.group import AsyncExtendedLinkedShutterGroup
from homematicip.base.enums import DoorCommand, DoorState
from homeassistant.components.cover import (
@@ -18,6 +19,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice
+from .hap import HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -41,6 +43,10 @@ async def async_setup_entry(
elif isinstance(device, AsyncGarageDoorModuleTormatic):
entities.append(HomematicipGarageDoorModuleTormatic(hap, device))
+ for group in hap.home.groups:
+ if isinstance(group, AsyncExtendedLinkedShutterGroup):
+ entities.append(HomematicipCoverShutterGroup(hap, group))
+
if entities:
async_add_entities(entities)
@@ -142,3 +148,12 @@ class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverDevice)
async def async_stop_cover(self, **kwargs) -> None:
"""Stop the cover."""
await self._device.send_door_command(DoorCommand.STOP)
+
+
+class HomematicipCoverShutterGroup(HomematicipCoverSlats, CoverDevice):
+ """Representation of a HomematicIP Cloud cover shutter group."""
+
+ def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None:
+ """Initialize switching group."""
+ device.modelType = f"HmIP-{post}"
+ super().__init__(hap, device, post)
diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py
index 3c97cc1af9f..0d6fc726050 100644
--- a/homeassistant/components/homematicip_cloud/hap.py
+++ b/homeassistant/components/homematicip_cloud/hap.py
@@ -81,6 +81,7 @@ class HomematicipHAP:
self._tries = 0
self._accesspoint_connected = True
self.hmip_device_by_entity_id = {}
+ self.reset_connection_listener = None
async def async_setup(self, tries: int = 0) -> bool:
"""Initialize connection."""
@@ -93,6 +94,9 @@ class HomematicipHAP:
)
except HmipcConnectionError:
raise ConfigEntryNotReady
+ except Exception as err: # pylint: disable=broad-except
+ _LOGGER.error("Error connecting with HomematicIP Cloud: %s", err)
+ return False
_LOGGER.info(
"Connected to HomematicIP with HAP %s", self.config_entry.unique_id
@@ -223,6 +227,17 @@ class HomematicipHAP:
self.hmip_device_by_entity_id = {}
return True
+ @callback
+ def shutdown(self, event) -> None:
+ """Wrap the call to async_reset.
+
+ Used as an argument to EventBus.async_listen_once.
+ """
+ self.hass.async_create_task(self.async_reset())
+ _LOGGER.debug(
+ "Reset connection to access point id %s", self.config_entry.unique_id
+ )
+
async def get_hap(
self, hass: HomeAssistantType, hapid: str, authtoken: str, name: str
) -> AsyncHome:
diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json
index d823621f6cb..9ecdb0ad80d 100644
--- a/homeassistant/components/homematicip_cloud/manifest.json
+++ b/homeassistant/components/homematicip_cloud/manifest.json
@@ -3,7 +3,7 @@
"name": "HomematicIP Cloud",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
- "requirements": ["homematicip==0.10.15"],
+ "requirements": ["homematicip==0.10.17"],
"dependencies": [],
"codeowners": ["@SukramJ"],
"quality_scale": "platinum"
diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py
index ebbee1abc44..d6a226a83dc 100644
--- a/homeassistant/components/homematicip_cloud/sensor.py
+++ b/homeassistant/components/homematicip_cloud/sensor.py
@@ -14,6 +14,7 @@ from homematicip.aio.device import (
AsyncPassageDetector,
AsyncPlugableSwitchMeasuring,
AsyncPresenceDetectorIndoor,
+ AsyncRoomControlDeviceAnalog,
AsyncTemperatureHumiditySensorDisplay,
AsyncTemperatureHumiditySensorOutdoor,
AsyncTemperatureHumiditySensorWithoutDisplay,
@@ -79,6 +80,8 @@ async def async_setup_entry(
):
entities.append(HomematicipTemperatureSensor(hap, device))
entities.append(HomematicipHumiditySensor(hap, device))
+ elif isinstance(device, (AsyncRoomControlDeviceAnalog,)):
+ entities.append(HomematicipTemperatureSensor(hap, device))
if isinstance(
device,
(
@@ -305,7 +308,7 @@ class HomematicipPowerSensor(HomematicipGenericDevice):
@property
def state(self) -> float:
- """Representation of the HomematicIP power comsumption value."""
+ """Representation of the HomematicIP power consumption value."""
return self._device.currentPowerConsumption
@property
@@ -356,7 +359,7 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice):
@property
def state(self) -> float:
- """Representation of the HomematicIP todays rain value."""
+ """Representation of the HomematicIP today's rain value."""
return round(self._device.todayRainCounter, 2)
@property
diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py
new file mode 100644
index 00000000000..193cac94629
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud/services.py
@@ -0,0 +1,357 @@
+"""Support for HomematicIP Cloud devices."""
+import logging
+from pathlib import Path
+from typing import Optional
+
+from homematicip.aio.device import AsyncSwitchMeasuring
+from homematicip.aio.group import AsyncHeatingGroup
+from homematicip.aio.home import AsyncHome
+from homematicip.base.helpers import handle_config
+import voluptuous as vol
+
+from homeassistant.const import ATTR_ENTITY_ID
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.config_validation import comp_entity_ids
+from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType
+
+from .const import DOMAIN as HMIPC_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_ACCESSPOINT_ID = "accesspoint_id"
+ATTR_ANONYMIZE = "anonymize"
+ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index"
+ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix"
+ATTR_CONFIG_OUTPUT_PATH = "config_output_path"
+ATTR_DURATION = "duration"
+ATTR_ENDTIME = "endtime"
+ATTR_TEMPERATURE = "temperature"
+
+DEFAULT_CONFIG_FILE_PREFIX = "hmip-config"
+
+SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = "activate_eco_mode_with_duration"
+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_DUMP_HAP_CONFIG = "dump_hap_config"
+SERVICE_RESET_ENERGY_COUNTER = "reset_energy_counter"
+SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile"
+
+HMIPC_SERVICES2 = {
+ SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION: "_async_activate_eco_mode_with_duration",
+ SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD: "_async_activate_eco_mode_with_period",
+ SERVICE_ACTIVATE_VACATION: "_async_activate_vacation",
+ SERVICE_DEACTIVATE_ECO_MODE: "SERVICE_DEACTIVATE_ECO_MODE",
+ SERVICE_DEACTIVATE_VACATION: "_async_deactivate_vacation",
+ SERVICE_DUMP_HAP_CONFIG: "_async_dump_hap_config",
+ SERVICE_RESET_ENERGY_COUNTER: "_async_reset_energy_counter",
+ SERVICE_SET_ACTIVE_CLIMATE_PROFILE: "_set_active_climate_profile",
+}
+
+HMIPC_SERVICES = [
+ SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION,
+ SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD,
+ SERVICE_ACTIVATE_VACATION,
+ SERVICE_DEACTIVATE_ECO_MODE,
+ SERVICE_DEACTIVATE_VACATION,
+ SERVICE_DUMP_HAP_CONFIG,
+ SERVICE_RESET_ENERGY_COUNTER,
+ SERVICE_SET_ACTIVE_CLIMATE_PROFILE,
+]
+
+SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema(
+ {
+ vol.Required(ATTR_DURATION): cv.positive_int,
+ vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)),
+ }
+)
+
+SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD = vol.Schema(
+ {
+ vol.Required(ATTR_ENDTIME): cv.datetime,
+ vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)),
+ }
+)
+
+SCHEMA_ACTIVATE_VACATION = vol.Schema(
+ {
+ vol.Required(ATTR_ENDTIME): cv.datetime,
+ vol.Required(ATTR_TEMPERATURE, default=18.0): vol.All(
+ vol.Coerce(float), vol.Range(min=0, max=55)
+ ),
+ vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)),
+ }
+)
+
+SCHEMA_DEACTIVATE_ECO_MODE = vol.Schema(
+ {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))}
+)
+
+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,
+ }
+)
+
+SCHEMA_DUMP_HAP_CONFIG = vol.Schema(
+ {
+ vol.Optional(ATTR_CONFIG_OUTPUT_PATH): cv.string,
+ vol.Optional(
+ ATTR_CONFIG_OUTPUT_FILE_PREFIX, default=DEFAULT_CONFIG_FILE_PREFIX
+ ): cv.string,
+ vol.Optional(ATTR_ANONYMIZE, default=True): cv.boolean,
+ }
+)
+
+SCHEMA_RESET_ENERGY_COUNTER = vol.Schema(
+ {vol.Required(ATTR_ENTITY_ID): comp_entity_ids}
+)
+
+
+async def async_setup_services(hass: HomeAssistantType) -> None:
+ """Set up the HomematicIP Cloud services."""
+
+ if hass.services.async_services().get(HMIPC_DOMAIN):
+ return
+
+ async def async_call_hmipc_service(service: ServiceCallType):
+ """Call correct HomematicIP Cloud service."""
+ service_name = service.service
+
+ if service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION:
+ await _async_activate_eco_mode_with_duration(hass, service)
+ elif service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD:
+ await _async_activate_eco_mode_with_period(hass, service)
+ elif service_name == SERVICE_ACTIVATE_VACATION:
+ await _async_activate_vacation(hass, service)
+ elif service_name == SERVICE_DEACTIVATE_ECO_MODE:
+ await _async_deactivate_eco_mode(hass, service)
+ elif service_name == SERVICE_DEACTIVATE_VACATION:
+ await _async_deactivate_vacation(hass, service)
+ elif service_name == SERVICE_DUMP_HAP_CONFIG:
+ await _async_dump_hap_config(hass, service)
+ elif service_name == SERVICE_RESET_ENERGY_COUNTER:
+ await _async_reset_energy_counter(hass, service)
+ elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE:
+ await _set_active_climate_profile(hass, service)
+
+ hass.services.async_register(
+ HMIPC_DOMAIN,
+ SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION,
+ async_call_hmipc_service,
+ schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION,
+ )
+
+ hass.services.async_register(
+ HMIPC_DOMAIN,
+ SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD,
+ async_call_hmipc_service,
+ schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD,
+ )
+
+ hass.services.async_register(
+ HMIPC_DOMAIN,
+ SERVICE_ACTIVATE_VACATION,
+ async_call_hmipc_service,
+ schema=SCHEMA_ACTIVATE_VACATION,
+ )
+
+ hass.services.async_register(
+ HMIPC_DOMAIN,
+ SERVICE_DEACTIVATE_ECO_MODE,
+ async_call_hmipc_service,
+ schema=SCHEMA_DEACTIVATE_ECO_MODE,
+ )
+
+ hass.services.async_register(
+ HMIPC_DOMAIN,
+ SERVICE_DEACTIVATE_VACATION,
+ async_call_hmipc_service,
+ schema=SCHEMA_DEACTIVATE_VACATION,
+ )
+
+ hass.services.async_register(
+ HMIPC_DOMAIN,
+ SERVICE_SET_ACTIVE_CLIMATE_PROFILE,
+ async_call_hmipc_service,
+ schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE,
+ )
+
+ hass.services.async_register(
+ HMIPC_DOMAIN,
+ SERVICE_DUMP_HAP_CONFIG,
+ async_call_hmipc_service,
+ schema=SCHEMA_DUMP_HAP_CONFIG,
+ )
+
+ hass.helpers.service.async_register_admin_service(
+ HMIPC_DOMAIN,
+ SERVICE_RESET_ENERGY_COUNTER,
+ async_call_hmipc_service,
+ schema=SCHEMA_RESET_ENERGY_COUNTER,
+ )
+
+
+async def async_unload_services(hass: HomeAssistantType):
+ """Unload HomematicIP Cloud services."""
+ if hass.data[HMIPC_DOMAIN]:
+ return
+
+ for hmipc_service in HMIPC_SERVICES:
+ hass.services.async_remove(HMIPC_DOMAIN, hmipc_service)
+
+
+async def _async_activate_eco_mode_with_duration(
+ hass: HomeAssistantType, service: ServiceCallType
+) -> None:
+ """Service to activate eco mode with duration."""
+ duration = service.data[ATTR_DURATION]
+ hapid = service.data.get(ATTR_ACCESSPOINT_ID)
+
+ if hapid:
+ home = _get_home(hass, hapid)
+ if home:
+ await home.activate_absence_with_duration(duration)
+ else:
+ for hap in hass.data[HMIPC_DOMAIN].values():
+ await hap.home.activate_absence_with_duration(duration)
+
+
+async def _async_activate_eco_mode_with_period(
+ hass: HomeAssistantType, service: ServiceCallType
+) -> None:
+ """Service to activate eco mode with period."""
+ endtime = service.data[ATTR_ENDTIME]
+ hapid = service.data.get(ATTR_ACCESSPOINT_ID)
+
+ if hapid:
+ home = _get_home(hass, hapid)
+ if home:
+ await home.activate_absence_with_period(endtime)
+ else:
+ for hap in hass.data[HMIPC_DOMAIN].values():
+ await hap.home.activate_absence_with_period(endtime)
+
+
+async def _async_activate_vacation(
+ hass: HomeAssistantType, service: ServiceCallType
+) -> None:
+ """Service to activate vacation."""
+ endtime = service.data[ATTR_ENDTIME]
+ temperature = service.data[ATTR_TEMPERATURE]
+ hapid = service.data.get(ATTR_ACCESSPOINT_ID)
+
+ if hapid:
+ home = _get_home(hass, hapid)
+ if home:
+ await home.activate_vacation(endtime, temperature)
+ else:
+ for hap in hass.data[HMIPC_DOMAIN].values():
+ await hap.home.activate_vacation(endtime, temperature)
+
+
+async def _async_deactivate_eco_mode(
+ hass: HomeAssistantType, service: ServiceCallType
+) -> None:
+ """Service to deactivate eco mode."""
+ hapid = service.data.get(ATTR_ACCESSPOINT_ID)
+
+ if hapid:
+ home = _get_home(hass, hapid)
+ if home:
+ await home.deactivate_absence()
+ else:
+ for hap in hass.data[HMIPC_DOMAIN].values():
+ await hap.home.deactivate_absence()
+
+
+async def _async_deactivate_vacation(
+ hass: HomeAssistantType, service: ServiceCallType
+) -> None:
+ """Service to deactivate vacation."""
+ hapid = service.data.get(ATTR_ACCESSPOINT_ID)
+
+ if hapid:
+ home = _get_home(hass, hapid)
+ if home:
+ await home.deactivate_vacation()
+ else:
+ for hap in hass.data[HMIPC_DOMAIN].values():
+ await hap.home.deactivate_vacation()
+
+
+async def _set_active_climate_profile(
+ hass: HomeAssistantType, service: ServiceCallType
+) -> None:
+ """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[HMIPC_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 and isinstance(group, AsyncHeatingGroup):
+ 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)
+
+
+async def _async_dump_hap_config(
+ hass: HomeAssistantType, service: ServiceCallType
+) -> None:
+ """Service to dump the configuration of a Homematic IP Access Point."""
+ config_path = service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir
+ config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX]
+ anonymize = service.data[ATTR_ANONYMIZE]
+
+ for hap in hass.data[HMIPC_DOMAIN].values():
+ hap_sgtin = hap.config_entry.unique_id
+
+ if anonymize:
+ hap_sgtin = hap_sgtin[-4:]
+
+ file_name = f"{config_file_prefix}_{hap_sgtin}.json"
+ path = Path(config_path)
+ config_file = path / file_name
+
+ json_state = await hap.home.download_configuration()
+ json_state = handle_config(json_state, anonymize)
+
+ config_file.write_text(json_state, encoding="utf8")
+
+
+async def _async_reset_energy_counter(
+ hass: HomeAssistantType, service: ServiceCallType
+):
+ """Service to reset the energy counter."""
+ entity_id_list = service.data[ATTR_ENTITY_ID]
+
+ for hap in hass.data[HMIPC_DOMAIN].values():
+ if entity_id_list != "all":
+ for entity_id in entity_id_list:
+ device = hap.hmip_device_by_entity_id.get(entity_id)
+ if device and isinstance(device, AsyncSwitchMeasuring):
+ await device.reset_energy_counter()
+ else:
+ for device in hap.home.devices:
+ if isinstance(device, AsyncSwitchMeasuring):
+ await device.reset_energy_counter()
+
+
+def _get_home(hass: HomeAssistantType, hapid: str) -> Optional[AsyncHome]:
+ """Return a HmIP home."""
+ hap = hass.data[HMIPC_DOMAIN].get(hapid)
+ if hap:
+ return hap.home
+
+ _LOGGER.info("No matching access point found for access point id %s", hapid)
+ return None
diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json
index 7a3ae23e275..a52d101a486 100644
--- a/homeassistant/components/huawei_lte/.translations/ca.json
+++ b/homeassistant/components/huawei_lte/.translations/ca.json
@@ -33,7 +33,7 @@
"step": {
"init": {
"data": {
- "name": "Nom del servei de notificacions",
+ "name": "Nom del servei de notificacions (reinici necessari si canvia)",
"recipient": "Destinataris de notificacions SMS",
"track_new_devices": "Segueix dispositius nous"
}
diff --git a/homeassistant/components/huawei_lte/.translations/hu.json b/homeassistant/components/huawei_lte/.translations/hu.json
new file mode 100644
index 00000000000..9f012c1c405
--- /dev/null
+++ b/homeassistant/components/huawei_lte/.translations/hu.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ez az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "Ez az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z"
+ },
+ "error": {
+ "connection_failed": "Kapcsol\u00f3d\u00e1s sikertelen",
+ "connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9se",
+ "incorrect_password": "Hib\u00e1s jelsz\u00f3",
+ "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v",
+ "incorrect_username_or_password": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3",
+ "invalid_url": "\u00c9rv\u00e9nytelen URL"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "url": "URL",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "Huawei LTE konfigur\u00e1l\u00e1sa"
+ }
+ },
+ "title": "Huawei LTE"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "name": "\u00c9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s neve (a m\u00f3dos\u00edt\u00e1s \u00fajraind\u00edt\u00e1st ig\u00e9nyel)",
+ "recipient": "SMS-\u00e9rtes\u00edt\u00e9s c\u00edmzettjei",
+ "track_new_devices": "\u00daj eszk\u00f6z\u00f6k nyomk\u00f6vet\u00e9se"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json
index a4e7d72852a..4029b24df3f 100644
--- a/homeassistant/components/huawei_lte/.translations/pl.json
+++ b/homeassistant/components/huawei_lte/.translations/pl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
- "already_in_progress": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_configured": "To urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_in_progress": "To urz\u0105dzenie jest ju\u017c skonfigurowane.",
"not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE"
},
"error": {
diff --git a/homeassistant/components/huawei_lte/.translations/sv.json b/homeassistant/components/huawei_lte/.translations/sv.json
index fb73612d897..16b192d16a1 100644
--- a/homeassistant/components/huawei_lte/.translations/sv.json
+++ b/homeassistant/components/huawei_lte/.translations/sv.json
@@ -1,7 +1,43 @@
{
"config": {
"abort": {
- "already_configured": "Den h\u00e4r enheten har redan konfigurerats"
+ "already_configured": "Den h\u00e4r enheten har redan konfigurerats",
+ "already_in_progress": "Den h\u00e4r enheten har redan konfigurerats",
+ "not_huawei_lte": "Inte en Huawei LTE-enhet"
+ },
+ "error": {
+ "connection_failed": "Anslutningen misslyckades",
+ "connection_timeout": "Timeout f\u00f6r anslutning",
+ "incorrect_password": "Felaktigt l\u00f6senord",
+ "incorrect_username": "Felaktigt anv\u00e4ndarnamn",
+ "incorrect_username_or_password": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord",
+ "invalid_url": "Ogiltig URL",
+ "login_attempts_exceeded": "Maximala inloggningsf\u00f6rs\u00f6k har \u00f6verskridits, f\u00f6rs\u00f6k igen senare",
+ "response_error": "Ok\u00e4nt fel fr\u00e5n enheten",
+ "unknown_connection_error": "Ok\u00e4nt fel vid anslutning till enheten"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "L\u00f6senord",
+ "url": "URL",
+ "username": "Anv\u00e4ndarnamn"
+ },
+ "description": "Ange information om enhets\u00e5tkomst. Det \u00e4r valfritt att ange anv\u00e4ndarnamn och l\u00f6senord, men st\u00f6djer d\u00e5 fler integrationsfunktioner. \u00c5 andra sidan kan anv\u00e4ndning av en auktoriserad anslutning orsaka problem med att komma \u00e5t enhetens webbgr\u00e4nssnitt utanf\u00f6r Home Assistant medan integrationen \u00e4r aktiv och tv\u00e4rtom.",
+ "title": "Konfigurera Huawei LTE"
+ }
+ },
+ "title": "Huawei LTE"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "name": "Namn p\u00e5 meddelandetj\u00e4nsten (\u00e4ndring kr\u00e4ver omstart)",
+ "recipient": "Mottagare av SMS-meddelanden",
+ "track_new_devices": "Sp\u00e5ra nya enheter"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py
index 1d54f972907..d3b2d5b1abd 100644
--- a/homeassistant/components/huawei_lte/__init__.py
+++ b/homeassistant/components/huawei_lte/__init__.py
@@ -5,6 +5,7 @@ from datetime import timedelta
from functools import partial
import ipaddress
import logging
+import time
from typing import Any, Callable, Dict, List, Set, Tuple
from urllib.parse import urlparse
@@ -65,6 +66,7 @@ from .const import (
KEY_MONITORING_STATUS,
KEY_MONITORING_TRAFFIC_STATISTICS,
KEY_WLAN_HOST_LIST,
+ NOTIFY_SUPPRESS_TIMEOUT,
SERVICE_CLEAR_TRAFFIC_STATISTICS,
SERVICE_REBOOT,
SERVICE_RESUME_INTEGRATION,
@@ -138,9 +140,11 @@ class Router:
init=False,
factory=lambda: defaultdict(set, ((x, {"initial_scan"}) for x in ALL_KEYS)),
)
+ inflight_gets: Set[str] = attr.ib(init=False, factory=set)
unload_handlers: List[CALLBACK_TYPE] = attr.ib(init=False, factory=list)
client: Client
suspended = attr.ib(init=False, default=False)
+ notify_last_attempt: float = attr.ib(init=False, default=-1)
def __attrs_post_init__(self):
"""Set up internal state on init."""
@@ -167,6 +171,10 @@ class Router:
def _get_data(self, key: str, func: Callable[[None], Any]) -> None:
if not self.subscriptions.get(key):
return
+ if key in self.inflight_gets:
+ _LOGGER.debug("Skipping already inflight get for %s", key)
+ return
+ self.inflight_gets.add(key)
_LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key])
try:
self.data[key] = func()
@@ -189,7 +197,21 @@ class Router:
"%s requires authorization, excluding from future updates", key
)
self.subscriptions.pop(key)
+ except Timeout:
+ grace_left = (
+ self.notify_last_attempt - time.monotonic() + NOTIFY_SUPPRESS_TIMEOUT
+ )
+ if grace_left > 0:
+ _LOGGER.debug(
+ "%s timed out, %.1fs notify timeout suppress grace remaining",
+ key,
+ grace_left,
+ exc_info=True,
+ )
+ else:
+ raise
finally:
+ self.inflight_gets.discard(key)
_LOGGER.debug("%s=%s", key, self.data.get(key))
def update(self) -> None:
diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py
index c6837fce06c..6d699420283 100644
--- a/homeassistant/components/huawei_lte/const.py
+++ b/homeassistant/components/huawei_lte/const.py
@@ -8,10 +8,10 @@ DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN
UPDATE_SIGNAL = f"{DOMAIN}_update"
UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update"
-UNIT_BYTES = "B"
UNIT_SECONDS = "s"
CONNECTION_TIMEOUT = 10
+NOTIFY_SUPPRESS_TIMEOUT = 30
SERVICE_CLEAR_TRAFFIC_STATISTICS = "clear_traffic_statistics"
SERVICE_REBOOT = "reboot"
diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py
index 5619a5d702c..91cc8864eb0 100644
--- a/homeassistant/components/huawei_lte/notify.py
+++ b/homeassistant/components/huawei_lte/notify.py
@@ -1,6 +1,7 @@
"""Support for Huawei LTE router notifications."""
import logging
+import time
from typing import Any, List
import attr
@@ -57,3 +58,5 @@ class HuaweiLteSmsNotificationService(BaseNotificationService):
_LOGGER.debug("Sent to %s: %s", targets, resp)
except ResponseErrorException as ex:
_LOGGER.error("Could not send to %s: %s", targets, ex)
+ finally:
+ self.router.notify_last_attempt = time.monotonic()
diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py
index 3b6b75edfba..54c5441c6e2 100644
--- a/homeassistant/components/huawei_lte/sensor.py
+++ b/homeassistant/components/huawei_lte/sensor.py
@@ -10,7 +10,7 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_SIGNAL_STRENGTH,
DOMAIN as SENSOR_DOMAIN,
)
-from homeassistant.const import CONF_URL, STATE_UNKNOWN
+from homeassistant.const import CONF_URL, DATA_BYTES, STATE_UNKNOWN
from . import HuaweiLteBaseEntity
from .const import (
@@ -18,7 +18,6 @@ from .const import (
KEY_DEVICE_INFORMATION,
KEY_DEVICE_SIGNAL,
KEY_MONITORING_TRAFFIC_STATISTICS,
- UNIT_BYTES,
UNIT_SECONDS,
)
@@ -126,19 +125,19 @@ SENSOR_META = {
name="Current connection duration", unit=UNIT_SECONDS, icon="mdi:timer"
),
(KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): dict(
- name="Current connection download", unit=UNIT_BYTES, icon="mdi:download"
+ name="Current connection download", unit=DATA_BYTES, icon="mdi:download"
),
(KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): dict(
- name="Current connection upload", unit=UNIT_BYTES, icon="mdi:upload"
+ name="Current connection upload", unit=DATA_BYTES, icon="mdi:upload"
),
(KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): dict(
name="Total connected duration", unit=UNIT_SECONDS, icon="mdi:timer"
),
(KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): dict(
- name="Total download", unit=UNIT_BYTES, icon="mdi:download"
+ name="Total download", unit=DATA_BYTES, icon="mdi:download"
),
(KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): dict(
- name="Total upload", unit=UNIT_BYTES, icon="mdi:upload"
+ name="Total upload", unit=DATA_BYTES, icon="mdi:upload"
),
}
diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json
index 3866af9d7fc..00b9374459c 100644
--- a/homeassistant/components/hue/.translations/pl.json
+++ b/homeassistant/components/hue/.translations/pl.json
@@ -1,9 +1,9 @@
{
"config": {
"abort": {
- "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane",
- "already_configured": "Mostek jest ju\u017c skonfigurowany",
- "already_in_progress": "Konfigurowanie mostka jest ju\u017c w toku.",
+ "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane.",
+ "already_configured": "Mostek jest ju\u017c skonfigurowany.",
+ "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.",
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem",
"discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue",
"no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue",
diff --git a/homeassistant/components/hue/.translations/zh-Hans.json b/homeassistant/components/hue/.translations/zh-Hans.json
index 1c6d78f9343..5e2f35bfea8 100644
--- a/homeassistant/components/hue/.translations/zh-Hans.json
+++ b/homeassistant/components/hue/.translations/zh-Hans.json
@@ -5,7 +5,7 @@
"already_configured": "\u98de\u5229\u6d66 Hue Bridge \u5df2\u914d\u7f6e\u5b8c\u6210",
"already_in_progress": "\u7f51\u6865\u7684\u914d\u7f6e\u6d41\u5df2\u5728\u8fdb\u884c\u4e2d\u3002",
"cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 \u98de\u5229\u6d66 Hue Bridge",
- "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2 Hue \u6865\u63a5\u5668",
+ "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2\u5230 Hue \u6865\u63a5\u5668",
"no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge",
"unknown": "\u51fa\u73b0\u672a\u77e5\u7684\u9519\u8bef"
},
diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py
index ff51fc667e6..7510ff22f16 100644
--- a/homeassistant/components/hue/__init__.py
+++ b/homeassistant/components/hue/__init__.py
@@ -11,22 +11,22 @@ from homeassistant.const import CONF_HOST
from homeassistant.helpers import config_validation as cv, device_registry as dr
from .bridge import HueBridge
-from .const import DOMAIN
+from .const import (
+ CONF_ALLOW_HUE_GROUPS,
+ CONF_ALLOW_UNREACHABLE,
+ DEFAULT_ALLOW_HUE_GROUPS,
+ DEFAULT_ALLOW_UNREACHABLE,
+ DOMAIN,
+)
_LOGGER = logging.getLogger(__name__)
CONF_BRIDGES = "bridges"
-CONF_ALLOW_UNREACHABLE = "allow_unreachable"
-DEFAULT_ALLOW_UNREACHABLE = False
-
DATA_CONFIGS = "hue_configs"
PHUE_CONFIG_FILE = "phue.conf"
-CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
-DEFAULT_ALLOW_HUE_GROUPS = True
-
BRIDGE_CONFIG_SCHEMA = vol.Schema(
{
# Validate as IP address and then convert back to a string.
@@ -46,13 +46,7 @@ CONFIG_SCHEMA = vol.Schema(
DOMAIN: vol.Schema(
{
vol.Optional(CONF_BRIDGES): vol.All(
- cv.ensure_list,
- [
- vol.All(
- cv.deprecated("filename", invalidation_version="0.106.0"),
- BRIDGE_CONFIG_SCHEMA,
- ),
- ],
+ cv.ensure_list, [BRIDGE_CONFIG_SCHEMA],
)
}
)
@@ -112,8 +106,10 @@ async def async_setup_entry(
config = hass.data[DATA_CONFIGS].get(host)
if config is None:
- allow_unreachable = DEFAULT_ALLOW_UNREACHABLE
- allow_groups = DEFAULT_ALLOW_HUE_GROUPS
+ allow_unreachable = entry.data.get(
+ CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE
+ )
+ allow_groups = entry.data.get(CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS)
else:
allow_unreachable = config[CONF_ALLOW_UNREACHABLE]
allow_groups = config[CONF_ALLOW_HUE_GROUPS]
diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py
index a153ed7a096..2c164e5769a 100644
--- a/homeassistant/components/hue/bridge.py
+++ b/homeassistant/components/hue/bridge.py
@@ -1,6 +1,8 @@
"""Code to handle a Hue bridge."""
import asyncio
+from functools import partial
+from aiohttp import client_exceptions
import aiohue
import async_timeout
import slugify as unicode_slug
@@ -21,6 +23,8 @@ ATTR_SCENE_NAME = "scene_name"
SCENE_SCHEMA = vol.Schema(
{vol.Required(ATTR_GROUP_NAME): cv.string, vol.Required(ATTR_SCENE_NAME): cv.string}
)
+# How long should we sleep if the hub is busy
+HUB_BUSY_SLEEP = 0.01
class HueBridge:
@@ -101,11 +105,33 @@ class HueBridge:
self.authorized = True
return True
- async def async_request_call(self, coro):
- """Process request batched."""
+ async def async_request_call(self, task):
+ """Limit parallel requests to Hue hub.
+ The Hue hub can only handle a certain amount of parallel requests, total.
+ Although we limit our parallel requests, we still will run into issues because
+ other products are hitting up Hue.
+
+ ClientOSError means hub closed the socket on us.
+ ContentResponseError means hub raised an error.
+ Since we don't make bad requests, this is on them.
+ """
async with self.parallel_updates_semaphore:
- return await coro
+ for tries in range(4):
+ try:
+ return await task()
+ except (
+ client_exceptions.ClientOSError,
+ client_exceptions.ClientResponseError,
+ ) as err:
+ if tries == 3 or (
+ # We only retry if it's a server error. So raise on all 4XX errors.
+ isinstance(err, client_exceptions.ClientResponseError)
+ and err.status < 500
+ ):
+ raise
+
+ await asyncio.sleep(HUB_BUSY_SLEEP * tries)
async def async_reset(self):
"""Reset this bridge to default state.
@@ -167,8 +193,8 @@ class HueBridge:
# If we can't find it, fetch latest info.
if not updated and (group is None or scene is None):
- await self.api.groups.update()
- await self.api.scenes.update()
+ await self.async_request_call(self.api.groups.update)
+ await self.async_request_call(self.api.scenes.update)
await self.hue_activate_scene(call, updated=True)
return
@@ -180,7 +206,7 @@ class HueBridge:
LOGGER.warning("Unable to find scene %s", scene_name)
return
- await group.set_action(scene=scene.id)
+ await self.async_request_call(partial(group.set_action, scene=scene.id))
async def handle_unauthorized_error(self):
"""Create a new config flow when the authorization is no longer valid."""
@@ -210,7 +236,7 @@ async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge):
except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
raise AuthenticationRequired
- except (asyncio.TimeoutError, aiohue.RequestError):
+ except (asyncio.TimeoutError, client_exceptions.ClientOSError):
raise CannotConnect
except aiohue.AiohueException:
LOGGER.exception("Unknown Hue linking error occurred")
diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py
index a46f8816fbb..d214c5509ea 100644
--- a/homeassistant/components/hue/config_flow.py
+++ b/homeassistant/components/hue/config_flow.py
@@ -14,7 +14,11 @@ from homeassistant.const import CONF_HOST
from homeassistant.helpers import aiohttp_client
from .bridge import authenticate_bridge
-from .const import DOMAIN, LOGGER # pylint: disable=unused-import
+from .const import ( # pylint: disable=unused-import
+ CONF_ALLOW_HUE_GROUPS,
+ DOMAIN,
+ LOGGER,
+)
from .errors import AuthenticationRequired, CannotConnect
HUE_MANUFACTURERURL = "http://www.philips.com"
@@ -125,7 +129,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(
title=bridge.config.name,
- data={"host": bridge.host, "username": bridge.username},
+ data={
+ "host": bridge.host,
+ "username": bridge.username,
+ CONF_ALLOW_HUE_GROUPS: False,
+ },
)
except AuthenticationRequired:
errors["base"] = "register_failed"
diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py
index e48cd4a8583..e2189515482 100644
--- a/homeassistant/components/hue/const.py
+++ b/homeassistant/components/hue/const.py
@@ -3,8 +3,13 @@ import logging
LOGGER = logging.getLogger(__package__)
DOMAIN = "hue"
-API_NUPNP = "https://www.meethue.com/api/nupnp"
# How long to wait to actually do the refresh after requesting it.
# We wait some time so if we control multiple lights, we batch requests.
REQUEST_REFRESH_DELAY = 0.3
+
+CONF_ALLOW_UNREACHABLE = "allow_unreachable"
+DEFAULT_ALLOW_UNREACHABLE = False
+
+CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
+DEFAULT_ALLOW_HUE_GROUPS = True
diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py
index 7ed2dcc84f2..1678dbbfc62 100644
--- a/homeassistant/components/hue/light.py
+++ b/homeassistant/components/hue/light.py
@@ -72,6 +72,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
pass
+def create_light(item_class, coordinator, bridge, is_group, api, item_id):
+ """Create the light."""
+ if is_group:
+ supported_features = 0
+ for light_id in api[item_id].lights:
+ if light_id not in bridge.api.lights:
+ continue
+ light = bridge.api.lights[light_id]
+ supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED)
+ supported_features = supported_features or SUPPORT_HUE_EXTENDED
+ else:
+ supported_features = SUPPORT_HUE.get(api[item_id].type, SUPPORT_HUE_EXTENDED)
+ return item_class(coordinator, bridge, is_group, api[item_id], supported_features)
+
+
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Hue lights from a config entry."""
bridge = hass.data[HUE_DOMAIN][config_entry.entry_id]
@@ -79,17 +94,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
light_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- "light",
- partial(async_safe_fetch, bridge, bridge.api.lights.update),
- SCAN_INTERVAL,
- Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True),
+ name="light",
+ update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
+ update_interval=SCAN_INTERVAL,
+ request_refresh_debouncer=Debouncer(
+ bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
+ ),
)
# First do a refresh to see if we can reach the hub.
# Otherwise we will declare not ready.
await light_coordinator.async_refresh()
- if light_coordinator.failed_last_update:
+ if not light_coordinator.last_update_success:
raise PlatformNotReady
update_lights = partial(
@@ -98,7 +115,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
bridge.api.lights,
{},
async_add_entities,
- partial(HueLight, light_coordinator, bridge, False),
+ partial(create_light, HueLight, light_coordinator, bridge, False),
)
# We add a listener after fetching the data, so manually trigger listener
@@ -122,10 +139,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
group_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- "group",
- partial(async_safe_fetch, bridge, bridge.api.groups.update),
- SCAN_INTERVAL,
- Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True),
+ name="group",
+ update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
+ update_interval=SCAN_INTERVAL,
+ request_refresh_debouncer=Debouncer(
+ bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
+ ),
)
update_groups = partial(
@@ -134,7 +153,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
bridge.api.groups,
{},
async_add_entities,
- partial(HueLight, group_coordinator, bridge, True),
+ partial(create_light, HueLight, group_coordinator, bridge, True),
)
group_coordinator.async_add_listener(update_groups)
@@ -149,7 +168,7 @@ async def async_safe_fetch(bridge, fetch_method):
"""Safely fetch data."""
try:
with async_timeout.timeout(4):
- return await bridge.async_request_call(fetch_method())
+ return await bridge.async_request_call(fetch_method)
except aiohue.Unauthorized:
await bridge.handle_unauthorized_error()
raise UpdateFailed
@@ -166,7 +185,7 @@ def async_update_items(bridge, api, current, async_add_entities, create_item):
if item_id in current:
continue
- current[item_id] = create_item(api[item_id])
+ current[item_id] = create_item(api, item_id)
new_items.append(current[item_id])
bridge.hass.async_create_task(remove_devices(bridge, api, current))
@@ -178,12 +197,13 @@ def async_update_items(bridge, api, current, async_add_entities, create_item):
class HueLight(Light):
"""Representation of a Hue light."""
- def __init__(self, coordinator, bridge, is_group, light):
+ def __init__(self, coordinator, bridge, is_group, light, supported_features):
"""Initialize the light."""
self.light = light
self.coordinator = coordinator
self.bridge = bridge
self.is_group = is_group
+ self._supported_features = supported_features
if is_group:
self.is_osram = False
@@ -277,7 +297,7 @@ class HueLight(Light):
@property
def available(self):
"""Return if light is available."""
- return not self.coordinator.failed_last_update and (
+ return self.coordinator.last_update_success and (
self.is_group
or self.bridge.allow_unreachable
or self.light.state["reachable"]
@@ -286,7 +306,7 @@ class HueLight(Light):
@property
def supported_features(self):
"""Flag supported features."""
- return SUPPORT_HUE.get(self.light.type, SUPPORT_HUE_EXTENDED)
+ return self._supported_features
@property
def effect(self):
@@ -372,9 +392,13 @@ class HueLight(Light):
command["effect"] = "none"
if self.is_group:
- await self.bridge.async_request_call(self.light.set_action(**command))
+ await self.bridge.async_request_call(
+ partial(self.light.set_action, **command)
+ )
else:
- await self.bridge.async_request_call(self.light.set_state(**command))
+ await self.bridge.async_request_call(
+ partial(self.light.set_state, **command)
+ )
await self.coordinator.async_request_refresh()
@@ -397,9 +421,13 @@ class HueLight(Light):
command["alert"] = "none"
if self.is_group:
- await self.bridge.async_request_call(self.light.set_action(**command))
+ await self.bridge.async_request_call(
+ partial(self.light.set_action, **command)
+ )
else:
- await self.bridge.async_request_call(self.light.set_state(**command))
+ await self.bridge.async_request_call(
+ partial(self.light.set_state, **command)
+ )
await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json
index ea01da0980f..5471632f9c5 100644
--- a/homeassistant/components/hue/manifest.json
+++ b/homeassistant/components/hue/manifest.json
@@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
- "requirements": ["aiohue==1.10.1"],
+ "requirements": ["aiohue==2.0.0"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",
diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py
index f57b0f98d30..0bc7cd53536 100644
--- a/homeassistant/components/hue/sensor_base.py
+++ b/homeassistant/components/hue/sensor_base.py
@@ -42,10 +42,12 @@ class SensorManager:
self.coordinator = DataUpdateCoordinator(
bridge.hass,
_LOGGER,
- "sensor",
- self.async_update_data,
- self.SCAN_INTERVAL,
- debounce.Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True),
+ name="sensor",
+ update_method=self.async_update_data,
+ update_interval=self.SCAN_INTERVAL,
+ request_refresh_debouncer=debounce.Debouncer(
+ bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
+ ),
)
async def async_update_data(self):
@@ -53,7 +55,7 @@ class SensorManager:
try:
with async_timeout.timeout(4):
return await self.bridge.async_request_call(
- self.bridge.api.sensors.update()
+ self.bridge.api.sensors.update
)
except Unauthorized:
await self.bridge.handle_unauthorized_error()
@@ -183,7 +185,7 @@ class GenericHueSensor(entity.Entity):
@property
def available(self):
"""Return if sensor is available."""
- return not self.bridge.sensor_manager.coordinator.failed_last_update and (
+ return self.bridge.sensor_manager.coordinator.last_update_success and (
self.bridge.allow_unreachable or self.sensor.config["reachable"]
)
diff --git a/homeassistant/components/iaqualink/.translations/hu.json b/homeassistant/components/iaqualink/.translations/hu.json
new file mode 100644
index 00000000000..b0b9393acde
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/hu.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v / e-mail c\u00edm"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/pl.json b/homeassistant/components/iaqualink/.translations/pl.json
index 211a65f5ccb..d14a2775c15 100644
--- a/homeassistant/components/iaqualink/.translations/pl.json
+++ b/homeassistant/components/iaqualink/.translations/pl.json
@@ -10,7 +10,7 @@
"user": {
"data": {
"password": "Has\u0142o",
- "username": "Nazwa u\u017cytkownika / adres e-mail"
+ "username": "Nazwa u\u017cytkownika/adres e-mail"
},
"description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o do konta iAqualink.",
"title": "Po\u0142\u0105cz z iAqualink"
diff --git a/homeassistant/components/iaqualink/.translations/sv.json b/homeassistant/components/iaqualink/.translations/sv.json
new file mode 100644
index 00000000000..aa2b4142616
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/sv.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Du kan bara konfigurera en enda iAqualink-anslutning."
+ },
+ "error": {
+ "connection_failure": "Det g\u00e5r inte att ansluta till iAqualink. Kontrollera ditt anv\u00e4ndarnamn och l\u00f6senord."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "L\u00f6senord",
+ "username": "Anv\u00e4ndarnamn / E-postadress"
+ },
+ "description": "V\u00e4nligen ange anv\u00e4ndarnamn och l\u00f6senord f\u00f6r ditt iAqualink-konto.",
+ "title": "Anslut till iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py
index d577fe448aa..d64ec711198 100644
--- a/homeassistant/components/iaqualink/config_flow.py
+++ b/homeassistant/components/iaqualink/config_flow.py
@@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from homeassistant.helpers import ConfigType
+from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
diff --git a/homeassistant/components/icloud/.translations/hu.json b/homeassistant/components/icloud/.translations/hu.json
new file mode 100644
index 00000000000..14c8c8e4e2f
--- /dev/null
+++ b/homeassistant/components/icloud/.translations/hu.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "error": {
+ "login": "Bejelentkez\u00e9si hiba: k\u00e9rj\u00fck, ellen\u0151rizze e-mail c\u00edm\u00e9t \u00e9s jelszav\u00e1t",
+ "send_verification_code": "Nem siker\u00fclt elk\u00fcldeni az ellen\u0151rz\u0151 k\u00f3dot",
+ "validate_verification_code": "Nem siker\u00fclt ellen\u0151rizni az ellen\u0151rz\u0151 k\u00f3dot, ki kell v\u00e1lasztania egy megb\u00edzhat\u00f3s\u00e1gi eszk\u00f6zt, \u00e9s \u00fajra kell ind\u00edtania az ellen\u0151rz\u00e9st"
+ },
+ "step": {
+ "trusted_device": {
+ "data": {
+ "trusted_device": "Megb\u00edzhat\u00f3 eszk\u00f6z"
+ },
+ "description": "V\u00e1lassza ki a megb\u00edzhat\u00f3 eszk\u00f6zt",
+ "title": "iCloud megb\u00edzhat\u00f3 eszk\u00f6z"
+ },
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "E-mail"
+ },
+ "description": "Adja meg hiteles\u00edt\u0151 adatait",
+ "title": "iCloud hiteles\u00edt\u0151 adatok"
+ },
+ "verification_code": {
+ "data": {
+ "verification_code": "Ellen\u0151rz\u0151 k\u00f3d"
+ },
+ "description": "K\u00e9rj\u00fck, \u00edrja be az iCloud-t\u00f3l \u00e9ppen kapott ellen\u0151rz\u0151 k\u00f3dot",
+ "title": "iCloud ellen\u0151rz\u0151 k\u00f3d"
+ }
+ },
+ "title": "Apple iCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/icloud/.translations/pl.json b/homeassistant/components/icloud/.translations/pl.json
index 169fe2eac2d..41e182eceee 100644
--- a/homeassistant/components/icloud/.translations/pl.json
+++ b/homeassistant/components/icloud/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane"
+ "already_configured": "Konto jest ju\u017c skonfigurowane."
},
"error": {
"login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o",
diff --git a/homeassistant/components/icloud/.translations/sv.json b/homeassistant/components/icloud/.translations/sv.json
index 8c4c45f9c89..fc5b81b6591 100644
--- a/homeassistant/components/icloud/.translations/sv.json
+++ b/homeassistant/components/icloud/.translations/sv.json
@@ -1,5 +1,37 @@
{
"config": {
- "title": ""
+ "abort": {
+ "already_configured": "Kontot har redan konfigurerats"
+ },
+ "error": {
+ "login": "Inloggningsfel: var god att kontrollera din e-postadress och l\u00f6senord",
+ "send_verification_code": "Det gick inte att skicka verifieringskod",
+ "validate_verification_code": "Det gick inte att verifiera verifieringskoden, v\u00e4lj en betrodd enhet och starta verifieringen igen"
+ },
+ "step": {
+ "trusted_device": {
+ "data": {
+ "trusted_device": "Betrodd enhet"
+ },
+ "description": "V\u00e4lj din betrodda enhet",
+ "title": "Betrodd iCloud-enhet"
+ },
+ "user": {
+ "data": {
+ "password": "L\u00f6senord",
+ "username": "E-post"
+ },
+ "description": "Ange dina autentiseringsuppgifter",
+ "title": "iCloud-autentiseringsuppgifter"
+ },
+ "verification_code": {
+ "data": {
+ "verification_code": "Verifieringskod"
+ },
+ "description": "V\u00e4nligen ange verifieringskoden som du just f\u00e5tt fr\u00e5n iCloud",
+ "title": "iCloud-verifieringskod"
+ }
+ },
+ "title": "Apple iCloud"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py
index 62eb2fb91ac..687c6bf93de 100644
--- a/homeassistant/components/icloud/__init__.py
+++ b/homeassistant/components/icloud/__init__.py
@@ -1,4 +1,5 @@
"""The iCloud component."""
+import asyncio
import logging
import voluptuous as vol
@@ -16,7 +17,7 @@ from .const import (
DEFAULT_GPS_ACCURACY_THRESHOLD,
DEFAULT_MAX_INTERVAL,
DOMAIN,
- ICLOUD_COMPONENTS,
+ PLATFORMS,
STORAGE_KEY,
STORAGE_VERSION,
)
@@ -127,9 +128,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
hass.data[DOMAIN][username] = account
- for component in ICLOUD_COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
def play_sound(service: ServiceDataType) -> None:
@@ -212,3 +213,19 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
)
return True
+
+
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.data[CONF_USERNAME])
+
+ return unload_ok
diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py
index 57a3f48936c..3349615ed57 100644
--- a/homeassistant/components/icloud/const.py
+++ b/homeassistant/components/icloud/const.py
@@ -13,7 +13,7 @@ DEFAULT_GPS_ACCURACY_THRESHOLD = 500 # meters
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
-ICLOUD_COMPONENTS = ["device_tracker", "sensor"]
+PLATFORMS = ["device_tracker", "sensor"]
# pyicloud.AppleDevice status
DEVICE_BATTERY_LEVEL = "batteryLevel"
diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json
index ac9f2f60218..559ed7c9060 100644
--- a/homeassistant/components/ihc/manifest.json
+++ b/homeassistant/components/ihc/manifest.json
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/ihc",
"requirements": [
"defusedxml==0.6.0",
- "ihcsdk==2.5.0"
+ "ihcsdk==2.6.0"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py
index df6fa626a4f..ce17cc6c77d 100644
--- a/homeassistant/components/insteon/__init__.py
+++ b/homeassistant/components/insteon/__init__.py
@@ -1,278 +1,43 @@
"""Support for INSTEON Modems (PLM and Hub)."""
-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 (
- CONF_ADDRESS,
- CONF_ENTITY_ID,
CONF_HOST,
CONF_PLATFORM,
CONF_PORT,
- ENTITY_MATCH_ALL,
EVENT_HOMEASSISTANT_STOP,
)
-from homeassistant.core import callback
-from homeassistant.helpers import discovery
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
-from homeassistant.helpers.entity import Entity
+
+from .const import (
+ CONF_CAT,
+ CONF_DIM_STEPS,
+ CONF_FIRMWARE,
+ CONF_HOUSECODE,
+ CONF_HUB_PASSWORD,
+ CONF_HUB_USERNAME,
+ CONF_HUB_VERSION,
+ CONF_IP_PORT,
+ CONF_OVERRIDE,
+ CONF_PRODUCT_KEY,
+ CONF_SUBCAT,
+ CONF_UNITCODE,
+ CONF_X10,
+ CONF_X10_ALL_LIGHTS_OFF,
+ CONF_X10_ALL_LIGHTS_ON,
+ CONF_X10_ALL_UNITS_OFF,
+ DOMAIN,
+ INSTEON_ENTITIES,
+)
+from .schemas import CONFIG_SCHEMA # noqa F440
+from .utils import async_register_services, register_new_device_callback
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "insteon"
-INSTEON_ENTITIES = "entities"
-
-CONF_IP_PORT = "ip_port"
-CONF_HUB_USERNAME = "username"
-CONF_HUB_PASSWORD = "password"
-CONF_HUB_VERSION = "hub_version"
-CONF_OVERRIDE = "device_override"
-CONF_PLM_HUB_MSG = "Must configure either a PLM port or a Hub host"
-CONF_CAT = "cat"
-CONF_SUBCAT = "subcat"
-CONF_FIRMWARE = "firmware"
-CONF_PRODUCT_KEY = "product_key"
-CONF_X10 = "x10_devices"
-CONF_HOUSECODE = "housecode"
-CONF_UNITCODE = "unitcode"
-CONF_DIM_STEPS = "dim_steps"
-CONF_X10_ALL_UNITS_OFF = "x10_all_units_off"
-CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on"
-CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off"
-
-SRV_ADD_ALL_LINK = "add_all_link"
-SRV_DEL_ALL_LINK = "delete_all_link"
-SRV_LOAD_ALDB = "load_all_link_database"
-SRV_PRINT_ALDB = "print_all_link_database"
-SRV_PRINT_IM_ALDB = "print_im_all_link_database"
-SRV_X10_ALL_UNITS_OFF = "x10_all_units_off"
-SRV_X10_ALL_LIGHTS_OFF = "x10_all_lights_off"
-SRV_X10_ALL_LIGHTS_ON = "x10_all_lights_on"
-SRV_ALL_LINK_GROUP = "group"
-SRV_ALL_LINK_MODE = "mode"
-SRV_LOAD_DB_RELOAD = "reload"
-SRV_CONTROLLER = "controller"
-SRV_RESPONDER = "responder"
-SRV_HOUSECODE = "housecode"
-SRV_SCENE_ON = "scene_on"
-SRV_SCENE_OFF = "scene_off"
-
-SIGNAL_LOAD_ALDB = "load_aldb"
-SIGNAL_PRINT_ALDB = "print_aldb"
-
-HOUSECODES = [
- "a",
- "b",
- "c",
- "d",
- "e",
- "f",
- "g",
- "h",
- "i",
- "j",
- "k",
- "l",
- "m",
- "n",
- "o",
- "p",
-]
-
-BUTTON_PRESSED_STATE_NAME = "onLevelButton"
-EVENT_BUTTON_ON = "insteon.button_on"
-EVENT_BUTTON_OFF = "insteon.button_off"
-EVENT_CONF_BUTTON = "button"
-
-
-def set_default_port(schema: Dict) -> Dict:
- """Set the default port based on the Hub version."""
- # If the ip_port is found do nothing
- # If it is not found the set the default
- ip_port = schema.get(CONF_IP_PORT)
- if not ip_port:
- hub_version = schema.get(CONF_HUB_VERSION)
- # Found hub_version but not ip_port
- if hub_version == 1:
- schema[CONF_IP_PORT] = 9761
- else:
- schema[CONF_IP_PORT] = 25105
- return schema
-
-
-CONF_DEVICE_OVERRIDE_SCHEMA = vol.All(
- cv.deprecated(CONF_PLATFORM),
- vol.Schema(
- {
- vol.Required(CONF_ADDRESS): cv.string,
- vol.Optional(CONF_CAT): cv.byte,
- vol.Optional(CONF_SUBCAT): cv.byte,
- vol.Optional(CONF_FIRMWARE): cv.byte,
- vol.Optional(CONF_PRODUCT_KEY): cv.byte,
- vol.Optional(CONF_PLATFORM): cv.string,
- }
- ),
-)
-
-
-CONF_X10_SCHEMA = vol.All(
- vol.Schema(
- {
- vol.Required(CONF_HOUSECODE): cv.string,
- vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16),
- vol.Required(CONF_PLATFORM): cv.string,
- vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255),
- }
- )
-)
-
-
-CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.All(
- vol.Schema(
- {
- vol.Exclusive(
- CONF_PORT, "plm_or_hub", msg=CONF_PLM_HUB_MSG
- ): cv.string,
- vol.Exclusive(
- CONF_HOST, "plm_or_hub", msg=CONF_PLM_HUB_MSG
- ): cv.string,
- vol.Optional(CONF_IP_PORT): cv.port,
- vol.Optional(CONF_HUB_USERNAME): cv.string,
- vol.Optional(CONF_HUB_PASSWORD): cv.string,
- vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]),
- vol.Optional(CONF_OVERRIDE): vol.All(
- cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]
- ),
- vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES),
- vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES),
- vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES),
- vol.Optional(CONF_X10): vol.All(
- cv.ensure_list_csv, [CONF_X10_SCHEMA]
- ),
- },
- extra=vol.ALLOW_EXTRA,
- required=True,
- ),
- cv.has_at_least_one_key(CONF_PORT, CONF_HOST),
- set_default_port,
- )
- },
- extra=vol.ALLOW_EXTRA,
-)
-
-
-ADD_ALL_LINK_SCHEMA = vol.Schema(
- {
- vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255),
- vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]),
- }
-)
-
-
-DEL_ALL_LINK_SCHEMA = vol.Schema(
- {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)}
-)
-
-
-LOAD_ALDB_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL),
- vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean,
- }
-)
-
-
-PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id})
-
-
-X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)})
-
-
-TRIGGER_SCENE_SCHEMA = vol.Schema(
- {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)}
-)
-
-
-STATE_NAME_LABEL_MAP = {
- "keypadButtonA": "Button A",
- "keypadButtonB": "Button B",
- "keypadButtonC": "Button C",
- "keypadButtonD": "Button D",
- "keypadButtonE": "Button E",
- "keypadButtonF": "Button F",
- "keypadButtonG": "Button G",
- "keypadButtonH": "Button H",
- "keypadButtonMain": "Main",
- "onOffButtonA": "Button A",
- "onOffButtonB": "Button B",
- "onOffButtonC": "Button C",
- "onOffButtonD": "Button D",
- "onOffButtonE": "Button E",
- "onOffButtonF": "Button F",
- "onOffButtonG": "Button G",
- "onOffButtonH": "Button H",
- "onOffButtonMain": "Main",
- "fanOnLevel": "Fan",
- "lightOnLevel": "Light",
- "coolSetPoint": "Cool Set",
- "heatSetPoint": "HeatSet",
- "statusReport": "Status",
- "generalSensor": "Sensor",
- "motionSensor": "Motion",
- "lightSensor": "Light",
- "batterySensor": "Battery",
- "dryLeakSensor": "Dry",
- "wetLeakSensor": "Wet",
- "heartbeatLeakSensor": "Heartbeat",
- "openClosedRelay": "Relay",
- "openClosedSensor": "Sensor",
- "lightOnOff": "Light",
- "outletTopOnOff": "Top",
- "outletBottomOnOff": "Bottom",
- "coverOpenLevel": "Cover",
-}
-
async def async_setup(hass, config):
"""Set up the connection to the modem."""
- ipdb = IPDB()
insteon_modem = None
conf = config[DOMAIN]
@@ -288,163 +53,6 @@ async def async_setup(hass, config):
x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON)
x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF)
- @callback
- def async_new_insteon_device(device):
- """Detect device from transport to be delegated to platform."""
- for state_key in device.states:
- platform_info = ipdb[device.states[state_key]]
- if platform_info and platform_info.platform:
- platform = platform_info.platform
-
- if platform == "on_off_events":
- device.states[state_key].register_updates(_fire_button_on_off_event)
-
- else:
- _LOGGER.info(
- "New INSTEON device: %s (%s) %s",
- device.address,
- device.states[state_key].name,
- platform,
- )
-
- hass.async_create_task(
- discovery.async_load_platform(
- hass,
- platform,
- DOMAIN,
- discovered={
- "address": device.address.id,
- "state_key": state_key,
- },
- hass_config=config,
- )
- )
-
- def add_all_link(service):
- """Add an INSTEON All-Link between two devices."""
- group = service.data.get(SRV_ALL_LINK_GROUP)
- mode = service.data.get(SRV_ALL_LINK_MODE)
- link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0
- insteon_modem.start_all_linking(link_mode, group)
-
- def del_all_link(service):
- """Delete an INSTEON All-Link between two devices."""
- group = service.data.get(SRV_ALL_LINK_GROUP)
- insteon_modem.start_all_linking(255, group)
-
- def load_aldb(service):
- """Load the device All-Link database."""
- entity_id = service.data[CONF_ENTITY_ID]
- reload = service.data[SRV_LOAD_DB_RELOAD]
- if entity_id.lower() == ENTITY_MATCH_ALL:
- for entity_id in hass.data[DOMAIN].get(INSTEON_ENTITIES):
- _send_load_aldb_signal(entity_id, reload)
- else:
- _send_load_aldb_signal(entity_id, reload)
-
- def _send_load_aldb_signal(entity_id, reload):
- """Send the load All-Link database signal to INSTEON entity."""
- signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
- dispatcher_send(hass, signal, reload)
-
- def print_aldb(service):
- """Print the All-Link Database for a device."""
- # For now this sends logs to the log file.
- # Furture direction is to create an INSTEON control panel.
- entity_id = service.data[CONF_ENTITY_ID]
- signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
- dispatcher_send(hass, signal)
-
- def print_im_aldb(service):
- """Print the All-Link Database for a device."""
- # For now this sends logs to the log file.
- # Furture direction is to create an INSTEON control panel.
- print_aldb_to_log(insteon_modem.aldb)
-
- def x10_all_units_off(service):
- """Send the X10 All Units Off command."""
- housecode = service.data.get(SRV_HOUSECODE)
- insteon_modem.x10_all_units_off(housecode)
-
- def x10_all_lights_off(service):
- """Send the X10 All Lights Off command."""
- housecode = service.data.get(SRV_HOUSECODE)
- insteon_modem.x10_all_lights_off(housecode)
-
- def x10_all_lights_on(service):
- """Send the X10 All Lights On command."""
- housecode = service.data.get(SRV_HOUSECODE)
- insteon_modem.x10_all_lights_on(housecode)
-
- def scene_on(service):
- """Trigger an INSTEON scene ON."""
- group = service.data.get(SRV_ALL_LINK_GROUP)
- insteon_modem.trigger_group_on(group)
-
- def scene_off(service):
- """Trigger an INSTEON scene ON."""
- group = service.data.get(SRV_ALL_LINK_GROUP)
- insteon_modem.trigger_group_off(group)
-
- def _register_services():
- hass.services.register(
- DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA
- )
- hass.services.register(
- DOMAIN, SRV_DEL_ALL_LINK, del_all_link, schema=DEL_ALL_LINK_SCHEMA
- )
- hass.services.register(
- DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA
- )
- hass.services.register(
- DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
- )
- hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
- hass.services.register(
- DOMAIN,
- SRV_X10_ALL_UNITS_OFF,
- x10_all_units_off,
- schema=X10_HOUSECODE_SCHEMA,
- )
- hass.services.register(
- DOMAIN,
- SRV_X10_ALL_LIGHTS_OFF,
- x10_all_lights_off,
- schema=X10_HOUSECODE_SCHEMA,
- )
- hass.services.register(
- DOMAIN,
- SRV_X10_ALL_LIGHTS_ON,
- x10_all_lights_on,
- schema=X10_HOUSECODE_SCHEMA,
- )
- hass.services.register(
- DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA
- )
- hass.services.register(
- DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA
- )
- _LOGGER.debug("Insteon Services registered")
-
- def _fire_button_on_off_event(address, group, val):
- # Firing an event when a button is pressed.
- device = insteon_modem.devices[address.hex]
- state_name = device.states[group].name
- button = (
- "" if state_name == BUTTON_PRESSED_STATE_NAME else state_name[-1].lower()
- )
- schema = {CONF_ADDRESS: address.hex}
- if button != "":
- schema[EVENT_CONF_BUTTON] = button
- if val:
- event = EVENT_BUTTON_ON
- else:
- event = EVENT_BUTTON_OFF
- _LOGGER.debug(
- "Firing event %s with address %s and button %s", event, address.hex, button
- )
- hass.bus.fire(event, schema)
-
if host:
_LOGGER.info("Connecting to Insteon Hub on %s", host)
conn = await insteonplm.Connection.create(
@@ -464,6 +72,14 @@ async def async_setup(hass, config):
insteon_modem = conn.protocol
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN]["modem"] = insteon_modem
+ hass.data[DOMAIN][INSTEON_ENTITIES] = set()
+
+ register_new_device_callback(hass, config, insteon_modem)
+ async_register_services(hass, config, insteon_modem)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close)
+
for device_override in overrides:
#
# Override the device default capabilities for a specific address
@@ -477,14 +93,6 @@ async def async_setup(hass, config):
address, CONF_PRODUCT_KEY, device_override[prop]
)
- hass.data[DOMAIN] = {}
- hass.data[DOMAIN]["modem"] = insteon_modem
- hass.data[DOMAIN][INSTEON_ENTITIES] = {}
-
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close)
-
- insteon_modem.devices.add_device_callback(async_new_insteon_device)
-
if x10_all_units_off_housecode:
device = insteon_modem.add_x10_device(
x10_all_units_off_housecode, 20, "allunitsoff"
@@ -513,199 +121,4 @@ async def async_setup(hass, config):
if device and hasattr(device.states[0x01], "steps"):
device.states[0x01].steps = steps
- hass.async_add_job(_register_services)
-
return True
-
-
-State = collections.namedtuple("Product", "stateType platform")
-
-
-class IPDB:
- """Embodies the INSTEON Product Database static data and access methods."""
-
- def __init__(self):
- """Create the INSTEON Product Database (IPDB)."""
- self.states = [
- State(Cover, "cover"),
- State(OnOffSwitch_OutletTop, "switch"),
- State(OnOffSwitch_OutletBottom, "switch"),
- State(OpenClosedRelay, "switch"),
- State(OnOffSwitch, "switch"),
- State(OnOffKeypadA, "switch"),
- State(OnOffKeypad, "switch"),
- State(LeakSensorDryWet, "binary_sensor"),
- State(IoLincSensor, "binary_sensor"),
- State(SmokeCO2Sensor, "sensor"),
- State(OnOffSensor, "binary_sensor"),
- State(VariableSensor, "sensor"),
- State(DimmableSwitch_Fan, "fan"),
- State(DimmableSwitch, "light"),
- State(DimmableRemote, "on_off_events"),
- State(DimmableKeypadA, "light"),
- State(X10DimmableSwitch, "light"),
- State(X10OnOffSwitch, "switch"),
- State(X10OnOffSensor, "binary_sensor"),
- State(X10AllUnitsOffSensor, "binary_sensor"),
- State(X10AllLightsOnSensor, "binary_sensor"),
- State(X10AllLightsOffSensor, "binary_sensor"),
- ]
-
- def __len__(self):
- """Return the number of INSTEON state types mapped to HA platforms."""
- return len(self.states)
-
- def __iter__(self):
- """Itterate through the INSTEON state types to HA platforms."""
- for product in self.states:
- yield product
-
- def __getitem__(self, key):
- """Return a Home Assistant platform from an INSTEON state type."""
- for state in self.states:
- if isinstance(key, state.stateType):
- return state
- return None
-
-
-class InsteonEntity(Entity):
- """INSTEON abstract base entity."""
-
- def __init__(self, device, state_key):
- """Initialize the INSTEON binary sensor."""
- self._insteon_device_state = device.states[state_key]
- self._insteon_device = device
- self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded)
-
- @property
- def should_poll(self):
- """No polling needed."""
- return False
-
- @property
- def address(self):
- """Return the address of the node."""
- return self._insteon_device.address.human
-
- @property
- def group(self):
- """Return the INSTEON group that the entity responds to."""
- return self._insteon_device_state.group
-
- @property
- def unique_id(self) -> str:
- """Return a unique ID."""
- if self._insteon_device_state.group == 0x01:
- uid = self._insteon_device.id
- else:
- uid = "{:s}_{:d}".format(
- self._insteon_device.id, self._insteon_device_state.group
- )
- return uid
-
- @property
- def name(self):
- """Return the name of the node (used for Entity_ID)."""
- # Set a base description
- description = self._insteon_device.description
- if self._insteon_device.description is None:
- description = "Unknown Device"
-
- # Get an extension label if there is one
- extension = self._get_label()
- if extension:
- extension = f" {extension}"
- name = "{:s} {:s}{:s}".format(
- description, self._insteon_device.address.human, extension
- )
- return name
-
- @property
- def device_state_attributes(self):
- """Provide attributes for display on device card."""
- attributes = {"INSTEON Address": self.address, "INSTEON Group": self.group}
- return attributes
-
- @callback
- def async_entity_update(self, deviceid, group, val):
- """Receive notification from transport that new data exists."""
- _LOGGER.debug(
- "Received update for device %s group %d value %s",
- deviceid.human,
- group,
- val,
- )
- self.async_schedule_update_ha_state()
-
- async def async_added_to_hass(self):
- """Register INSTEON update events."""
- _LOGGER.debug(
- "Tracking updates for device %s group %d statename %s",
- self.address,
- self.group,
- self._insteon_device_state.name,
- )
- self._insteon_device_state.register_updates(self.async_entity_update)
- self.hass.data[DOMAIN][INSTEON_ENTITIES][self.entity_id] = self
- load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}"
- async_dispatcher_connect(self.hass, load_signal, self._load_aldb)
- print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}"
- async_dispatcher_connect(self.hass, print_signal, self._print_aldb)
-
- def _load_aldb(self, reload=False):
- """Load the device All-Link Database."""
- if reload:
- self._insteon_device.aldb.clear()
- self._insteon_device.read_aldb()
-
- def _print_aldb(self):
- """Print the device ALDB to the log file."""
- print_aldb_to_log(self._insteon_device.aldb)
-
- @callback
- def _aldb_loaded(self):
- """All-Link Database loaded for the device."""
- self._print_aldb()
-
- def _get_label(self):
- """Get the device label for grouped devices."""
- label = ""
- if len(self._insteon_device.states) > 1:
- if self._insteon_device_state.name in STATE_NAME_LABEL_MAP:
- label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name]
- else:
- label = f"Group {self.group:d}"
- return label
-
-
-def print_aldb_to_log(aldb):
- """Print the All-Link Database to the log file."""
- _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")
- _LOGGER.warning("Use service insteon.load_aldb first")
- return
-
- _LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3")
- _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------")
- for mem_addr in aldb:
- rec = aldb[mem_addr]
- # For now we write this to the log
- # Roadmap is to create a configuration panel
- in_use = "Y" if rec.control_flags.is_in_use else "N"
- mode = "C" if rec.control_flags.is_controller else "R"
- hwm = "Y" if rec.control_flags.is_high_water_mark else "N"
- _LOGGER.info(
- " {:04x} {:s} {:s} {:s} {:3d} {:s}"
- " {:3d} {:3d} {:3d}".format(
- rec.mem_addr,
- in_use,
- mode,
- hwm,
- rec.group,
- rec.address.human,
- rec.data1,
- rec.data2,
- rec.data3,
- )
- )
diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py
index 68ea07cdb49..395c0a3ac20 100644
--- a/homeassistant/components/insteon/binary_sensor.py
+++ b/homeassistant/components/insteon/binary_sensor.py
@@ -3,7 +3,7 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
-from . import InsteonEntity
+from .insteon_entity import InsteonEntity
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py
new file mode 100644
index 00000000000..b01409f49ff
--- /dev/null
+++ b/homeassistant/components/insteon/const.py
@@ -0,0 +1,106 @@
+"""Constants used by insteon component."""
+
+DOMAIN = "insteon"
+INSTEON_ENTITIES = "entities"
+
+CONF_IP_PORT = "ip_port"
+CONF_HUB_USERNAME = "username"
+CONF_HUB_PASSWORD = "password"
+CONF_HUB_VERSION = "hub_version"
+CONF_OVERRIDE = "device_override"
+CONF_PLM_HUB_MSG = "Must configure either a PLM port or a Hub host"
+CONF_CAT = "cat"
+CONF_SUBCAT = "subcat"
+CONF_FIRMWARE = "firmware"
+CONF_PRODUCT_KEY = "product_key"
+CONF_X10 = "x10_devices"
+CONF_HOUSECODE = "housecode"
+CONF_UNITCODE = "unitcode"
+CONF_DIM_STEPS = "dim_steps"
+CONF_X10_ALL_UNITS_OFF = "x10_all_units_off"
+CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on"
+CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off"
+
+SRV_ADD_ALL_LINK = "add_all_link"
+SRV_DEL_ALL_LINK = "delete_all_link"
+SRV_LOAD_ALDB = "load_all_link_database"
+SRV_PRINT_ALDB = "print_all_link_database"
+SRV_PRINT_IM_ALDB = "print_im_all_link_database"
+SRV_X10_ALL_UNITS_OFF = "x10_all_units_off"
+SRV_X10_ALL_LIGHTS_OFF = "x10_all_lights_off"
+SRV_X10_ALL_LIGHTS_ON = "x10_all_lights_on"
+SRV_ALL_LINK_GROUP = "group"
+SRV_ALL_LINK_MODE = "mode"
+SRV_LOAD_DB_RELOAD = "reload"
+SRV_CONTROLLER = "controller"
+SRV_RESPONDER = "responder"
+SRV_HOUSECODE = "housecode"
+SRV_SCENE_ON = "scene_on"
+SRV_SCENE_OFF = "scene_off"
+
+SIGNAL_LOAD_ALDB = "load_aldb"
+SIGNAL_PRINT_ALDB = "print_aldb"
+
+HOUSECODES = [
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "f",
+ "g",
+ "h",
+ "i",
+ "j",
+ "k",
+ "l",
+ "m",
+ "n",
+ "o",
+ "p",
+]
+
+BUTTON_PRESSED_STATE_NAME = "onLevelButton"
+EVENT_BUTTON_ON = "insteon.button_on"
+EVENT_BUTTON_OFF = "insteon.button_off"
+EVENT_CONF_BUTTON = "button"
+
+
+STATE_NAME_LABEL_MAP = {
+ "keypadButtonA": "Button A",
+ "keypadButtonB": "Button B",
+ "keypadButtonC": "Button C",
+ "keypadButtonD": "Button D",
+ "keypadButtonE": "Button E",
+ "keypadButtonF": "Button F",
+ "keypadButtonG": "Button G",
+ "keypadButtonH": "Button H",
+ "keypadButtonMain": "Main",
+ "onOffButtonA": "Button A",
+ "onOffButtonB": "Button B",
+ "onOffButtonC": "Button C",
+ "onOffButtonD": "Button D",
+ "onOffButtonE": "Button E",
+ "onOffButtonF": "Button F",
+ "onOffButtonG": "Button G",
+ "onOffButtonH": "Button H",
+ "onOffButtonMain": "Main",
+ "fanOnLevel": "Fan",
+ "lightOnLevel": "Light",
+ "coolSetPoint": "Cool Set",
+ "heatSetPoint": "HeatSet",
+ "statusReport": "Status",
+ "generalSensor": "Sensor",
+ "motionSensor": "Motion",
+ "lightSensor": "Light",
+ "batterySensor": "Battery",
+ "dryLeakSensor": "Dry",
+ "wetLeakSensor": "Wet",
+ "heartbeatLeakSensor": "Heartbeat",
+ "openClosedRelay": "Relay",
+ "openClosedSensor": "Sensor",
+ "lightOnOff": "Light",
+ "outletTopOnOff": "Top",
+ "outletBottomOnOff": "Bottom",
+ "coverOpenLevel": "Cover",
+}
diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py
index f9399d7b13f..575799cbf67 100644
--- a/homeassistant/components/insteon/cover.py
+++ b/homeassistant/components/insteon/cover.py
@@ -10,7 +10,7 @@ from homeassistant.components.cover import (
CoverDevice,
)
-from . import InsteonEntity
+from .insteon_entity import InsteonEntity
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py
index d88348b1a5d..6ad7436faf5 100644
--- a/homeassistant/components/insteon/fan.py
+++ b/homeassistant/components/insteon/fan.py
@@ -11,7 +11,7 @@ from homeassistant.components.fan import (
)
from homeassistant.const import STATE_OFF
-from . import InsteonEntity
+from .insteon_entity import InsteonEntity
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py
new file mode 100644
index 00000000000..c489dd8e382
--- /dev/null
+++ b/homeassistant/components/insteon/insteon_entity.py
@@ -0,0 +1,123 @@
+"""Insteon base entity."""
+import logging
+
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity
+
+from .const import (
+ DOMAIN,
+ INSTEON_ENTITIES,
+ SIGNAL_LOAD_ALDB,
+ SIGNAL_PRINT_ALDB,
+ STATE_NAME_LABEL_MAP,
+)
+from .utils import print_aldb_to_log
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class InsteonEntity(Entity):
+ """INSTEON abstract base entity."""
+
+ def __init__(self, device, state_key):
+ """Initialize the INSTEON binary sensor."""
+ self._insteon_device_state = device.states[state_key]
+ self._insteon_device = device
+ self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded)
+
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
+
+ @property
+ def address(self):
+ """Return the address of the node."""
+ return self._insteon_device.address.human
+
+ @property
+ def group(self):
+ """Return the INSTEON group that the entity responds to."""
+ return self._insteon_device_state.group
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ if self._insteon_device_state.group == 0x01:
+ uid = self._insteon_device.id
+ else:
+ uid = f"{self._insteon_device.id}_{self._insteon_device_state.group}"
+ return uid
+
+ @property
+ def name(self):
+ """Return the name of the node (used for Entity_ID)."""
+ # Set a base description
+ description = self._insteon_device.description
+ if self._insteon_device.description is None:
+ description = "Unknown Device"
+
+ # Get an extension label if there is one
+ extension = self._get_label()
+ if extension:
+ extension = f" {extension}"
+ name = f"{description} {self._insteon_device.address.human}{extension}"
+ return name
+
+ @property
+ def device_state_attributes(self):
+ """Provide attributes for display on device card."""
+ attributes = {"insteon_address": self.address, "insteon_group": self.group}
+ return attributes
+
+ @callback
+ def async_entity_update(self, deviceid, group, val):
+ """Receive notification from transport that new data exists."""
+ _LOGGER.debug(
+ "Received update for device %s group %d value %s",
+ deviceid.human,
+ group,
+ val,
+ )
+ self.async_schedule_update_ha_state()
+
+ async def async_added_to_hass(self):
+ """Register INSTEON update events."""
+ _LOGGER.debug(
+ "Tracking updates for device %s group %d statename %s",
+ self.address,
+ self.group,
+ self._insteon_device_state.name,
+ )
+ self._insteon_device_state.register_updates(self.async_entity_update)
+ self.hass.data[DOMAIN][INSTEON_ENTITIES].add(self.entity_id)
+ load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}"
+ async_dispatcher_connect(self.hass, load_signal, self._load_aldb)
+ print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}"
+ async_dispatcher_connect(self.hass, print_signal, self._print_aldb)
+
+ def _load_aldb(self, reload=False):
+ """Load the device All-Link Database."""
+ if reload:
+ self._insteon_device.aldb.clear()
+ self._insteon_device.read_aldb()
+
+ def _print_aldb(self):
+ """Print the device ALDB to the log file."""
+ print_aldb_to_log(self._insteon_device.aldb)
+
+ @callback
+ def _aldb_loaded(self):
+ """All-Link Database loaded for the device."""
+ self._print_aldb()
+
+ def _get_label(self):
+ """Get the device label for grouped devices."""
+ label = ""
+ if len(self._insteon_device.states) > 1:
+ if self._insteon_device_state.name in STATE_NAME_LABEL_MAP:
+ label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name]
+ else:
+ label = f"Group {self.group:d}"
+ return label
diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py
new file mode 100644
index 00000000000..1618518a0eb
--- /dev/null
+++ b/homeassistant/components/insteon/ipdb.py
@@ -0,0 +1,82 @@
+"""Insteon product database."""
+import collections
+
+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,
+)
+
+State = collections.namedtuple("Product", "stateType platform")
+
+
+class IPDB:
+ """Embodies the INSTEON Product Database static data and access methods."""
+
+ def __init__(self):
+ """Create the INSTEON Product Database (IPDB)."""
+ self.states = [
+ State(Cover, "cover"),
+ State(OnOffSwitch_OutletTop, "switch"),
+ State(OnOffSwitch_OutletBottom, "switch"),
+ State(OpenClosedRelay, "switch"),
+ State(OnOffSwitch, "switch"),
+ State(OnOffKeypadA, "switch"),
+ State(OnOffKeypad, "switch"),
+ State(LeakSensorDryWet, "binary_sensor"),
+ State(IoLincSensor, "binary_sensor"),
+ State(SmokeCO2Sensor, "sensor"),
+ State(OnOffSensor, "binary_sensor"),
+ State(VariableSensor, "sensor"),
+ State(DimmableSwitch_Fan, "fan"),
+ State(DimmableSwitch, "light"),
+ State(DimmableRemote, "on_off_events"),
+ State(DimmableKeypadA, "light"),
+ State(X10DimmableSwitch, "light"),
+ State(X10OnOffSwitch, "switch"),
+ State(X10OnOffSensor, "binary_sensor"),
+ State(X10AllUnitsOffSensor, "binary_sensor"),
+ State(X10AllLightsOnSensor, "binary_sensor"),
+ State(X10AllLightsOffSensor, "binary_sensor"),
+ ]
+
+ def __len__(self):
+ """Return the number of INSTEON state types mapped to HA platforms."""
+ return len(self.states)
+
+ def __iter__(self):
+ """Itterate through the INSTEON state types to HA platforms."""
+ for product in self.states:
+ yield product
+
+ def __getitem__(self, key):
+ """Return a Home Assistant platform from an INSTEON state type."""
+ for state in self.states:
+ if isinstance(key, state.stateType):
+ return state
+ return None
diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py
index 3a44d89add0..60a27b3acb8 100644
--- a/homeassistant/components/insteon/light.py
+++ b/homeassistant/components/insteon/light.py
@@ -3,7 +3,7 @@ import logging
from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light
-from . import InsteonEntity
+from .insteon_entity import InsteonEntity
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json
index 74d8274796b..69c35477b8d 100644
--- a/homeassistant/components/insteon/manifest.json
+++ b/homeassistant/components/insteon/manifest.json
@@ -2,7 +2,7 @@
"domain": "insteon",
"name": "Insteon",
"documentation": "https://www.home-assistant.io/integrations/insteon",
- "requirements": ["insteonplm==0.16.6"],
+ "requirements": ["insteonplm==0.16.7"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py
new file mode 100644
index 00000000000..20399195365
--- /dev/null
+++ b/homeassistant/components/insteon/schemas.py
@@ -0,0 +1,156 @@
+"""Schemas used by insteon component."""
+
+from typing import Dict
+
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_ENTITY_ID,
+ CONF_HOST,
+ CONF_PLATFORM,
+ CONF_PORT,
+ ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
+)
+import homeassistant.helpers.config_validation as cv
+
+from .const import (
+ CONF_CAT,
+ CONF_DIM_STEPS,
+ CONF_FIRMWARE,
+ CONF_HOUSECODE,
+ CONF_HUB_PASSWORD,
+ CONF_HUB_USERNAME,
+ CONF_HUB_VERSION,
+ CONF_IP_PORT,
+ CONF_OVERRIDE,
+ CONF_PLM_HUB_MSG,
+ CONF_PRODUCT_KEY,
+ CONF_SUBCAT,
+ CONF_UNITCODE,
+ CONF_X10,
+ CONF_X10_ALL_LIGHTS_OFF,
+ CONF_X10_ALL_LIGHTS_ON,
+ CONF_X10_ALL_UNITS_OFF,
+ DOMAIN,
+ HOUSECODES,
+ SRV_ALL_LINK_GROUP,
+ SRV_ALL_LINK_MODE,
+ SRV_CONTROLLER,
+ SRV_HOUSECODE,
+ SRV_LOAD_DB_RELOAD,
+ SRV_RESPONDER,
+)
+
+
+def set_default_port(schema: Dict) -> Dict:
+ """Set the default port based on the Hub version."""
+ # If the ip_port is found do nothing
+ # If it is not found the set the default
+ ip_port = schema.get(CONF_IP_PORT)
+ if not ip_port:
+ hub_version = schema.get(CONF_HUB_VERSION)
+ # Found hub_version but not ip_port
+ if hub_version == 1:
+ schema[CONF_IP_PORT] = 9761
+ else:
+ schema[CONF_IP_PORT] = 25105
+ return schema
+
+
+CONF_DEVICE_OVERRIDE_SCHEMA = vol.All(
+ cv.deprecated(CONF_PLATFORM),
+ vol.Schema(
+ {
+ vol.Required(CONF_ADDRESS): cv.string,
+ vol.Optional(CONF_CAT): cv.byte,
+ vol.Optional(CONF_SUBCAT): cv.byte,
+ vol.Optional(CONF_FIRMWARE): cv.byte,
+ vol.Optional(CONF_PRODUCT_KEY): cv.byte,
+ vol.Optional(CONF_PLATFORM): cv.string,
+ }
+ ),
+)
+
+
+CONF_X10_SCHEMA = vol.All(
+ vol.Schema(
+ {
+ vol.Required(CONF_HOUSECODE): cv.string,
+ vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16),
+ vol.Required(CONF_PLATFORM): cv.string,
+ vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255),
+ }
+ )
+)
+
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.All(
+ vol.Schema(
+ {
+ vol.Exclusive(
+ CONF_PORT, "plm_or_hub", msg=CONF_PLM_HUB_MSG
+ ): cv.string,
+ vol.Exclusive(
+ CONF_HOST, "plm_or_hub", msg=CONF_PLM_HUB_MSG
+ ): cv.string,
+ vol.Optional(CONF_IP_PORT): cv.port,
+ vol.Optional(CONF_HUB_USERNAME): cv.string,
+ vol.Optional(CONF_HUB_PASSWORD): cv.string,
+ vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]),
+ vol.Optional(CONF_OVERRIDE): vol.All(
+ cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]
+ ),
+ vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES),
+ vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES),
+ vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES),
+ vol.Optional(CONF_X10): vol.All(
+ cv.ensure_list_csv, [CONF_X10_SCHEMA]
+ ),
+ },
+ extra=vol.ALLOW_EXTRA,
+ required=True,
+ ),
+ cv.has_at_least_one_key(CONF_PORT, CONF_HOST),
+ set_default_port,
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+ADD_ALL_LINK_SCHEMA = vol.Schema(
+ {
+ vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255),
+ vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]),
+ }
+)
+
+
+DEL_ALL_LINK_SCHEMA = vol.Schema(
+ {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)}
+)
+
+
+LOAD_ALDB_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_ENTITY_ID): vol.Any(
+ cv.entity_id, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
+ ),
+ vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean,
+ }
+)
+
+
+PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id})
+
+
+X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)})
+
+
+TRIGGER_SCENE_SCHEMA = vol.Schema(
+ {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)}
+)
diff --git a/homeassistant/components/insteon/sensor.py b/homeassistant/components/insteon/sensor.py
index 0e8a592b92d..475723b105d 100644
--- a/homeassistant/components/insteon/sensor.py
+++ b/homeassistant/components/insteon/sensor.py
@@ -3,7 +3,7 @@ import logging
from homeassistant.helpers.entity import Entity
-from . import InsteonEntity
+from .insteon_entity import InsteonEntity
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py
index c36e60c2eff..eec7874c7fb 100644
--- a/homeassistant/components/insteon/switch.py
+++ b/homeassistant/components/insteon/switch.py
@@ -3,7 +3,7 @@ import logging
from homeassistant.components.switch import SwitchDevice
-from . import InsteonEntity
+from .insteon_entity import InsteonEntity
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py
new file mode 100644
index 00000000000..f195a458477
--- /dev/null
+++ b/homeassistant/components/insteon/utils.py
@@ -0,0 +1,239 @@
+"""Utilities used by insteon component."""
+
+import logging
+
+from insteonplm.devices import ALDBStatus
+
+from homeassistant.const import CONF_ADDRESS, CONF_ENTITY_ID, ENTITY_MATCH_ALL
+from homeassistant.core import callback
+from homeassistant.helpers import discovery
+from homeassistant.helpers.dispatcher import dispatcher_send
+
+from .const import (
+ BUTTON_PRESSED_STATE_NAME,
+ DOMAIN,
+ EVENT_BUTTON_OFF,
+ EVENT_BUTTON_ON,
+ EVENT_CONF_BUTTON,
+ INSTEON_ENTITIES,
+ SIGNAL_LOAD_ALDB,
+ SIGNAL_PRINT_ALDB,
+ SRV_ADD_ALL_LINK,
+ SRV_ALL_LINK_GROUP,
+ SRV_ALL_LINK_MODE,
+ SRV_CONTROLLER,
+ SRV_DEL_ALL_LINK,
+ SRV_HOUSECODE,
+ SRV_LOAD_ALDB,
+ SRV_LOAD_DB_RELOAD,
+ SRV_PRINT_ALDB,
+ SRV_PRINT_IM_ALDB,
+ SRV_SCENE_OFF,
+ SRV_SCENE_ON,
+ SRV_X10_ALL_LIGHTS_OFF,
+ SRV_X10_ALL_LIGHTS_ON,
+ SRV_X10_ALL_UNITS_OFF,
+)
+from .ipdb import IPDB
+from .schemas import (
+ ADD_ALL_LINK_SCHEMA,
+ DEL_ALL_LINK_SCHEMA,
+ LOAD_ALDB_SCHEMA,
+ PRINT_ALDB_SCHEMA,
+ TRIGGER_SCENE_SCHEMA,
+ X10_HOUSECODE_SCHEMA,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def register_new_device_callback(hass, config, insteon_modem):
+ """Register callback for new Insteon device."""
+
+ def _fire_button_on_off_event(address, group, val):
+ # Firing an event when a button is pressed.
+ device = insteon_modem.devices[address.hex]
+ state_name = device.states[group].name
+ button = (
+ "" if state_name == BUTTON_PRESSED_STATE_NAME else state_name[-1].lower()
+ )
+ schema = {CONF_ADDRESS: address.hex}
+ if button != "":
+ schema[EVENT_CONF_BUTTON] = button
+ if val:
+ event = EVENT_BUTTON_ON
+ else:
+ event = EVENT_BUTTON_OFF
+ _LOGGER.debug(
+ "Firing event %s with address %s and button %s", event, address.hex, button
+ )
+ hass.bus.fire(event, schema)
+
+ @callback
+ def async_new_insteon_device(device):
+ """Detect device from transport to be delegated to platform."""
+ ipdb = IPDB()
+ for state_key in device.states:
+ platform_info = ipdb[device.states[state_key]]
+ if platform_info and platform_info.platform:
+ platform = platform_info.platform
+
+ if platform == "on_off_events":
+ device.states[state_key].register_updates(_fire_button_on_off_event)
+
+ else:
+ _LOGGER.info(
+ "New INSTEON device: %s (%s) %s",
+ device.address,
+ device.states[state_key].name,
+ platform,
+ )
+
+ hass.async_create_task(
+ discovery.async_load_platform(
+ hass,
+ platform,
+ DOMAIN,
+ discovered={
+ "address": device.address.id,
+ "state_key": state_key,
+ },
+ hass_config=config,
+ )
+ )
+
+ insteon_modem.devices.add_device_callback(async_new_insteon_device)
+
+
+@callback
+def async_register_services(hass, config, insteon_modem):
+ """Register services used by insteon component."""
+
+ def add_all_link(service):
+ """Add an INSTEON All-Link between two devices."""
+ group = service.data.get(SRV_ALL_LINK_GROUP)
+ mode = service.data.get(SRV_ALL_LINK_MODE)
+ link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0
+ insteon_modem.start_all_linking(link_mode, group)
+
+ def del_all_link(service):
+ """Delete an INSTEON All-Link between two devices."""
+ group = service.data.get(SRV_ALL_LINK_GROUP)
+ insteon_modem.start_all_linking(255, group)
+
+ def load_aldb(service):
+ """Load the device All-Link database."""
+ entity_id = service.data[CONF_ENTITY_ID]
+ reload = service.data[SRV_LOAD_DB_RELOAD]
+ if entity_id.lower() == ENTITY_MATCH_ALL:
+ for entity_id in hass.data[DOMAIN][INSTEON_ENTITIES]:
+ _send_load_aldb_signal(entity_id, reload)
+ else:
+ _send_load_aldb_signal(entity_id, reload)
+
+ def _send_load_aldb_signal(entity_id, reload):
+ """Send the load All-Link database signal to INSTEON entity."""
+ signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
+ dispatcher_send(hass, signal, reload)
+
+ def print_aldb(service):
+ """Print the All-Link Database for a device."""
+ # For now this sends logs to the log file.
+ # Future direction is to create an INSTEON control panel.
+ entity_id = service.data[CONF_ENTITY_ID]
+ signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
+ dispatcher_send(hass, signal)
+
+ def print_im_aldb(service):
+ """Print the All-Link Database for a device."""
+ # For now this sends logs to the log file.
+ # Future direction is to create an INSTEON control panel.
+ print_aldb_to_log(insteon_modem.aldb)
+
+ def x10_all_units_off(service):
+ """Send the X10 All Units Off command."""
+ housecode = service.data.get(SRV_HOUSECODE)
+ insteon_modem.x10_all_units_off(housecode)
+
+ def x10_all_lights_off(service):
+ """Send the X10 All Lights Off command."""
+ housecode = service.data.get(SRV_HOUSECODE)
+ insteon_modem.x10_all_lights_off(housecode)
+
+ def x10_all_lights_on(service):
+ """Send the X10 All Lights On command."""
+ housecode = service.data.get(SRV_HOUSECODE)
+ insteon_modem.x10_all_lights_on(housecode)
+
+ def scene_on(service):
+ """Trigger an INSTEON scene ON."""
+ group = service.data.get(SRV_ALL_LINK_GROUP)
+ insteon_modem.trigger_group_on(group)
+
+ def scene_off(service):
+ """Trigger an INSTEON scene ON."""
+ group = service.data.get(SRV_ALL_LINK_GROUP)
+ insteon_modem.trigger_group_off(group)
+
+ hass.services.async_register(
+ DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, SRV_DEL_ALL_LINK, del_all_link, schema=DEL_ALL_LINK_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
+ )
+ hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
+ hass.services.async_register(
+ DOMAIN, SRV_X10_ALL_UNITS_OFF, x10_all_units_off, schema=X10_HOUSECODE_SCHEMA,
+ )
+ hass.services.async_register(
+ DOMAIN, SRV_X10_ALL_LIGHTS_OFF, x10_all_lights_off, schema=X10_HOUSECODE_SCHEMA,
+ )
+ hass.services.async_register(
+ DOMAIN, SRV_X10_ALL_LIGHTS_ON, x10_all_lights_on, schema=X10_HOUSECODE_SCHEMA,
+ )
+ hass.services.async_register(
+ DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA
+ )
+ _LOGGER.debug("Insteon Services registered")
+
+
+def print_aldb_to_log(aldb):
+ """Print the All-Link Database to the log file."""
+ _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")
+ _LOGGER.warning("Use service insteon.load_aldb first")
+ return
+
+ _LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3")
+ _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------")
+ for mem_addr in aldb:
+ rec = aldb[mem_addr]
+ # For now we write this to the log
+ # Roadmap is to create a configuration panel
+ in_use = "Y" if rec.control_flags.is_in_use else "N"
+ mode = "C" if rec.control_flags.is_controller else "R"
+ hwm = "Y" if rec.control_flags.is_high_water_mark else "N"
+ _LOGGER.info(
+ " {:04x} {:s} {:s} {:s} {:3d} {:s}"
+ " {:3d} {:3d} {:3d}".format(
+ rec.mem_addr,
+ in_use,
+ mode,
+ hwm,
+ rec.group,
+ rec.address.human,
+ rec.data1,
+ rec.data2,
+ rec.data3,
+ )
+ )
diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py
index bdf612b2e83..37761e88347 100644
--- a/homeassistant/components/intent/__init__.py
+++ b/homeassistant/components/intent/__init__.py
@@ -5,7 +5,8 @@ import voluptuous as vol
from homeassistant.components import http
from homeassistant.components.http.data_validator import RequestDataValidator
-from homeassistant.core import HomeAssistant
+from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON
+from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv, integration_platform, intent
from .const import DOMAIN
@@ -22,6 +23,22 @@ async def async_setup(hass: HomeAssistant, config: dict):
hass, DOMAIN, _async_process_intent
)
+ hass.helpers.intent.async_register(
+ intent.ServiceIntentHandler(
+ intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON, "Turned {} on"
+ )
+ )
+ hass.helpers.intent.async_register(
+ intent.ServiceIntentHandler(
+ intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF, "Turned {} off"
+ )
+ )
+ hass.helpers.intent.async_register(
+ intent.ServiceIntentHandler(
+ intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE, "Toggled {}"
+ )
+ )
+
return True
diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py
index 75622c29b1c..3f193993c2b 100644
--- a/homeassistant/components/ios/__init__.py
+++ b/homeassistant/components/ios/__init__.py
@@ -279,7 +279,7 @@ class iOSIdentifyDeviceView(HomeAssistantView):
name = "api:ios:identify"
def __init__(self, config_path):
- """Initiliaze the view."""
+ """Initialize the view."""
self._config_path = config_path
async def post(self, request):
diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py
index 9272a725bb7..bd5aeac099a 100644
--- a/homeassistant/components/iperf3/__init__.py
+++ b/homeassistant/components/iperf3/__init__.py
@@ -13,6 +13,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_PROTOCOL,
CONF_SCAN_INTERVAL,
+ DATA_RATE_MEGABITS_PER_SECOND,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
@@ -39,11 +40,9 @@ ATTR_UPLOAD = "upload"
ATTR_VERSION = "Version"
ATTR_HOST = "host"
-UNIT_OF_MEASUREMENT = "Mbit/s"
-
SENSOR_TYPES = {
- ATTR_DOWNLOAD: [ATTR_DOWNLOAD.capitalize(), UNIT_OF_MEASUREMENT],
- ATTR_UPLOAD: [ATTR_UPLOAD.capitalize(), UNIT_OF_MEASUREMENT],
+ ATTR_DOWNLOAD: [ATTR_DOWNLOAD.capitalize(), DATA_RATE_MEGABITS_PER_SECOND],
+ ATTR_UPLOAD: [ATTR_UPLOAD.capitalize(), DATA_RATE_MEGABITS_PER_SECOND],
}
PROTOCOLS = ["tcp", "udp"]
diff --git a/homeassistant/components/ipma/.translations/ca.json b/homeassistant/components/ipma/.translations/ca.json
index 29dbaa4f58d..ad2d37524c5 100644
--- a/homeassistant/components/ipma/.translations/ca.json
+++ b/homeassistant/components/ipma/.translations/ca.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Latitud",
"longitude": "Longitud",
+ "mode": "Mode",
"name": "Nom"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/da.json b/homeassistant/components/ipma/.translations/da.json
index 017aff4d0ec..e2f72db7c4d 100644
--- a/homeassistant/components/ipma/.translations/da.json
+++ b/homeassistant/components/ipma/.translations/da.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Breddegrad",
"longitude": "L\u00e6ngdegrad",
+ "mode": "Tilstand",
"name": "Navn"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/de.json b/homeassistant/components/ipma/.translations/de.json
index 9e717b77843..977b69576de 100644
--- a/homeassistant/components/ipma/.translations/de.json
+++ b/homeassistant/components/ipma/.translations/de.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad",
+ "mode": "Modus",
"name": "Name"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/en.json b/homeassistant/components/ipma/.translations/en.json
index 15459b91f2a..d47f0dfb501 100644
--- a/homeassistant/components/ipma/.translations/en.json
+++ b/homeassistant/components/ipma/.translations/en.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Latitude",
"longitude": "Longitude",
+ "mode": "Mode",
"name": "Name"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/es.json b/homeassistant/components/ipma/.translations/es.json
index acb8b51a44c..d6a43fc790b 100644
--- a/homeassistant/components/ipma/.translations/es.json
+++ b/homeassistant/components/ipma/.translations/es.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Latitud",
"longitude": "Longitud",
+ "mode": "Modo",
"name": "Nombre"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/fr.json b/homeassistant/components/ipma/.translations/fr.json
index 64d03c6ae71..46b99e6651a 100644
--- a/homeassistant/components/ipma/.translations/fr.json
+++ b/homeassistant/components/ipma/.translations/fr.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Latitude",
"longitude": "Longitude",
+ "mode": "Mode",
"name": "Nom"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/hu.json b/homeassistant/components/ipma/.translations/hu.json
index 62ddd85e6ef..5165fec9d90 100644
--- a/homeassistant/components/ipma/.translations/hu.json
+++ b/homeassistant/components/ipma/.translations/hu.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Sz\u00e9less\u00e9g",
"longitude": "Hossz\u00fas\u00e1g",
+ "mode": "M\u00f3d",
"name": "N\u00e9v"
},
"description": "Portug\u00e1l Atmoszf\u00e9ra Int\u00e9zet",
diff --git a/homeassistant/components/ipma/.translations/it.json b/homeassistant/components/ipma/.translations/it.json
index 954ff6e9ee1..6e89d425934 100644
--- a/homeassistant/components/ipma/.translations/it.json
+++ b/homeassistant/components/ipma/.translations/it.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Latitudine",
"longitude": "Longitudine",
+ "mode": "Modalit\u00e0",
"name": "Nome"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/ko.json b/homeassistant/components/ipma/.translations/ko.json
index 828733c9195..c5614e17034 100644
--- a/homeassistant/components/ipma/.translations/ko.json
+++ b/homeassistant/components/ipma/.translations/ko.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "\uc704\ub3c4",
"longitude": "\uacbd\ub3c4",
+ "mode": "\ubaa8\ub4dc",
"name": "\uc774\ub984"
},
"description": "\ud3ec\ub974\ud22c\uac08 \ud574\uc591 \ubc0f \ub300\uae30 \uc5f0\uad6c\uc18c (Instituto Portugu\u00eas do Mar e Atmosfera)",
diff --git a/homeassistant/components/ipma/.translations/lb.json b/homeassistant/components/ipma/.translations/lb.json
index c9eb3a01941..7d8280998fe 100644
--- a/homeassistant/components/ipma/.translations/lb.json
+++ b/homeassistant/components/ipma/.translations/lb.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Breedegrad",
"longitude": "L\u00e4ngegrad",
+ "mode": "Modus",
"name": "Numm"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/nl.json b/homeassistant/components/ipma/.translations/nl.json
index bc10eb3573e..00b9881fd97 100644
--- a/homeassistant/components/ipma/.translations/nl.json
+++ b/homeassistant/components/ipma/.translations/nl.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Latitude",
"longitude": "Longitude",
+ "mode": "Mode",
"name": "Naam"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/no.json b/homeassistant/components/ipma/.translations/no.json
index 1d5aa9c40cf..d7261732431 100644
--- a/homeassistant/components/ipma/.translations/no.json
+++ b/homeassistant/components/ipma/.translations/no.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Breddegrad",
"longitude": "Lengdegrad",
+ "mode": "Modus",
"name": "Navn"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/pl.json b/homeassistant/components/ipma/.translations/pl.json
index 735f5a4a126..267b4e79137 100644
--- a/homeassistant/components/ipma/.translations/pl.json
+++ b/homeassistant/components/ipma/.translations/pl.json
@@ -1,13 +1,14 @@
{
"config": {
"error": {
- "name_exists": "Nazwa ju\u017c istnieje"
+ "name_exists": "Nazwa ju\u017c istnieje."
},
"step": {
"user": {
"data": {
"latitude": "Szeroko\u015b\u0107 geograficzna",
"longitude": "D\u0142ugo\u015b\u0107 geograficzna",
+ "mode": "Tryb",
"name": "Nazwa"
},
"description": "Portugalski Instytut Morza i Atmosfery",
diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json
index 0db504c629c..96d4ee90407 100644
--- a/homeassistant/components/ipma/.translations/ru.json
+++ b/homeassistant/components/ipma/.translations/ru.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
+ "mode": "\u0420\u0435\u0436\u0438\u043c",
"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.",
diff --git a/homeassistant/components/ipma/.translations/sl.json b/homeassistant/components/ipma/.translations/sl.json
index da6a1dac859..2dcfcde7404 100644
--- a/homeassistant/components/ipma/.translations/sl.json
+++ b/homeassistant/components/ipma/.translations/sl.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Zemljepisna \u0161irina",
"longitude": "Zemljepisna dol\u017eina",
+ "mode": "Na\u010din",
"name": "Ime"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/.translations/sv.json b/homeassistant/components/ipma/.translations/sv.json
index 4bdba6f0d08..e8cba56a0a0 100644
--- a/homeassistant/components/ipma/.translations/sv.json
+++ b/homeassistant/components/ipma/.translations/sv.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "Latitud",
"longitude": "Longitud",
+ "mode": "L\u00e4ge",
"name": "Namn"
},
"description": "Portugisiska institutet f\u00f6r hav och atmosf\u00e4ren",
diff --git a/homeassistant/components/ipma/.translations/zh-Hans.json b/homeassistant/components/ipma/.translations/zh-Hans.json
index 6c5654b6388..10d51832964 100644
--- a/homeassistant/components/ipma/.translations/zh-Hans.json
+++ b/homeassistant/components/ipma/.translations/zh-Hans.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "\u7eac\u5ea6",
"longitude": "\u7ecf\u5ea6",
+ "mode": "\u6a21\u5f0f",
"name": "\u540d\u79f0"
},
"description": "\u8461\u8404\u7259\u56fd\u5bb6\u5927\u6c14\u7814\u7a76\u6240",
diff --git a/homeassistant/components/ipma/.translations/zh-Hant.json b/homeassistant/components/ipma/.translations/zh-Hant.json
index 25c832e51c6..de36336f2c4 100644
--- a/homeassistant/components/ipma/.translations/zh-Hant.json
+++ b/homeassistant/components/ipma/.translations/zh-Hant.json
@@ -8,6 +8,7 @@
"data": {
"latitude": "\u7def\u5ea6",
"longitude": "\u7d93\u5ea6",
+ "mode": "\u6a21\u5f0f",
"name": "\u540d\u7a31"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py
index d1532066f68..3811d30bfbe 100644
--- a/homeassistant/components/ipma/config_flow.py
+++ b/homeassistant/components/ipma/config_flow.py
@@ -2,10 +2,11 @@
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN, HOME_LOCATION_NAME
+from .weather import FORECAST_MODE
@config_entries.HANDLERS.register(DOMAIN)
@@ -49,6 +50,7 @@ class IpmaFlowHandler(config_entries.ConfigFlow):
vol.Required(CONF_NAME, default=name): str,
vol.Required(CONF_LATITUDE, default=latitude): cv.latitude,
vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude,
+ vol.Required(CONF_MODE, default="daily"): vol.In(FORECAST_MODE),
}
),
errors=self._errors,
diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json
index cd66ce7461b..02d4e459f72 100644
--- a/homeassistant/components/ipma/manifest.json
+++ b/homeassistant/components/ipma/manifest.json
@@ -3,7 +3,7 @@
"name": "Instituto Português do Mar e Atmosfera (IPMA)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ipma",
- "requirements": ["pyipma==2.0.2"],
+ "requirements": ["pyipma==2.0.3"],
"dependencies": [],
"codeowners": ["@dgomes", "@abmantis"]
}
diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json
index f22d1b62fe4..ea8b9edcc86 100644
--- a/homeassistant/components/ipma/strings.json
+++ b/homeassistant/components/ipma/strings.json
@@ -8,7 +8,8 @@
"data": {
"name": "Name",
"latitude": "Latitude",
- "longitude": "Longitude"
+ "longitude": "Longitude",
+ "mode": "Mode"
}
}
},
diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py
index 7b07406d007..1fce3922b58 100644
--- a/homeassistant/components/ipma/weather.py
+++ b/homeassistant/components/ipma/weather.py
@@ -13,13 +13,22 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
+ 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.const import (
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_MODE,
+ CONF_NAME,
+ TEMP_CELSIUS,
+)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import Throttle
+from homeassistant.util.dt import now, parse_datetime
_LOGGER = logging.getLogger(__name__)
@@ -44,11 +53,14 @@ CONDITION_CLASSES = {
"exceptional": [],
}
+FORECAST_MODE = ["hourly", "daily"]
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
+ vol.Optional(CONF_MODE, default="daily"): vol.In(FORECAST_MODE),
}
)
@@ -96,10 +108,12 @@ async def async_get_location(hass, api, latitude, longitude):
location = await Location.get(api, float(latitude), float(longitude))
_LOGGER.debug(
- "Initializing for coordinates %s, %s -> station %s",
+ "Initializing for coordinates %s, %s -> station %s (%d, %d)",
latitude,
longitude,
location.station,
+ location.id_station,
+ location.global_id_local,
)
return location
@@ -112,6 +126,7 @@ class IPMAWeather(WeatherEntity):
"""Initialise the platform with a data instance and station name."""
self._api = api
self._location_name = config.get(CONF_NAME, location.name)
+ self._mode = config.get(CONF_MODE)
self._location = location
self._observation = None
self._forecast = None
@@ -129,7 +144,7 @@ class IPMAWeather(WeatherEntity):
_LOGGER.warning("Could not update weather observation")
if new_forecast:
- self._forecast = [f for f in new_forecast if f.forecasted_hours == 24]
+ self._forecast = new_forecast
else:
_LOGGER.warning("Could not update weather forecast")
@@ -220,22 +235,57 @@ class IPMAWeather(WeatherEntity):
if not self._forecast:
return []
- fcdata_out = [
- {
- ATTR_FORECAST_TIME: data_in.forecast_date,
- ATTR_FORECAST_CONDITION: next(
- (
- k
- for k, v in CONDITION_CLASSES.items()
- if int(data_in.weather_type) in v
+ if self._mode == "hourly":
+ forecast_filtered = [
+ x
+ for x in self._forecast
+ if x.forecasted_hours == 1
+ and parse_datetime(x.forecast_date)
+ > (now().utcnow() - timedelta(hours=1))
+ ]
+
+ fcdata_out = [
+ {
+ ATTR_FORECAST_TIME: data_in.forecast_date,
+ ATTR_FORECAST_CONDITION: next(
+ (
+ k
+ for k, v in CONDITION_CLASSES.items()
+ if int(data_in.weather_type) in v
+ ),
+ None,
),
- None,
- ),
- ATTR_FORECAST_TEMP_LOW: data_in.min_temperature,
- ATTR_FORECAST_TEMP: data_in.max_temperature,
- ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability,
- }
- for data_in in self._forecast
- ]
+ ATTR_FORECAST_TEMP: float(data_in.feels_like_temperature),
+ ATTR_FORECAST_PRECIPITATION: (
+ data_in.precipitation_probability
+ if float(data_in.precipitation_probability) >= 0
+ else None
+ ),
+ ATTR_FORECAST_WIND_SPEED: data_in.wind_strength,
+ ATTR_FORECAST_WIND_BEARING: data_in.wind_direction,
+ }
+ for data_in in forecast_filtered
+ ]
+ else:
+ forecast_filtered = [f for f in self._forecast if f.forecasted_hours == 24]
+ fcdata_out = [
+ {
+ ATTR_FORECAST_TIME: data_in.forecast_date,
+ ATTR_FORECAST_CONDITION: next(
+ (
+ k
+ for k, v in CONDITION_CLASSES.items()
+ if int(data_in.weather_type) in v
+ ),
+ None,
+ ),
+ ATTR_FORECAST_TEMP_LOW: data_in.min_temperature,
+ ATTR_FORECAST_TEMP: data_in.max_temperature,
+ ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability,
+ ATTR_FORECAST_WIND_SPEED: data_in.wind_strength,
+ ATTR_FORECAST_WIND_BEARING: data_in.wind_direction,
+ }
+ for data_in in forecast_filtered
+ ]
return fcdata_out
diff --git a/homeassistant/components/iqvia/.translations/pl.json b/homeassistant/components/iqvia/.translations/pl.json
index b528cdeb70f..b8c014c3dc9 100644
--- a/homeassistant/components/iqvia/.translations/pl.json
+++ b/homeassistant/components/iqvia/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "identifier_exists": "Kod pocztowy ju\u017c zarejestrowany",
+ "identifier_exists": "Kod pocztowy jest ju\u017c zarejestrowany.",
"invalid_zip_code": "Kod pocztowy jest nieprawid\u0142owy"
},
"step": {
diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py
index 09548ee929a..52e657bc2c0 100644
--- a/homeassistant/components/iqvia/const.py
+++ b/homeassistant/components/iqvia/const.py
@@ -6,7 +6,7 @@ CONF_ZIP_CODE = "zip_code"
DATA_CLIENT = "client"
DATA_LISTENER = "listener"
-TOPIC_DATA_UPDATE = "data_update"
+TOPIC_DATA_UPDATE = f"{DOMAIN}_data_update"
TYPE_ALLERGY_FORECAST = "allergy_average_forecasted"
TYPE_ALLERGY_INDEX = "allergy_index"
diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json
index 7a5eb7e56df..363269bc589 100644
--- a/homeassistant/components/iqvia/manifest.json
+++ b/homeassistant/components/iqvia/manifest.json
@@ -3,7 +3,7 @@
"name": "IQVIA",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iqvia",
- "requirements": ["numpy==1.17.4", "pyiqvia==0.2.1"],
+ "requirements": ["numpy==1.18.1", "pyiqvia==0.2.1"],
"dependencies": [],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/izone/.translations/hu.json b/homeassistant/components/izone/.translations/hu.json
new file mode 100644
index 00000000000..79c621ce125
--- /dev/null
+++ b/homeassistant/components/izone/.translations/hu.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "confirm": {
+ "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iZone-t?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/sv.json b/homeassistant/components/izone/.translations/sv.json
new file mode 100644
index 00000000000..c2c952d69fe
--- /dev/null
+++ b/homeassistant/components/izone/.translations/sv.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Inga iZone-enheter hittades i n\u00e4tverket.",
+ "single_instance_allowed": "Endast en enda konfiguration av iZone \u00e4r n\u00f6dv\u00e4ndig."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vill du konfigurera iZone?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py
index c49144f1db9..7690600786e 100644
--- a/homeassistant/components/izone/discovery.py
+++ b/homeassistant/components/izone/discovery.py
@@ -44,11 +44,11 @@ class DiscoveryService(pizone.Listener):
async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl)
def controller_update(self, ctrl: pizone.Controller) -> None:
- """System update message is recieved from the controller."""
+ """System update message is received from the controller."""
async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl)
def zone_update(self, ctrl: pizone.Controller, zone: pizone.Zone) -> None:
- """Zone update message is recieved from the controller."""
+ """Zone update message is received from the controller."""
async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone)
diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py
index 2e4644d7ef5..45f979874f7 100644
--- a/homeassistant/components/jewish_calendar/__init__.py
+++ b/homeassistant/components/jewish_calendar/__init__.py
@@ -21,6 +21,7 @@ SENSOR_TYPES = {
"weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"],
"holiday": ["Holiday", "mdi:calendar-star"],
"omer_count": ["Day of the Omer", "mdi:counter"],
+ "daf_yomi": ["Daf Yomi", "mdi:book-open-variant"],
},
"time": {
"first_light": ["Alot Hashachar", "mdi:weather-sunset-up"],
diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json
index 8e1781f310b..c4ebb382a44 100644
--- a/homeassistant/components/jewish_calendar/manifest.json
+++ b/homeassistant/components/jewish_calendar/manifest.json
@@ -2,7 +2,7 @@
"domain": "jewish_calendar",
"name": "Jewish Calendar",
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
- "requirements": ["hdate==0.9.3"],
+ "requirements": ["hdate==0.9.5"],
"dependencies": [],
"codeowners": ["@tsvi"]
}
diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py
index d0376694a44..7da9d7e31d0 100644
--- a/homeassistant/components/jewish_calendar/sensor.py
+++ b/homeassistant/components/jewish_calendar/sensor.py
@@ -73,7 +73,7 @@ class JewishCalendarSensor(Entity):
_LOGGER.debug("Now: %s Sunset: %s", now, sunset)
- date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew)
+ daytime_date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew)
# The Jewish day starts after darkness (called "tzais") and finishes at
# sunset ("shkia"). The time in between is a gray area (aka "Bein
@@ -82,16 +82,16 @@ class JewishCalendarSensor(Entity):
# For some sensors, it is more interesting to consider the date to be
# tomorrow based on sunset ("shkia"), for others based on "tzais".
# Hence the following variables.
- after_tzais_date = after_shkia_date = date
+ after_tzais_date = after_shkia_date = daytime_date
today_times = self.make_zmanim(today)
if now > sunset:
- after_shkia_date = date.next_day
+ after_shkia_date = daytime_date.next_day
if today_times.havdalah and now > today_times.havdalah:
- after_tzais_date = date.next_day
+ after_tzais_date = daytime_date.next_day
- self._state = self.get_state(after_shkia_date, after_tzais_date)
+ self._state = self.get_state(daytime_date, after_shkia_date, after_tzais_date)
_LOGGER.debug("New value for %s: %s", self._type, self._state)
def make_zmanim(self, date):
@@ -112,7 +112,7 @@ class JewishCalendarSensor(Entity):
return {}
- def get_state(self, after_shkia_date, after_tzais_date):
+ def get_state(self, daytime_date, 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.
@@ -128,6 +128,8 @@ class JewishCalendarSensor(Entity):
return after_shkia_date.holiday_description
if self._type == "omer_count":
return after_shkia_date.omer_day
+ if self._type == "daf_yomi":
+ return daytime_date.daf_yomi
return None
@@ -157,7 +159,7 @@ class JewishCalendarTimeSensor(JewishCalendarSensor):
return attrs
- def get_state(self, after_shkia_date, after_tzais_date):
+ def get_state(self, daytime_date, 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(
diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json
index a2769cd8eb6..135f8e1cf54 100644
--- a/homeassistant/components/kef/manifest.json
+++ b/homeassistant/components/kef/manifest.json
@@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/integrations/kef",
"dependencies": [],
"codeowners": ["@basnijholt"],
- "requirements": ["aiokef==0.2.6", "getmac==0.8.1"]
+ "requirements": ["aiokef==0.2.7", "getmac==0.8.1"]
}
diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py
index dc91b94f5ef..d4a1d7a4df3 100644
--- a/homeassistant/components/kef/media_player.py
+++ b/homeassistant/components/kef/media_player.py
@@ -11,6 +11,10 @@ import voluptuous as vol
from homeassistant.components.media_player import (
PLATFORM_SCHEMA,
+ SUPPORT_NEXT_TRACK,
+ SUPPORT_PAUSE,
+ SUPPORT_PLAY,
+ SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
@@ -214,6 +218,10 @@ class KefMediaPlayer(MediaPlayerDevice):
| SUPPORT_VOLUME_MUTE
| SUPPORT_SELECT_SOURCE
| SUPPORT_TURN_OFF
+ | SUPPORT_NEXT_TRACK # only in Bluetooth and Wifi
+ | SUPPORT_PAUSE # only in Bluetooth and Wifi
+ | SUPPORT_PLAY # only in Bluetooth and Wifi
+ | SUPPORT_PREVIOUS_TRACK # only in Bluetooth and Wifi
)
if self._supports_on:
support_kef |= SUPPORT_TURN_ON
@@ -280,3 +288,19 @@ class KefMediaPlayer(MediaPlayerDevice):
await self._speaker.set_source(source)
else:
raise ValueError(f"Unknown input source: {source}.")
+
+ async def async_media_play(self):
+ """Send play command."""
+ await self._speaker.play_pause()
+
+ async def async_media_pause(self):
+ """Send pause command."""
+ await self._speaker.play_pause()
+
+ async def async_media_previous_track(self):
+ """Send previous track command."""
+ await self._speaker.prev_track()
+
+ async def async_media_next_track(self):
+ """Send next track command."""
+ await self._speaker.next_track()
diff --git a/homeassistant/components/konnected/.translations/ca.json b/homeassistant/components/konnected/.translations/ca.json
new file mode 100644
index 00000000000..ccb03ef7add
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/ca.json
@@ -0,0 +1,100 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
+ "not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io",
+ "unknown": "S'ha produ\u00eft un error desconegut"
+ },
+ "error": {
+ "cannot_connect": "No s'ha pogut connectar amb el panell Konnected a {host}:{port}"
+ },
+ "step": {
+ "confirm": {
+ "description": "Model: {model} \nAmfitri\u00f3: {host} \nPort: {port} \n\nPots configurar el comportament de les E/S (I/O) i del panell a la configuraci\u00f3 del panell d\u2019alarma Konnected.",
+ "title": "Dispositiu Konnected llest"
+ },
+ "user": {
+ "data": {
+ "host": "Adre\u00e7a IP del dispositiu Konnected",
+ "port": "Port del dispositiu Konnected"
+ },
+ "description": "Introdueix la informaci\u00f3 d'amfitri\u00f3 del panell Konnected.",
+ "title": "Descoberta de dispositiu Konnected"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Inverteix l'estat obert/tancat",
+ "name": "Nom (opcional)",
+ "type": "Tipus de sensor binari"
+ },
+ "description": "Selecciona les opcions pel sensor binari de {zone}",
+ "title": "Configuraci\u00f3 de sensor binari"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Nom (opcional)",
+ "poll_interval": "Interval de sondeig (minuts) (opcional)",
+ "type": "Tipus de sensor"
+ },
+ "description": "Selecciona les opcions pel sensor digital de {zone}",
+ "title": "Configuraci\u00f3 de sensor digital"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zona 1",
+ "2": "Zona 2",
+ "3": "Zona 3",
+ "4": "Zona 4",
+ "5": "Zona 5",
+ "6": "Zona 6",
+ "7": "Zona 7",
+ "out": "OUT (sortida)"
+ },
+ "description": "S'ha descobert {model} a {host}. Selecciona la configuraci\u00f3 b\u00e0sica de cada Entrada/Sortida (I/O), en funci\u00f3 del tipus que sigui pot ser que et permeti sensors binaris (contactes oberts/tancats), sensors digitals (DHT i ds18b20) o sortides commutables. M\u00e9s endavant les podr\u00e0s configurar de manera m\u00e9s detallada.",
+ "title": "Configuraci\u00f3 E/S (I/O)"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zona 10",
+ "11": "Zona 11",
+ "12": "Zona 12",
+ "8": "Zona 8",
+ "9": "Zona 9",
+ "alarm1": "ALARMA1",
+ "alarm2_out2": "OUT2/ALARMA2",
+ "out1": "OUT1"
+ },
+ "description": "Selecciona la configuraci\u00f3 de les E/S restants. Podr\u00e0s configurar opcions m\u00e9s detallades en els passos seg\u00fcents.",
+ "title": "Configuraci\u00f3 E/S (I/O) ampliades"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Parpelleja el LED del panell quan s'envien canvis d'estat"
+ },
+ "description": "Selecciona el comportament desitjat del panell",
+ "title": "Configuraci\u00f3 diversos"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Sortida quan estigui ON",
+ "momentary": "Durada del pols (ms) (opcional)",
+ "name": "Nom (opcional)",
+ "pause": "Pausa entre polsos (ms) (opcional)",
+ "repeat": "Repeticions (-1 = infinit) (opcional)"
+ },
+ "description": "Selecciona les opcions de sortida per a {zone}",
+ "title": "Configuraci\u00f3 de sortida commutable"
+ }
+ },
+ "title": "Opcions del panell d'alarma Konnected"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/da.json b/homeassistant/components/konnected/.translations/da.json
new file mode 100644
index 00000000000..db37ad73610
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/da.json
@@ -0,0 +1,104 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheden er allerede konfigureret",
+ "already_in_progress": "Enhedskonfiguration er allerede i gang.",
+ "not_konn_panel": "Ikke en genkendt Konnected.io-enhed",
+ "unknown": "Ukendt fejl opstod"
+ },
+ "error": {
+ "cannot_connect": "Der kan ikke oprettes forbindelse til et Konnected-panel p\u00e5 {host}:{port}"
+ },
+ "step": {
+ "confirm": {
+ "description": "Model: {model}\nV\u00e6rt: {host}\nPort: {port}\n\nDu kan konfigurere IO og panelfunktionsm\u00e5den i indstillingerne for Konnected-alarmpanel.",
+ "title": "Konnected-enhed klar"
+ },
+ "user": {
+ "data": {
+ "host": "Konnected-enhedens IP-adresse",
+ "port": "Konnected-enhedsport"
+ },
+ "description": "Indtast v\u00e6rtsinformationen for dit Konnected-panel.",
+ "title": "Find Konnected-enhed"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Ikke en genkendt Konnected.io-enhed"
+ },
+ "error": {
+ "one": "en",
+ "other": "anden"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Inverter tilstanden \u00e5ben/lukket",
+ "name": "Navn (valgfrit)",
+ "type": "Bin\u00e6r sensortype"
+ },
+ "description": "V\u00e6lg indstillingerne for den bin\u00e6re sensor, der er knyttet til {zone}",
+ "title": "Konfigurer bin\u00e6r sensor"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Navn (valgfrit)",
+ "poll_interval": "Foresp\u00f8rgselsinterval (minutter) (valgfrit)",
+ "type": "Sensortype"
+ },
+ "description": "V\u00e6lg indstillingerne for den digitale sensor, der er knyttet til {zone}",
+ "title": "Konfigurer digital sensor"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zone 1",
+ "2": "Zone 2",
+ "3": "Zone 3",
+ "4": "Zone 4",
+ "5": "Zone 5",
+ "6": "Zone 6",
+ "7": "Zone 7",
+ "out": "OUT"
+ },
+ "description": "Der blev fundet en {model} p\u00e5 {host}. V\u00e6lg basiskonfigurationen af hver I/O nedenfor - afh\u00e6ngigt af I/O kan det give mulighed for bin\u00e6re sensorer (\u00e5ben-/lukket-kontakter), digitale sensorer (dht og ds18b20) eller omskiftelige outputs. Du kan konfigurere detaljerede indstillinger i de n\u00e6ste trin.",
+ "title": "Konfigurer I/O"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zone 10",
+ "11": "Zone 11",
+ "12": "Zone 12",
+ "8": "Zone 8",
+ "9": "Zone 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "V\u00e6lg konfigurationen af det resterende I/O nedenfor. Du kan konfigurere detaljerede indstillinger i de n\u00e6ste trin.",
+ "title": "Konfigurer udvidet I/O"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Blink panel-LED ved sending af tilstands\u00e6ndring"
+ },
+ "description": "V\u00e6lg den \u00f8nskede funktionsm\u00e5de for panelet",
+ "title": "Konfigurer diverse"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Output n\u00e5r der er t\u00e6ndt",
+ "momentary": "Impulsvarighed (ms) (valgfrit)",
+ "name": "Navn (valgfrit)",
+ "pause": "Pause mellem impulser (ms) (valgfrit)",
+ "repeat": "Gange til gentagelse (-1=uendelig) (valgfrit)"
+ },
+ "description": "V\u00e6lg outputindstillingerne for {zone}",
+ "title": "Konfigurer skifteligt output"
+ }
+ },
+ "title": "Indstillinger for Konnected-alarmpanel"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/de.json b/homeassistant/components/konnected/.translations/de.json
new file mode 100644
index 00000000000..fa5b1f53dfb
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/de.json
@@ -0,0 +1,95 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.",
+ "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t",
+ "unknown": "Unbekannter Fehler ist aufgetreten"
+ },
+ "error": {
+ "cannot_connect": "Es konnte keine Verbindung zu einem Konnected-Panel unter {host}:{port} hergestellt werden."
+ },
+ "step": {
+ "confirm": {
+ "description": "Modell: {model} \nHost: {host} \nPort: {port} \n\nSie k\u00f6nnen das I / O - und Bedienfeldverhalten in den Einstellungen der verbundenen Alarmzentrale konfigurieren.",
+ "title": "Konnected Device Bereit"
+ },
+ "import_confirm": {
+ "title": "Importieren von Konnected Ger\u00e4t"
+ },
+ "user": {
+ "data": {
+ "host": "Konnected Ger\u00e4t IP-Adresse",
+ "port": "Konnected Device Port"
+ },
+ "description": "Bitte geben Sie die Hostinformationen f\u00fcr Ihr Konnected Panel ein.",
+ "title": "Entdecken Sie Konnected Ger\u00e4t"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Invertieren Sie den \u00d6ffnungs- / Schlie\u00dfzustand",
+ "name": "Name (optional)",
+ "type": "Bin\u00e4rer Sensortyp"
+ },
+ "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor",
+ "title": "Konfigurieren Sie den Bin\u00e4rsensor"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Name (optional)",
+ "poll_interval": "Abfrageintervall (Minuten) (optional)",
+ "type": "Sensortyp"
+ },
+ "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus",
+ "title": "Konfigurieren Sie den digitalen Sensor"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zone 1",
+ "2": "Zone 2",
+ "3": "Zone 3",
+ "4": "Zone 4",
+ "5": "Zone 5",
+ "6": "Zone 6",
+ "7": "Zone 7",
+ "out": "OUT"
+ },
+ "title": "Konfigurieren von I/O"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zone 10",
+ "11": "Zone 11",
+ "12": "Zone 12",
+ "8": "Zone 8",
+ "9": "Zone 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ }
+ },
+ "options_misc": {
+ "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Ausgabe, wenn eingeschaltet",
+ "momentary": "Impulsdauer (ms) (optional)",
+ "name": "Name (optional)",
+ "pause": "Pause zwischen Impulsen (ms) (optional)",
+ "repeat": "Zeit zum Wiederholen (-1 = unendlich) (optional)"
+ },
+ "description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone}"
+ }
+ },
+ "title": "Konnected Alarm Panel-Optionen"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/en.json b/homeassistant/components/konnected/.translations/en.json
new file mode 100644
index 00000000000..fd0a8e84e37
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/en.json
@@ -0,0 +1,104 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Config flow for device is already in progress.",
+ "not_konn_panel": "Not a recognized Konnected.io device",
+ "unknown": "Unknown error occurred"
+ },
+ "error": {
+ "cannot_connect": "Unable to connect to a Konnected Panel at {host}:{port}"
+ },
+ "step": {
+ "confirm": {
+ "description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings.",
+ "title": "Konnected Device Ready"
+ },
+ "import_confirm": {
+ "description": "A Konnected Alarm Panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry.",
+ "title": "Import Konnected Device"
+ },
+ "user": {
+ "data": {
+ "host": "Konnected device IP address",
+ "port": "Konnected device port"
+ },
+ "description": "Please enter the host information for your Konnected Panel.",
+ "title": "Discover Konnected Device"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Not a recognized Konnected.io device"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Invert the open/close state",
+ "name": "Name (optional)",
+ "type": "Binary Sensor Type"
+ },
+ "description": "Please select the options for the binary sensor attached to {zone}",
+ "title": "Configure Binary Sensor"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Name (optional)",
+ "poll_interval": "Poll Interval (minutes) (optional)",
+ "type": "Sensor Type"
+ },
+ "description": "Please select the options for the digital sensor attached to {zone}",
+ "title": "Configure Digital Sensor"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zone 1",
+ "2": "Zone 2",
+ "3": "Zone 3",
+ "4": "Zone 4",
+ "5": "Zone 5",
+ "6": "Zone 6",
+ "7": "Zone 7",
+ "out": "OUT"
+ },
+ "description": "Discovered a {model} at {host}. Select the base configuration of each I/O below - depending on the I/O it may allow for binary sensors (open/close contacts), digital sensors (dht and ds18b20), or switchable outputs. You'll be able to configure detailed options in the next steps.",
+ "title": "Configure I/O"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zone 10",
+ "11": "Zone 11",
+ "12": "Zone 12",
+ "8": "Zone 8",
+ "9": "Zone 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
+ "title": "Configure Extended I/O"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Blink panel LED on when sending state change"
+ },
+ "description": "Please select the desired behavior for your panel",
+ "title": "Configure Misc"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Output when on",
+ "momentary": "Pulse duration (ms) (optional)",
+ "name": "Name (optional)",
+ "pause": "Pause between pulses (ms) (optional)",
+ "repeat": "Times to repeat (-1=infinite) (optional)"
+ },
+ "description": "Please select the output options for {zone}",
+ "title": "Configure Switchable Output"
+ }
+ },
+ "title": "Konnected Alarm Panel Options"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/es.json b/homeassistant/components/konnected/.translations/es.json
new file mode 100644
index 00000000000..f72a58cf649
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/es.json
@@ -0,0 +1,104 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en curso.",
+ "not_konn_panel": "No es un dispositivo Konnected.io reconocido",
+ "unknown": "Se produjo un error desconocido"
+ },
+ "error": {
+ "cannot_connect": "No se puede conectar a un Panel conectado en {host}:{port}"
+ },
+ "step": {
+ "confirm": {
+ "description": "Modelo: {model}\nHost: {host}\nPuerto: {port}\n\nPuede configurar las E/S y el comportamiento del panel en los ajustes del panel de alarmas Konnected.",
+ "title": "Dispositivo Konnected Listo"
+ },
+ "user": {
+ "data": {
+ "host": "Direcci\u00f3n IP del dispositivo Konnected",
+ "port": "Puerto del dispositivo Konnected"
+ },
+ "description": "Introduzca la informaci\u00f3n del host de su panel Konnected.",
+ "title": "Descubrir el dispositivo Konnected"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "No es un dispositivo Konnected.io reconocido"
+ },
+ "error": {
+ "one": "",
+ "other": "otros"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Invertir el estado de apertura/cierre",
+ "name": "Nombre (opcional)",
+ "type": "Tipo de sensor binario"
+ },
+ "description": "Seleccione las opciones para el sensor binario conectado a {zone}",
+ "title": "Configurar sensor binario"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Nombre (opcional)",
+ "poll_interval": "Intervalo de sondeo (minutos) (opcional)",
+ "type": "Tipo de sensor"
+ },
+ "description": "Seleccione las opciones para el sensor digital conectado a {zone}",
+ "title": "Configurar el sensor digital"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zona 1",
+ "2": "Zona 2",
+ "3": "Zona 3",
+ "4": "Zona 4",
+ "5": "Zona 5",
+ "6": "Zona 6",
+ "7": "Zona 7",
+ "out": "OUT"
+ },
+ "description": "Descubierto un {model} en {host} . Seleccione la configuraci\u00f3n base de cada E / S a continuaci\u00f3n: seg\u00fan la E / S, puede permitir sensores binarios (contactos de apertura / cierre), sensores digitales (dht y ds18b20) o salidas conmutables. Podr\u00e1 configurar opciones detalladas en los pr\u00f3ximos pasos.",
+ "title": "Configurar E/S"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zona 10",
+ "11": "Zona 11",
+ "12": "Zona 12",
+ "8": "Zona 8",
+ "9": "Zona 9",
+ "alarm1": "ALARMA1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "Seleccione la configuraci\u00f3n de las E/S restantes a continuaci\u00f3n. Podr\u00e1s configurar opciones detalladas en los pr\u00f3ximos pasos.",
+ "title": "Configurar E/S extendidas"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado"
+ },
+ "description": "Seleccione el comportamiento deseado para su panel",
+ "title": "Configurar miscel\u00e1neos"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Salida cuando est\u00e1 activada",
+ "momentary": "Duraci\u00f3n del pulso (ms) (opcional)",
+ "name": "Nombre (opcional)",
+ "pause": "Pausa entre pulsos (ms) (opcional)",
+ "repeat": "Tiempos de repetici\u00f3n (-1 = infinito) (opcional)"
+ },
+ "description": "Por favor, seleccione las opciones de salida para {zone}",
+ "title": "Configurar la salida conmutable"
+ }
+ },
+ "title": "Opciones del panel de alarma Konnected"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/fr.json b/homeassistant/components/konnected/.translations/fr.json
new file mode 100644
index 00000000000..e6c0cded9fc
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/fr.json
@@ -0,0 +1,72 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "not_konn_panel": "Non reconnu comme appareil Konnected.io",
+ "unknown": "Une erreur inconnue s'est produite"
+ },
+ "step": {
+ "confirm": {
+ "title": "Appareil Konnected pr\u00eat"
+ },
+ "user": {
+ "data": {
+ "host": "Adresse IP de l\u2019appareil Konnected"
+ }
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "error": {
+ "one": "Vide",
+ "other": "Vide"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Inverser l'\u00e9tat ouvert / ferm\u00e9",
+ "name": "Nom (facultatif)",
+ "type": "Type de capteur binaire"
+ },
+ "description": "Veuillez s\u00e9lectionner les options du capteur binaire attach\u00e9 \u00e0 {zone}",
+ "title": "Configurer le capteur binaire"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Nom (facultatif)",
+ "poll_interval": "Intervalle d'interrogation (minutes) (facultatif)",
+ "type": "Type de capteur"
+ },
+ "description": "Veuillez s\u00e9lectionner les options du capteur digital attach\u00e9 \u00e0 {zone}",
+ "title": "Configurer le capteur digital"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zone 1",
+ "2": "Zone 2",
+ "3": "Zone 3",
+ "4": "Zone 4",
+ "5": "Zone 5",
+ "6": "Zone 6",
+ "7": "Zone 7"
+ }
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zone 10",
+ "11": "Zone 11",
+ "12": "Zone 12",
+ "8": "Zone 8",
+ "9": "Zone 9",
+ "alarm1": "ALARME1"
+ }
+ },
+ "options_switch": {
+ "data": {
+ "name": "Nom (facultatif)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/hu.json b/homeassistant/components/konnected/.translations/hu.json
new file mode 100644
index 00000000000..35a4adfebe3
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/hu.json
@@ -0,0 +1,17 @@
+{
+ "options": {
+ "step": {
+ "options_digital": {
+ "data": {
+ "name": "N\u00e9v (nem k\u00f6telez\u0151)",
+ "type": "\u00c9rz\u00e9kel\u0151 t\u00edpusa"
+ }
+ },
+ "options_switch": {
+ "data": {
+ "name": "N\u00e9v (nem k\u00f6telez\u0151)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/it.json b/homeassistant/components/konnected/.translations/it.json
new file mode 100644
index 00000000000..fb18ece10f8
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/it.json
@@ -0,0 +1,104 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.",
+ "not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto",
+ "unknown": "Si \u00e8 verificato un errore sconosciuto"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi ad un Pannello Konnected su {host}:{port}."
+ },
+ "step": {
+ "confirm": {
+ "description": "Modello: {model}\nHost: {host}\nPorta: {port}\n\n\u00c8 possibile configurare il comportamento di I/O e del pannello nelle impostazioni del Pannello Allarmi di Konnected.",
+ "title": "Dispositivo Konnected pronto"
+ },
+ "user": {
+ "data": {
+ "host": "Indirizzo IP del dispositivo Konnected",
+ "port": "Porta del dispositivo Konnected"
+ },
+ "description": "Si prega di inserire le informazioni dell'host per il tuo Pannello Konnected.",
+ "title": "Rileva il dispositivo Konnected"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto"
+ },
+ "error": {
+ "one": "uno",
+ "other": "altro"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Invertire lo stato di apertura/chiusura",
+ "name": "Nome (opzionale)",
+ "type": "Tipo di sensore binario"
+ },
+ "description": "Si prega di selezionare le opzioni per il sensore binario collegato alla {zone}",
+ "title": "Configurare il Sensore Binario"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Nome (opzionale)",
+ "poll_interval": "Intervallo di sondaggio (minuti) (opzionale)",
+ "type": "Tipo di sensore"
+ },
+ "description": "Si prega di selezionare le opzioni per il sensore digitale collegato alla {zone}",
+ "title": "Configurare il Sensore Digitale"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zona 1",
+ "2": "Zona 2",
+ "3": "Zona 3",
+ "4": "Zona 4",
+ "5": "Zona 5",
+ "6": "Zona 6",
+ "7": "Zona 7",
+ "out": "OUT"
+ },
+ "description": "Rilevato un {model} su {host}. Selezionare la configurazione di base di ciascun I/O di seguito: a seconda dell'I/O, essa pu\u00f2 consentire sensori binari (contatti aperti/chiusi), sensori digitali (DHT e DS18B20) o uscite commutabili. Sarai in grado di configurare le opzioni dettagliate nei prossimi passi.",
+ "title": "Configura I/O"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zona 10",
+ "11": "Zona 11",
+ "12": "Zona 12",
+ "8": "Zona 8",
+ "9": "Zona 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2 / ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "Selezionare di seguito la configurazione degli I/O rimanenti. Potrete configurare opzioni dettagliate nei prossimi passi.",
+ "title": "Configurazione I/O Esteso"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Attiva il lampeggio del LED del pannello durante l'invio del cambiamento di stato "
+ },
+ "description": "Seleziona il comportamento desiderato per il tuo pannello",
+ "title": "Configura Altro"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Uscita quando acceso",
+ "momentary": "Durata impulso (ms) (opzionale)",
+ "name": "Nome (opzionale)",
+ "pause": "Pausa tra gli impulsi (ms) (opzionale)",
+ "repeat": "Numero di volte da ripetere (-1 = infinito) (opzionale)"
+ },
+ "description": "Selezionare le opzioni di uscita per {zona}",
+ "title": "Configurare l'uscita commutabile"
+ }
+ },
+ "title": "Opzioni del pannello di allarme Konnected"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/ko.json b/homeassistant/components/konnected/.translations/ko.json
new file mode 100644
index 00000000000..fe196050766
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/ko.json
@@ -0,0 +1,104 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.",
+ "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4",
+ "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "{host}:{port} \uc758 Konnected \ud328\ub110\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "confirm": {
+ "description": "\ubaa8\ub378: {model}\n\ud638\uc2a4\ud2b8: {host}\n\ud3ec\ud2b8: {port}\n\nKonnected \uc54c\ub78c \ud328\ub110 \uc124\uc815\uc5d0\uc11c IO \uc640 \ud328\ub110 \ub3d9\uc791\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "title": "Konnected \uae30\uae30 \uc900\ube44"
+ },
+ "import_confirm": {
+ "description": "Konnected \uc54c\ub78c \ud328\ub110 ID {id} \uac00 configuration.yaml \uc5d0\uc11c \ubc1c\uacac\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \uacfc\uc815\uc744 \ud1b5\ud574 \uad6c\uc131 \ud56d\ubaa9\uc73c\ub85c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "title": "Konnected \uae30\uae30 \uac00\uc838\uc624\uae30"
+ },
+ "user": {
+ "data": {
+ "host": "Konnected \uae30\uae30 IP \uc8fc\uc18c",
+ "port": "Konnected \uae30\uae30 \ud3ec\ud2b8"
+ },
+ "description": "Konnected \ud328\ub110\uc758 \ud638\uc2a4\ud2b8 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "Konnected \uae30\uae30 \ucc3e\uae30"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "\uc5f4\ub9bc / \ub2eb\ud798 \uc0c1\ud0dc \ubc18\uc804",
+ "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)",
+ "type": "\uc774\uc9c4 \uc13c\uc11c \uc720\ud615"
+ },
+ "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \uc774\uc9c4 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
+ "title": "\uc774\uc9c4 \uc13c\uc11c \uad6c\uc131"
+ },
+ "options_digital": {
+ "data": {
+ "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)",
+ "poll_interval": "\ud3f4\ub9c1 \uac04\uaca9 (\ubd84) (\uc120\ud0dd \uc0ac\ud56d)",
+ "type": "\uc13c\uc11c \uc720\ud615"
+ },
+ "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \ub514\uc9c0\ud138 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
+ "title": "\ub514\uc9c0\ud138 \uc13c\uc11c \uad6c\uc131"
+ },
+ "options_io": {
+ "data": {
+ "1": "\uad6c\uc5ed 1",
+ "2": "\uad6c\uc5ed 2",
+ "3": "\uad6c\uc5ed 3",
+ "4": "\uad6c\uc5ed 4",
+ "5": "\uad6c\uc5ed 5",
+ "6": "\uad6c\uc5ed 6",
+ "7": "\uad6c\uc5ed 7",
+ "out": "\uc678\ubd80"
+ },
+ "description": "{host} \uc5d0\uc11c {model} \uc744(\ub97c) \ubc1c\uacac\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc9c4 \uc13c\uc11c(\uac1c\ud3d0 \uc811\uc810), \ub514\uc9c0\ud138 \uc13c\uc11c(dht \ubc0f ds18b20) \ub610\ub294 \uc2a4\uc704\uce58\uac00 \uac00\ub2a5\ud55c \uc13c\uc11c\uc758 I/O \uc5d0 \ub530\ub77c \uc544\ub798\uc5d0\uc11c \uac01 I/O \uc758 \uae30\ubcf8 \uad6c\uc131\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub2e4\uc74c \ub2e8\uacc4\uc5d0\uc11c \uc138\ubd80 \uc635\uc158\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "title": "I/O \uad6c\uc131"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "\uad6c\uc5ed 10",
+ "11": "\uad6c\uc5ed 11",
+ "12": "\uad6c\uc5ed 12",
+ "8": "\uad6c\uc5ed 8",
+ "9": "\uad6c\uc5ed 9",
+ "alarm1": "\uc54c\ub78c 1",
+ "alarm2_out2": "\ucd9c\ub825 2 / \uc54c\ub78c 2",
+ "out1": "\ucd9c\ub825 1"
+ },
+ "description": "\uc544\ub798\uc758 \ub098\uba38\uc9c0 I/O \uad6c\uc131\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub2e4\uc74c \ub2e8\uacc4\uc5d0\uc11c \uc138\ubd80 \uc635\uc158\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "title": "\ud655\uc7a5 I/O \uad6c\uc131"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "\uc0c1\ud0dc \ubcc0\uacbd\uc744 \ubcf4\ub0bc \ub54c \uae5c\ubc15\uc784 \ud328\ub110 LED \ub97c \ucf2d\ub2c8\ub2e4"
+ },
+ "description": "\ud328\ub110\uc5d0 \uc6d0\ud558\ub294 \ub3d9\uc791\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
+ "title": "\uae30\ud0c0 \uad6c\uc131"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "\uc2a4\uc704\uce58\uac00 \ucf1c\uc9c8 \ub54c \ucd9c\ub825",
+ "momentary": "\ud384\uc2a4 \uc9c0\uc18d\uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)",
+ "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)",
+ "pause": "\ud384\uc2a4 \uac04 \uc77c\uc2dc\uc815\uc9c0 \uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)",
+ "repeat": "\ubc18\ubcf5 \uc2dc\uac04 (-1 = \ubb34\ud55c) (\uc120\ud0dd \uc0ac\ud56d)"
+ },
+ "description": "{zone} \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
+ "title": "\uc2a4\uc704\uce58 \ucd9c\ub825 \uad6c\uc131"
+ }
+ },
+ "title": "Konnected \uc54c\ub78c \ud328\ub110 \uc635\uc158"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/lb.json b/homeassistant/components/konnected/.translations/lb.json
new file mode 100644
index 00000000000..2e37ecb8e92
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/lb.json
@@ -0,0 +1,104 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
+ "already_in_progress": "Konfiguratioun's Oflaf fir den Apparat ass schonn am gaangen.",
+ "not_konn_panel": "Keen erkannten Konnected.io Apparat",
+ "unknown": "Onbekannten Fehler opgetrueden"
+ },
+ "error": {
+ "cannot_connect": "Kann sech net mam Konnected Panel um {host}:{port} verbannen"
+ },
+ "step": {
+ "confirm": {
+ "description": "Modell: {model}\nHost: {host}\nPort: {port}\n\nDir k\u00ebnnt den I/O a Panel Verhaalen an de Konnected Alarm Panel Astellunge konfigur\u00e9ieren.",
+ "title": "Konnected Apparat parat"
+ },
+ "user": {
+ "data": {
+ "host": "Konnected Apparat IP Adress",
+ "port": "Konnected Apparat Port"
+ },
+ "description": "Informatioune vum Konnected Panel aginn.",
+ "title": "Konnected Apparat entdecken"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Keen erkannten Konnected.io Apparat"
+ },
+ "error": {
+ "one": "Ee",
+ "other": "M\u00e9i"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Op/Zou Zoustand vertauschen",
+ "name": "Numm (optional)",
+ "type": "Typ vun Bin\u00e4re Sensor"
+ },
+ "description": "Wiel d'Optioune fir den bin\u00e4ren Sensor dee mat {zone} verbonnen ass",
+ "title": "Bin\u00e4re Sensor konfigur\u00e9ieren"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Numm (optional)",
+ "poll_interval": "Intervall vun den Offroen (Minutten) (optional)",
+ "type": "Typ vum Sensor"
+ },
+ "description": "Wiel d'Optioune fir den digitale Sensor dee mat {zone} verbonnen ass",
+ "title": "Digitale Sensor konfigur\u00e9ieren"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zon 2",
+ "2": "Zon 1",
+ "3": "Zon 3",
+ "4": "Zon 4",
+ "5": "Zon 5",
+ "6": "Zon 6",
+ "7": "Zon 7",
+ "out": "OUT"
+ },
+ "description": "{model} um {host} entdeckt.\u00a0Wiel Basis Konfiguratioun vun den I/O hei dr\u00ebnner aus - ofh\u00e4ngeg vum I/O erlaabt et bin\u00e4r Sensoren (op / zou Kontakter), digital Sensoren (dht an ds18b20) oder schaltbar Ausgab. D\u00e9i detaill\u00e9iert Optioune k\u00ebnnen en an den n\u00e4chste Schr\u00ebtt konfigur\u00e9iert ginn.",
+ "title": "I/O konfigur\u00e9ieren"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zon 10",
+ "11": "Zon 11",
+ "12": "Zon 12",
+ "8": "Zon 8",
+ "9": "Zon 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "Wiel d'Konfiguratioun vun de verbleiwenden I/O hei dr\u00ebnner. D\u00e9i detaill\u00e9iert Optioune k\u00ebnnen en an den n\u00e4chste Schr\u00ebtt konfigur\u00e9iert ginn.",
+ "title": "Erweiderten I/O konfigur\u00e9ieren"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Blink panel LED un wann Status \u00c4nnerung gesch\u00e9ckt g\u00ebtt"
+ },
+ "description": "Wielt w.e.g. dat gew\u00ebnschte Verhalen fir \u00c4re Panel aus",
+ "title": "Divers Optioune astellen"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Ausgang wann un",
+ "momentary": "Pulsatiounsdauer (ms) (optional)",
+ "name": "Numm (optional)",
+ "pause": "Pausen zw\u00ebscht den Impulser (ms) (optional)",
+ "repeat": "Unzuel vu Widderhuelungen (-1= onendlech) (optional)"
+ },
+ "description": "Wielt w.e.g. d'Ausgaboptiounen fir {zone}",
+ "title": "\u00cbmschltbaren Ausgang konfigur\u00e9ieren"
+ }
+ },
+ "title": "Konnected Alarm Panneau Optiounen"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/nl.json b/homeassistant/components/konnected/.translations/nl.json
new file mode 100644
index 00000000000..1b6242b37f4
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/nl.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "unknown": "Onbekende fout opgetreden"
+ },
+ "step": {
+ "confirm": {
+ "title": "Konnected Apparaat Klaar"
+ },
+ "user": {
+ "data": {
+ "host": "IP-adres van Konnected apparaat",
+ "port": "Konnected apparaat poort"
+ },
+ "description": "Voer de host-informatie in voor uw Konnected-paneel.",
+ "title": "Ontdek Konnected Device"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Geen herkend Konnected.io apparaat"
+ },
+ "error": {
+ "one": "Leeg",
+ "other": "Leeg"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Keer de open / dicht status om",
+ "name": "Naam (optioneel)",
+ "type": "Type binaire sensor"
+ },
+ "title": "Binaire sensor configureren"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Naam (optioneel)",
+ "type": "Type sensor"
+ }
+ },
+ "options_io": {
+ "data": {
+ "1": "Zone 1",
+ "2": "Zone 2",
+ "3": "Zone 3",
+ "4": "Zone 4",
+ "5": "Zone 5",
+ "6": "Zone 6",
+ "7": "Zone 7"
+ }
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zone 10",
+ "11": "Zone 11",
+ "12": "Zone 12",
+ "8": "Zone 8",
+ "9": "Zone 9"
+ }
+ },
+ "options_switch": {
+ "data": {
+ "name": "Naam (optioneel)"
+ },
+ "title": "Schakelbare uitgang configureren"
+ }
+ },
+ "title": "Konnected Alarm Paneel Opties"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/no.json b/homeassistant/components/konnected/.translations/no.json
new file mode 100644
index 00000000000..569dac5756f
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/no.json
@@ -0,0 +1,100 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
+ "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet",
+ "unknown": "Ukjent feil oppstod"
+ },
+ "error": {
+ "cannot_connect": "Kan ikke koble til et Konnected Panel p\u00e5 {host} : {port}"
+ },
+ "step": {
+ "confirm": {
+ "description": "Modell: {model}\nVert: {host}\nPort: {port}\n\nDu kan konfigurere IO og panel atferd i Konnected Alarm Panel innstillinger.",
+ "title": "Konnected Enhet klar"
+ },
+ "user": {
+ "data": {
+ "host": "Konnected enhet IP-adresse",
+ "port": "Koblet enhetsport"
+ },
+ "description": "Vennligst skriv inn verten informasjon for din Konnected Panel.",
+ "title": "Oppdag Konnected Enheten"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Inverter \u00e5pen / lukk tilstand",
+ "name": "Navn (valgfritt)",
+ "type": "Bin\u00e6r sensortype"
+ },
+ "description": "Vennligst velg alternativer for bin\u00e6re sensor koblet til {sone}",
+ "title": "Konfigurer bin\u00e6r sensor"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Navn (valgfritt)",
+ "poll_interval": "Avstemningsintervall (minutter) (valgfritt)",
+ "type": "Sensortype"
+ },
+ "description": "Vennligst velg alternativene for den digitale sensor som er koblet til {sone}",
+ "title": "Konfigurere Digital Sensor"
+ },
+ "options_io": {
+ "data": {
+ "1": "Sone 1",
+ "2": "Sone 2",
+ "3": "Sone 3",
+ "4": "Sone 4",
+ "5": "Sone 5",
+ "6": "Sone 6",
+ "7": "Sone 7",
+ "out": "OUT"
+ },
+ "description": "Oppdaget en {model} hos {host} . Velg basiskonfigurasjon for hver I / O nedenfor - avhengig av I / O kan det gi rom for bin\u00e6re sensorer (\u00e5pne / lukke kontakter), digitale sensorer (dht og ds18b20), eller switchbare utganger. Du vil kunne konfigurere detaljerte alternativer i de neste trinnene.",
+ "title": "Konfigurere I/O"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Sone 10",
+ "11": "Sone 11",
+ "12": "Sone 12",
+ "8": "Sone 8",
+ "9": "Sone 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "Velg konfigurasjonen av de gjenv\u00e6rende I/O nedenfor. Du vil v\u00e6re i stand til \u00e5 konfigurere detaljerte alternativer i de neste trinnene.",
+ "title": "Konfigurer utvidet I / O"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Blink p\u00e5 LED-lampen n\u00e5r du sender statusendring"
+ },
+ "description": "Vennligst velg \u00f8nsket atferd for din panel",
+ "title": "Konfigurere Diverse"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Utgang n\u00e5r den er p\u00e5",
+ "momentary": "Pulsvarighet (ms) (valgfritt)",
+ "name": "Navn (valgfritt)",
+ "pause": "Pause mellom pulser (ms) (valgfritt)",
+ "repeat": "Tider \u00e5 gjenta (-1 = uendelig) (valgfritt)"
+ },
+ "description": "Velg outputalternativer for {zone}",
+ "title": "Konfigurere Valgbare Utgang"
+ }
+ },
+ "title": "Alternativer for Konnected Alarm Panel"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/pl.json b/homeassistant/components/konnected/.translations/pl.json
new file mode 100644
index 00000000000..b0d721891c0
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/pl.json
@@ -0,0 +1,106 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
+ "not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io",
+ "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d."
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z panelem Konnected na {host}:{port}"
+ },
+ "step": {
+ "confirm": {
+ "description": "Model: {model} \nHost: {host} \nPort: {port} \n\nMo\u017cesz skonfigurowa\u0107 IO i zachowanie panelu w ustawieniach Konnected Alarm Panel.",
+ "title": "Urz\u0105dzenie Konnected gotowe"
+ },
+ "user": {
+ "data": {
+ "host": "Adres IP urz\u0105dzenia Konnected",
+ "port": "Port urz\u0105dzenia Konnected urz\u0105dzenia"
+ },
+ "description": "Wprowad\u017a informacje o ho\u015bcie panelu Konnected.",
+ "title": "Wykryj urz\u0105dzenie Konnected"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io"
+ },
+ "error": {
+ "few": "kilka",
+ "many": "wiele",
+ "one": "jeden",
+ "other": "inny"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Odwr\u00f3\u0107 stan otwarty/zamkni\u0119ty",
+ "name": "Nazwa (opcjonalnie)",
+ "type": "Typ sensora binarnego"
+ },
+ "description": "Wybierz opcje dla sensora binarnego powi\u0105zanego ze {zone}",
+ "title": "Konfiguracja sensora binarnego"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Nazwa (opcjonalnie)",
+ "poll_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (minuty) (opcjonalnie)",
+ "type": "Typ sensora"
+ },
+ "description": "Wybierz opcje dla cyfrowego sensora powi\u0105zanego ze {zone}",
+ "title": "Konfiguracja sensora cyfrowego"
+ },
+ "options_io": {
+ "data": {
+ "1": "Strefa 1",
+ "2": "Strefa 2",
+ "3": "Strefa 3",
+ "4": "Strefa 4",
+ "5": "Strefa 5",
+ "6": "Strefa 6",
+ "7": "Strefa 7",
+ "out": "OUT"
+ },
+ "description": "Wykryto {model} na ho\u015bcie {host}. Wybierz podstawow\u0105 konfiguracj\u0119 ka\u017cdego wej\u015bcia/wyj\u015bcia poni\u017cej \u2014 w zale\u017cno\u015bci od typu wej\u015b\u0107/wyj\u015b\u0107 mo\u017ce zastosowa\u0107 sensory binarne (otwarte/ amkni\u0119te), sensory cyfrowe (dht i ds18b20) lub prze\u0142\u0105czane wyj\u015bcia. B\u0119dziesz m\u00f3g\u0142 skonfigurowa\u0107 szczeg\u00f3\u0142owe opcje w kolejnych krokach.",
+ "title": "Konfiguracja wej\u015bcia/wyj\u015bcia"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Strefa 10",
+ "11": "Strefa 11",
+ "12": "Strefa 12",
+ "8": "Strefa 8",
+ "9": "Strefa 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "Wybierz konfiguracj\u0119 pozosta\u0142ych wej\u015b\u0107/wyj\u015b\u0107 poni\u017cej. B\u0119dziesz m\u00f3g\u0142 skonfigurowa\u0107 szczeg\u00f3\u0142owe opcje w kolejnych krokach.",
+ "title": "Konfiguracja rozszerzonego wej\u015bcia/wyj\u015bcia"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Miganie diody LED panelu podczas wysy\u0142ania zmiany stanu"
+ },
+ "description": "Wybierz po\u017c\u0105dane zachowanie dla swojego panelu",
+ "title": "R\u00f3\u017cne opcje"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Wyj\u015bcie, gdy w\u0142\u0105czone",
+ "momentary": "Czas trwania impulsu (ms) (opcjonalnie)",
+ "name": "Nazwa (opcjonalnie)",
+ "pause": "Przerwa mi\u0119dzy impulsami (ms) (opcjonalnie)",
+ "repeat": "Ilo\u015b\u0107 powt\u00f3rze\u0144 (-1=niesko\u0144czenie) (opcjonalnie)"
+ },
+ "description": "Wybierz opcje wyj\u015bcia dla {zone}",
+ "title": "Konfiguracja prze\u0142\u0105czalne wyj\u015bcie"
+ }
+ },
+ "title": "Opcje panelu alarmu Konnected"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/ru.json b/homeassistant/components/konnected/.translations/ru.json
new file mode 100644
index 00000000000..25cb03b1578
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/ru.json
@@ -0,0 +1,100 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e.",
+ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\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 \u043f\u0430\u043d\u0435\u043b\u0438 Konnected {host}:{port}."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u041c\u043e\u0434\u0435\u043b\u044c: {model}\n\u0425\u043e\u0441\u0442: {host}\n\u041f\u043e\u0440\u0442: {port}\n\n\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043b\u043e\u0433\u0438\u043a\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u043f\u0430\u043d\u0435\u043b\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0445\u043e\u0434\u043e\u0432 \u0438 \u0432\u044b\u0445\u043e\u0434\u043e\u0432 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u0430\u043d\u0435\u043b\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Konnected.",
+ "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected \u0433\u043e\u0442\u043e\u0432\u043e \u043a \u0440\u0430\u0431\u043e\u0442\u0435."
+ },
+ "user": {
+ "data": {
+ "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441",
+ "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 \u043f\u0430\u043d\u0435\u043b\u0438 Konnected.",
+ "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e."
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0435/\u0437\u0430\u043a\u0440\u044b\u0442\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
+ "type": "\u0422\u0438\u043f \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
+ },
+ "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a {zone}.",
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
+ },
+ "options_digital": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
+ "poll_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 \u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
+ "type": "\u0422\u0438\u043f \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
+ },
+ "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u0435\u043d\u0441\u043e\u0440\u0430, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a {zone}.",
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
+ },
+ "options_io": {
+ "data": {
+ "1": "\u0417\u043e\u043d\u0430 1",
+ "2": "\u0417\u043e\u043d\u0430 2",
+ "3": "\u0417\u043e\u043d\u0430 3",
+ "4": "\u0417\u043e\u043d\u0430 4",
+ "5": "\u0417\u043e\u043d\u0430 5",
+ "6": "\u0417\u043e\u043d\u0430 6",
+ "7": "\u0417\u043e\u043d\u0430 7",
+ "out": "\u0412\u042b\u0425\u041e\u0414"
+ },
+ "description": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {model} \u0441 \u0430\u0434\u0440\u0435\u0441\u043e\u043c {host}. \u0412 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432, \u043a \u043f\u0430\u043d\u0435\u043b\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b (\u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u044f/\u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b (dht \u0438 ds18b20) \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u044b\u0435 \u0432\u044b\u0445\u043e\u0434\u044b. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0448\u0430\u0433\u0430\u0445.",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "\u0417\u043e\u043d\u0430 10",
+ "11": "\u0417\u043e\u043d\u0430 11",
+ "12": "\u0417\u043e\u043d\u0430 12",
+ "8": "\u0417\u043e\u043d\u0430 8",
+ "9": "\u0417\u043e\u043d\u0430 9",
+ "alarm1": "\u0422\u0420\u0415\u0412\u041e\u0413\u04101",
+ "alarm2_out2": "\u0412\u042b\u0425\u041e\u04142/\u0422\u0420\u0415\u0412\u041e\u0413\u04102",
+ "out1": "\u0412\u042b\u0425\u041e\u04141"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0448\u0430\u0433\u0430\u0445.",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "LED-\u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0436\u0435\u043b\u0430\u0435\u043c\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u043f\u0430\u043d\u0435\u043b\u0438.",
+ "title": "\u041f\u0440\u043e\u0447\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "\u0412\u044b\u0445\u043e\u0434 \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438",
+ "momentary": "\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
+ "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0435\u0436\u0434\u0443 \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
+ "repeat": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0439 (-1 = \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u044b\u0445\u043e\u0434\u0430 \u0434\u043b\u044f {zone}.",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u043e\u0433\u043e \u0432\u044b\u0445\u043e\u0434\u0430"
+ }
+ },
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Konnected"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/sl.json b/homeassistant/components/konnected/.translations/sl.json
new file mode 100644
index 00000000000..38396d0832d
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/sl.json
@@ -0,0 +1,106 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Naprava je \u017ee konfigurirana",
+ "already_in_progress": "Konfiguracijski tok za napravo je \u017ee v teku.",
+ "not_konn_panel": "Ni prepoznana kot Konnected.io naprava",
+ "unknown": "Pri\u0161lo je do neznane napake"
+ },
+ "error": {
+ "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s Konnected plo\u0161\u010do v {Host}: {Port}"
+ },
+ "step": {
+ "confirm": {
+ "description": "Model: {model}\nGostitelj: {host}\nVrata: {port}\n\nV nastavitvah lahko nastavite vedenje I / O in plo\u0161\u010de Konnected alarma. ",
+ "title": "Konnected naprava pripravljena"
+ },
+ "user": {
+ "data": {
+ "host": "IP-naslov Konnected naprave",
+ "port": "Vrata Konnected naprave"
+ },
+ "description": "Vnesite podatke o gostitelju v svoj Konnected Panel.",
+ "title": "Odkrijte Konnected napravo"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Ni prepoznana kot Konnected.io naprava"
+ },
+ "error": {
+ "few": "nekaj",
+ "one": "ena",
+ "other": "drugo",
+ "two": "dva"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Zamenjajte odprto / zaprto stanje",
+ "name": "Ime (neobvezno)",
+ "type": "Vrsta binarnega senzorja"
+ },
+ "description": "Izberite mo\u017enosti za binarni senzor, priklju\u010den na {zone}",
+ "title": "Konfigurirajte binarni senzor"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Ime (neobvezno)",
+ "poll_interval": "Interval osve\u017eevanja (minute) (neobvezno)",
+ "type": "Vrsta tipala"
+ },
+ "description": "Izberite mo\u017enosti za digitalni senzor, priklju\u010den na {zone}",
+ "title": "Konfigurirajte digitalni senzor"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zone 1",
+ "2": "Zone 2",
+ "3": "Zone 3",
+ "4": "Zone 4",
+ "5": "Zone 5",
+ "6": "Zone 6",
+ "7": "Zone 7",
+ "out": "OUT"
+ },
+ "description": "Odkrili {model} na {host} . Spodaj izberite osnovno konfiguracijo vsakega I / O - odvisno od I / O lahko omogo\u010da binarne senzorje (odpiranje / zapiranje kontaktov), digitalne senzorje (dht in ds18b20) ali preklopne izhode. Podrobne mo\u017enosti boste lahko konfigurirali v naslednjih korakih.",
+ "title": "Konfigurirajte I / O"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Obmo\u010dje 10",
+ "11": "Obmo\u010dje 11",
+ "12": "Obmo\u010dje 12",
+ "8": "Obmo\u010dje 8",
+ "9": "Obmo\u010dje 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2 / ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "Izberite konfiguracijo preostalega I/O spodaj. Podrobne mo\u017enosti boste lahko konfigurirali v naslednjih korakih.",
+ "title": "Konfigurirajte raz\u0161irjeni I/O"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Lu\u010dka LED na zaslonu utripa, ko po\u0161iljate spremembo stanja"
+ },
+ "description": "Izberite \u017eeleno vedenje za va\u0161o plo\u0161\u010do",
+ "title": "Konfigurirajte Razno"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Izhod, ko je vklopljen",
+ "momentary": "Trajanje impulza (ms) (neobvezno)",
+ "name": "Ime (neobvezno)",
+ "pause": "Premor med impulzi (ms) (neobvezno)",
+ "repeat": "\u010casi ponovitve (-1 = neskon\u010dno) (neobvezno)"
+ },
+ "description": "Izberite izhodne mo\u017enosti za {zone}",
+ "title": "Konfigurirajte preklopni izhod"
+ }
+ },
+ "title": "Mo\u017enosti Konnected alarm-a"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/sv.json b/homeassistant/components/konnected/.translations/sv.json
new file mode 100644
index 00000000000..7e035264215
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/sv.json
@@ -0,0 +1,104 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten \u00e4r redan konfigurerad",
+ "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan.",
+ "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet",
+ "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat"
+ },
+ "error": {
+ "cannot_connect": "Det g\u00e5r inte att ansluta till en ansluten panel p\u00e5 {host}:{port}"
+ },
+ "step": {
+ "confirm": {
+ "description": "Modell: {modell}\nV\u00e4rd: {host}\nPort: {port}\n\nDu kan konfigurera IO- och panelbeteendet i inst\u00e4llningarna f\u00f6r Konnected Alarm Panel.",
+ "title": "Konnected-enheten redo"
+ },
+ "user": {
+ "data": {
+ "host": "Konnected-enhetens IP-adress",
+ "port": "Konnected-enhetens port"
+ },
+ "description": "Ange v\u00e4rdinformationen f\u00f6r din Konnected Panel.",
+ "title": "Uppt\u00e4ck Konnected-enhet"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet"
+ },
+ "error": {
+ "one": "Tom",
+ "other": "Tomma"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Invertera \u00f6ppet/st\u00e4ngt tillst\u00e5nd",
+ "name": "Namn (valfritt)",
+ "type": "Bin\u00e4r sensortyp"
+ },
+ "description": "V\u00e4lj alternativ f\u00f6r den bin\u00e4ra sensorn som \u00e4r ansluten till {zone}",
+ "title": "Konfigurera Bin\u00e4r Sensor"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Namn (valfritt)",
+ "poll_interval": "H\u00e4mtningsintervall (minuter) (valfritt)",
+ "type": "Sensortyp"
+ },
+ "description": "V\u00e4lj alternativ f\u00f6r den digitala sensorn som \u00e4r ansluten till {zone}",
+ "title": "Konfigurera Digital Sensor"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zon 1",
+ "2": "Zon 2",
+ "3": "Zon 3",
+ "4": "Zon 4",
+ "5": "Zon 5",
+ "6": "Zon 6",
+ "7": "Zon 7",
+ "out": "UT"
+ },
+ "description": "Uppt\u00e4ckte en {model} p\u00e5 {host}. V\u00e4lj baskonfigurationen f\u00f6r varje I/O nedan - beroende p\u00e5 I/O kan det m\u00f6jligg\u00f6ra bin\u00e4ra sensorer (\u00f6ppen/st\u00e4ngd kontakter), digitala sensorer (dht och ds18b20) eller omkopplingsbara utg\u00e5ngar. Du kan konfigurera detaljerade alternativ i n\u00e4sta steg.",
+ "title": "Konfigurera I/O"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zon 10",
+ "11": "Zon 11",
+ "12": "Zon 12",
+ "8": "Zon 8",
+ "9": "Zon 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "V\u00e4lj den konfiguration av resterande I/O nedan. Du kommer att kunna konfigurera detaljerade alternativ i n\u00e4sta steg.",
+ "title": "Konfigurera ut\u00f6kat I/O"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "Blinka p\u00e5 panel-LED n\u00e4r du skickar tillst\u00e5nds\u00e4ndring"
+ },
+ "description": "V\u00e4lj \u00f6nskat beteende f\u00f6r din panel",
+ "title": "Konfigurera \u00d6vrigt"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Utdata n\u00e4r den \u00e4r p\u00e5",
+ "momentary": "Pulsvarighet (ms) (valfritt)",
+ "name": "Namn (valfritt)",
+ "pause": "Paus mellan pulser (ms) (valfritt)",
+ "repeat": "G\u00e5nger att upprepa (-1=o\u00e4ndligt) (tillval)"
+ },
+ "description": "V\u00e4lj utdataalternativ f\u00f6r {zone}",
+ "title": "Konfigurera v\u00e4xelbar utdata"
+ }
+ },
+ "title": "Alternativ f\u00f6r Konnected alarmpanel"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/zh-Hans.json b/homeassistant/components/konnected/.translations/zh-Hans.json
new file mode 100644
index 00000000000..2bba1260764
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/zh-Hans.json
@@ -0,0 +1,9 @@
+{
+ "options": {
+ "step": {
+ "options_switch": {
+ "description": "\u8bf7\u9009\u62e9 {zone}\u8f93\u51fa\u9009\u9879"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/.translations/zh-Hant.json b/homeassistant/components/konnected/.translations/zh-Hant.json
new file mode 100644
index 00000000000..0ecd6c9fc25
--- /dev/null
+++ b/homeassistant/components/konnected/.translations/zh-Hant.json
@@ -0,0 +1,100 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099",
+ "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Konnected \u9762\u677f\uff1a{host}:{port}"
+ },
+ "step": {
+ "confirm": {
+ "description": "\u578b\u865f\uff1a{model}\n\u4e3b\u6a5f\u7aef\uff1a{host}\n\u901a\u8a0a\u57e0\uff1a{port}\n\n\u53ef\u4ee5\u65bc Konncected \u8b66\u5831\u9762\u677f\u8a2d\u5b9a\u4e2d\u8a2d\u5b9a IO \u8207\u9762\u677f\u884c\u70ba\u3002",
+ "title": "Konnected \u8a2d\u5099\u5df2\u5099\u59a5"
+ },
+ "user": {
+ "data": {
+ "host": "Konnected \u8a2d\u5099 IP \u4f4d\u5740",
+ "port": "Konnected \u8a2d\u5099\u901a\u8a0a\u57e0"
+ },
+ "description": "\u8acb\u8f38\u5165 Konnected \u9762\u677f\u4e3b\u6a5f\u7aef\u8cc7\u8a0a\u3002",
+ "title": "\u641c\u7d22 Konnected \u8a2d\u5099"
+ }
+ },
+ "title": "Konnected.io"
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "\u53cd\u8f49\u958b\u555f/\u95dc\u9589\u72c0\u614b",
+ "name": "\u540d\u7a31\uff08\u9078\u9805\uff09",
+ "type": "\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u985e\u578b"
+ },
+ "description": "\u8acb\u9078\u64c7\u6b78\u7d0d\u70ba {zone}\u7684\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u8f38\u51fa\u9078\u9805",
+ "title": "\u8a2d\u5b9a\u4e8c\u9032\u4f4d\u611f\u61c9\u5668"
+ },
+ "options_digital": {
+ "data": {
+ "name": "\u540d\u7a31\uff08\u9078\u9805\uff09",
+ "poll_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u5206\u9418\uff09\uff08\u9078\u9805\uff09",
+ "type": "\u611f\u61c9\u5668\u985e\u578b"
+ },
+ "description": "\u8acb\u9078\u64c7\u6b78\u7d0d\u70ba {zone}\u7684\u6578\u4f4d\u611f\u61c9\u5668\u8f38\u51fa\u9078\u9805",
+ "title": "\u8a2d\u5b9a\u6578\u4f4d\u611f\u61c9\u5668"
+ },
+ "options_io": {
+ "data": {
+ "1": "\u5340\u57df 1",
+ "2": "\u5340\u57df 2",
+ "3": "\u5340\u57df 3",
+ "4": "\u5340\u57df 4",
+ "5": "\u5340\u57df 5",
+ "6": "\u5340\u57df 6",
+ "7": "\u5340\u57df 7",
+ "out": "OUT"
+ },
+ "description": "\u65bc {host} \u767c\u73fe {model}\u3002\u8acb\u65bc\u4e0b\u65b9\u6bcf\u4e00\u500b I/O \u9078\u64c7\u57fa\u672c\u8a2d\u5b9a - \u96a8\u8457 I/O \u4e0d\u540c\uff0c\u53ef\u5141\u8a31\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\uff08\u958b\u555f/\u95dc\u9589\u72c0\u614b\uff09\u3001\u6578\u4f4d\u611f\u61c9\u5668\uff08DHT \u53ca ds18b20\uff09\uff0c\u6216\u8005\u53ef\u5207\u63db\u8f38\u51fa\u3002\u53ef\u4ee5\u65bc\u4e0b\u4e00\u6b65\u8a2d\u5b9a\u8a73\u7d30\u9078\u9805\u3002",
+ "title": "\u8a2d\u5b9a I/O"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "\u5340\u57df 10",
+ "11": "\u5340\u57df 11",
+ "12": "\u5340\u57df 12",
+ "8": "\u5340\u57df 8",
+ "9": "\u5340\u57df 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "\u9078\u64c7\u4e0b\u65b9\u5269\u9918 I/O \u8a2d\u5b9a\u3002\u53ef\u4ee5\u65bc\u4e0b\u4e00\u6b65\u8a2d\u5b9a\u8a73\u7d30\u9078\u9805\u3002",
+ "title": "\u8a2d\u5b9a\u5ef6\u4f38 I/O"
+ },
+ "options_misc": {
+ "data": {
+ "blink": "\u7576\u50b3\u9001\u72c0\u614b\u8b8a\u66f4\u6642\u3001\u9583\u720d\u9762\u677f LED"
+ },
+ "description": "\u8acb\u9078\u64c7\u9762\u677f\u671f\u671b\u884c\u70ba",
+ "title": "\u5176\u4ed6\u8a2d\u5b9a"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "\u958b\u555f\u6642\u8f38\u51fa",
+ "momentary": "\u6301\u7e8c\u6642\u9593\uff08ms\uff09\uff08\u9078\u9805\uff09",
+ "name": "\u540d\u7a31\uff08\u9078\u9805\uff09",
+ "pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09\uff08\u9078\u9805\uff09",
+ "repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09\uff08\u9078\u9805\uff09"
+ },
+ "description": "\u8acb\u9078\u64c7 {zone}\u8f38\u51fa\u9078\u9805",
+ "title": "\u8a2d\u5b9a Switchable \u8f38\u51fa"
+ }
+ },
+ "title": "Konnected \u8b66\u5831\u9762\u677f\u9078\u9805"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py
index 28e62c322ad..94508b01483 100644
--- a/homeassistant/components/konnected/__init__.py
+++ b/homeassistant/components/konnected/__init__.py
@@ -1,20 +1,20 @@
"""Support for Konnected devices."""
import asyncio
+import copy
import hmac
import json
import logging
from aiohttp.hdrs import AUTHORIZATION
from aiohttp.web import Request, Response
-import konnected
import voluptuous as vol
+from homeassistant import config_entries
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
-from homeassistant.components.discovery import SERVICE_KONNECTED
from homeassistant.components.http import HomeAssistantView
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
- ATTR_STATE,
CONF_ACCESS_TOKEN,
CONF_BINARY_SENSORS,
CONF_DEVICES,
@@ -27,45 +27,106 @@ from homeassistant.const import (
CONF_SWITCHES,
CONF_TYPE,
CONF_ZONE,
- EVENT_HOMEASSISTANT_START,
HTTP_BAD_REQUEST,
HTTP_NOT_FOUND,
HTTP_UNAUTHORIZED,
+ STATE_OFF,
STATE_ON,
)
-from homeassistant.helpers import config_validation as cv, discovery
-from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import config_validation as cv
+from .config_flow import ( # Loading the config flow file will register the flow
+ CONF_DEFAULT_OPTIONS,
+ CONF_IO,
+ CONF_IO_BIN,
+ CONF_IO_DIG,
+ CONF_IO_SWI,
+ OPTIONS_SCHEMA,
+)
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,
DOMAIN,
- ENDPOINT_ROOT,
PIN_TO_ZONE,
- SIGNAL_SENSOR_UPDATE,
STATE_HIGH,
STATE_LOW,
UPDATE_ENDPOINT,
ZONE_TO_PIN,
+ ZONES,
)
+from .errors import CannotConnect
from .handlers import HANDLERS
+from .panel import AlarmPanel
_LOGGER = logging.getLogger(__name__)
-_BINARY_SENSOR_SCHEMA = vol.All(
+
+def ensure_pin(value):
+ """Check if valid pin and coerce to string."""
+ if value is None:
+ raise vol.Invalid("pin value is None")
+
+ if PIN_TO_ZONE.get(str(value)) is None:
+ raise vol.Invalid("pin not valid")
+
+ return str(value)
+
+
+def ensure_zone(value):
+ """Check if valid zone and coerce to string."""
+ if value is None:
+ raise vol.Invalid("zone value is None")
+
+ if str(value) not in ZONES is None:
+ raise vol.Invalid("zone not valid")
+
+ return str(value)
+
+
+def import_validator(config):
+ """Validate zones and reformat for import."""
+ config = copy.deepcopy(config)
+ io_cfgs = {}
+ # Replace pins with zones
+ for conf_platform, conf_io in (
+ (CONF_BINARY_SENSORS, CONF_IO_BIN),
+ (CONF_SENSORS, CONF_IO_DIG),
+ (CONF_SWITCHES, CONF_IO_SWI),
+ ):
+ for zone in config.get(conf_platform, []):
+ if zone.get(CONF_PIN):
+ zone[CONF_ZONE] = PIN_TO_ZONE[zone[CONF_PIN]]
+ del zone[CONF_PIN]
+ io_cfgs[zone[CONF_ZONE]] = conf_io
+
+ # Migrate config_entry data into default_options structure
+ config[CONF_IO] = io_cfgs
+ config[CONF_DEFAULT_OPTIONS] = OPTIONS_SCHEMA(config)
+
+ # clean up fields migrated to options
+ config.pop(CONF_BINARY_SENSORS, None)
+ config.pop(CONF_SENSORS, None)
+ config.pop(CONF_SWITCHES, None)
+ config.pop(CONF_BLINK, None)
+ config.pop(CONF_DISCOVERY, None)
+ config.pop(CONF_IO, None)
+ return config
+
+
+# configuration.yaml schemas (legacy)
+BINARY_SENSOR_SCHEMA_YAML = vol.All(
vol.Schema(
{
- vol.Exclusive(CONF_PIN, "s_pin"): vol.Any(*PIN_TO_ZONE),
- vol.Exclusive(CONF_ZONE, "s_pin"): vol.Any(*ZONE_TO_PIN),
+ vol.Exclusive(CONF_ZONE, "s_io"): ensure_zone,
+ vol.Exclusive(CONF_PIN, "s_io"): ensure_pin,
vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_INVERSE, default=False): cv.boolean,
@@ -74,14 +135,14 @@ _BINARY_SENSOR_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_PIN, CONF_ZONE),
)
-_SENSOR_SCHEMA = vol.All(
+SENSOR_SCHEMA_YAML = vol.All(
vol.Schema(
{
- vol.Exclusive(CONF_PIN, "s_pin"): vol.Any(*PIN_TO_ZONE),
- vol.Exclusive(CONF_ZONE, "s_pin"): vol.Any(*ZONE_TO_PIN),
+ vol.Exclusive(CONF_ZONE, "s_io"): ensure_zone,
+ vol.Exclusive(CONF_PIN, "s_io"): ensure_pin,
vol.Required(CONF_TYPE): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])),
vol.Optional(CONF_NAME): cv.string,
- vol.Optional(CONF_POLL_INTERVAL): vol.All(
+ vol.Optional(CONF_POLL_INTERVAL, default=3): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
}
@@ -89,11 +150,11 @@ _SENSOR_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_PIN, CONF_ZONE),
)
-_SWITCH_SCHEMA = vol.All(
+SWITCH_SCHEMA_YAML = vol.All(
vol.Schema(
{
- vol.Exclusive(CONF_PIN, "a_pin"): vol.Any(*PIN_TO_ZONE),
- vol.Exclusive(CONF_ZONE, "a_pin"): vol.Any(*ZONE_TO_PIN),
+ vol.Exclusive(CONF_ZONE, "s_io"): ensure_zone,
+ vol.Exclusive(CONF_PIN, "s_io"): ensure_pin,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All(
vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)
@@ -106,6 +167,24 @@ _SWITCH_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_PIN, CONF_ZONE),
)
+DEVICE_SCHEMA_YAML = vol.All(
+ vol.Schema(
+ {
+ vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"),
+ vol.Optional(CONF_BINARY_SENSORS): vol.All(
+ cv.ensure_list, [BINARY_SENSOR_SCHEMA_YAML]
+ ),
+ vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA_YAML]),
+ vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA_YAML]),
+ vol.Inclusive(CONF_HOST, "host_info"): cv.string,
+ vol.Inclusive(CONF_PORT, "host_info"): cv.port,
+ vol.Optional(CONF_BLINK, default=True): cv.boolean,
+ vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
+ }
+ ),
+ import_validator,
+)
+
# pylint: disable=no-value-for-parameter
CONFIG_SCHEMA = vol.Schema(
{
@@ -113,352 +192,88 @@ CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_API_HOST): vol.Url(),
- vol.Required(CONF_DEVICES): [
- {
- vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"),
- vol.Optional(CONF_BINARY_SENSORS): vol.All(
- cv.ensure_list, [_BINARY_SENSOR_SCHEMA]
- ),
- vol.Optional(CONF_SENSORS): vol.All(
- cv.ensure_list, [_SENSOR_SCHEMA]
- ),
- vol.Optional(CONF_SWITCHES): vol.All(
- cv.ensure_list, [_SWITCH_SCHEMA]
- ),
- vol.Optional(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT): cv.port,
- vol.Optional(CONF_BLINK, default=True): cv.boolean,
- vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
- }
- ],
+ vol.Optional(CONF_DEVICES): vol.All(
+ cv.ensure_list, [DEVICE_SCHEMA_YAML]
+ ),
}
)
},
extra=vol.ALLOW_EXTRA,
)
+YAML_CONFIGS = "yaml_configs"
+PLATFORMS = ["binary_sensor", "sensor", "switch"]
-async def async_setup(hass, config):
+
+async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Konnected platform."""
cfg = config.get(DOMAIN)
if cfg is None:
cfg = {}
- access_token = cfg.get(CONF_ACCESS_TOKEN)
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {
- CONF_ACCESS_TOKEN: access_token,
+ CONF_ACCESS_TOKEN: cfg.get(CONF_ACCESS_TOKEN),
CONF_API_HOST: cfg.get(CONF_API_HOST),
+ CONF_DEVICES: {},
}
- def setup_device(host, port):
- """Set up a Konnected device at `host` listening on `port`."""
- discovered = DiscoveredDevice(hass, host, port)
- if discovered.is_configured:
- discovered.setup()
- else:
- _LOGGER.warning(
- "Konnected device %s was discovered on the network"
- " but not specified in configuration.yaml",
- discovered.device_id,
+ hass.http.register_view(KonnectedView)
+
+ # Check if they have yaml configured devices
+ if CONF_DEVICES not in cfg:
+ return True
+
+ for device in cfg.get(CONF_DEVICES, []):
+ # Attempt to importing the cfg. Use
+ # hass.async_add_job to avoid a deadlock.
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=device,
)
-
- def device_discovered(service, info):
- """Call when a Konnected device has been discovered."""
- host = info.get(CONF_HOST)
- port = info.get(CONF_PORT)
- setup_device(host, port)
-
- async def manual_discovery(event):
- """Init devices on the network with manually assigned addresses."""
- specified = [
- dev
- for dev in cfg.get(CONF_DEVICES)
- if dev.get(CONF_HOST) and dev.get(CONF_PORT)
- ]
-
- while specified:
- for dev in specified:
- _LOGGER.debug(
- "Discovering Konnected device %s at %s:%s",
- dev.get(CONF_ID),
- dev.get(CONF_HOST),
- dev.get(CONF_PORT),
- )
- try:
- await hass.async_add_executor_job(
- setup_device, dev.get(CONF_HOST), dev.get(CONF_PORT)
- )
- specified.remove(dev)
- except konnected.Client.ClientError as err:
- _LOGGER.error(err)
- await asyncio.sleep(10) # try again in 10 seconds
-
- # Initialize devices specified in the configuration on boot
- for device in cfg.get(CONF_DEVICES):
- ConfiguredDevice(hass, device, config).save_data()
-
- discovery.async_listen(hass, SERVICE_KONNECTED, device_discovered)
-
- hass.http.register_view(KonnectedView(access_token))
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, manual_discovery)
-
+ )
return True
-class ConfiguredDevice:
- """A representation of a configured Konnected device."""
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up panel from a config entry."""
+ client = AlarmPanel(hass, entry)
+ # create a data store in hass.data[DOMAIN][CONF_DEVICES]
+ await client.async_save_data()
- def __init__(self, hass, config, hass_config):
- """Initialize the Konnected device."""
- self.hass = hass
- self.config = config
- self.hass_config = hass_config
+ try:
+ await client.async_connect()
+ except CannotConnect:
+ # this will trigger a retry in the future
+ raise config_entries.ConfigEntryNotReady
- @property
- def device_id(self):
- """Device id is the MAC address as string with punctuation removed."""
- return self.config.get(CONF_ID)
-
- def save_data(self):
- """Save the device configuration to `hass.data`."""
- binary_sensors = {}
- for entity in self.config.get(CONF_BINARY_SENSORS) or []:
- if CONF_ZONE in entity:
- pin = ZONE_TO_PIN[entity[CONF_ZONE]]
- else:
- pin = entity[CONF_PIN]
-
- binary_sensors[pin] = {
- CONF_TYPE: entity[CONF_TYPE],
- CONF_NAME: entity.get(
- CONF_NAME,
- "Konnected {} Zone {}".format(self.device_id[6:], PIN_TO_ZONE[pin]),
- ),
- CONF_INVERSE: entity.get(CONF_INVERSE),
- ATTR_STATE: None,
- }
- _LOGGER.debug(
- "Set up binary_sensor %s (initial state: %s)",
- binary_sensors[pin].get("name"),
- binary_sensors[pin].get(ATTR_STATE),
- )
-
- actuators = []
- for entity in self.config.get(CONF_SWITCHES) or []:
- if CONF_ZONE in entity:
- pin = ZONE_TO_PIN[entity[CONF_ZONE]]
- else:
- pin = entity[CONF_PIN]
-
- act = {
- CONF_PIN: pin,
- CONF_NAME: entity.get(
- CONF_NAME,
- "Konnected {} Actuator {}".format(
- self.device_id[6:], PIN_TO_ZONE[pin]
- ),
- ),
- ATTR_STATE: None,
- CONF_ACTIVATION: entity[CONF_ACTIVATION],
- CONF_MOMENTARY: entity.get(CONF_MOMENTARY),
- CONF_PAUSE: entity.get(CONF_PAUSE),
- CONF_REPEAT: entity.get(CONF_REPEAT),
- }
- actuators.append(act)
- _LOGGER.debug("Set up switch %s", act)
-
- sensors = []
- for entity in self.config.get(CONF_SENSORS) or []:
- if CONF_ZONE in entity:
- pin = ZONE_TO_PIN[entity[CONF_ZONE]]
- else:
- pin = entity[CONF_PIN]
-
- sensor = {
- CONF_PIN: pin,
- CONF_NAME: entity.get(
- CONF_NAME,
- "Konnected {} Sensor {}".format(
- self.device_id[6:], PIN_TO_ZONE[pin]
- ),
- ),
- CONF_TYPE: entity[CONF_TYPE],
- CONF_POLL_INTERVAL: entity.get(CONF_POLL_INTERVAL),
- }
- sensors.append(sensor)
- _LOGGER.debug(
- "Set up %s sensor %s (initial state: %s)",
- sensor.get(CONF_TYPE),
- sensor.get(CONF_NAME),
- sensor.get(ATTR_STATE),
- )
-
- device_data = {
- CONF_BINARY_SENSORS: binary_sensors,
- CONF_SENSORS: sensors,
- CONF_SWITCHES: actuators,
- CONF_BLINK: self.config.get(CONF_BLINK),
- CONF_DISCOVERY: self.config.get(CONF_DISCOVERY),
- }
-
- if CONF_DEVICES not in self.hass.data[DOMAIN]:
- self.hass.data[DOMAIN][CONF_DEVICES] = {}
-
- _LOGGER.debug(
- "Storing data in hass.data[%s][%s][%s]: %s",
- DOMAIN,
- CONF_DEVICES,
- self.device_id,
- device_data,
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
)
- self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data
-
- for platform in ["binary_sensor", "sensor", "switch"]:
- discovery.load_platform(
- self.hass,
- platform,
- DOMAIN,
- {"device_id": self.device_id},
- self.hass_config,
- )
+ entry.add_update_listener(async_entry_updated)
+ return True
-class DiscoveredDevice:
- """A representation of a discovered Konnected device."""
-
- def __init__(self, hass, host, port):
- """Initialize the Konnected device."""
- self.hass = hass
- self.host = host
- self.port = port
-
- self.client = konnected.Client(host, str(port))
- self.status = self.client.get_status()
-
- def setup(self):
- """Set up a newly discovered Konnected device."""
- _LOGGER.info(
- "Discovered Konnected device %s. Open http://%s:%s in a "
- "web browser to view device status.",
- self.device_id,
- self.host,
- self.port,
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
)
- self.save_data()
- self.update_initial_states()
- self.sync_device_config()
+ )
+ if unload_ok:
+ hass.data[DOMAIN][CONF_DEVICES].pop(entry.data[CONF_ID])
- def save_data(self):
- """Save the discovery information to `hass.data`."""
- self.stored_configuration["client"] = self.client
- self.stored_configuration["host"] = self.host
- self.stored_configuration["port"] = self.port
+ return unload_ok
- @property
- def device_id(self):
- """Device id is the MAC address as string with punctuation removed."""
- return self.status["mac"].replace(":", "")
- @property
- def is_configured(self):
- """Return true if device_id is specified in the configuration."""
- return bool(self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id))
-
- @property
- def stored_configuration(self):
- """Return the configuration stored in `hass.data` for this device."""
- return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)
-
- def binary_sensor_configuration(self):
- """Return the configuration map for syncing binary sensors."""
- return [{"pin": p} for p in self.stored_configuration[CONF_BINARY_SENSORS]]
-
- def actuator_configuration(self):
- """Return the configuration map for syncing actuators."""
- return [
- {
- "pin": data.get(CONF_PIN),
- "trigger": (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] else 1),
- }
- for data in self.stored_configuration[CONF_SWITCHES]
- ]
-
- def dht_sensor_configuration(self):
- """Return the configuration map for syncing DHT sensors."""
- return [
- {CONF_PIN: sensor[CONF_PIN], CONF_POLL_INTERVAL: sensor[CONF_POLL_INTERVAL]}
- for sensor in self.stored_configuration[CONF_SENSORS]
- if sensor[CONF_TYPE] == "dht"
- ]
-
- def ds18b20_sensor_configuration(self):
- """Return the configuration map for syncing DS18B20 sensors."""
- return [
- {"pin": sensor[CONF_PIN]}
- for sensor in self.stored_configuration[CONF_SENSORS]
- if sensor[CONF_TYPE] == "ds18b20"
- ]
-
- def update_initial_states(self):
- """Update the initial state of each sensor from status poll."""
- for sensor_data in self.status.get("sensors"):
- sensor_config = self.stored_configuration[CONF_BINARY_SENSORS].get(
- sensor_data.get(CONF_PIN), {}
- )
- entity_id = sensor_config.get(ATTR_ENTITY_ID)
-
- state = bool(sensor_data.get(ATTR_STATE))
- if sensor_config.get(CONF_INVERSE):
- state = not state
-
- dispatcher_send(self.hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state)
-
- def desired_settings_payload(self):
- """Return a dict representing the desired device configuration."""
- desired_api_host = (
- self.hass.data[DOMAIN].get(CONF_API_HOST) or self.hass.config.api.base_url
- )
- desired_api_endpoint = desired_api_host + ENDPOINT_ROOT
-
- return {
- "sensors": self.binary_sensor_configuration(),
- "actuators": self.actuator_configuration(),
- "dht_sensors": self.dht_sensor_configuration(),
- "ds18b20_sensors": self.ds18b20_sensor_configuration(),
- "auth_token": self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN),
- "endpoint": desired_api_endpoint,
- "blink": self.stored_configuration.get(CONF_BLINK),
- "discovery": self.stored_configuration.get(CONF_DISCOVERY),
- }
-
- def current_settings_payload(self):
- """Return a dict of configuration currently stored on the device."""
- settings = self.status["settings"]
- if not settings:
- settings = {}
-
- return {
- "sensors": [{"pin": s[CONF_PIN]} for s in self.status.get("sensors")],
- "actuators": self.status.get("actuators"),
- "dht_sensors": self.status.get(CONF_DHT_SENSORS),
- "ds18b20_sensors": self.status.get(CONF_DS18B20_SENSORS),
- "auth_token": settings.get("token"),
- "endpoint": settings.get("apiUrl"),
- "blink": settings.get(CONF_BLINK),
- "discovery": settings.get(CONF_DISCOVERY),
- }
-
- def sync_device_config(self):
- """Sync the new pin configuration to the Konnected device if needed."""
- _LOGGER.debug(
- "Device %s settings payload: %s",
- self.device_id,
- self.desired_settings_payload(),
- )
- if self.desired_settings_payload() != self.current_settings_payload():
- _LOGGER.info("pushing settings to device %s", self.device_id)
- self.client.put_settings(**self.desired_settings_payload())
+async def async_entry_updated(hass: HomeAssistant, entry: ConfigEntry):
+ """Reload the config entry when options change."""
+ await hass.config_entries.async_reload(entry.entry_id)
class KonnectedView(HomeAssistantView):
@@ -468,9 +283,8 @@ class KonnectedView(HomeAssistantView):
name = "api:konnected"
requires_auth = False # Uses access token from configuration
- def __init__(self, auth_token):
+ def __init__(self):
"""Initialize the view."""
- self.auth_token = auth_token
@staticmethod
def binary_value(state, activation):
@@ -479,50 +293,29 @@ class KonnectedView(HomeAssistantView):
return 1 if state == STATE_ON else 0
return 0 if state == STATE_ON else 1
- async def get(self, request: Request, device_id) -> Response:
- """Return the current binary state of a switch."""
+ async def update_sensor(self, request: Request, device_id) -> Response:
+ """Process a put or post."""
hass = request.app["hass"]
- pin_num = int(request.query.get("pin"))
data = hass.data[DOMAIN]
- device = data[CONF_DEVICES][device_id]
- if not device:
- return self.json_message(
- f"Device {device_id} not configured", status_code=HTTP_NOT_FOUND
- )
-
- try:
- pin = next(
- filter(
- lambda switch: switch[CONF_PIN] == pin_num, device[CONF_SWITCHES]
- )
- )
- except StopIteration:
- pin = None
-
- if not pin:
- return self.json_message(
- format("Switch on pin {} not configured", pin_num),
- status_code=HTTP_NOT_FOUND,
- )
-
- return self.json(
- {
- "pin": pin_num,
- "state": self.binary_value(
- hass.states.get(pin[ATTR_ENTITY_ID]).state, pin[CONF_ACTIVATION]
- ),
- }
+ auth = request.headers.get(AUTHORIZATION, None)
+ tokens = []
+ if hass.data[DOMAIN].get(CONF_ACCESS_TOKEN):
+ tokens.extend([hass.data[DOMAIN][CONF_ACCESS_TOKEN]])
+ tokens.extend(
+ [
+ entry.data[CONF_ACCESS_TOKEN]
+ for entry in hass.config_entries.async_entries(DOMAIN)
+ ]
)
-
- async def put(self, request: Request, device_id) -> Response:
- """Receive a sensor update via PUT request and async set state."""
- hass = request.app["hass"]
- data = hass.data[DOMAIN]
+ if auth is None or not next(
+ (True for token in tokens if hmac.compare_digest(f"Bearer {token}", auth)),
+ False,
+ ):
+ return self.json_message("unauthorized", status_code=HTTP_UNAUTHORIZED)
try: # Konnected 2.2.0 and above supports JSON payloads
payload = await request.json()
- pin_num = payload["pin"]
except json.decoder.JSONDecodeError:
_LOGGER.error(
(
@@ -532,30 +325,97 @@ class KonnectedView(HomeAssistantView):
)
)
- auth = request.headers.get(AUTHORIZATION, None)
- if not hmac.compare_digest(f"Bearer {self.auth_token}", auth):
- return self.json_message("unauthorized", status_code=HTTP_UNAUTHORIZED)
- pin_num = int(pin_num)
device = data[CONF_DEVICES].get(device_id)
if device is None:
return self.json_message(
"unregistered device", status_code=HTTP_BAD_REQUEST
)
- pin_data = device[CONF_BINARY_SENSORS].get(pin_num) or next(
- (s for s in device[CONF_SENSORS] if s[CONF_PIN] == pin_num), None
- )
- if pin_data is None:
+ try:
+ zone_num = str(payload.get(CONF_ZONE) or PIN_TO_ZONE[payload[CONF_PIN]])
+ zone_data = device[CONF_BINARY_SENSORS].get(zone_num) or next(
+ (s for s in device[CONF_SENSORS] if s[CONF_ZONE] == zone_num), None
+ )
+ except KeyError:
+ zone_data = None
+
+ if zone_data is None:
return self.json_message(
"unregistered sensor/actuator", status_code=HTTP_BAD_REQUEST
)
- pin_data["device_id"] = device_id
+ zone_data["device_id"] = device_id
for attr in ["state", "temp", "humi", "addr"]:
value = payload.get(attr)
handler = HANDLERS.get(attr)
if value is not None and handler:
- hass.async_create_task(handler(hass, pin_data, payload))
+ hass.async_create_task(handler(hass, zone_data, payload))
return self.json_message("ok")
+
+ async def get(self, request: Request, device_id) -> Response:
+ """Return the current binary state of a switch."""
+ hass = request.app["hass"]
+ data = hass.data[DOMAIN]
+
+ device = data[CONF_DEVICES].get(device_id)
+ if not device:
+ return self.json_message(
+ f"Device {device_id} not configured", status_code=HTTP_NOT_FOUND
+ )
+
+ # Our data model is based on zone ids but we convert from/to pin ids
+ # based on whether they are specified in the request
+ try:
+ zone_num = str(
+ request.query.get(CONF_ZONE) or PIN_TO_ZONE[request.query[CONF_PIN]]
+ )
+ zone = next(
+ (
+ switch
+ for switch in device[CONF_SWITCHES]
+ if switch[CONF_ZONE] == zone_num
+ )
+ )
+
+ except StopIteration:
+ zone = None
+ except KeyError:
+ zone = None
+ zone_num = None
+
+ if not zone:
+ target = request.query.get(
+ CONF_ZONE, request.query.get(CONF_PIN, "unknown")
+ )
+ return self.json_message(
+ f"Switch on zone or pin {target} not configured",
+ status_code=HTTP_NOT_FOUND,
+ )
+
+ resp = {}
+ if request.query.get(CONF_ZONE):
+ resp[CONF_ZONE] = zone_num
+ else:
+ resp[CONF_PIN] = ZONE_TO_PIN[zone_num]
+
+ # Make sure entity is setup
+ zone_entity_id = zone.get(ATTR_ENTITY_ID)
+ if zone_entity_id:
+ resp["state"] = self.binary_value(
+ hass.states.get(zone_entity_id).state, zone[CONF_ACTIVATION],
+ )
+ return self.json(resp)
+
+ _LOGGER.warning("Konnected entity not yet setup, returning default")
+ resp["state"] = self.binary_value(STATE_OFF, zone[CONF_ACTIVATION])
+ return self.json(resp)
+
+ async def put(self, request: Request, device_id) -> Response:
+ """Receive a sensor update via PUT request and async set state."""
+ return await self.update_sensor(request, device_id)
+
+ async def post(self, request: Request, device_id) -> Response:
+ """Receive a sensor update via POST request and async set state."""
+ return await self.update_sensor(request, device_id)
diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py
index 486c228d6fb..dc4dae7787f 100644
--- a/homeassistant/components/konnected/binary_sensor.py
+++ b/homeassistant/components/konnected/binary_sensor.py
@@ -13,18 +13,15 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from . import DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE
+from .const import DOMAIN as KONNECTED_DOMAIN, SIGNAL_SENSOR_UPDATE
_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up binary sensors attached to a Konnected device."""
- if discovery_info is None:
- return
-
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up binary sensors attached to a Konnected device from a config entry."""
data = hass.data[KONNECTED_DOMAIN]
- device_id = discovery_info["device_id"]
+ device_id = config_entry.data["id"]
sensors = [
KonnectedBinarySensor(device_id, pin_num, pin_data)
for pin_num, pin_data in data[CONF_DEVICES][device_id][
@@ -37,14 +34,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class KonnectedBinarySensor(BinarySensorDevice):
"""Representation of a Konnected binary sensor."""
- def __init__(self, device_id, pin_num, data):
+ def __init__(self, device_id, zone_num, data):
"""Initialize the Konnected binary sensor."""
self._data = data
self._device_id = device_id
- self._pin_num = pin_num
+ self._zone_num = zone_num
self._state = self._data.get(ATTR_STATE)
self._device_class = self._data.get(CONF_TYPE)
- self._unique_id = "{}-{}".format(device_id, PIN_TO_ZONE[pin_num])
+ self._unique_id = f"{device_id}-{zone_num}"
self._name = self._data.get(CONF_NAME)
@property
@@ -72,6 +69,13 @@ class KonnectedBinarySensor(BinarySensorDevice):
"""Return the device class."""
return self._device_class
+ @property
+ def device_info(self):
+ """Return the device info."""
+ return {
+ "identifiers": {(KONNECTED_DOMAIN, self._device_id)},
+ }
+
async def async_added_to_hass(self):
"""Store entity_id and register state change callback."""
self._data[ATTR_ENTITY_ID] = self.entity_id
diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py
new file mode 100644
index 00000000000..cb9004c9efe
--- /dev/null
+++ b/homeassistant/components/konnected/config_flow.py
@@ -0,0 +1,766 @@
+"""Config flow for konnected.io integration."""
+import asyncio
+import copy
+import logging
+import random
+import string
+from urllib.parse import urlparse
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_DOOR,
+ DEVICE_CLASSES_SCHEMA,
+)
+from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MODEL_NAME
+from homeassistant.const import (
+ CONF_ACCESS_TOKEN,
+ CONF_BINARY_SENSORS,
+ CONF_HOST,
+ CONF_ID,
+ CONF_NAME,
+ CONF_PORT,
+ CONF_SENSORS,
+ CONF_SWITCHES,
+ CONF_TYPE,
+ CONF_ZONE,
+)
+from homeassistant.core import callback
+from homeassistant.helpers import config_validation as cv
+
+from .const import (
+ CONF_ACTIVATION,
+ CONF_BLINK,
+ CONF_DEFAULT_OPTIONS,
+ CONF_DISCOVERY,
+ CONF_INVERSE,
+ CONF_MODEL,
+ CONF_MOMENTARY,
+ CONF_PAUSE,
+ CONF_POLL_INTERVAL,
+ CONF_REPEAT,
+ DOMAIN,
+ STATE_HIGH,
+ STATE_LOW,
+ ZONES,
+)
+from .errors import CannotConnect
+from .panel import KONN_MODEL, KONN_MODEL_PRO, get_status
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_KONN_UPNP_MODEL_NAME = "model_name" # standard upnp is modelName
+CONF_IO = "io"
+CONF_IO_DIS = "Disabled"
+CONF_IO_BIN = "Binary Sensor"
+CONF_IO_DIG = "Digital Sensor"
+CONF_IO_SWI = "Switchable Output"
+
+KONN_MANUFACTURER = "konnected.io"
+KONN_PANEL_MODEL_NAMES = {
+ KONN_MODEL: "Konnected Alarm Panel",
+ KONN_MODEL_PRO: "Konnected Alarm Panel Pro",
+}
+
+OPTIONS_IO_ANY = vol.In([CONF_IO_DIS, CONF_IO_BIN, CONF_IO_DIG, CONF_IO_SWI])
+OPTIONS_IO_INPUT_ONLY = vol.In([CONF_IO_DIS, CONF_IO_BIN, CONF_IO_DIG])
+OPTIONS_IO_OUTPUT_ONLY = vol.In([CONF_IO_DIS, CONF_IO_SWI])
+
+
+# Config entry schemas
+IO_SCHEMA = vol.Schema(
+ {
+ vol.Optional("1", default=CONF_IO_DIS): OPTIONS_IO_ANY,
+ vol.Optional("2", default=CONF_IO_DIS): OPTIONS_IO_ANY,
+ vol.Optional("3", default=CONF_IO_DIS): OPTIONS_IO_ANY,
+ vol.Optional("4", default=CONF_IO_DIS): OPTIONS_IO_ANY,
+ vol.Optional("5", default=CONF_IO_DIS): OPTIONS_IO_ANY,
+ vol.Optional("6", default=CONF_IO_DIS): OPTIONS_IO_ANY,
+ vol.Optional("7", default=CONF_IO_DIS): OPTIONS_IO_ANY,
+ vol.Optional("8", default=CONF_IO_DIS): OPTIONS_IO_ANY,
+ vol.Optional("9", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY,
+ vol.Optional("10", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY,
+ vol.Optional("11", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY,
+ vol.Optional("12", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY,
+ vol.Optional("out", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY,
+ vol.Optional("alarm1", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY,
+ vol.Optional("out1", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY,
+ vol.Optional("alarm2_out2", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY,
+ }
+)
+
+BINARY_SENSOR_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_ZONE): vol.In(ZONES),
+ vol.Required(CONF_TYPE, default=DEVICE_CLASS_DOOR): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_INVERSE, default=False): cv.boolean,
+ }
+)
+
+SENSOR_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_ZONE): vol.In(ZONES),
+ vol.Required(CONF_TYPE, default="dht"): vol.All(
+ vol.Lower, vol.In(["dht", "ds18b20"])
+ ),
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_POLL_INTERVAL, default=3): vol.All(
+ vol.Coerce(int), vol.Range(min=1)
+ ),
+ }
+)
+
+SWITCH_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_ZONE): vol.In(ZONES),
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All(
+ vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)
+ ),
+ vol.Optional(CONF_MOMENTARY): vol.All(vol.Coerce(int), vol.Range(min=10)),
+ vol.Optional(CONF_PAUSE): vol.All(vol.Coerce(int), vol.Range(min=10)),
+ vol.Optional(CONF_REPEAT): vol.All(vol.Coerce(int), vol.Range(min=-1)),
+ }
+)
+
+OPTIONS_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_IO): IO_SCHEMA,
+ vol.Optional(CONF_BINARY_SENSORS): vol.All(
+ cv.ensure_list, [BINARY_SENSOR_SCHEMA]
+ ),
+ vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
+ vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]),
+ vol.Optional(CONF_BLINK, default=True): cv.boolean,
+ vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
+ },
+ extra=vol.REMOVE_EXTRA,
+)
+
+CONFIG_ENTRY_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"),
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_PORT): cv.port,
+ vol.Required(CONF_MODEL): vol.Any(*KONN_PANEL_MODEL_NAMES),
+ vol.Required(CONF_ACCESS_TOKEN): cv.matches_regex("[a-zA-Z0-9]+"),
+ vol.Required(CONF_DEFAULT_OPTIONS): OPTIONS_SCHEMA,
+ },
+ extra=vol.REMOVE_EXTRA,
+)
+
+
+class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for NEW_NAME."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ # class variable to store/share discovered host information
+ discovered_hosts = {}
+
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+
+ def __init__(self):
+ """Initialize the Konnected flow."""
+ self.data = {}
+ self.options = OPTIONS_SCHEMA({CONF_IO: {}})
+
+ async def async_gen_config(self, host, port):
+ """Populate self.data based on panel status.
+
+ This will raise CannotConnect if an error occurs
+ """
+ self.data[CONF_HOST] = host
+ self.data[CONF_PORT] = port
+ try:
+ status = await get_status(self.hass, host, port)
+ self.data[CONF_ID] = status["mac"].replace(":", "")
+ except (CannotConnect, KeyError):
+ raise CannotConnect
+ else:
+ self.data[CONF_MODEL] = status.get("model", KONN_MODEL)
+ self.data[CONF_ACCESS_TOKEN] = "".join(
+ random.choices(f"{string.ascii_uppercase}{string.digits}", k=20)
+ )
+
+ async def async_step_import(self, device_config):
+ """Import a configuration.yaml config.
+
+ This flow is triggered by `async_setup` for configured panels.
+ """
+ _LOGGER.debug(device_config)
+
+ # save the data and confirm connection via user step
+ await self.async_set_unique_id(device_config["id"])
+ self.options = device_config[CONF_DEFAULT_OPTIONS]
+
+ # config schema ensures we have port if we have host
+ if device_config.get(CONF_HOST):
+ # automatically connect if we have host info
+ return await self.async_step_user(
+ user_input={
+ CONF_HOST: device_config[CONF_HOST],
+ CONF_PORT: device_config[CONF_PORT],
+ }
+ )
+
+ # if we have no host info wait for it or abort if previously configured
+ self._abort_if_unique_id_configured()
+ return await self.async_step_import_confirm()
+
+ async def async_step_import_confirm(self, user_input=None):
+ """Confirm the user wants to import the config entry."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="import_confirm",
+ description_placeholders={"id": self.unique_id},
+ )
+
+ # if we have ssdp discovered applicable host info use it
+ if KonnectedFlowHandler.discovered_hosts.get(self.unique_id):
+ return await self.async_step_user(
+ user_input={
+ CONF_HOST: KonnectedFlowHandler.discovered_hosts[self.unique_id][
+ CONF_HOST
+ ],
+ CONF_PORT: KonnectedFlowHandler.discovered_hosts[self.unique_id][
+ CONF_PORT
+ ],
+ }
+ )
+ return await self.async_step_user()
+
+ async def async_step_ssdp(self, discovery_info):
+ """Handle a discovered konnected panel.
+
+ This flow is triggered by the SSDP component. It will check if the
+ device is already configured and attempt to finish the config if not.
+ """
+ _LOGGER.debug(discovery_info)
+
+ try:
+ if discovery_info[ATTR_UPNP_MANUFACTURER] != KONN_MANUFACTURER:
+ return self.async_abort(reason="not_konn_panel")
+
+ if not any(
+ name in discovery_info[ATTR_UPNP_MODEL_NAME]
+ for name in KONN_PANEL_MODEL_NAMES
+ ):
+ _LOGGER.warning(
+ "Discovered unrecognized Konnected device %s",
+ discovery_info.get(ATTR_UPNP_MODEL_NAME, "Unknown"),
+ )
+ return self.async_abort(reason="not_konn_panel")
+
+ # If MAC is missing it is a bug in the device fw but we'll guard
+ # against it since the field is so vital
+ except KeyError:
+ _LOGGER.error("Malformed Konnected SSDP info")
+ else:
+ # extract host/port from ssdp_location
+ netloc = urlparse(discovery_info["ssdp_location"]).netloc.split(":")
+ return await self.async_step_user(
+ user_input={CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])}
+ )
+
+ return self.async_abort(reason="unknown")
+
+ async def async_step_user(self, user_input=None):
+ """Connect to panel and get config."""
+ errors = {}
+ if user_input:
+ # build config info and wait for user confirmation
+ self.data[CONF_HOST] = user_input[CONF_HOST]
+ self.data[CONF_PORT] = user_input[CONF_PORT]
+ self.data[CONF_ACCESS_TOKEN] = self.hass.data.get(DOMAIN, {}).get(
+ CONF_ACCESS_TOKEN
+ ) or "".join(
+ random.choices(f"{string.ascii_uppercase}{string.digits}", k=20)
+ )
+
+ # brief delay to allow processing of recent status req
+ await asyncio.sleep(0.1)
+ try:
+ status = await get_status(
+ self.hass, self.data[CONF_HOST], self.data[CONF_PORT]
+ )
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ else:
+ self.data[CONF_ID] = status["mac"].replace(":", "")
+ self.data[CONF_MODEL] = status.get("model", KONN_MODEL)
+
+ # save off our discovered host info
+ KonnectedFlowHandler.discovered_hosts[self.data[CONF_ID]] = {
+ CONF_HOST: self.data[CONF_HOST],
+ CONF_PORT: self.data[CONF_PORT],
+ }
+ return await self.async_step_confirm()
+
+ return self.async_show_form(
+ step_id="user",
+ description_placeholders={
+ "host": self.data.get(CONF_HOST, "Unknown"),
+ "port": self.data.get(CONF_PORT, "Unknown"),
+ },
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_HOST, default=self.data.get(CONF_HOST)): str,
+ vol.Required(CONF_PORT, default=self.data.get(CONF_PORT)): int,
+ }
+ ),
+ errors=errors,
+ )
+
+ async def async_step_confirm(self, user_input=None):
+ """Attempt to link with the Konnected panel.
+
+ Given a configured host, will ask the user to confirm and finalize
+ the connection.
+ """
+ if user_input is None:
+ # abort and update an existing config entry if host info changes
+ await self.async_set_unique_id(self.data[CONF_ID])
+ self._abort_if_unique_id_configured(updates=self.data)
+ return self.async_show_form(
+ step_id="confirm",
+ description_placeholders={
+ "model": KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]],
+ "id": self.unique_id,
+ "host": self.data[CONF_HOST],
+ "port": self.data[CONF_PORT],
+ },
+ )
+
+ # Attach default options and create entry
+ self.data[CONF_DEFAULT_OPTIONS] = self.options
+ return self.async_create_entry(
+ title=KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]], data=self.data,
+ )
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Return the Options Flow."""
+ return OptionsFlowHandler(config_entry)
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle a option flow for a Konnected Panel."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry):
+ """Initialize options flow."""
+ self.entry = config_entry
+ self.model = self.entry.data[CONF_MODEL]
+ self.current_opt = self.entry.options or self.entry.data[CONF_DEFAULT_OPTIONS]
+
+ # as config proceeds we'll build up new options and then replace what's in the config entry
+ self.new_opt = {CONF_IO: {}}
+ self.active_cfg = None
+ self.io_cfg = {}
+
+ @callback
+ def get_current_cfg(self, io_type, zone):
+ """Get the current zone config."""
+ return next(
+ (
+ cfg
+ for cfg in self.current_opt.get(io_type, [])
+ if cfg[CONF_ZONE] == zone
+ ),
+ {},
+ )
+
+ async def async_step_init(self, user_input=None):
+ """Handle options flow."""
+ return await self.async_step_options_io()
+
+ async def async_step_options_io(self, user_input=None):
+ """Configure legacy panel IO or first half of pro IO."""
+ errors = {}
+ current_io = self.current_opt.get(CONF_IO, {})
+
+ if user_input is not None:
+ # strip out disabled io and save for options cfg
+ for key, value in user_input.items():
+ if value != CONF_IO_DIS:
+ self.new_opt[CONF_IO][key] = value
+ return await self.async_step_options_io_ext()
+
+ if self.model == KONN_MODEL:
+ return self.async_show_form(
+ step_id="options_io",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ "1", default=current_io.get("1", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "2", default=current_io.get("2", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "3", default=current_io.get("3", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "4", default=current_io.get("4", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "5", default=current_io.get("5", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "6", default=current_io.get("6", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "out", default=current_io.get("out", CONF_IO_DIS)
+ ): OPTIONS_IO_OUTPUT_ONLY,
+ }
+ ),
+ description_placeholders={
+ "model": KONN_PANEL_MODEL_NAMES[self.model],
+ "host": self.entry.data[CONF_HOST],
+ },
+ errors=errors,
+ )
+
+ # configure the first half of the pro board io
+ if self.model == KONN_MODEL_PRO:
+ return self.async_show_form(
+ step_id="options_io",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ "1", default=current_io.get("1", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "2", default=current_io.get("2", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "3", default=current_io.get("3", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "4", default=current_io.get("4", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "5", default=current_io.get("5", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "6", default=current_io.get("6", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "7", default=current_io.get("7", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ }
+ ),
+ description_placeholders={
+ "model": KONN_PANEL_MODEL_NAMES[self.model],
+ "host": self.entry.data[CONF_HOST],
+ },
+ errors=errors,
+ )
+
+ return self.async_abort(reason="not_konn_panel")
+
+ async def async_step_options_io_ext(self, user_input=None):
+ """Allow the user to configure the extended IO for pro."""
+ errors = {}
+ current_io = self.current_opt.get(CONF_IO, {})
+
+ if user_input is not None:
+ # strip out disabled io and save for options cfg
+ for key, value in user_input.items():
+ if value != CONF_IO_DIS:
+ self.new_opt[CONF_IO].update({key: value})
+ self.io_cfg = copy.deepcopy(self.new_opt[CONF_IO])
+ return await self.async_step_options_binary()
+
+ if self.model == KONN_MODEL:
+ self.io_cfg = copy.deepcopy(self.new_opt[CONF_IO])
+ return await self.async_step_options_binary()
+
+ if self.model == KONN_MODEL_PRO:
+ return self.async_show_form(
+ step_id="options_io_ext",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ "8", default=current_io.get("8", CONF_IO_DIS)
+ ): OPTIONS_IO_ANY,
+ vol.Required(
+ "9", default=current_io.get("9", CONF_IO_DIS)
+ ): OPTIONS_IO_INPUT_ONLY,
+ vol.Required(
+ "10", default=current_io.get("10", CONF_IO_DIS)
+ ): OPTIONS_IO_INPUT_ONLY,
+ vol.Required(
+ "11", default=current_io.get("11", CONF_IO_DIS)
+ ): OPTIONS_IO_INPUT_ONLY,
+ vol.Required(
+ "12", default=current_io.get("12", CONF_IO_DIS)
+ ): OPTIONS_IO_INPUT_ONLY,
+ vol.Required(
+ "alarm1", default=current_io.get("alarm1", CONF_IO_DIS)
+ ): OPTIONS_IO_OUTPUT_ONLY,
+ vol.Required(
+ "out1", default=current_io.get("out1", CONF_IO_DIS)
+ ): OPTIONS_IO_OUTPUT_ONLY,
+ vol.Required(
+ "alarm2_out2",
+ default=current_io.get("alarm2_out2", CONF_IO_DIS),
+ ): OPTIONS_IO_OUTPUT_ONLY,
+ }
+ ),
+ description_placeholders={
+ "model": KONN_PANEL_MODEL_NAMES[self.model],
+ "host": self.entry.data[CONF_HOST],
+ },
+ errors=errors,
+ )
+
+ return self.async_abort(reason="not_konn_panel")
+
+ async def async_step_options_binary(self, user_input=None):
+ """Allow the user to configure the IO options for binary sensors."""
+ errors = {}
+ if user_input is not None:
+ zone = {"zone": self.active_cfg}
+ zone.update(user_input)
+ self.new_opt[CONF_BINARY_SENSORS] = self.new_opt.get(
+ CONF_BINARY_SENSORS, []
+ ) + [zone]
+ self.io_cfg.pop(self.active_cfg)
+ self.active_cfg = None
+
+ if self.active_cfg:
+ current_cfg = self.get_current_cfg(CONF_BINARY_SENSORS, self.active_cfg)
+ return self.async_show_form(
+ step_id="options_binary",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_TYPE,
+ default=current_cfg.get(CONF_TYPE, DEVICE_CLASS_DOOR),
+ ): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(
+ CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED)
+ ): str,
+ vol.Optional(
+ CONF_INVERSE, default=current_cfg.get(CONF_INVERSE, False)
+ ): bool,
+ }
+ ),
+ description_placeholders={
+ "zone": f"Zone {self.active_cfg}"
+ if len(self.active_cfg) < 3
+ else self.active_cfg.upper
+ },
+ errors=errors,
+ )
+
+ # find the next unconfigured binary sensor
+ for key, value in self.io_cfg.items():
+ if value == CONF_IO_BIN:
+ self.active_cfg = key
+ current_cfg = self.get_current_cfg(CONF_BINARY_SENSORS, self.active_cfg)
+ return self.async_show_form(
+ step_id="options_binary",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_TYPE,
+ default=current_cfg.get(CONF_TYPE, DEVICE_CLASS_DOOR),
+ ): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(
+ CONF_NAME,
+ default=current_cfg.get(CONF_NAME, vol.UNDEFINED),
+ ): str,
+ vol.Optional(
+ CONF_INVERSE,
+ default=current_cfg.get(CONF_INVERSE, False),
+ ): bool,
+ }
+ ),
+ description_placeholders={
+ "zone": f"Zone {self.active_cfg}"
+ if len(self.active_cfg) < 3
+ else self.active_cfg.upper
+ },
+ errors=errors,
+ )
+
+ return await self.async_step_options_digital()
+
+ async def async_step_options_digital(self, user_input=None):
+ """Allow the user to configure the IO options for digital sensors."""
+ errors = {}
+ if user_input is not None:
+ zone = {"zone": self.active_cfg}
+ zone.update(user_input)
+ self.new_opt[CONF_SENSORS] = self.new_opt.get(CONF_SENSORS, []) + [zone]
+ self.io_cfg.pop(self.active_cfg)
+ self.active_cfg = None
+
+ if self.active_cfg:
+ current_cfg = self.get_current_cfg(CONF_SENSORS, self.active_cfg)
+ return self.async_show_form(
+ step_id="options_digital",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_TYPE, default=current_cfg.get(CONF_TYPE, "dht")
+ ): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])),
+ vol.Optional(
+ CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED)
+ ): str,
+ vol.Optional(
+ CONF_POLL_INTERVAL,
+ default=current_cfg.get(CONF_POLL_INTERVAL, 3),
+ ): vol.All(vol.Coerce(int), vol.Range(min=1)),
+ }
+ ),
+ description_placeholders={
+ "zone": f"Zone {self.active_cfg}"
+ if len(self.active_cfg) < 3
+ else self.active_cfg.upper()
+ },
+ errors=errors,
+ )
+
+ # find the next unconfigured digital sensor
+ for key, value in self.io_cfg.items():
+ if value == CONF_IO_DIG:
+ self.active_cfg = key
+ current_cfg = self.get_current_cfg(CONF_SENSORS, self.active_cfg)
+ return self.async_show_form(
+ step_id="options_digital",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_TYPE, default=current_cfg.get(CONF_TYPE, "dht")
+ ): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])),
+ vol.Optional(
+ CONF_NAME,
+ default=current_cfg.get(CONF_NAME, vol.UNDEFINED),
+ ): str,
+ vol.Optional(
+ CONF_POLL_INTERVAL,
+ default=current_cfg.get(CONF_POLL_INTERVAL, 3),
+ ): vol.All(vol.Coerce(int), vol.Range(min=1)),
+ }
+ ),
+ description_placeholders={
+ "zone": f"Zone {self.active_cfg}"
+ if len(self.active_cfg) < 3
+ else self.active_cfg.upper()
+ },
+ errors=errors,
+ )
+
+ return await self.async_step_options_switch()
+
+ async def async_step_options_switch(self, user_input=None):
+ """Allow the user to configure the IO options for switches."""
+ errors = {}
+ if user_input is not None:
+ zone = {"zone": self.active_cfg}
+ zone.update(user_input)
+ self.new_opt[CONF_SWITCHES] = self.new_opt.get(CONF_SWITCHES, []) + [zone]
+ self.io_cfg.pop(self.active_cfg)
+ self.active_cfg = None
+
+ if self.active_cfg:
+ current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg)
+ return self.async_show_form(
+ step_id="options_switch",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED)
+ ): str,
+ vol.Optional(
+ CONF_ACTIVATION,
+ default=current_cfg.get(CONF_ACTIVATION, STATE_HIGH),
+ ): vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)),
+ vol.Optional(
+ CONF_MOMENTARY,
+ default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED),
+ ): vol.All(vol.Coerce(int), vol.Range(min=10)),
+ vol.Optional(
+ CONF_PAUSE,
+ default=current_cfg.get(CONF_PAUSE, vol.UNDEFINED),
+ ): vol.All(vol.Coerce(int), vol.Range(min=10)),
+ vol.Optional(
+ CONF_REPEAT,
+ default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED),
+ ): vol.All(vol.Coerce(int), vol.Range(min=-1)),
+ }
+ ),
+ description_placeholders={
+ "zone": f"Zone {self.active_cfg}"
+ if len(self.active_cfg) < 3
+ else self.active_cfg.upper()
+ },
+ errors=errors,
+ )
+
+ # find the next unconfigured switch
+ for key, value in self.io_cfg.items():
+ if value == CONF_IO_SWI:
+ self.active_cfg = key
+ current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg)
+ return self.async_show_form(
+ step_id="options_switch",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_NAME,
+ default=current_cfg.get(CONF_NAME, vol.UNDEFINED),
+ ): str,
+ vol.Optional(
+ CONF_ACTIVATION,
+ default=current_cfg.get(CONF_ACTIVATION, "high"),
+ ): vol.In(["low", "high"]),
+ vol.Optional(
+ CONF_MOMENTARY,
+ default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED),
+ ): vol.All(vol.Coerce(int), vol.Range(min=10)),
+ vol.Optional(
+ CONF_PAUSE,
+ default=current_cfg.get(CONF_PAUSE, vol.UNDEFINED),
+ ): vol.All(vol.Coerce(int), vol.Range(min=10)),
+ vol.Optional(
+ CONF_REPEAT,
+ default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED),
+ ): vol.All(vol.Coerce(int), vol.Range(min=-1)),
+ }
+ ),
+ description_placeholders={
+ "zone": f"Zone {self.active_cfg}"
+ if len(self.active_cfg) < 3
+ else self.active_cfg.upper()
+ },
+ errors=errors,
+ )
+
+ return await self.async_step_options_misc()
+
+ async def async_step_options_misc(self, user_input=None):
+ """Allow the user to configure the LED behavior."""
+ errors = {}
+ if user_input is not None:
+ self.new_opt[CONF_BLINK] = user_input[CONF_BLINK]
+ return self.async_create_entry(title="", data=self.new_opt)
+
+ return self.async_show_form(
+ step_id="options_misc",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_BLINK, default=self.current_opt.get(CONF_BLINK, True)
+ ): bool,
+ }
+ ),
+ errors=errors,
+ )
diff --git a/homeassistant/components/konnected/const.py b/homeassistant/components/konnected/const.py
index 0107b341532..d6819dcf71f 100644
--- a/homeassistant/components/konnected/const.py
+++ b/homeassistant/components/konnected/const.py
@@ -4,6 +4,7 @@ DOMAIN = "konnected"
CONF_ACTIVATION = "activation"
CONF_API_HOST = "api_host"
+CONF_DEFAULT_OPTIONS = "default_options"
CONF_MOMENTARY = "momentary"
CONF_PAUSE = "pause"
CONF_POLL_INTERVAL = "poll_interval"
@@ -14,11 +15,33 @@ CONF_BLINK = "blink"
CONF_DISCOVERY = "discovery"
CONF_DHT_SENSORS = "dht_sensors"
CONF_DS18B20_SENSORS = "ds18b20_sensors"
+CONF_MODEL = "model"
STATE_LOW = "low"
STATE_HIGH = "high"
-PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: "out", 9: 6}
+ZONES = [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12",
+ "alarm1",
+ "out1",
+ "alarm2_out2",
+ "out",
+]
+
+# alarm panel pro only handles zones,
+# alarm panel allows specifying pins via configuration.yaml
+PIN_TO_ZONE = {"1": "1", "2": "2", "5": "3", "6": "4", "7": "5", "8": "out", "9": "6"}
ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()}
ENDPOINT_ROOT = "/api/konnected"
diff --git a/homeassistant/components/konnected/errors.py b/homeassistant/components/konnected/errors.py
new file mode 100644
index 00000000000..5a0207f3f8d
--- /dev/null
+++ b/homeassistant/components/konnected/errors.py
@@ -0,0 +1,10 @@
+"""Errors for the Konnected component."""
+from homeassistant.exceptions import HomeAssistantError
+
+
+class KonnectedException(HomeAssistantError):
+ """Base class for Konnected exceptions."""
+
+
+class CannotConnect(KonnectedException):
+ """Unable to connect to the panel."""
diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json
index feb6a4589cb..3a74e2165df 100644
--- a/homeassistant/components/konnected/manifest.json
+++ b/homeassistant/components/konnected/manifest.json
@@ -1,8 +1,21 @@
{
"domain": "konnected",
"name": "Konnected",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/konnected",
- "requirements": ["konnected==0.1.5"],
- "dependencies": ["http"],
- "codeowners": ["@heythisisnate"]
+ "requirements": [
+ "konnected==1.1.0"
+ ],
+ "ssdp": [
+ {
+ "manufacturer": "konnected.io"
+ }
+ ],
+ "dependencies": [
+ "http"
+ ],
+ "codeowners": [
+ "@heythisisnate",
+ "@kit-klein"
+ ]
}
diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py
new file mode 100644
index 00000000000..2668a382ccc
--- /dev/null
+++ b/homeassistant/components/konnected/panel.py
@@ -0,0 +1,362 @@
+"""Support for Konnected devices."""
+import asyncio
+import logging
+
+import konnected
+
+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,
+)
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client, device_registry as dr
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+from .const import (
+ CONF_ACTIVATION,
+ CONF_API_HOST,
+ CONF_BLINK,
+ CONF_DEFAULT_OPTIONS,
+ CONF_DHT_SENSORS,
+ CONF_DISCOVERY,
+ CONF_DS18B20_SENSORS,
+ CONF_INVERSE,
+ CONF_MOMENTARY,
+ CONF_PAUSE,
+ CONF_POLL_INTERVAL,
+ CONF_REPEAT,
+ DOMAIN,
+ ENDPOINT_ROOT,
+ SIGNAL_SENSOR_UPDATE,
+ STATE_LOW,
+ ZONE_TO_PIN,
+)
+from .errors import CannotConnect
+
+_LOGGER = logging.getLogger(__name__)
+
+KONN_MODEL = "Konnected"
+KONN_MODEL_PRO = "Konnected Pro"
+
+# Indicate how each unit is controlled (pin or zone)
+KONN_API_VERSIONS = {
+ KONN_MODEL: CONF_PIN,
+ KONN_MODEL_PRO: CONF_ZONE,
+}
+
+
+class AlarmPanel:
+ """A representation of a Konnected alarm panel."""
+
+ def __init__(self, hass, config_entry):
+ """Initialize the Konnected device."""
+ self.hass = hass
+ self.config_entry = config_entry
+ self.config = config_entry.data
+ self.options = config_entry.options or config_entry.data.get(
+ CONF_DEFAULT_OPTIONS, {}
+ )
+ self.host = self.config.get(CONF_HOST)
+ self.port = self.config.get(CONF_PORT)
+ self.client = None
+ self.status = None
+ self.api_version = KONN_API_VERSIONS[KONN_MODEL]
+
+ @property
+ def device_id(self):
+ """Device id is the MAC address as string with punctuation removed."""
+ return self.config.get(CONF_ID)
+
+ @property
+ def stored_configuration(self):
+ """Return the configuration stored in `hass.data` for this device."""
+ return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)
+
+ def format_zone(self, zone, other_items=None):
+ """Get zone or pin based dict based on the client type."""
+ payload = {
+ self.api_version: zone
+ if self.api_version == CONF_ZONE
+ else ZONE_TO_PIN[zone]
+ }
+ payload.update(other_items or {})
+ return payload
+
+ async def async_connect(self):
+ """Connect to and setup a Konnected device."""
+ try:
+ self.client = konnected.Client(
+ host=self.host,
+ port=str(self.port),
+ websession=aiohttp_client.async_get_clientsession(self.hass),
+ )
+ self.status = await self.client.get_status()
+ self.api_version = KONN_API_VERSIONS.get(
+ self.status.get("model", KONN_MODEL), KONN_API_VERSIONS[KONN_MODEL]
+ )
+ _LOGGER.info(
+ "Connected to new %s device", self.status.get("model", "Konnected")
+ )
+ _LOGGER.debug(self.status)
+
+ await self.async_update_initial_states()
+ # brief delay to allow processing of recent status req
+ await asyncio.sleep(0.1)
+ await self.async_sync_device_config()
+
+ except self.client.ClientError as err:
+ _LOGGER.warning("Exception trying to connect to panel: %s", err)
+ raise CannotConnect
+
+ _LOGGER.info(
+ "Set up Konnected device %s. Open http://%s:%s in a "
+ "web browser to view device status",
+ self.device_id,
+ self.host,
+ self.port,
+ )
+
+ device_registry = await dr.async_get_registry(self.hass)
+
+ device_registry.async_get_or_create(
+ config_entry_id=self.config_entry.entry_id,
+ connections={(dr.CONNECTION_NETWORK_MAC, self.status.get("mac"))},
+ identifiers={(DOMAIN, self.device_id)},
+ manufacturer="Konnected.io",
+ name=self.config_entry.title,
+ model=self.config_entry.title,
+ sw_version=self.status.get("swVersion"),
+ )
+
+ async def update_switch(self, zone, state, momentary=None, times=None, pause=None):
+ """Update the state of a switchable output."""
+ try:
+ if self.client:
+ if self.api_version == CONF_ZONE:
+ return await self.client.put_zone(
+ zone, state, momentary, times, pause,
+ )
+
+ # device endpoint uses pin number instead of zone
+ return await self.client.put_device(
+ ZONE_TO_PIN[zone], state, momentary, times, pause,
+ )
+
+ except self.client.ClientError as err:
+ _LOGGER.warning("Exception trying to update panel: %s", err)
+
+ raise CannotConnect
+
+ async def async_save_data(self):
+ """Save the device configuration to `hass.data`."""
+ binary_sensors = {}
+ for entity in self.options.get(CONF_BINARY_SENSORS) or []:
+ zone = entity[CONF_ZONE]
+
+ binary_sensors[zone] = {
+ CONF_TYPE: entity[CONF_TYPE],
+ CONF_NAME: entity.get(
+ CONF_NAME, f"Konnected {self.device_id[6:]} Zone {zone}"
+ ),
+ CONF_INVERSE: entity.get(CONF_INVERSE),
+ ATTR_STATE: None,
+ }
+ _LOGGER.debug(
+ "Set up binary_sensor %s (initial state: %s)",
+ binary_sensors[zone].get("name"),
+ binary_sensors[zone].get(ATTR_STATE),
+ )
+
+ actuators = []
+ for entity in self.options.get(CONF_SWITCHES) or []:
+ zone = entity[CONF_ZONE]
+
+ act = {
+ CONF_ZONE: zone,
+ CONF_NAME: entity.get(
+ CONF_NAME, f"Konnected {self.device_id[6:]} Actuator {zone}",
+ ),
+ ATTR_STATE: None,
+ CONF_ACTIVATION: entity[CONF_ACTIVATION],
+ CONF_MOMENTARY: entity.get(CONF_MOMENTARY),
+ CONF_PAUSE: entity.get(CONF_PAUSE),
+ CONF_REPEAT: entity.get(CONF_REPEAT),
+ }
+ actuators.append(act)
+ _LOGGER.debug("Set up switch %s", act)
+
+ sensors = []
+ for entity in self.options.get(CONF_SENSORS) or []:
+ zone = entity[CONF_ZONE]
+
+ sensor = {
+ CONF_ZONE: zone,
+ CONF_NAME: entity.get(
+ CONF_NAME, f"Konnected {self.device_id[6:]} Sensor {zone}"
+ ),
+ CONF_TYPE: entity[CONF_TYPE],
+ CONF_POLL_INTERVAL: entity.get(CONF_POLL_INTERVAL),
+ }
+ sensors.append(sensor)
+ _LOGGER.debug(
+ "Set up %s sensor %s (initial state: %s)",
+ sensor.get(CONF_TYPE),
+ sensor.get(CONF_NAME),
+ sensor.get(ATTR_STATE),
+ )
+
+ device_data = {
+ CONF_BINARY_SENSORS: binary_sensors,
+ CONF_SENSORS: sensors,
+ CONF_SWITCHES: actuators,
+ CONF_BLINK: self.options.get(CONF_BLINK),
+ CONF_DISCOVERY: self.options.get(CONF_DISCOVERY),
+ CONF_HOST: self.host,
+ CONF_PORT: self.port,
+ "panel": self,
+ }
+
+ if CONF_DEVICES not in self.hass.data[DOMAIN]:
+ self.hass.data[DOMAIN][CONF_DEVICES] = {}
+
+ _LOGGER.debug(
+ "Storing data in hass.data[%s][%s][%s]: %s",
+ DOMAIN,
+ CONF_DEVICES,
+ self.device_id,
+ device_data,
+ )
+ self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data
+
+ @callback
+ def async_binary_sensor_configuration(self):
+ """Return the configuration map for syncing binary sensors."""
+ return [
+ self.format_zone(p) for p in self.stored_configuration[CONF_BINARY_SENSORS]
+ ]
+
+ @callback
+ def async_actuator_configuration(self):
+ """Return the configuration map for syncing actuators."""
+ return [
+ self.format_zone(
+ data[CONF_ZONE],
+ {"trigger": (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] else 1)},
+ )
+ for data in self.stored_configuration[CONF_SWITCHES]
+ ]
+
+ @callback
+ def async_dht_sensor_configuration(self):
+ """Return the configuration map for syncing DHT sensors."""
+ return [
+ self.format_zone(
+ sensor[CONF_ZONE], {CONF_POLL_INTERVAL: sensor[CONF_POLL_INTERVAL]}
+ )
+ for sensor in self.stored_configuration[CONF_SENSORS]
+ if sensor[CONF_TYPE] == "dht"
+ ]
+
+ @callback
+ def async_ds18b20_sensor_configuration(self):
+ """Return the configuration map for syncing DS18B20 sensors."""
+ return [
+ self.format_zone(sensor[CONF_ZONE])
+ for sensor in self.stored_configuration[CONF_SENSORS]
+ if sensor[CONF_TYPE] == "ds18b20"
+ ]
+
+ async def async_update_initial_states(self):
+ """Update the initial state of each sensor from status poll."""
+ for sensor_data in self.status.get("sensors"):
+ sensor_config = self.stored_configuration[CONF_BINARY_SENSORS].get(
+ sensor_data.get(CONF_ZONE, sensor_data.get(CONF_PIN)), {}
+ )
+ entity_id = sensor_config.get(ATTR_ENTITY_ID)
+
+ state = bool(sensor_data.get(ATTR_STATE))
+ if sensor_config.get(CONF_INVERSE):
+ state = not state
+
+ async_dispatcher_send(
+ self.hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state
+ )
+
+ @callback
+ def async_desired_settings_payload(self):
+ """Return a dict representing the desired device configuration."""
+ desired_api_host = (
+ self.hass.data[DOMAIN].get(CONF_API_HOST) or self.hass.config.api.base_url
+ )
+ desired_api_endpoint = desired_api_host + ENDPOINT_ROOT
+
+ return {
+ "sensors": self.async_binary_sensor_configuration(),
+ "actuators": self.async_actuator_configuration(),
+ "dht_sensors": self.async_dht_sensor_configuration(),
+ "ds18b20_sensors": self.async_ds18b20_sensor_configuration(),
+ "auth_token": self.config.get(CONF_ACCESS_TOKEN),
+ "endpoint": desired_api_endpoint,
+ "blink": self.options.get(CONF_BLINK, True),
+ "discovery": self.options.get(CONF_DISCOVERY, True),
+ }
+
+ @callback
+ def async_current_settings_payload(self):
+ """Return a dict of configuration currently stored on the device."""
+ settings = self.status["settings"]
+ if not settings:
+ settings = {}
+
+ return {
+ "sensors": [
+ {self.api_version: s[self.api_version]}
+ for s in self.status.get("sensors")
+ ],
+ "actuators": self.status.get("actuators"),
+ "dht_sensors": self.status.get(CONF_DHT_SENSORS),
+ "ds18b20_sensors": self.status.get(CONF_DS18B20_SENSORS),
+ "auth_token": settings.get("token"),
+ "endpoint": settings.get("endpoint"),
+ "blink": settings.get(CONF_BLINK),
+ "discovery": settings.get(CONF_DISCOVERY),
+ }
+
+ async def async_sync_device_config(self):
+ """Sync the new zone configuration to the Konnected device if needed."""
+ _LOGGER.debug(
+ "Device %s settings payload: %s",
+ self.device_id,
+ self.async_desired_settings_payload(),
+ )
+ if (
+ self.async_desired_settings_payload()
+ != self.async_current_settings_payload()
+ ):
+ _LOGGER.info("pushing settings to device %s", self.device_id)
+ await self.client.put_settings(**self.async_desired_settings_payload())
+
+
+async def get_status(hass, host, port):
+ """Get the status of a Konnected Panel."""
+ client = konnected.Client(
+ host, str(port), aiohttp_client.async_get_clientsession(hass)
+ )
+ try:
+ return await client.get_status()
+
+ except client.ClientError as err:
+ _LOGGER.error("Exception trying to get panel status: %s", err)
+ raise CannotConnect
diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py
index 7498f2bde1d..d189ac8809a 100644
--- a/homeassistant/components/konnected/sensor.py
+++ b/homeassistant/components/konnected/sensor.py
@@ -4,9 +4,9 @@ import logging
from homeassistant.const import (
CONF_DEVICES,
CONF_NAME,
- CONF_PIN,
CONF_SENSORS,
CONF_TYPE,
+ CONF_ZONE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
TEMP_CELSIUS,
@@ -25,13 +25,10 @@ SENSOR_TYPES = {
}
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up sensors attached to a Konnected device."""
- if discovery_info is None:
- return
-
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up sensors attached to a Konnected device from a config entry."""
data = hass.data[KONNECTED_DOMAIN]
- device_id = discovery_info["device_id"]
+ device_id = config_entry.data["id"]
sensors = []
# Initialize all DHT sensors.
@@ -53,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
(
s
for s in data[CONF_DEVICES][device_id][CONF_SENSORS]
- if s[CONF_TYPE] == "ds18b20" and s[CONF_PIN] == attrs.get(CONF_PIN)
+ if s[CONF_TYPE] == "ds18b20" and s[CONF_ZONE] == attrs.get(CONF_ZONE)
),
None,
)
@@ -85,10 +82,10 @@ class KonnectedSensor(Entity):
self._data = data
self._device_id = device_id
self._type = sensor_type
- self._pin_num = self._data.get(CONF_PIN)
+ self._zone_num = self._data.get(CONF_ZONE)
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self._unique_id = addr or "{}-{}-{}".format(
- device_id, self._pin_num, sensor_type
+ device_id, self._zone_num, sensor_type
)
# set initial state if known at initialization
@@ -99,7 +96,7 @@ class KonnectedSensor(Entity):
# set entity name if given
self._name = self._data.get(CONF_NAME)
if self._name:
- self._name += " " + SENSOR_TYPES[sensor_type][0]
+ self._name += f" {SENSOR_TYPES[sensor_type][0]}"
@property
def unique_id(self) -> str:
@@ -121,6 +118,13 @@ class KonnectedSensor(Entity):
"""Return the unit of measurement."""
return self._unit_of_measurement
+ @property
+ def device_info(self):
+ """Return the device info."""
+ return {
+ "identifiers": {(KONNECTED_DOMAIN, self._device_id)},
+ }
+
async def async_added_to_hass(self):
"""Store entity_id and register state change callback."""
entity_id_key = self._addr or self._type
diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json
new file mode 100644
index 00000000000..4d923238df4
--- /dev/null
+++ b/homeassistant/components/konnected/strings.json
@@ -0,0 +1,105 @@
+{
+ "config": {
+ "title": "Konnected.io",
+ "step": {
+ "import_confirm": {
+ "title": "Import Konnected Device",
+ "description": "A Konnected Alarm Panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry."
+ },
+ "user": {
+ "title": "Discover Konnected Device",
+ "description": "Please enter the host information for your Konnected Panel.",
+ "data": {
+ "host": "Konnected device IP address",
+ "port": "Konnected device port"
+ }
+ },
+ "confirm": {
+ "title": "Konnected Device Ready",
+ "description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings."
+ }
+ },
+ "error": {
+ "cannot_connect": "Unable to connect to a Konnected Panel at {host}:{port}"
+ },
+ "abort": {
+ "unknown": "Unknown error occurred",
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Config flow for device is already in progress.",
+ "not_konn_panel": "Not a recognized Konnected.io device"
+ }
+ },
+ "options": {
+ "title": "Konnected Alarm Panel Options",
+ "step": {
+ "options_io": {
+ "title": "Configure I/O",
+ "description": "Discovered a {model} at {host}. Select the base configuration of each I/O below - depending on the I/O it may allow for binary sensors (open/close contacts), digital sensors (dht and ds18b20), or switchable outputs. You'll be able to configure detailed options in the next steps.",
+ "data": {
+ "1": "Zone 1",
+ "2": "Zone 2",
+ "3": "Zone 3",
+ "4": "Zone 4",
+ "5": "Zone 5",
+ "6": "Zone 6",
+ "7": "Zone 7",
+ "out": "OUT"
+ }
+ },
+ "options_io_ext": {
+ "title": "Configure Extended I/O",
+ "description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
+ "data": {
+ "8": "Zone 8",
+ "9": "Zone 9",
+ "10": "Zone 10",
+ "11": "Zone 11",
+ "12": "Zone 12",
+ "out1": "OUT1",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2"
+ }
+ },
+ "options_binary": {
+ "title": "Configure Binary Sensor",
+ "description": "Please select the options for the binary sensor attached to {zone}",
+ "data": {
+ "type": "Binary Sensor Type",
+ "name": "Name (optional)",
+ "inverse": "Invert the open/close state"
+ }
+ },
+ "options_digital": {
+ "title": "Configure Digital Sensor",
+ "description": "Please select the options for the digital sensor attached to {zone}",
+ "data": {
+ "type": "Sensor Type",
+ "name": "Name (optional)",
+ "poll_interval": "Poll Interval (minutes) (optional)"
+ }
+ },
+ "options_switch": {
+ "title": "Configure Switchable Output",
+ "description": "Please select the output options for {zone}",
+ "data": {
+ "name": "Name (optional)",
+ "activation": "Output when on",
+ "momentary": "Pulse duration (ms) (optional)",
+ "pause": "Pause between pulses (ms) (optional)",
+ "repeat": "Times to repeat (-1=infinite) (optional)"
+ }
+ },
+ "options_misc": {
+ "title": "Configure Misc",
+ "description": "Please select the desired behavior for your panel",
+ "data": {
+ "blink": "Blink panel LED on when sending state change"
+ }
+ }
+ },
+ "error": {},
+ "abort": {
+ "not_konn_panel": "Not a recognized Konnected.io device"
+ }
+ }
+}
diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py
index a88281826c0..d16051eb8da 100644
--- a/homeassistant/components/konnected/switch.py
+++ b/homeassistant/components/konnected/switch.py
@@ -5,12 +5,12 @@ from homeassistant.const import (
ATTR_STATE,
CONF_DEVICES,
CONF_NAME,
- CONF_PIN,
CONF_SWITCHES,
+ CONF_ZONE,
)
from homeassistant.helpers.entity import ToggleEntity
-from . import (
+from .const import (
CONF_ACTIVATION,
CONF_MOMENTARY,
CONF_PAUSE,
@@ -23,16 +23,13 @@ from . import (
_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set switches attached to a Konnected device."""
- if discovery_info is None:
- return
-
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up switches attached to a Konnected device from a config entry."""
data = hass.data[KONNECTED_DOMAIN]
- device_id = discovery_info["device_id"]
+ device_id = config_entry.data["id"]
switches = [
- KonnectedSwitch(device_id, pin_data.get(CONF_PIN), pin_data)
- for pin_data in data[CONF_DEVICES][device_id][CONF_SWITCHES]
+ KonnectedSwitch(device_id, zone_data.get(CONF_ZONE), zone_data)
+ for zone_data in data[CONF_DEVICES][device_id][CONF_SWITCHES]
]
async_add_entities(switches)
@@ -40,11 +37,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class KonnectedSwitch(ToggleEntity):
"""Representation of a Konnected switch."""
- def __init__(self, device_id, pin_num, data):
+ def __init__(self, device_id, zone_num, data):
"""Initialize the Konnected switch."""
self._data = data
self._device_id = device_id
- self._pin_num = pin_num
+ self._zone_num = zone_num
self._activation = self._data.get(CONF_ACTIVATION, STATE_HIGH)
self._momentary = self._data.get(CONF_MOMENTARY)
self._pause = self._data.get(CONF_PAUSE)
@@ -52,7 +49,7 @@ class KonnectedSwitch(ToggleEntity):
self._state = self._boolean_state(self._data.get(ATTR_STATE))
self._name = self._data.get(CONF_NAME)
self._unique_id = "{}-{}-{}-{}-{}".format(
- device_id, self._pin_num, self._momentary, self._pause, self._repeat
+ device_id, self._zone_num, self._momentary, self._pause, self._repeat
)
@property
@@ -71,16 +68,22 @@ class KonnectedSwitch(ToggleEntity):
return self._state
@property
- def client(self):
+ def panel(self):
"""Return the Konnected HTTP client."""
- return self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id].get(
- "client"
- )
+ device_data = self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id]
+ return device_data.get("panel")
- def turn_on(self, **kwargs):
+ @property
+ def device_info(self):
+ """Return the device info."""
+ return {
+ "identifiers": {(KONNECTED_DOMAIN, self._device_id)},
+ }
+
+ async def async_turn_on(self, **kwargs):
"""Send a command to turn on the switch."""
- resp = self.client.put_device(
- self._pin_num,
+ resp = await self.panel.update_switch(
+ self._zone_num,
int(self._activation == STATE_HIGH),
self._momentary,
self._repeat,
@@ -94,9 +97,11 @@ class KonnectedSwitch(ToggleEntity):
# Immediately set the state back off for momentary switches
self._set_state(False)
- def turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs):
"""Send a command to turn off the switch."""
- resp = self.client.put_device(self._pin_num, int(self._activation == STATE_LOW))
+ resp = await self.panel.update_switch(
+ self._zone_num, int(self._activation == STATE_LOW)
+ )
if resp.get(ATTR_STATE) is not None:
self._set_state(self._boolean_state(resp.get(ATTR_STATE)))
@@ -111,9 +116,9 @@ class KonnectedSwitch(ToggleEntity):
def _set_state(self, state):
self._state = state
- self.schedule_update_ha_state()
+ self.async_schedule_update_ha_state()
_LOGGER.debug(
- "Setting status of %s actuator pin %s to %s",
+ "Setting status of %s actuator zone %s to %s",
self._device_id,
self.name,
state,
diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py
index 3a830b9f4e6..1a5b7a56e8e 100644
--- a/homeassistant/components/lastfm/sensor.py
+++ b/homeassistant/components/lastfm/sensor.py
@@ -58,7 +58,6 @@ class LastfmSensor(Entity):
self._unique_id = hashlib.sha256(user.encode("utf-8")).hexdigest()
self._user = lastfm_api.get_user(user)
self._name = user
- self._entity_id = user
self._lastfm = lastfm_api
self._state = "Not Scrobbling"
self._playcount = None
@@ -76,11 +75,6 @@ class LastfmSensor(Entity):
"""Return the name of the sensor."""
return self._name
- @property
- def entity_id(self):
- """Return the entity ID."""
- return f"sensor.lastfm_{self._entity_id}"
-
@property
def state(self):
"""Return the state of the sensor."""
diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py
index c35a0cc00bf..ba9d52b7721 100644
--- a/homeassistant/components/lcn/services.py
+++ b/homeassistant/components/lcn/services.py
@@ -178,7 +178,7 @@ class VarAbs(LcnServiceCall):
"""Set absolute value of a variable or setpoint.
Variable has to be set as counter!
- Reguator setpoints can also be set using R1VARSETPOINT, R2VARSETPOINT.
+ Regulator setpoints can also be set using R1VARSETPOINT, R2VARSETPOINT.
"""
schema = LcnServiceCall.schema.extend(
diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py
index 0be51c337e8..cb91257f83d 100644
--- a/homeassistant/components/lg_netcast/media_player.py
+++ b/homeassistant/components/lg_netcast/media_player.py
@@ -1,5 +1,5 @@
"""Support for LG TV running on NetCast 3 or 4."""
-from datetime import timedelta
+from datetime import datetime, timedelta
import logging
from pylgnetcast import LgNetCastClient, LgNetCastError
@@ -16,6 +16,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF,
+ SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_STEP,
)
@@ -28,11 +29,14 @@ from homeassistant.const import (
STATE_PLAYING,
)
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.script import Script
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "LG TV Remote"
+CONF_ON_ACTION = "turn_on_action"
+
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@@ -49,6 +53,7 @@ SUPPORT_LGTV = (
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
+ vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_ACCESS_TOKEN): vol.All(cv.string, vol.Length(max=6)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -62,20 +67,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
host = config.get(CONF_HOST)
access_token = config.get(CONF_ACCESS_TOKEN)
name = config.get(CONF_NAME)
+ on_action = config.get(CONF_ON_ACTION)
client = LgNetCastClient(host, access_token)
+ on_action_script = Script(hass, on_action) if on_action else None
- add_entities([LgTVDevice(client, name)], True)
+ add_entities([LgTVDevice(client, name, on_action_script)], True)
class LgTVDevice(MediaPlayerDevice):
"""Representation of a LG TV."""
- def __init__(self, client, name):
+ def __init__(self, client, name, on_action_script):
"""Initialize the LG TV device."""
self._client = client
self._name = name
self._muted = False
+ self._on_action_script = on_action_script
# Assume that the TV is in Play mode
self._playing = True
self._volume = 0
@@ -112,6 +120,10 @@ class LgTVDevice(MediaPlayerDevice):
channel_info = channel_info[0]
self._channel_name = channel_info.find("chname").text
self._program_name = channel_info.find("progName").text
+ if self._channel_name is None:
+ self._channel_name = channel_info.find("inputSourceName").text
+ if self._program_name is None:
+ self._program_name = channel_info.find("labelName").text
channel_list = client.query_data("channel_list")
if channel_list:
@@ -180,17 +192,26 @@ class LgTVDevice(MediaPlayerDevice):
@property
def supported_features(self):
"""Flag media player features that are supported."""
+ if self._on_action_script:
+ return SUPPORT_LGTV | SUPPORT_TURN_ON
return SUPPORT_LGTV
@property
def media_image_url(self):
"""URL for obtaining a screen capture."""
- return self._client.url + "data?target=screen_image"
+ return (
+ f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}"
+ )
def turn_off(self):
"""Turn off media player."""
self.send_command(1)
+ def turn_on(self):
+ """Turn on the media player."""
+ if self._on_action_script:
+ self._on_action_script.run()
+
def volume_up(self):
"""Volume up the media player."""
self.send_command(24)
diff --git a/homeassistant/components/life360/.translations/hu.json b/homeassistant/components/life360/.translations/hu.json
index 227e784b065..7f158a24622 100644
--- a/homeassistant/components/life360/.translations/hu.json
+++ b/homeassistant/components/life360/.translations/hu.json
@@ -1,7 +1,16 @@
{
"config": {
"error": {
+ "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v",
"unexpected": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt a kommunik\u00e1ci\u00f3ban a Life360 szerverrel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/life360/.translations/pl.json b/homeassistant/components/life360/.translations/pl.json
index e9cd9920304..f82c8325828 100644
--- a/homeassistant/components/life360/.translations/pl.json
+++ b/homeassistant/components/life360/.translations/pl.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce",
- "user_already_configured": "Konto jest ju\u017c skonfigurowane"
+ "user_already_configured": "Konto jest ju\u017c skonfigurowane."
},
"create_entry": {
"default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})."
@@ -11,7 +11,7 @@
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce",
"invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika",
"unexpected": "Nieoczekiwany b\u0142\u0105d komunikacji z serwerem Life360",
- "user_already_configured": "Konto jest ju\u017c skonfigurowane"
+ "user_already_configured": "Konto jest ju\u017c skonfigurowane."
},
"step": {
"user": {
diff --git a/homeassistant/components/life360/.translations/sv.json b/homeassistant/components/life360/.translations/sv.json
index 836680aad6a..ba28d973ec3 100644
--- a/homeassistant/components/life360/.translations/sv.json
+++ b/homeassistant/components/life360/.translations/sv.json
@@ -10,6 +10,7 @@
"error": {
"invalid_credentials": "Ogiltiga autentiseringsuppgifter",
"invalid_username": "Ogiltigt anv\u00e4ndarnmn",
+ "unexpected": "Ov\u00e4ntat fel vid kommunikation med Life360-servern",
"user_already_configured": "Konto har redan konfigurerats"
},
"step": {
diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py
index 4e845a07854..5bc0c1bc53b 100644
--- a/homeassistant/components/lifx/light.py
+++ b/homeassistant/components/lifx/light.py
@@ -39,6 +39,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import callback
@@ -374,6 +375,9 @@ class LIFXManager:
async def async_service_to_entities(self, service):
"""Return the known entities that a service call mentions."""
+ if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
+ return []
+
if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
return self.entities.values()
diff --git a/homeassistant/components/light/.translations/sv.json b/homeassistant/components/light/.translations/sv.json
new file mode 100644
index 00000000000..8df3f3d382b
--- /dev/null
+++ b/homeassistant/components/light/.translations/sv.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "V\u00e4xla {entity_name}",
+ "turn_off": "St\u00e4ng av {entity_name}",
+ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u00e4r avst\u00e4ngd",
+ "is_on": "{entity_name} \u00e4r p\u00e5"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} avst\u00e4ngd",
+ "turned_on": "{entity_name} slogs p\u00e5"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index 791f7328cf8..5b9b923cc56 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -1,5 +1,4 @@
"""Provides functionality to interact with lights."""
-import asyncio
import csv
from datetime import timedelta
import logging
@@ -8,15 +7,12 @@ from typing import Dict, Optional, Tuple
import voluptuous as vol
-from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.const import (
- ATTR_ENTITY_ID,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
)
-from homeassistant.exceptions import Unauthorized, UnknownUser
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@@ -61,6 +57,8 @@ ATTR_WHITE_VALUE = "white_value"
# Brightness of the light, 0..255 or percentage
ATTR_BRIGHTNESS = "brightness"
ATTR_BRIGHTNESS_PCT = "brightness_pct"
+ATTR_BRIGHTNESS_STEP = "brightness_step"
+ATTR_BRIGHTNESS_STEP_PCT = "brightness_step_pct"
# String representing a profile (built-in ones or external defined).
ATTR_PROFILE = "profile"
@@ -87,12 +85,16 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
+VALID_BRIGHTNESS_STEP = vol.All(vol.Coerce(int), vol.Clamp(min=-255, max=255))
+VALID_BRIGHTNESS_STEP_PCT = vol.All(vol.Coerce(float), vol.Clamp(min=-100, max=100))
LIGHT_TURN_ON_SCHEMA = {
vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string,
ATTR_TRANSITION: VALID_TRANSITION,
- ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
- ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
+ vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
+ vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
+ vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP,
+ vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT,
vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All(
vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple)
@@ -169,7 +171,7 @@ def preprocess_turn_off(params):
"""Process data for turning light off if brightness is 0."""
if ATTR_BRIGHTNESS in params and params[ATTR_BRIGHTNESS] == 0:
# Zero brightness: Light will be turned off
- params = {k: v for k, v in params.items() if k in [ATTR_TRANSITION, ATTR_FLASH]}
+ params = {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)}
return (True, params) # Light should be turned off
return (False, None) # Light should be turned on
@@ -187,70 +189,65 @@ async def async_setup(hass, config):
if not profiles_valid:
return False
- async def async_handle_light_on_service(service):
- """Handle a turn light on service call."""
- # Get the validated data
- params = service.data.copy()
+ def preprocess_data(data):
+ """Preprocess the service data."""
+ base = {}
- # Convert the entity ids to valid light ids
- target_lights = await component.async_extract_from_service(service)
- params.pop(ATTR_ENTITY_ID, None)
+ for entity_field in cv.ENTITY_SERVICE_FIELDS:
+ if entity_field in data:
+ base[entity_field] = data.pop(entity_field)
- if service.context.user_id:
- user = await hass.auth.async_get_user(service.context.user_id)
- if user is None:
- raise UnknownUser(context=service.context)
+ preprocess_turn_on_alternatives(data)
+ turn_lights_off, off_params = preprocess_turn_off(data)
- entity_perms = user.permissions.check_entity
+ base["params"] = data
+ base["turn_lights_off"] = turn_lights_off
+ base["off_params"] = off_params
- for light in target_lights:
- if not entity_perms(light, POLICY_CONTROL):
- raise Unauthorized(
- context=service.context,
- entity_id=light,
- permission=POLICY_CONTROL,
- )
+ return base
- preprocess_turn_on_alternatives(params)
- turn_lights_off, off_params = preprocess_turn_off(params)
+ async def async_handle_light_on_service(light, call):
+ """Handle turning a light on.
- poll_lights = []
- change_tasks = []
- for light in target_lights:
- light.async_set_context(service.context)
+ If brightness is set to 0, this service will turn the light off.
+ """
+ params = call.data["params"]
+ turn_light_off = call.data["turn_lights_off"]
+ off_params = call.data["off_params"]
+
+ if not params:
+ default_profile = Profiles.get_default(light.entity_id)
+
+ if default_profile is not None:
+ params = {ATTR_PROFILE: default_profile}
+ preprocess_turn_on_alternatives(params)
+ turn_light_off, off_params = preprocess_turn_off(params)
+
+ elif ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params:
+ brightness = light.brightness if light.is_on else 0
+
+ params = params.copy()
+
+ if ATTR_BRIGHTNESS_STEP in params:
+ brightness += params.pop(ATTR_BRIGHTNESS_STEP)
- pars = params
- off_pars = off_params
- turn_light_off = turn_lights_off
- if not pars:
- pars = params.copy()
- pars[ATTR_PROFILE] = Profiles.get_default(light.entity_id)
- preprocess_turn_on_alternatives(pars)
- turn_light_off, off_pars = preprocess_turn_off(pars)
- if turn_light_off:
- task = light.async_request_call(light.async_turn_off(**off_pars))
else:
- task = light.async_request_call(light.async_turn_on(**pars))
+ brightness += int(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255)
- change_tasks.append(task)
+ params[ATTR_BRIGHTNESS] = max(0, min(255, brightness))
+ turn_light_off, off_params = preprocess_turn_off(params)
- if light.should_poll:
- poll_lights.append(light)
-
- 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]
- )
+ if turn_light_off:
+ await light.async_turn_off(**off_params)
+ else:
+ await light.async_turn_on(**params)
# Listen for light on and light off service calls.
- hass.services.async_register(
- DOMAIN,
+
+ component.async_register_entity_service(
SERVICE_TURN_ON,
+ vol.All(cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), preprocess_data),
async_handle_light_on_service,
- schema=cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA),
)
component.async_register_entity_service(
diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py
index c436ce7886a..99f5b6b12bc 100644
--- a/homeassistant/components/light/device_action.py
+++ b/homeassistant/components/light/device_action.py
@@ -4,13 +4,32 @@ from typing import List
import voluptuous as vol
from homeassistant.components.device_automation import toggle_entity
-from homeassistant.const import CONF_DOMAIN
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ CONF_DOMAIN,
+ CONF_TYPE,
+ SERVICE_TURN_ON,
+)
from homeassistant.core import Context, HomeAssistant
+from homeassistant.helpers import config_validation as cv, entity_registry
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
-from . import DOMAIN
+from . import ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS
-ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN})
+TYPE_BRIGHTNESS_INCREASE = "brightness_increase"
+TYPE_BRIGHTNESS_DECREASE = "brightness_decrease"
+
+ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
+ {
+ vol.Required(ATTR_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_DOMAIN): DOMAIN,
+ vol.Required(CONF_TYPE): vol.In(
+ toggle_entity.DEVICE_ACTION_TYPES
+ + [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE]
+ ),
+ }
+)
async def async_call_action_from_config(
@@ -20,11 +39,57 @@ async def async_call_action_from_config(
context: Context,
) -> None:
"""Change state based on configuration."""
- await toggle_entity.async_call_action_from_config(
- hass, config, variables, context, DOMAIN
+ if config[CONF_TYPE] in toggle_entity.DEVICE_ACTION_TYPES:
+ await toggle_entity.async_call_action_from_config(
+ hass, config, variables, context, DOMAIN
+ )
+ return
+
+ data = {ATTR_ENTITY_ID: config[ATTR_ENTITY_ID]}
+
+ if config[CONF_TYPE] == TYPE_BRIGHTNESS_INCREASE:
+ data[ATTR_BRIGHTNESS_STEP_PCT] = 10
+ else:
+ data[ATTR_BRIGHTNESS_STEP_PCT] = -10
+
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=context
)
async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
"""List device actions."""
- return await toggle_entity.async_get_actions(hass, device_id, DOMAIN)
+ actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN)
+
+ registry = await entity_registry.async_get_registry(hass)
+
+ for entry in entity_registry.async_entries_for_device(registry, device_id):
+ if entry.domain != DOMAIN:
+ continue
+
+ state = hass.states.get(entry.entity_id)
+
+ if state:
+ supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ else:
+ supported_features = entry.supported_features
+
+ if supported_features & SUPPORT_BRIGHTNESS:
+ actions.extend(
+ (
+ {
+ CONF_TYPE: TYPE_BRIGHTNESS_INCREASE,
+ "device_id": device_id,
+ "entity_id": entry.entity_id,
+ "domain": DOMAIN,
+ },
+ {
+ CONF_TYPE: TYPE_BRIGHTNESS_DECREASE,
+ "device_id": device_id,
+ "entity_id": entry.entity_id,
+ "domain": DOMAIN,
+ },
+ )
+ )
+
+ return actions
diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py
index ea8899c44fc..c172ac1330a 100644
--- a/homeassistant/components/light/intent.py
+++ b/homeassistant/components/light/intent.py
@@ -1,6 +1,7 @@
"""Intents for the light integration."""
import voluptuous as vol
+from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
@@ -8,7 +9,6 @@ import homeassistant.util.color as color_util
from . import (
ATTR_BRIGHTNESS_PCT,
- ATTR_ENTITY_ID,
ATTR_RGB_COLOR,
DOMAIN,
SERVICE_TURN_ON,
diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml
index 449e5ea5aaf..a2b71f5632b 100644
--- a/homeassistant/components/light/services.yaml
+++ b/homeassistant/components/light/services.yaml
@@ -36,6 +36,12 @@ turn_on:
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
+ brightness_step:
+ description: Change brightness by an amount. Should be between -255..255.
+ example: -25.5
+ brightness_step_pct:
+ description: Change brightness by a percentage. Should be between -100..100.
+ example: -10
profile:
description: Name of a light profile to use.
example: relax
diff --git a/homeassistant/components/linky/.translations/hu.json b/homeassistant/components/linky/.translations/hu.json
index 436e8b1fb7d..f5c5f788063 100644
--- a/homeassistant/components/linky/.translations/hu.json
+++ b/homeassistant/components/linky/.translations/hu.json
@@ -2,6 +2,19 @@
"config": {
"abort": {
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "access": "Nem siker\u00fclt el\u00e9rni az Enedis.fr webhelyet, ellen\u0151rizze internet-kapcsolat\u00e1t",
+ "enedis": "Az Enedis.fr hib\u00e1val v\u00e1laszolt: k\u00e9rj\u00fck, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra (\u00e1ltal\u00e1ban nem 23:00 \u00e9s 2:00 k\u00f6z\u00f6tt)",
+ "unknown": "Ismeretlen hiba: pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb (\u00e1ltal\u00e1ban nem 23:00 \u00e9s 2:00 \u00f3ra k\u00f6z\u00f6tt)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "E-mail"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/nl.json b/homeassistant/components/linky/.translations/nl.json
index ecc566c8b87..5654edb08f4 100644
--- a/homeassistant/components/linky/.translations/nl.json
+++ b/homeassistant/components/linky/.translations/nl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Account al geconfigureerd"
+ },
"error": {
"access": "Geen toegang tot Enedis.fr, controleer uw internetverbinding",
"enedis": "Enedis.fr antwoordde met een fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)",
diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json
index 51f96dcf17a..62da10e1c96 100644
--- a/homeassistant/components/linky/.translations/pl.json
+++ b/homeassistant/components/linky/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane"
+ "already_configured": "Konto jest ju\u017c skonfigurowane."
},
"error": {
"access": "Nie mo\u017cna uzyska\u0107 dost\u0119pu do Enedis.fr, sprawd\u017a po\u0142\u0105czenie internetowe",
diff --git a/homeassistant/components/linky/.translations/sl.json b/homeassistant/components/linky/.translations/sl.json
index ab5e054db1e..6ebe598e882 100644
--- a/homeassistant/components/linky/.translations/sl.json
+++ b/homeassistant/components/linky/.translations/sl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ra\u010dun \u017ee nastavljen"
+ },
"error": {
"access": "Do Enedis.fr ni bilo mogo\u010de dostopati, preverite internetno povezavo",
"enedis": "Enedis.fr je odgovoril z napako: poskusite pozneje (ponavadi med 23. in 2. uro)",
diff --git a/homeassistant/components/linky/.translations/sv.json b/homeassistant/components/linky/.translations/sv.json
index 4e7be709482..4880e065fa2 100644
--- a/homeassistant/components/linky/.translations/sv.json
+++ b/homeassistant/components/linky/.translations/sv.json
@@ -2,6 +2,23 @@
"config": {
"abort": {
"already_configured": "Kontot har redan konfigurerats."
- }
+ },
+ "error": {
+ "access": "Det gick inte att komma \u00e5t Enedis.fr, kontrollera din internetanslutning",
+ "enedis": "Enedis.fr svarade med ett fel: f\u00f6rs\u00f6k igen senare (vanligtvis inte mellan 23:00 och 02:00)",
+ "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare (vanligtvis inte mellan 23:00 och 02:00)",
+ "wrong_login": "Inloggningsfel: v\u00e4nligen kontrollera din e-post och l\u00f6senord"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "L\u00f6senord",
+ "username": "E-post"
+ },
+ "description": "Ange dina autentiseringsuppgifter",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/zh-Hans.json b/homeassistant/components/linky/.translations/zh-Hans.json
index 2c6b3ba34b5..62138856078 100644
--- a/homeassistant/components/linky/.translations/zh-Hans.json
+++ b/homeassistant/components/linky/.translations/zh-Hans.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "wrong_login": "\u767b\u5f55\u51fa\u9519\uff1a\u8bf7\u68c0\u67e5\u60a8\u7684\u7535\u5b50\u90ae\u7bb1\u548c\u5bc6\u7801"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py
index 9beb9acc403..846b7eeb99f 100644
--- a/homeassistant/components/linky/sensor.py
+++ b/homeassistant/components/linky/sensor.py
@@ -13,6 +13,7 @@ from homeassistant.const import (
CONF_USERNAME,
ENERGY_KILO_WATT_HOUR,
)
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType
@@ -73,6 +74,7 @@ class LinkyAccount:
_LOGGER.debug(json.dumps(self._data, indent=2))
except PyLinkyException as exp:
_LOGGER.error(exp)
+ raise PlatformNotReady
finally:
client.close_session()
@@ -146,6 +148,9 @@ class LinkySensor(Entity):
async def async_update(self) -> None:
"""Retrieve the new data for the sensor."""
+ if self._account.data is None:
+ return
+
data = self._account.data[self._scale][self._when]
self._consumption = data[CONSUMPTION]
self._time = data[TIME]
diff --git a/homeassistant/components/liveboxplaytv/__init__.py b/homeassistant/components/liveboxplaytv/__init__.py
deleted file mode 100644
index 384c0e4c34b..00000000000
--- a/homeassistant/components/liveboxplaytv/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The liveboxplaytv component."""
diff --git a/homeassistant/components/liveboxplaytv/manifest.json b/homeassistant/components/liveboxplaytv/manifest.json
deleted file mode 100644
index a05ff27ca90..00000000000
--- a/homeassistant/components/liveboxplaytv/manifest.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "domain": "liveboxplaytv",
- "name": "Orange Livebox Play TV",
- "documentation": "https://www.home-assistant.io/integrations/liveboxplaytv",
- "requirements": ["liveboxplaytv==2.0.3", "pyteleloisirs==3.6"],
- "dependencies": [],
- "codeowners": ["@pschmitt"]
-}
diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py
deleted file mode 100644
index 66fb383d677..00000000000
--- a/homeassistant/components/liveboxplaytv/media_player.py
+++ /dev/null
@@ -1,273 +0,0 @@
-"""Support for interface with an Orange Livebox Play TV appliance."""
-from datetime import timedelta
-import logging
-
-from liveboxplaytv import LiveboxPlayTv
-import pyteleloisirs
-import requests
-import voluptuous as vol
-
-from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
-from homeassistant.components.media_player.const import (
- MEDIA_TYPE_CHANNEL,
- SUPPORT_NEXT_TRACK,
- SUPPORT_PAUSE,
- SUPPORT_PLAY,
- SUPPORT_PREVIOUS_TRACK,
- SUPPORT_SELECT_SOURCE,
- SUPPORT_TURN_OFF,
- SUPPORT_TURN_ON,
- SUPPORT_VOLUME_MUTE,
- SUPPORT_VOLUME_STEP,
-)
-from homeassistant.const import (
- CONF_HOST,
- CONF_NAME,
- CONF_PORT,
- STATE_OFF,
- STATE_ON,
- STATE_PAUSED,
- STATE_PLAYING,
-)
-import homeassistant.helpers.config_validation as cv
-import homeassistant.util.dt as dt_util
-
-_LOGGER = logging.getLogger(__name__)
-
-DEFAULT_NAME = "Livebox Play TV"
-DEFAULT_PORT = 8080
-
-SUPPORT_LIVEBOXPLAYTV = (
- SUPPORT_TURN_OFF
- | SUPPORT_TURN_ON
- | SUPPORT_NEXT_TRACK
- | SUPPORT_PAUSE
- | SUPPORT_PREVIOUS_TRACK
- | SUPPORT_VOLUME_STEP
- | SUPPORT_VOLUME_MUTE
- | SUPPORT_SELECT_SOURCE
- | SUPPORT_PLAY
-)
-
-MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
-MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
-)
-
-
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the Orange Livebox Play TV platform."""
- host = config.get(CONF_HOST)
- port = config.get(CONF_PORT)
- name = config.get(CONF_NAME)
-
- livebox_devices = []
-
- try:
- device = LiveboxPlayTvDevice(host, port, name)
- livebox_devices.append(device)
- except OSError:
- _LOGGER.error(
- "Failed to connect to Livebox Play TV at %s:%s. "
- "Please check your configuration",
- host,
- port,
- )
- async_add_entities(livebox_devices, True)
-
-
-class LiveboxPlayTvDevice(MediaPlayerDevice):
- """Representation of an Orange Livebox Play TV."""
-
- def __init__(self, host, port, name):
- """Initialize the Livebox Play TV device."""
-
- self._client = LiveboxPlayTv(host, port)
- # Assume that the appliance is not muted
- self._muted = False
- self._name = name
- self._current_source = None
- self._state = None
- self._channel_list = {}
- self._current_channel = None
- self._current_program = None
- self._media_duration = None
- self._media_remaining_time = None
- self._media_image_url = None
- self._media_last_updated = None
-
- async def async_update(self):
- """Retrieve the latest data."""
-
- try:
- self._state = self.refresh_state()
- # Update channel list
- self.refresh_channel_list()
- # Update current channel
- channel = self._client.channel
- if channel is not None:
- self._current_channel = channel
- program = await self._client.async_get_current_program()
- if program and self._current_program != program.get("name"):
- self._current_program = program.get("name")
- # Media progress info
- self._media_duration = pyteleloisirs.get_program_duration(program)
- rtime = pyteleloisirs.get_remaining_time(program)
- if rtime != self._media_remaining_time:
- self._media_remaining_time = rtime
- self._media_last_updated = dt_util.utcnow()
- # Set media image to current program if a thumbnail is
- # available. Otherwise we'll use the channel's image.
- img_size = 800
- prg_img_url = await self._client.async_get_current_program_image(
- img_size
- )
- if prg_img_url:
- self._media_image_url = prg_img_url
- else:
- chan_img_url = self._client.get_current_channel_image(img_size)
- self._media_image_url = chan_img_url
- except requests.ConnectionError:
- self._state = None
-
- @property
- def name(self):
- """Return the name of the device."""
- return self._name
-
- @property
- def state(self):
- """Return the state of the device."""
- return self._state
-
- @property
- def is_volume_muted(self):
- """Boolean if volume is currently muted."""
- return self._muted
-
- @property
- def source(self):
- """Return the current input source."""
- return self._current_channel
-
- @property
- def source_list(self):
- """List of available input sources."""
- # Sort channels by tvIndex
- return [self._channel_list[c] for c in sorted(self._channel_list.keys())]
-
- @property
- def media_content_type(self):
- """Content type of current playing media."""
- # return self._client.media_type
- return MEDIA_TYPE_CHANNEL
-
- @property
- def media_image_url(self):
- """Image url of current playing media."""
- return self._media_image_url
-
- @property
- def media_title(self):
- """Title of current playing media."""
- if self._current_channel:
- if self._current_program:
- return f"{self._current_channel}: {self._current_program}"
- return self._current_channel
-
- @property
- def media_duration(self):
- """Duration of current playing media in seconds."""
- return self._media_duration
-
- @property
- def media_position(self):
- """Position of current playing media in seconds."""
- return self._media_remaining_time
-
- @property
- def media_position_updated_at(self):
- """When was the position of the current playing media valid.
-
- Returns value from homeassistant.util.dt.utcnow().
- """
- return self._media_last_updated
-
- @property
- def supported_features(self):
- """Flag media player features that are supported."""
- return SUPPORT_LIVEBOXPLAYTV
-
- def refresh_channel_list(self):
- """Refresh the list of available channels."""
- new_channel_list = {}
- # update channels
- for channel in self._client.get_channels():
- new_channel_list[int(channel["index"])] = channel["name"]
- self._channel_list = new_channel_list
-
- def refresh_state(self):
- """Refresh the current media state."""
- state = self._client.media_state
- if state == "PLAY":
- return STATE_PLAYING
- if state == "PAUSE":
- return STATE_PAUSED
-
- return STATE_ON if self._client.is_on else STATE_OFF
-
- def turn_off(self):
- """Turn off media player."""
- self._state = STATE_OFF
- self._client.turn_off()
-
- def turn_on(self):
- """Turn on the media player."""
- self._state = STATE_ON
- self._client.turn_on()
-
- def volume_up(self):
- """Volume up the media player."""
- self._client.volume_up()
-
- def volume_down(self):
- """Volume down media player."""
- self._client.volume_down()
-
- def mute_volume(self, mute):
- """Send mute command."""
- self._muted = mute
- self._client.mute()
-
- def media_play_pause(self):
- """Simulate play pause media player."""
- self._client.play_pause()
-
- def select_source(self, source):
- """Select input source."""
- self._current_source = source
- self._client.set_channel(source)
-
- def media_play(self):
- """Send play command."""
- self._state = STATE_PLAYING
- self._client.play()
-
- def media_pause(self):
- """Send media pause command to media player."""
- self._state = STATE_PAUSED
- self._client.pause()
-
- def media_next_track(self):
- """Send next track command."""
- self._client.channel_up()
-
- def media_previous_track(self):
- """Send the previous track command."""
- self._client.channel_down()
diff --git a/homeassistant/components/local_ip/.translations/hu.json b/homeassistant/components/local_ip/.translations/hu.json
new file mode 100644
index 00000000000..7a78029c379
--- /dev/null
+++ b/homeassistant/components/local_ip/.translations/hu.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az integr\u00e1ci\u00f3 m\u00e1r konfigur\u00e1lva van egy ilyen nev\u0171 l\u00e9tez\u0151 \u00e9rz\u00e9kel\u0151vel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u00c9rz\u00e9kel\u0151 neve"
+ },
+ "title": "Helyi IP c\u00edm"
+ }
+ },
+ "title": "Helyi IP c\u00edm"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/local_ip/.translations/nl.json b/homeassistant/components/local_ip/.translations/nl.json
index 4f0d9a437db..6f22d2c585a 100644
--- a/homeassistant/components/local_ip/.translations/nl.json
+++ b/homeassistant/components/local_ip/.translations/nl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Integratie is al geconfigureerd met een bestaande sensor met die naam"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/local_ip/.translations/pl.json b/homeassistant/components/local_ip/.translations/pl.json
index a4032eeebd1..82b614a8e17 100644
--- a/homeassistant/components/local_ip/.translations/pl.json
+++ b/homeassistant/components/local_ip/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Integracja jest ju\u017c skonfigurowana z istniej\u0105cym sensorem o tej nazwie"
+ "already_configured": "Integracja jest ju\u017c skonfigurowana z istniej\u0105cym sensorem o tej nazwie."
},
"step": {
"user": {
diff --git a/homeassistant/components/local_ip/.translations/ru.json b/homeassistant/components/local_ip/.translations/ru.json
index de92b9680f0..2cf8791e505 100644
--- a/homeassistant/components/local_ip/.translations/ru.json
+++ b/homeassistant/components/local_ip/.translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u0430\u0442\u0447\u0438\u043a\u0430."
+ "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c."
},
"step": {
"user": {
diff --git a/homeassistant/components/local_ip/.translations/sv.json b/homeassistant/components/local_ip/.translations/sv.json
new file mode 100644
index 00000000000..d9f9b474f9c
--- /dev/null
+++ b/homeassistant/components/local_ip/.translations/sv.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Integrationen \u00e4r redan konfigurerad med en befintlig sensor med det namnet"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Sensor Namn"
+ },
+ "title": "Lokal IP-adress"
+ }
+ },
+ "title": "Lokal IP-adress"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/locative/.translations/pl.json b/homeassistant/components/locative/.translations/pl.json
index 9c22a8e3fea..23a4c98a54c 100644
--- a/homeassistant/components/locative/.translations/pl.json
+++ b/homeassistant/components/locative/.translations/pl.json
@@ -10,7 +10,7 @@
"step": {
"user": {
"description": "Na pewno chcesz skonfigurowa\u0107 Locative Webhook?",
- "title": "Skonfiguruj Locative Webhook"
+ "title": "Konfiguracja Locative Webhook"
}
},
"title": "Locative Webhook"
diff --git a/homeassistant/components/lock/.translations/sv.json b/homeassistant/components/lock/.translations/sv.json
new file mode 100644
index 00000000000..7d50b4ea61a
--- /dev/null
+++ b/homeassistant/components/lock/.translations/sv.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "lock": "L\u00e5s {entity_name}",
+ "open": "\u00d6ppna {entity_name}",
+ "unlock": "L\u00e5s upp {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} \u00e4r l\u00e5st",
+ "is_unlocked": "{entity_name} \u00e4r ol\u00e5st"
+ },
+ "trigger_type": {
+ "locked": "{entity_name} l\u00e5st",
+ "unlocked": "{entity_name} ol\u00e5st"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py
index 8043469d43b..961718a30ee 100644
--- a/homeassistant/components/logger/__init__.py
+++ b/homeassistant/components/logger/__init__.py
@@ -1,4 +1,4 @@
-"""Support for settting the level of logging for components."""
+"""Support for setting the level of logging for components."""
from collections import OrderedDict
import logging
diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py
index 6c99356907f..fc8cb67894b 100644
--- a/homeassistant/components/lovelace/__init__.py
+++ b/homeassistant/components/lovelace/__init__.py
@@ -89,6 +89,9 @@ class LovelaceStorage:
async def async_load(self, force):
"""Load config."""
+ if self._hass.config.safe_mode:
+ raise ConfigNotFound
+
if self._data is None:
await self._load()
diff --git a/homeassistant/components/luftdaten/.translations/pl.json b/homeassistant/components/luftdaten/.translations/pl.json
index 5a2c30db44c..19e71b5156f 100644
--- a/homeassistant/components/luftdaten/.translations/pl.json
+++ b/homeassistant/components/luftdaten/.translations/pl.json
@@ -3,7 +3,7 @@
"error": {
"communication_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z API Luftdaten",
"invalid_sensor": "Sensor niedost\u0119pny lub nieprawid\u0142owy",
- "sensor_exists": "Sensor zosta\u0142 ju\u017c zarejestrowany"
+ "sensor_exists": "Sensor zosta\u0142 ju\u017c zarejestrowany."
},
"step": {
"user": {
diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json
index 1a05137f82d..759fd926bdc 100644
--- a/homeassistant/components/luftdaten/.translations/ru.json
+++ b/homeassistant/components/luftdaten/.translations/ru.json
@@ -2,14 +2,14 @@
"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.",
- "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."
+ "invalid_sensor": "\u0421\u0435\u043d\u0441\u043e\u0440 \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": "\u0421\u0435\u043d\u0441\u043e\u0440 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d."
},
"step": {
"user": {
"data": {
"show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435",
- "station_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 Luftdaten"
+ "station_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 Luftdaten"
},
"title": "Luftdaten"
}
diff --git a/homeassistant/components/lutron_caseta/.translations/hu.json b/homeassistant/components/lutron_caseta/.translations/hu.json
new file mode 100644
index 00000000000..cfc3c290afe
--- /dev/null
+++ b/homeassistant/components/lutron_caseta/.translations/hu.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Lutron Cas\u00e9ta"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/.translations/sv.json b/homeassistant/components/lutron_caseta/.translations/sv.json
new file mode 100644
index 00000000000..cfc3c290afe
--- /dev/null
+++ b/homeassistant/components/lutron_caseta/.translations/sv.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Lutron Cas\u00e9ta"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py
index 1b65cb161e1..3e6ecbc948b 100644
--- a/homeassistant/components/maxcube/__init__.py
+++ b/homeassistant/components/maxcube/__init__.py
@@ -90,14 +90,14 @@ class MaxCubeHandle:
self.cube = cube
self.scan_interval = scan_interval
self.mutex = Lock()
- self._updatets = time.time()
+ self._updatets = time.monotonic()
def update(self):
"""Pull the latest data from the MAX! Cube."""
# Acquire mutex to prevent simultaneous update from multiple threads
with self.mutex:
# Only update every update_interval
- if (time.time() - self._updatets) >= self.scan_interval:
+ if (time.monotonic() - self._updatets) >= self.scan_interval:
_LOGGER.debug("Updating")
try:
@@ -106,6 +106,6 @@ class MaxCubeHandle:
_LOGGER.error("Max!Cube connection failed")
return False
- self._updatets = time.time()
+ self._updatets = time.monotonic()
else:
_LOGGER.debug("Skipping update")
diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py
index e95b91389cd..59f268e657c 100644
--- a/homeassistant/components/mcp23017/binary_sensor.py
+++ b/homeassistant/components/mcp23017/binary_sensor.py
@@ -1,7 +1,7 @@
"""Support for binary sensor using I2C MCP23017 chip."""
import logging
-import adafruit_mcp230xx # pylint: disable=import-error
+from adafruit_mcp230xx.mcp23017 import MCP23017 # pylint: disable=import-error
import board # pylint: disable=import-error
import busio # pylint: disable=import-error
import digitalio # pylint: disable=import-error
@@ -46,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
i2c_address = config[CONF_I2C_ADDRESS]
i2c = busio.I2C(board.SCL, board.SDA)
- mcp = adafruit_mcp230xx.MCP23017(i2c, address=i2c_address)
+ mcp = MCP23017(i2c, address=i2c_address)
binary_sensors = []
pins = config[CONF_PINS]
diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json
index ebf796fe3db..8bdd897d34e 100644
--- a/homeassistant/components/mcp23017/manifest.json
+++ b/homeassistant/components/mcp23017/manifest.json
@@ -4,8 +4,8 @@
"documentation": "https://www.home-assistant.io/integrations/mcp23017",
"requirements": [
"RPi.GPIO==0.7.0",
- "adafruit-blinka==1.2.1",
- "adafruit-circuitpython-mcp230xx==1.1.2"
+ "adafruit-blinka==3.9.0",
+ "adafruit-circuitpython-mcp230xx==2.2.2"
],
"dependencies": [],
"codeowners": ["@jardiamj"]
diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py
index 8506106b705..fe76f4ce632 100644
--- a/homeassistant/components/mcp23017/switch.py
+++ b/homeassistant/components/mcp23017/switch.py
@@ -1,7 +1,7 @@
"""Support for switch sensor using I2C MCP23017 chip."""
import logging
-import adafruit_mcp230xx # pylint: disable=import-error
+from adafruit_mcp230xx.mcp23017 import MCP23017 # pylint: disable=import-error
import board # pylint: disable=import-error
import busio # pylint: disable=import-error
import digitalio # pylint: disable=import-error
@@ -39,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
i2c_address = config.get(CONF_I2C_ADDRESS)
i2c = busio.I2C(board.SCL, board.SDA)
- mcp = adafruit_mcp230xx.MCP23017(i2c, address=i2c_address)
+ mcp = MCP23017(i2c, address=i2c_address)
switches = []
pins = config.get(CONF_PINS)
diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json
index 28bbc92b850..815a00c5223 100644
--- a/homeassistant/components/media_extractor/manifest.json
+++ b/homeassistant/components/media_extractor/manifest.json
@@ -2,7 +2,7 @@
"domain": "media_extractor",
"name": "Media Extractor",
"documentation": "https://www.home-assistant.io/integrations/media_extractor",
- "requirements": ["youtube_dl==2020.01.24"],
+ "requirements": ["youtube_dl==2020.02.16"],
"dependencies": ["media_player"],
"codeowners": [],
"quality_scale": "internal"
diff --git a/homeassistant/components/media_player/.translations/hu.json b/homeassistant/components/media_player/.translations/hu.json
new file mode 100644
index 00000000000..fbefbc43e08
--- /dev/null
+++ b/homeassistant/components/media_player/.translations/hu.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_idle": "{entity_name} t\u00e9tlen",
+ "is_off": "{entity_name} ki van kapcsolva",
+ "is_on": "{entity_name} be van kapcsolva",
+ "is_paused": "{entity_name} sz\u00fcneteltetve van",
+ "is_playing": "{entity_name} lej\u00e1tszik"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 2911a143a3c..8a31dbe6bdb 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -766,7 +766,7 @@ class MediaPlayerDevice(Entity):
@property
def capability_attributes(self):
- """Return capabilitiy attributes."""
+ """Return capability attributes."""
supported_features = self.supported_features or 0
data = {}
diff --git a/homeassistant/components/melcloud/.translations/ca.json b/homeassistant/components/melcloud/.translations/ca.json
new file mode 100644
index 00000000000..1dc5156f7e7
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/ca.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La integraci\u00f3 MELCloud ja est\u00e0 configurada amb aquest correu electr\u00f2nic. El testimoni d'acc\u00e9s s'ha actualitzat."
+ },
+ "error": {
+ "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya de MELCloud.",
+ "username": "Correu electr\u00f2nic d'inici de sessi\u00f3 a MELCloud."
+ },
+ "description": "Connecta\u2019t amb el teu compte de MELCloud.",
+ "title": "Connexi\u00f3 amb MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/da.json b/homeassistant/components/melcloud/.translations/da.json
new file mode 100644
index 00000000000..6901ed22934
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/da.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "MELCloud-integration er allerede konfigureret for denne e-mail. Adgangstoken er blevet opdateret."
+ },
+ "error": {
+ "cannot_connect": "Kunne ikke oprette forbindelse. Pr\u00f8v igen",
+ "invalid_auth": "Ugyldig godkendelse",
+ "unknown": "Uventet fejl"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud-adgangskode.",
+ "username": "E-mail, der bruges til at logge ind p\u00e5 MELCloud."
+ },
+ "description": "Opret forbindelse ved hj\u00e6lp af din MELCloud-konto.",
+ "title": "Opret forbindelse til MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/de.json b/homeassistant/components/melcloud/.translations/de.json
new file mode 100644
index 00000000000..f4e2a3b1ebc
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/de.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Die MELCloud-Integration ist bereits f\u00fcr diese E-Mail konfiguriert. Das Zugriffstoken wurde aktualisiert."
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud Passwort.",
+ "username": "E-Mail-Adresse f\u00fcr die Anmeldung bei MELCloud."
+ },
+ "description": "Verbinden Sie sich mit Ihrem MELCloud-Konto.",
+ "title": "Stellen Sie eine Verbindung zu MELCloud her"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/en.json b/homeassistant/components/melcloud/.translations/en.json
new file mode 100644
index 00000000000..48682f617a3
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/en.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed."
+ },
+ "error": {
+ "cannot_connect": "Failed to connect, please try again",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud password.",
+ "username": "Email used to login to MELCloud."
+ },
+ "description": "Connect using your MELCloud account.",
+ "title": "Connect to MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/es.json b/homeassistant/components/melcloud/.translations/es.json
new file mode 100644
index 00000000000..182f06c33c3
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/es.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Integraci\u00f3n mELCloud ya configurada para este correo electr\u00f3nico. Se ha actualizado el token de acceso."
+ },
+ "error": {
+ "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntelo de nuevo.",
+ "invalid_auth": "Autentificaci\u00f3n inv\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a de MELCloud.",
+ "username": "Correo electr\u00f3nico utilizado para iniciar sesi\u00f3n en MELCloud."
+ },
+ "description": "Con\u00e9ctate usando tu cuenta de MELCloud.",
+ "title": "Con\u00e9ctese a MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/fr.json b/homeassistant/components/melcloud/.translations/fr.json
new file mode 100644
index 00000000000..e442325d9dc
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/fr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer",
+ "invalid_auth": "Authentification non valide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "username": "E-mail utilis\u00e9e pour vous connecter \u00e0 MELCloud."
+ },
+ "description": "Se connecter en utilisant votre MELCloud compte.",
+ "title": "Se connecter \u00e0 MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/it.json b/homeassistant/components/melcloud/.translations/it.json
new file mode 100644
index 00000000000..029fc2526b2
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/it.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Integrazione MELCloud gi\u00e0 configurata per questa e-mail. Il token di accesso \u00e8 stato aggiornato."
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi, si prega di riprovare.",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password MELCloud.",
+ "username": "Email utilizzata per accedere a MELCloud."
+ },
+ "description": "Connettiti utilizzando il tuo account MELCloud.",
+ "title": "Connettersi a MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/ko.json b/homeassistant/components/melcloud/.translations/ko.json
new file mode 100644
index 00000000000..1557abf5a32
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/ko.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc774 \uc774\uba54\uc77c\uc5d0 \ub300\ud55c MELCloud \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uac31\uc2e0\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud \uc758 \ube44\ubc00\ubc88\ud638\ub97c \ub123\uc5b4\uc8fc\uc138\uc694.",
+ "username": "MELCloud \ub85c\uadf8\uc778 \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \ub123\uc5b4\uc8fc\uc138\uc694."
+ },
+ "description": "MELCloud \uacc4\uc815\uc73c\ub85c \uc5f0\uacb0\ud558\uc138\uc694.",
+ "title": "MELCloud \uc5f0\uacb0"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/lb.json b/homeassistant/components/melcloud/.translations/lb.json
new file mode 100644
index 00000000000..b082ef78965
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/lb.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "MELCloud Integratioun ass scho konfigur\u00e9iert fir d\u00ebs Email. Acc\u00e8s Jeton gouf erneiert."
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
+ "unknown": "Onerwaarte Feeler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud Passwuert",
+ "username": "Email d\u00e9i benotz g\u00ebtt fir sech mat MELCloud ze verbannen"
+ },
+ "description": "Verbann dech mat dengem MElCloud Kont.",
+ "title": "Mat MELCloud verbannen"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/nl.json b/homeassistant/components/melcloud/.translations/nl.json
new file mode 100644
index 00000000000..b60495e7f47
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/nl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "MELCloud integratie is al geconfigureerd voor deze e-mail. Toegangstoken is vernieuwd."
+ },
+ "error": {
+ "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud wachtwoord.",
+ "username": "E-mail gebruikt om in te loggen op MELCloud."
+ },
+ "description": "Maak verbinding via uw MELCloud account.",
+ "title": "Maak verbinding met MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/no.json b/homeassistant/components/melcloud/.translations/no.json
new file mode 100644
index 00000000000..a464bbfda19
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/no.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "MELCloud integrasjon er allerede konfigurert p\u00e5 denne e-posten. Access token har blitt oppdatert."
+ },
+ "error": {
+ "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud passord.",
+ "username": "E-post som blir brukt til \u00e5 logge inn p\u00e5 MELCloud."
+ },
+ "description": "Koble til ved hjelp av MELCloud-kontoen din.",
+ "title": "Koble til MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/pl.json b/homeassistant/components/melcloud/.translations/pl.json
new file mode 100644
index 00000000000..9abb68ca85a
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/pl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Integracja MELCloud jest ju\u017c skonfigurowana dla tego adresu e-mail. Token dost\u0119pu zosta\u0142 od\u015bwie\u017cony."
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
+ "invalid_auth": "Niepoprawne uwierzytelnienie.",
+ "unknown": "Niespodziewany b\u0142\u0105d."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o MELCloud.",
+ "username": "Adres e-mail u\u017cywany do logowania do MELCloud"
+ },
+ "description": "Po\u0142\u0105cz u\u017cywaj\u0105c swojego konta MELCloud.",
+ "title": "Po\u0142\u0105cz si\u0119 z MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/ru.json b/homeassistant/components/melcloud/.translations/ru.json
new file mode 100644
index 00000000000..d4bab0e417e
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/ru.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MELCloud \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0430\u0434\u0440\u0435\u0441\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c MELCloud.",
+ "username": "\u042d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430\u044f \u043f\u043e\u0447\u0442\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 MELCloud."
+ },
+ "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u0441\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c MELCloud.",
+ "title": "MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/sl.json b/homeassistant/components/melcloud/.translations/sl.json
new file mode 100644
index 00000000000..04dbb953d0d
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/sl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Za to e-po\u0161to je \u017ee konfigurirana integracija MELCloud. \u017deton za dostop je bil osve\u017een."
+ },
+ "error": {
+ "cannot_connect": "Povezava ni uspela, poskusite znova",
+ "invalid_auth": "Neveljavna avtentikacija",
+ "unknown": "Nepri\u010dakovana napaka"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud geslo.",
+ "username": "E-po\u0161tni naslov za prijavo v MELCloud."
+ },
+ "description": "Pove\u017eite se s svojim ra\u010dunom MELCloud.",
+ "title": "Pove\u017eite se z MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/sv.json b/homeassistant/components/melcloud/.translations/sv.json
new file mode 100644
index 00000000000..72a251ef9d0
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/sv.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "MELCloud-integration redan konfigurerad f\u00f6r den h\u00e4r e-postadressen. \u00c5tkomsttoken har uppdaterats."
+ },
+ "error": {
+ "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen",
+ "invalid_auth": "Ogiltig autentisering",
+ "unknown": "Ov\u00e4ntat fel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud-l\u00f6senord.",
+ "username": "E-post som anv\u00e4nds f\u00f6r att logga in p\u00e5 MELCloud."
+ },
+ "description": "Anslut med ditt MELCloud-konto.",
+ "title": "Anslut till MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/.translations/zh-Hant.json b/homeassistant/components/melcloud/.translations/zh-Hant.json
new file mode 100644
index 00000000000..c098d041598
--- /dev/null
+++ b/homeassistant/components/melcloud/.translations/zh-Hant.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5df2\u4f7f\u7528\u6b64\u90f5\u4ef6\u8a2d\u5b9a MELCloud \u6574\u5408\u3002\u5b58\u53d6\u5bc6\u9470\u5df2\u66f4\u65b0\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "MELCloud \u5bc6\u78bc\u3002",
+ "username": "MELCloud \u767b\u5165\u90f5\u4ef6\u3002"
+ },
+ "description": "\u4f7f\u7528 MELCloud \u5e33\u865f\u9032\u884c\u9023\u7dda\u3002",
+ "title": "\u9023\u7dda\u81f3 MELCloud"
+ }
+ },
+ "title": "MELCloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py
new file mode 100644
index 00000000000..ef932f36aa4
--- /dev/null
+++ b/homeassistant/components/melcloud/__init__.py
@@ -0,0 +1,160 @@
+"""The MELCloud Climate integration."""
+import asyncio
+from datetime import timedelta
+import logging
+from typing import Any, Dict, List
+
+from aiohttp import ClientConnectionError
+from async_timeout import timeout
+from pymelcloud import Device, get_devices
+import voluptuous as vol
+
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.const import CONF_TOKEN, CONF_USERNAME
+from homeassistant.exceptions import ConfigEntryNotReady
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.util import Throttle
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
+
+PLATFORMS = ["climate", "sensor"]
+
+CONF_LANGUAGE = "language"
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_TOKEN): cv.string,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass: HomeAssistantType, config: ConfigEntry):
+ """Establish connection with MELCloud."""
+ if DOMAIN not in config:
+ return True
+
+ username = config[DOMAIN][CONF_USERNAME]
+ token = config[DOMAIN][CONF_TOKEN]
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={CONF_USERNAME: username, CONF_TOKEN: token},
+ )
+ )
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
+ """Establish connection with MELClooud."""
+ conf = entry.data
+ mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN])
+ hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices})
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+ return True
+
+
+async def async_unload_entry(hass, config_entry):
+ """Unload a config entry."""
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+ if not hass.data[DOMAIN]:
+ hass.data.pop(DOMAIN)
+ return True
+
+
+class MelCloudDevice:
+ """MELCloud Device instance."""
+
+ def __init__(self, device: Device):
+ """Construct a device wrapper."""
+ self.device = device
+ self.name = device.name
+ self._available = True
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ async def async_update(self, **kwargs):
+ """Pull the latest data from MELCloud."""
+ try:
+ await self.device.update()
+ self._available = True
+ except ClientConnectionError:
+ _LOGGER.warning("Connection failed for %s", self.name)
+ self._available = False
+
+ async def async_set(self, properties: Dict[str, Any]):
+ """Write state changes to the MELCloud API."""
+ try:
+ await self.device.set(properties)
+ self._available = True
+ except ClientConnectionError:
+ _LOGGER.warning("Connection failed for %s", self.name)
+ self._available = False
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
+
+ @property
+ def device_id(self):
+ """Return device ID."""
+ return self.device.device_id
+
+ @property
+ def building_id(self):
+ """Return building ID of the device."""
+ return self.device.building_id
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ _device_info = {
+ "identifiers": {(DOMAIN, f"{self.device.mac}-{self.device.serial}")},
+ "manufacturer": "Mitsubishi Electric",
+ "name": self.name,
+ }
+ unit_infos = self.device.units
+ if unit_infos is not None:
+ _device_info["model"] = ", ".join(
+ [x["model"] for x in unit_infos if x["model"]]
+ )
+ return _device_info
+
+
+async def mel_devices_setup(hass, token) -> List[MelCloudDevice]:
+ """Query connected devices from MELCloud."""
+ session = hass.helpers.aiohttp_client.async_get_clientsession()
+ try:
+ with timeout(10):
+ all_devices = await get_devices(
+ token,
+ session,
+ conf_update_interval=timedelta(minutes=5),
+ device_set_debounce=timedelta(seconds=1),
+ )
+ except (asyncio.TimeoutError, ClientConnectionError) as ex:
+ raise ConfigEntryNotReady() from ex
+
+ wrapped_devices = {}
+ for device_type, devices in all_devices.items():
+ wrapped_devices[device_type] = [MelCloudDevice(device) for device in devices]
+ return wrapped_devices
diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py
new file mode 100644
index 00000000000..95cb1489f45
--- /dev/null
+++ b/homeassistant/components/melcloud/climate.py
@@ -0,0 +1,171 @@
+"""Platform for climate integration."""
+from datetime import timedelta
+import logging
+from typing import List, Optional
+
+from pymelcloud import DEVICE_TYPE_ATA
+
+from homeassistant.components.climate import ClimateDevice
+from homeassistant.components.climate.const import (
+ DEFAULT_MAX_TEMP,
+ DEFAULT_MIN_TEMP,
+ HVAC_MODE_OFF,
+ SUPPORT_FAN_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import TEMP_CELSIUS
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.util.temperature import convert as convert_temperature
+
+from . import MelCloudDevice
+from .const import DOMAIN, HVAC_MODE_LOOKUP, HVAC_MODE_REVERSE_LOOKUP, TEMP_UNIT_LOOKUP
+
+SCAN_INTERVAL = timedelta(seconds=60)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+):
+ """Set up MelCloud device climate based on config_entry."""
+ mel_devices = hass.data[DOMAIN][entry.entry_id]
+ async_add_entities(
+ [AtaDeviceClimate(mel_device) for mel_device in mel_devices[DEVICE_TYPE_ATA]],
+ True,
+ )
+
+
+class AtaDeviceClimate(ClimateDevice):
+ """Air-to-Air climate device."""
+
+ def __init__(self, device: MelCloudDevice):
+ """Initialize the climate."""
+ self._api = device
+ self._device = self._api.device
+ self._name = device.name
+
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Return a unique ID."""
+ return f"{self._device.serial}-{self._device.mac}"
+
+ @property
+ def name(self):
+ """Return the display name of this light."""
+ return self._name
+
+ async def async_update(self):
+ """Update state from MELCloud."""
+ await self._api.async_update()
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ return self._api.device_info
+
+ @property
+ def temperature_unit(self) -> str:
+ """Return the unit of measurement used by the platform."""
+ return TEMP_UNIT_LOOKUP.get(self._device.temp_unit, TEMP_CELSIUS)
+
+ @property
+ def hvac_mode(self) -> str:
+ """Return hvac operation ie. heat, cool mode."""
+ mode = self._device.operation_mode
+ if not self._device.power or mode is None:
+ return HVAC_MODE_OFF
+ return HVAC_MODE_LOOKUP.get(mode)
+
+ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
+ """Set new target hvac mode."""
+ if hvac_mode == HVAC_MODE_OFF:
+ await self._device.set({"power": False})
+ return
+
+ operation_mode = HVAC_MODE_REVERSE_LOOKUP.get(hvac_mode)
+ if operation_mode is None:
+ raise ValueError(f"Invalid hvac_mode [{hvac_mode}]")
+
+ props = {"operation_mode": operation_mode}
+ if self.hvac_mode == HVAC_MODE_OFF:
+ props["power"] = True
+ await self._device.set(props)
+
+ @property
+ def hvac_modes(self) -> List[str]:
+ """Return the list of available hvac operation modes."""
+ return [HVAC_MODE_OFF] + [
+ HVAC_MODE_LOOKUP.get(mode) for mode in self._device.operation_modes
+ ]
+
+ @property
+ def current_temperature(self) -> Optional[float]:
+ """Return the current temperature."""
+ return self._device.room_temperature
+
+ @property
+ def target_temperature(self) -> Optional[float]:
+ """Return the temperature we try to reach."""
+ return self._device.target_temperature
+
+ async def async_set_temperature(self, **kwargs) -> None:
+ """Set new target temperature."""
+ await self._device.set(
+ {"target_temperature": kwargs.get("temperature", self.target_temperature)}
+ )
+
+ @property
+ def target_temperature_step(self) -> Optional[float]:
+ """Return the supported step of target temperature."""
+ return self._device.target_temperature_step
+
+ @property
+ def fan_mode(self) -> Optional[str]:
+ """Return the fan setting."""
+ return self._device.fan_speed
+
+ async def async_set_fan_mode(self, fan_mode: str) -> None:
+ """Set new target fan mode."""
+ await self._device.set({"fan_speed": fan_mode})
+
+ @property
+ def fan_modes(self) -> Optional[List[str]]:
+ """Return the list of available fan modes."""
+ return self._device.fan_speeds
+
+ async def async_turn_on(self) -> None:
+ """Turn the entity on."""
+ await self._device.set({"power": True})
+
+ async def async_turn_off(self) -> None:
+ """Turn the entity off."""
+ await self._device.set({"power": False})
+
+ @property
+ def supported_features(self) -> int:
+ """Return the list of supported features."""
+ return SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE
+
+ @property
+ def min_temp(self) -> float:
+ """Return the minimum temperature."""
+ min_value = self._device.target_temperature_min
+ if min_value is not None:
+ return min_value
+
+ return convert_temperature(
+ DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit
+ )
+
+ @property
+ def max_temp(self) -> float:
+ """Return the maximum temperature."""
+ max_value = self._device.target_temperature_max
+ if max_value is not None:
+ return max_value
+
+ return convert_temperature(
+ DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit
+ )
diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py
new file mode 100644
index 00000000000..6bda8cc3c28
--- /dev/null
+++ b/homeassistant/components/melcloud/config_flow.py
@@ -0,0 +1,84 @@
+"""Config flow for the MELCloud platform."""
+import asyncio
+import logging
+from typing import Optional
+
+from aiohttp import ClientError, ClientResponseError
+from async_timeout import timeout
+import pymelcloud
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
+
+from .const import DOMAIN # pylint: disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def _create_entry(self, username: str, token: str):
+ """Register new entry."""
+ await self.async_set_unique_id(username)
+ self._abort_if_unique_id_configured({CONF_TOKEN: token})
+ return self.async_create_entry(
+ title=username, data={CONF_USERNAME: username, CONF_TOKEN: token},
+ )
+
+ async def _create_client(
+ self,
+ username: str,
+ *,
+ password: Optional[str] = None,
+ token: Optional[str] = None,
+ ):
+ """Create client."""
+ if password is None and token is None:
+ raise ValueError(
+ "Invalid internal state. Called without either password or token",
+ )
+
+ try:
+ with timeout(10):
+ acquired_token = token
+ if acquired_token is None:
+ acquired_token = await pymelcloud.login(
+ username,
+ password,
+ self.hass.helpers.aiohttp_client.async_get_clientsession(),
+ )
+ await pymelcloud.get_devices(
+ acquired_token,
+ self.hass.helpers.aiohttp_client.async_get_clientsession(),
+ )
+ except ClientResponseError as err:
+ if err.status == 401 or err.status == 403:
+ return self.async_abort(reason="invalid_auth")
+ return self.async_abort(reason="cannot_connect")
+ except (asyncio.TimeoutError, ClientError):
+ return self.async_abort(reason="cannot_connect")
+
+ return await self._create_entry(username, acquired_token)
+
+ async def async_step_user(self, user_input=None):
+ """User initiated config flow."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
+ ),
+ )
+ username = user_input[CONF_USERNAME]
+ return await self._create_client(username, password=user_input[CONF_PASSWORD])
+
+ async def async_step_import(self, user_input):
+ """Import a config entry."""
+ return await self._create_client(
+ user_input[CONF_USERNAME], token=user_input[CONF_TOKEN]
+ )
diff --git a/homeassistant/components/melcloud/const.py b/homeassistant/components/melcloud/const.py
new file mode 100644
index 00000000000..e262be2c3fb
--- /dev/null
+++ b/homeassistant/components/melcloud/const.py
@@ -0,0 +1,29 @@
+"""Constants for the MELCloud Climate integration."""
+import pymelcloud.ata_device as ata_device
+from pymelcloud.const import UNIT_TEMP_CELSIUS, UNIT_TEMP_FAHRENHEIT
+
+from homeassistant.components.climate.const import (
+ HVAC_MODE_COOL,
+ HVAC_MODE_DRY,
+ HVAC_MODE_FAN_ONLY,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_HEAT_COOL,
+)
+from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
+
+DOMAIN = "melcloud"
+
+HVAC_MODE_LOOKUP = {
+ ata_device.OPERATION_MODE_HEAT: HVAC_MODE_HEAT,
+ ata_device.OPERATION_MODE_DRY: HVAC_MODE_DRY,
+ ata_device.OPERATION_MODE_COOL: HVAC_MODE_COOL,
+ ata_device.OPERATION_MODE_FAN_ONLY: HVAC_MODE_FAN_ONLY,
+ ata_device.OPERATION_MODE_HEAT_COOL: HVAC_MODE_HEAT_COOL,
+}
+HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in HVAC_MODE_LOOKUP.items()}
+
+TEMP_UNIT_LOOKUP = {
+ UNIT_TEMP_CELSIUS: TEMP_CELSIUS,
+ UNIT_TEMP_FAHRENHEIT: TEMP_FAHRENHEIT,
+}
+TEMP_UNIT_REVERSE_LOOKUP = {v: k for k, v in TEMP_UNIT_LOOKUP.items()}
diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json
new file mode 100644
index 00000000000..55edcdd0d9f
--- /dev/null
+++ b/homeassistant/components/melcloud/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "melcloud",
+ "name": "MELCloud",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/melcloud",
+ "requirements": ["pymelcloud==2.1.0"],
+ "dependencies": [],
+ "codeowners": ["@vilppuvuorinen"]
+}
diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py
new file mode 100644
index 00000000000..8f55906443e
--- /dev/null
+++ b/homeassistant/components/melcloud/sensor.py
@@ -0,0 +1,102 @@
+"""Support for MelCloud device sensors."""
+import logging
+
+from pymelcloud import DEVICE_TYPE_ATA
+
+from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.helpers.entity import Entity
+
+from . import MelCloudDevice
+from .const import DOMAIN, TEMP_UNIT_LOOKUP
+
+ATTR_MEASUREMENT_NAME = "measurement_name"
+ATTR_ICON = "icon"
+ATTR_UNIT_FN = "unit_fn"
+ATTR_DEVICE_CLASS = "device_class"
+ATTR_VALUE_FN = "value_fn"
+ATTR_ENABLED_FN = "enabled"
+
+SENSORS = {
+ "room_temperature": {
+ ATTR_MEASUREMENT_NAME: "Room Temperature",
+ ATTR_ICON: "mdi:thermometer",
+ ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS),
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ ATTR_VALUE_FN: lambda x: x.device.room_temperature,
+ ATTR_ENABLED_FN: lambda x: True,
+ },
+ "energy": {
+ ATTR_MEASUREMENT_NAME: "Energy",
+ ATTR_ICON: "mdi:factory",
+ ATTR_UNIT_FN: lambda x: "kWh",
+ ATTR_DEVICE_CLASS: None,
+ ATTR_VALUE_FN: lambda x: x.device.total_energy_consumed,
+ ATTR_ENABLED_FN: lambda x: x.device.has_energy_consumed_meter,
+ },
+}
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up MELCloud device sensors based on config_entry."""
+ mel_devices = hass.data[DOMAIN].get(entry.entry_id)
+ async_add_entities(
+ [
+ MelCloudSensor(mel_device, measurement, definition)
+ for measurement, definition in SENSORS.items()
+ for mel_device in mel_devices[DEVICE_TYPE_ATA]
+ if definition[ATTR_ENABLED_FN](mel_device)
+ ],
+ True,
+ )
+
+
+class MelCloudSensor(Entity):
+ """Representation of a Sensor."""
+
+ def __init__(self, device: MelCloudDevice, measurement, definition):
+ """Initialize the sensor."""
+ self._api = device
+ self._name_slug = device.name
+ self._measurement = measurement
+ self._def = definition
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return f"{self._api.device.serial}-{self._api.device.mac}-{self._measurement}"
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return self._def[ATTR_ICON]
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return f"{self._name_slug} {self._def[ATTR_MEASUREMENT_NAME]}"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._def[ATTR_VALUE_FN](self._api)
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._def[ATTR_UNIT_FN](self._api)
+
+ @property
+ def device_class(self):
+ """Return device class."""
+ return self._def[ATTR_DEVICE_CLASS]
+
+ async def async_update(self):
+ """Retrieve latest state."""
+ await self._api.async_update()
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ return self._api.device_info
diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json
new file mode 100644
index 00000000000..477ca7eb5e2
--- /dev/null
+++ b/homeassistant/components/melcloud/strings.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "title": "MELCloud",
+ "step": {
+ "user": {
+ "title": "Connect to MELCloud",
+ "description": "Connect using your MELCloud account.",
+ "data": {
+ "username": "Email used to login to MELCloud.",
+ "password": "MELCloud password."
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "Failed to connect, please try again",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "abort": {
+ "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed."
+ }
+ }
+}
diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json
index f647dcf7b45..e22ac763d56 100644
--- a/homeassistant/components/met/.translations/pl.json
+++ b/homeassistant/components/met/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "Lokalizacja ju\u017c istnieje"
+ "name_exists": "Lokalizacja ju\u017c istnieje."
},
"step": {
"user": {
diff --git a/homeassistant/components/met/.translations/sv.json b/homeassistant/components/met/.translations/sv.json
index aa860e27307..d8b461913da 100644
--- a/homeassistant/components/met/.translations/sv.json
+++ b/homeassistant/components/met/.translations/sv.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "Namnet finns redan"
+ "name_exists": "Plats finns redan"
},
"step": {
"user": {
diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py
index 759f7f6fc89..683390429c3 100644
--- a/homeassistant/components/met/config_flow.py
+++ b/homeassistant/components/met/config_flow.py
@@ -12,15 +12,15 @@ from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME
@callback
def configured_instances(hass):
"""Return a set of configured SimpliSafe instances."""
- entites = []
+ entries = []
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.data.get("track_home"):
- entites.append("home")
+ entries.append("home")
continue
- entites.append(
+ entries.append(
f"{entry.data.get(CONF_LATITUDE)}-{entry.data.get(CONF_LONGITUDE)}"
)
- return set(entites)
+ return set(entries)
class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/meteo_france/.translations/ca.json b/homeassistant/components/meteo_france/.translations/ca.json
index 6f2fd707045..aeceb80a063 100644
--- a/homeassistant/components/meteo_france/.translations/ca.json
+++ b/homeassistant/components/meteo_france/.translations/ca.json
@@ -1,10 +1,15 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ciutat ja configurada",
+ "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard"
+ },
"step": {
"user": {
"data": {
"city": "Ciutat"
},
+ "description": "Introdueix el codi postal (nom\u00e9s recomanat per Fran\u00e7a) o nom de la ciutat",
"title": "M\u00e9t\u00e9o-France"
}
},
diff --git a/homeassistant/components/meteo_france/.translations/de.json b/homeassistant/components/meteo_france/.translations/de.json
index 0e99c1de0ce..8f05ad18df3 100644
--- a/homeassistant/components/meteo_france/.translations/de.json
+++ b/homeassistant/components/meteo_france/.translations/de.json
@@ -9,6 +9,7 @@
"data": {
"city": "Stadt"
},
+ "description": "Geben Sie die Postleitzahl (nur f\u00fcr Frankreich empfohlen) oder den St\u00e4dtenamen ein",
"title": "M\u00e9t\u00e9o-France"
}
},
diff --git a/homeassistant/components/meteo_france/.translations/es.json b/homeassistant/components/meteo_france/.translations/es.json
new file mode 100644
index 00000000000..3cd7ee56252
--- /dev/null
+++ b/homeassistant/components/meteo_france/.translations/es.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La ciudad ya est\u00e1 configurada",
+ "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "city": "Ciudad"
+ },
+ "description": "Introduzca el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad",
+ "title": "M\u00e9t\u00e9o-France"
+ }
+ },
+ "title": "M\u00e9t\u00e9o-France"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/.translations/fr.json b/homeassistant/components/meteo_france/.translations/fr.json
new file mode 100644
index 00000000000..7dff0d237fd
--- /dev/null
+++ b/homeassistant/components/meteo_france/.translations/fr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ville d\u00e9j\u00e0 configur\u00e9e",
+ "unknown": "Erreur inconnue: veuillez r\u00e9essayer plus tard"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "city": "Ville"
+ },
+ "description": "Entrez le code postal (uniquement pour la France, recommand\u00e9) ou le nom de la ville",
+ "title": "M\u00e9t\u00e9o-France"
+ }
+ },
+ "title": "M\u00e9t\u00e9o-France"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/.translations/hu.json b/homeassistant/components/meteo_france/.translations/hu.json
new file mode 100644
index 00000000000..f1719f4bf30
--- /dev/null
+++ b/homeassistant/components/meteo_france/.translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A v\u00e1ros m\u00e1r konfigur\u00e1lva van",
+ "unknown": "Ismeretlen hiba: k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "city": "V\u00e1ros"
+ },
+ "description": "\u00cdrja be az ir\u00e1ny\u00edt\u00f3sz\u00e1mot (csak Franciaorsz\u00e1g eset\u00e9ben aj\u00e1nlott) vagy a v\u00e1ros nev\u00e9t",
+ "title": "M\u00e9t\u00e9o-France"
+ }
+ },
+ "title": "M\u00e9t\u00e9o-France"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/.translations/it.json b/homeassistant/components/meteo_france/.translations/it.json
new file mode 100644
index 00000000000..5a067430906
--- /dev/null
+++ b/homeassistant/components/meteo_france/.translations/it.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Citt\u00e0 gi\u00e0 configurata",
+ "unknown": "Errore sconosciuto: riprovare pi\u00f9 tardi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "city": "Citt\u00e0"
+ },
+ "description": "Inserisci il codice postale (solo per la Francia, consigliato) o il nome della citt\u00e0",
+ "title": "M\u00e9t\u00e9o-France"
+ }
+ },
+ "title": "M\u00e9t\u00e9o-France"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/.translations/nl.json b/homeassistant/components/meteo_france/.translations/nl.json
new file mode 100644
index 00000000000..648ef0c5fbd
--- /dev/null
+++ b/homeassistant/components/meteo_france/.translations/nl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Stad al geconfigureerd",
+ "unknown": "Onbekende fout: probeer het later nog eens"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "city": "Stad"
+ },
+ "description": "Vul de postcode (alleen voor Frankrijk, aanbevolen) of de plaatsnaam in",
+ "title": "M\u00e9t\u00e9o-France"
+ }
+ },
+ "title": "M\u00e9t\u00e9o-France"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/.translations/pl.json b/homeassistant/components/meteo_france/.translations/pl.json
index a519eaead5d..38aa1944fac 100644
--- a/homeassistant/components/meteo_france/.translations/pl.json
+++ b/homeassistant/components/meteo_france/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Miasto jest ju\u017c skonfigurowane",
+ "already_configured": "Miasto jest ju\u017c skonfigurowane.",
"unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej"
},
"step": {
diff --git a/homeassistant/components/meteo_france/.translations/sl.json b/homeassistant/components/meteo_france/.translations/sl.json
new file mode 100644
index 00000000000..845a89c4775
--- /dev/null
+++ b/homeassistant/components/meteo_france/.translations/sl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Mesto je \u017ee konfigurirano",
+ "unknown": "Neznana napaka: poskusite pozneje"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "city": "Mesto"
+ },
+ "description": "Vnesite po\u0161tno \u0161tevilko (samo za Francijo) ali ime mesta",
+ "title": "M\u00e9t\u00e9o-France"
+ }
+ },
+ "title": "M\u00e9t\u00e9o-France"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py
index 73b8dbb0e39..b7eda51b955 100644
--- a/homeassistant/components/meteo_france/__init__.py
+++ b/homeassistant/components/meteo_france/__init__.py
@@ -1,4 +1,5 @@
"""Support for Meteo-France weather data."""
+import asyncio
import datetime
import logging
@@ -6,116 +7,96 @@ from meteofrance.client import meteofranceClient, meteofranceError
from vigilancemeteo import VigilanceMeteoError, VigilanceMeteoFranceProxy
import voluptuous as vol
-from homeassistant.const import CONF_MONITORED_CONDITIONS
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.discovery import load_platform
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util import Throttle
-from .const import CONF_CITY, DATA_METEO_FRANCE, DOMAIN, SENSOR_TYPES
+from .const import CONF_CITY, DOMAIN, PLATFORMS
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = datetime.timedelta(minutes=5)
-def has_all_unique_cities(value):
- """Validate that all cities are unique."""
- cities = [location[CONF_CITY] for location in value]
- vol.Schema(vol.Unique())(cities)
- return value
-
+CITY_SCHEMA = vol.Schema({vol.Required(CONF_CITY): cv.string})
CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.All(
- cv.ensure_list,
- [
- vol.Schema(
- {
- vol.Required(CONF_CITY): cv.string,
- vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(
- cv.ensure_list, [vol.In(SENSOR_TYPES)]
- ),
- }
- )
- ],
- has_all_unique_cities,
- )
- },
- extra=vol.ALLOW_EXTRA,
+ {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CITY_SCHEMA]))}, extra=vol.ALLOW_EXTRA,
)
-def setup(hass, config):
- """Set up the Meteo-France component."""
- hass.data[DATA_METEO_FRANCE] = {}
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+ """Set up Meteo-France from legacy config file."""
- # Check if at least weather alert have to be monitored for one location.
- need_weather_alert_watcher = False
- for location in config[DOMAIN]:
- if (
- CONF_MONITORED_CONDITIONS in location
- and "weather_alert" in location[CONF_MONITORED_CONDITIONS]
- ):
- need_weather_alert_watcher = True
+ conf = config.get(DOMAIN)
+ if conf is None:
+ return True
- # If weather alert monitoring is expected initiate a client to be used by
- # all weather_alert entities.
- if need_weather_alert_watcher:
- _LOGGER.debug("Weather Alert monitoring expected. Loading vigilancemeteo")
-
- weather_alert_client = VigilanceMeteoFranceProxy()
- try:
- weather_alert_client.update_data()
- except VigilanceMeteoError as exp:
- _LOGGER.error(
- "Unexpected error when creating the vigilance_meteoFrance proxy: %s ",
- exp,
+ for city_conf in conf:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=city_conf.copy()
)
- else:
- weather_alert_client = None
- hass.data[DATA_METEO_FRANCE]["weather_alert_client"] = weather_alert_client
-
- for location in config[DOMAIN]:
-
- city = location[CONF_CITY]
-
- try:
- client = meteofranceClient(city)
- except meteofranceError as exp:
- _LOGGER.error(
- "Unexpected error when creating the meteofrance proxy: %s", exp
- )
- return
-
- client.need_rain_forecast = bool(
- CONF_MONITORED_CONDITIONS in location
- and "next_rain" in location[CONF_MONITORED_CONDITIONS]
)
- hass.data[DATA_METEO_FRANCE][city] = MeteoFranceUpdater(client)
- hass.data[DATA_METEO_FRANCE][city].update()
-
- if CONF_MONITORED_CONDITIONS in location:
- monitored_conditions = location[CONF_MONITORED_CONDITIONS]
- _LOGGER.debug("meteo_france sensor platform loaded for %s", city)
- load_platform(
- hass,
- "sensor",
- DOMAIN,
- {CONF_CITY: city, CONF_MONITORED_CONDITIONS: monitored_conditions},
- config,
- )
-
- load_platform(hass, "weather", DOMAIN, {CONF_CITY: city}, config)
-
return True
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Set up an Meteo-France account from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
+
+ # Weather alert
+ weather_alert_client = VigilanceMeteoFranceProxy()
+ try:
+ await hass.async_add_executor_job(weather_alert_client.update_data)
+ except VigilanceMeteoError as exp:
+ _LOGGER.error(
+ "Unexpected error when creating the vigilance_meteoFrance proxy: %s ", exp
+ )
+ return False
+ hass.data[DOMAIN]["weather_alert_client"] = weather_alert_client
+
+ # Weather
+ city = entry.data[CONF_CITY]
+ try:
+ client = await hass.async_add_executor_job(meteofranceClient, city)
+ except meteofranceError as exp:
+ _LOGGER.error("Unexpected error when creating the meteofrance proxy: %s", exp)
+ return False
+
+ hass.data[DOMAIN][city] = MeteoFranceUpdater(client)
+ await hass.async_add_executor_job(hass.data[DOMAIN][city].update)
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+ _LOGGER.debug("meteo_france sensor platform loaded for %s", city)
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.data[CONF_CITY])
+
+ return unload_ok
+
+
class MeteoFranceUpdater:
"""Update data from Meteo-France."""
- def __init__(self, client):
+ def __init__(self, client: meteofranceClient):
"""Initialize the data object."""
self._client = client
diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py
new file mode 100644
index 00000000000..c7673020360
--- /dev/null
+++ b/homeassistant/components/meteo_france/config_flow.py
@@ -0,0 +1,62 @@
+"""Config flow to configure the Meteo-France integration."""
+import logging
+
+from meteofrance.client import meteofranceClient, meteofranceError
+import voluptuous as vol
+
+from homeassistant import config_entries
+
+from .const import CONF_CITY
+from .const import DOMAIN # pylint: disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a Meteo-France config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def _show_setup_form(self, user_input=None, errors=None):
+ """Show the setup form to the user."""
+
+ if user_input is None:
+ user_input = {}
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {vol.Required(CONF_CITY, default=user_input.get(CONF_CITY, "")): str}
+ ),
+ errors=errors or {},
+ )
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initiated by the user."""
+ errors = {}
+
+ if user_input is None:
+ return self._show_setup_form(user_input, errors)
+
+ city = user_input[CONF_CITY] # Might be a city name or a postal code
+ city_name = None
+
+ try:
+ client = await self.hass.async_add_executor_job(meteofranceClient, city)
+ city_name = client.get_data()["name"]
+ except meteofranceError as exp:
+ _LOGGER.error(
+ "Unexpected error when creating the meteofrance proxy: %s", exp
+ )
+ return self.async_abort(reason="unknown")
+
+ # Check if already configured
+ await self.async_set_unique_id(city_name)
+ self._abort_if_unique_id_configured()
+
+ return self.async_create_entry(title=city_name, data={CONF_CITY: city})
+
+ async def async_step_import(self, user_input):
+ """Import a config entry."""
+ return await self.async_step_user(user_input)
diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py
index 223aca20bac..fae2000b19a 100644
--- a/homeassistant/components/meteo_france/const.py
+++ b/homeassistant/components/meteo_france/const.py
@@ -3,7 +3,7 @@
from homeassistant.const import TEMP_CELSIUS
DOMAIN = "meteo_france"
-DATA_METEO_FRANCE = "data_meteo_france"
+PLATFORMS = ["sensor", "weather"]
ATTRIBUTION = "Data provided by Météo-France"
CONF_CITY = "city"
diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json
index 41a003ea4f7..77f8fca984d 100644
--- a/homeassistant/components/meteo_france/manifest.json
+++ b/homeassistant/components/meteo_france/manifest.json
@@ -1,8 +1,9 @@
{
"domain": "meteo_france",
"name": "Météo-France",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/meteo_france",
"requirements": ["meteofrance==0.3.7", "vigilancemeteo==3.0.0"],
"dependencies": [],
- "codeowners": ["@victorcerutti", "@oncleben31"]
+ "codeowners": ["@victorcerutti", "@oncleben31", "@Quentame"]
}
diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py
index f0c08ac1822..cf28b9ea558 100644
--- a/homeassistant/components/meteo_france/sensor.py
+++ b/homeassistant/components/meteo_france/sensor.py
@@ -1,15 +1,18 @@
"""Support for Meteo-France raining forecast sensor."""
import logging
-from vigilancemeteo import DepartmentWeatherAlert
+from meteofrance.client import meteofranceClient
+from vigilancemeteo import DepartmentWeatherAlert, VigilanceMeteoFranceProxy
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import HomeAssistantType
from .const import (
ATTRIBUTION,
CONF_CITY,
- DATA_METEO_FRANCE,
+ DOMAIN,
SENSOR_TYPE_CLASS,
SENSOR_TYPE_ICON,
SENSOR_TYPE_NAME,
@@ -23,52 +26,47 @@ STATE_ATTR_FORECAST = "1h rain forecast"
STATE_ATTR_BULLETIN_TIME = "Bulletin date"
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Meteo-France sensor."""
- if discovery_info is None:
- return
-
- city = discovery_info[CONF_CITY]
- monitored_conditions = discovery_info[CONF_MONITORED_CONDITIONS]
- client = hass.data[DATA_METEO_FRANCE][city]
- weather_alert_client = hass.data[DATA_METEO_FRANCE]["weather_alert_client"]
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Meteo-France sensor platform."""
+ city = entry.data[CONF_CITY]
+ client = hass.data[DOMAIN][city]
+ weather_alert_client = hass.data[DOMAIN]["weather_alert_client"]
alert_watcher = None
- if "weather_alert" in monitored_conditions:
- datas = hass.data[DATA_METEO_FRANCE][city].get_data()
- # Check if a department code is available for this city.
- if "dept" in datas:
- try:
- # If yes create the watcher DepartmentWeatherAlert object.
- alert_watcher = DepartmentWeatherAlert(
- datas["dept"], weather_alert_client
- )
- except ValueError as exp:
- _LOGGER.error(
- "Unexpected error when creating the weather alert sensor for %s in department %s: %s",
- city,
- datas["dept"],
- exp,
- )
- alert_watcher = None
- else:
- _LOGGER.info(
- "Weather alert watcher added for %s in department %s",
- city,
- datas["dept"],
- )
- else:
- _LOGGER.warning(
- "No 'dept' key found for '%s'. So weather alert information won't be available",
- city,
+ datas = client.get_data()
+ # Check if a department code is available for this city.
+ if "dept" in datas:
+ try:
+ # If yes create the watcher DepartmentWeatherAlert object.
+ alert_watcher = await hass.async_add_executor_job(
+ DepartmentWeatherAlert, datas["dept"], weather_alert_client
)
- # Exit and don't create the sensor if no department code available.
- return
+ _LOGGER.info(
+ "Weather alert watcher added for %s in department %s",
+ city,
+ datas["dept"],
+ )
+ except ValueError as exp:
+ _LOGGER.error(
+ "Unexpected error when creating the weather alert sensor for %s in department %s: %s",
+ city,
+ datas["dept"],
+ exp,
+ )
+ else:
+ _LOGGER.warning(
+ "No 'dept' key found for '%s'. So weather alert information won't be available",
+ city,
+ )
+ # Exit and don't create the sensor if no department code available.
+ return
- add_entities(
+ async_add_entities(
[
- MeteoFranceSensor(variable, client, alert_watcher)
- for variable in monitored_conditions
+ MeteoFranceSensor(sensor_type, client, alert_watcher)
+ for sensor_type in SENSOR_TYPES
],
True,
)
@@ -77,9 +75,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class MeteoFranceSensor(Entity):
"""Representation of a Meteo-France sensor."""
- def __init__(self, condition, client, alert_watcher):
+ def __init__(
+ self,
+ sensor_type: str,
+ client: meteofranceClient,
+ alert_watcher: VigilanceMeteoFranceProxy,
+ ):
"""Initialize the Meteo-France sensor."""
- self._condition = condition
+ self._type = sensor_type
self._client = client
self._alert_watcher = alert_watcher
self._state = None
@@ -88,7 +91,12 @@ class MeteoFranceSensor(Entity):
@property
def name(self):
"""Return the name of the sensor."""
- return f"{self._data['name']} {SENSOR_TYPES[self._condition][SENSOR_TYPE_NAME]}"
+ return f"{self._data['name']} {SENSOR_TYPES[self._type][SENSOR_TYPE_NAME]}"
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the sensor."""
+ return self.name
@property
def state(self):
@@ -99,7 +107,7 @@ class MeteoFranceSensor(Entity):
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
# Attributes for next_rain sensor.
- if self._condition == "next_rain" and "rain_forecast" in self._data:
+ if self._type == "next_rain" and "rain_forecast" in self._data:
return {
**{STATE_ATTR_FORECAST: self._data["rain_forecast"]},
**self._data["next_rain_intervals"],
@@ -107,7 +115,7 @@ class MeteoFranceSensor(Entity):
}
# Attributes for weather_alert sensor.
- if self._condition == "weather_alert" and self._alert_watcher is not None:
+ if self._type == "weather_alert" and self._alert_watcher is not None:
return {
**{STATE_ATTR_BULLETIN_TIME: self._alert_watcher.bulletin_date},
**self._alert_watcher.alerts_list,
@@ -120,17 +128,17 @@ class MeteoFranceSensor(Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
- return SENSOR_TYPES[self._condition][SENSOR_TYPE_UNIT]
+ return SENSOR_TYPES[self._type][SENSOR_TYPE_UNIT]
@property
def icon(self):
"""Return the icon."""
- return SENSOR_TYPES[self._condition][SENSOR_TYPE_ICON]
+ return SENSOR_TYPES[self._type][SENSOR_TYPE_ICON]
@property
def device_class(self):
"""Return the device class of the sensor."""
- return SENSOR_TYPES[self._condition][SENSOR_TYPE_CLASS]
+ return SENSOR_TYPES[self._type][SENSOR_TYPE_CLASS]
def update(self):
"""Fetch new state data for the sensor."""
@@ -138,13 +146,12 @@ class MeteoFranceSensor(Entity):
self._client.update()
self._data = self._client.get_data()
- if self._condition == "weather_alert":
+ if self._type == "weather_alert":
if self._alert_watcher is not None:
self._alert_watcher.update_department_status()
self._state = self._alert_watcher.department_color
_LOGGER.debug(
- "weather alert watcher for %s updated. Proxy"
- " have the status: %s",
+ "weather alert watcher for %s updated. Proxy have the status: %s",
self._data["name"],
self._alert_watcher.proxy.status,
)
@@ -153,9 +160,9 @@ class MeteoFranceSensor(Entity):
"No weather alert data for location %s", self._data["name"]
)
else:
- self._state = self._data[self._condition]
+ self._state = self._data[self._type]
except KeyError:
_LOGGER.error(
- "No condition %s for location %s", self._condition, self._data["name"]
+ "No condition %s for location %s", self._type, self._data["name"]
)
self._state = None
diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json
new file mode 100644
index 00000000000..8bb02f28bd0
--- /dev/null
+++ b/homeassistant/components/meteo_france/strings.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "title": "Météo-France",
+ "step": {
+ "user": {
+ "title": "Météo-France",
+ "description": "Enter the postal code (only for France, recommended) or city name",
+ "data": {
+ "city": "City"
+ }
+ }
+ },
+ "abort":{
+ "already_configured": "City already configured",
+ "unknown": "Unknown error: please retry later"
+ }
+ }
+}
diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py
index c96080808e9..1bdea073aae 100644
--- a/homeassistant/components/meteo_france/weather.py
+++ b/homeassistant/components/meteo_france/weather.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+from meteofrance.client import meteofranceClient
+
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_TEMP,
@@ -9,29 +11,30 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TIME,
WeatherEntity,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS
+from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.dt as dt_util
-from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE
+from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DOMAIN
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
"""Set up the Meteo-France weather platform."""
- if discovery_info is None:
- return
+ city = entry.data[CONF_CITY]
+ client = hass.data[DOMAIN][city]
- city = discovery_info[CONF_CITY]
- client = hass.data[DATA_METEO_FRANCE][city]
-
- add_entities([MeteoFranceWeather(client)], True)
+ async_add_entities([MeteoFranceWeather(client)], True)
class MeteoFranceWeather(WeatherEntity):
"""Representation of a weather condition."""
- def __init__(self, client):
+ def __init__(self, client: meteofranceClient):
"""Initialise the platform with a data instance and station name."""
self._client = client
self._data = {}
@@ -46,6 +49,11 @@ class MeteoFranceWeather(WeatherEntity):
"""Return the name of the sensor."""
return self._data["name"]
+ @property
+ def unique_id(self):
+ """Return the unique id of the sensor."""
+ return self.name
+
@property
def condition(self):
"""Return the current condition."""
diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py
index 18809f08d4f..b3d3e0ea285 100644
--- a/homeassistant/components/mfi/switch.py
+++ b/homeassistant/components/mfi/switch.py
@@ -113,7 +113,7 @@ class MfiSwitch(SwitchDevice):
@property
def device_state_attributes(self):
- """Return the state attributes fof the device."""
+ """Return the state attributes for the device."""
attr = {}
attr["volts"] = round(self._port.data.get("v_rms", 0), 1)
attr["amps"] = round(self._port.data.get("i_rms", 0), 1)
diff --git a/homeassistant/components/mikrotik/.translations/de.json b/homeassistant/components/mikrotik/.translations/de.json
index d3328e9c305..97d28db4cfb 100644
--- a/homeassistant/components/mikrotik/.translations/de.json
+++ b/homeassistant/components/mikrotik/.translations/de.json
@@ -27,6 +27,7 @@
"step": {
"device_tracker": {
"data": {
+ "arp_ping": "ARP Ping aktivieren",
"force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP"
}
}
diff --git a/homeassistant/components/mikrotik/.translations/fr.json b/homeassistant/components/mikrotik/.translations/fr.json
new file mode 100644
index 00000000000..220da6fcbaf
--- /dev/null
+++ b/homeassistant/components/mikrotik/.translations/fr.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Mikrotik est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de la connexion",
+ "name_exists": "Le nom existe",
+ "wrong_credentials": "Identifiants erron\u00e9s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "name": "Nom",
+ "password": "Mot de passe",
+ "port": "Port",
+ "username": "Nom d'utilisateur",
+ "verify_ssl": "Utiliser SSL"
+ },
+ "title": "Configurer le routeur Mikrotik"
+ }
+ },
+ "title": "Mikrotik"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "arp_ping": "Activer le ping ARP",
+ "force_dhcp": "Forcer l'analyse \u00e0 l'aide de DHCP"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/.translations/hu.json b/homeassistant/components/mikrotik/.translations/hu.json
new file mode 100644
index 00000000000..8afbeb69925
--- /dev/null
+++ b/homeassistant/components/mikrotik/.translations/hu.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A Mikrotik m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "A kapcsolat sikertelen",
+ "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik",
+ "wrong_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Kiszolg\u00e1l\u00f3",
+ "name": "N\u00e9v",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v",
+ "verify_ssl": "SSL haszn\u00e1lata"
+ },
+ "title": "Mikrotik \u00fatv\u00e1laszt\u00f3 be\u00e1ll\u00edt\u00e1sa"
+ }
+ },
+ "title": "Mikrotik"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "arp_ping": "ARP-ping enged\u00e9lyez\u00e9se",
+ "detection_time": "Otthoni intervallumk\u00e9nt vegye figyelembe",
+ "force_dhcp": "A szkennel\u00e9s k\u00e9nyszer\u00edt\u00e9se DHCP seg\u00edts\u00e9g\u00e9vel"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/.translations/nl.json b/homeassistant/components/mikrotik/.translations/nl.json
new file mode 100644
index 00000000000..d4996d492a5
--- /dev/null
+++ b/homeassistant/components/mikrotik/.translations/nl.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Mikrotik is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Verbinding niet geslaagd",
+ "name_exists": "Naam bestaat al",
+ "wrong_credentials": "Ongeldige inloggegevens"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Naam",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "username": "Gebruikersnaam",
+ "verify_ssl": "Gebruik SSL"
+ },
+ "title": "Mikrotik Router instellen"
+ }
+ },
+ "title": "Mikrotik"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "arp_ping": "ARP-ping inschakelen",
+ "detection_time": "Overweeg thuisinterval",
+ "force_dhcp": "Forceer scannen met DHCP"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/.translations/pl.json b/homeassistant/components/mikrotik/.translations/pl.json
index 1971f1866e1..6d807672398 100644
--- a/homeassistant/components/mikrotik/.translations/pl.json
+++ b/homeassistant/components/mikrotik/.translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Mikronik jest ju\u017c skonfigurowany"
+ "already_configured": "Mikronik jest ju\u017c skonfigurowany."
},
"error": {
"cannot_connect": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119",
- "name_exists": "Nazwa ju\u017c istnieje",
+ "name_exists": "Nazwa ju\u017c istnieje.",
"wrong_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce"
},
"step": {
@@ -18,7 +18,7 @@
"username": "Nazwa u\u017cytkownika",
"verify_ssl": "U\u017cyj SSL"
},
- "title": "Skonfiguruj router Mikrotik"
+ "title": "Konfiguracja routera Mikrotik"
}
},
"title": "Mikrotik"
diff --git a/homeassistant/components/mikrotik/.translations/sl.json b/homeassistant/components/mikrotik/.translations/sl.json
new file mode 100644
index 00000000000..a10508f8bbe
--- /dev/null
+++ b/homeassistant/components/mikrotik/.translations/sl.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Mikrotik je \u017ee konfiguriran"
+ },
+ "error": {
+ "cannot_connect": "Povezava ni uspela",
+ "name_exists": "Ime obstaja",
+ "wrong_credentials": "Napa\u010dne poverilnice"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Gostitelj",
+ "name": "Ime",
+ "password": "Geslo",
+ "port": "Vrata",
+ "username": "Uporabni\u0161ko ime",
+ "verify_ssl": "Uporaba SSL"
+ },
+ "title": "Nastavite Mikrotik usmerjevalnik"
+ }
+ },
+ "title": "Mikrotik"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "arp_ping": "Omogo\u010di ARP ping",
+ "detection_time": "Interval \"doma\" ",
+ "force_dhcp": "Vsilite skeniranje z uporabo DHCP-ja"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/.translations/sv.json b/homeassistant/components/mikrotik/.translations/sv.json
index 572b159221d..7be080d96a2 100644
--- a/homeassistant/components/mikrotik/.translations/sv.json
+++ b/homeassistant/components/mikrotik/.translations/sv.json
@@ -1,10 +1,37 @@
{
"config": {
+ "abort": {
+ "already_configured": "Mikrotik \u00e4r redan konfigurerad"
+ },
+ "error": {
+ "cannot_connect": "Anslutningen misslyckades",
+ "name_exists": "Namnet finns",
+ "wrong_credentials": "Fel autentiseringsuppgifter"
+ },
"step": {
"user": {
+ "data": {
+ "host": "V\u00e4rd",
+ "name": "Namn",
+ "password": "L\u00f6senord",
+ "port": "Port",
+ "username": "Anv\u00e4ndarnamn",
+ "verify_ssl": "Anv\u00e4nd ssl"
+ },
"title": "Konfigurera Mikrotik-router"
}
},
"title": "Mikrotik"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "arp_ping": "Aktivera ARP-ping",
+ "detection_time": "Intervall f\u00f6r att betraktas som hemma",
+ "force_dhcp": "Tvinga skanning med DHCP"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/.translations/zh-Hans.json b/homeassistant/components/mikrotik/.translations/zh-Hans.json
new file mode 100644
index 00000000000..9604af53495
--- /dev/null
+++ b/homeassistant/components/mikrotik/.translations/zh-Hans.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u673a",
+ "name": "\u540d\u5b57",
+ "password": "\u5bc6\u7801",
+ "port": "\u7aef\u53e3",
+ "username": "\u7528\u6237\u540d",
+ "verify_ssl": "\u4f7f\u7528 ssl"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py
index d66a441aaf7..d81e8878d1c 100644
--- a/homeassistant/components/mikrotik/const.py
+++ b/homeassistant/components/mikrotik/const.py
@@ -24,6 +24,7 @@ CAPSMAN = "capsman"
DHCP = "dhcp"
WIRELESS = "wireless"
IS_WIRELESS = "is_wireless"
+IS_CAPSMAN = "is_capsman"
MIKROTIK_SERVICES = {
ARP: "/ip/arp/getall",
@@ -33,6 +34,7 @@ MIKROTIK_SERVICES = {
INFO: "/system/routerboard/getall",
WIRELESS: "/interface/wireless/registration-table/getall",
IS_WIRELESS: "/interface/wireless/print",
+ IS_CAPSMAN: "/caps-man/interface/print",
}
ATTR_DEVICE_TRACKER = [
diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py
index 6e64c443d8b..300d73b6b11 100644
--- a/homeassistant/components/mikrotik/hub.py
+++ b/homeassistant/components/mikrotik/hub.py
@@ -27,6 +27,7 @@ from .const import (
DHCP,
IDENTITY,
INFO,
+ IS_CAPSMAN,
IS_WIRELESS,
MIKROTIK_SERVICES,
NAME,
@@ -95,7 +96,8 @@ class MikrotikData:
self.all_devices = {}
self.devices = {}
self.available = True
- self.support_wireless = bool(self.command(MIKROTIK_SERVICES[IS_WIRELESS]))
+ self.support_capsman = False
+ self.support_wireless = False
self.hostname = None
self.model = None
self.firmware = None
@@ -135,6 +137,8 @@ class MikrotikData:
self.model = self.get_info(ATTR_MODEL)
self.firmware = self.get_info(ATTR_FIRMWARE)
self.serial_number = self.get_info(ATTR_SERIAL_NUMBER)
+ self.support_capsman = bool(self.command(MIKROTIK_SERVICES[IS_CAPSMAN]))
+ self.support_wireless = bool(self.command(MIKROTIK_SERVICES[IS_WIRELESS]))
def connect_to_hub(self):
"""Connect to hub."""
@@ -158,25 +162,23 @@ class MikrotikData:
def update_devices(self):
"""Get list of devices with latest status."""
arp_devices = {}
- wireless_devices = {}
device_list = {}
+ wireless_devices = {}
try:
self.all_devices = self.get_list_from_interface(DHCP)
- if self.support_wireless:
- _LOGGER.debug("wireless is supported")
- for interface in [CAPSMAN, WIRELESS]:
- wireless_devices = self.get_list_from_interface(interface)
- if wireless_devices:
- _LOGGER.debug("Scanning wireless devices using %s", interface)
- break
+ if self.support_capsman:
+ _LOGGER.debug("Hub is a CAPSman manager")
+ device_list = wireless_devices = self.get_list_from_interface(CAPSMAN)
+ elif self.support_wireless:
+ _LOGGER.debug("Hub supports wireless Interface")
+ device_list = wireless_devices = self.get_list_from_interface(WIRELESS)
- if self.support_wireless and not self.force_dhcp:
- device_list = wireless_devices
- else:
+ if not device_list or self.force_dhcp:
device_list = self.all_devices
_LOGGER.debug("Falling back to DHCP for scanning devices")
if self.arp_enabled:
+ _LOGGER.debug("Using arp-ping to check devices")
arp_devices = self.get_list_from_interface(ARP)
# get new hub firmware version if updated
@@ -299,7 +301,7 @@ class MikrotikHub:
@property
def firmware(self):
- """Return the firware of the hub."""
+ """Return the firmware of the hub."""
return self._mk_data.firmware
@property
diff --git a/homeassistant/components/minecraft_server/.translations/ca.json b/homeassistant/components/minecraft_server/.translations/ca.json
new file mode 100644
index 00000000000..86856ac2d11
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/ca.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat."
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3 amb el servidor. Comprova l'amfitri\u00f3 i el port i torna-ho a provar. Assegurat que estas utilitzant la versi\u00f3 del servidor 1.7 o superior.",
+ "invalid_ip": "L\u2019adre\u00e7a IP \u00e9s inv\u00e0lida (no s\u2019ha pogut determinar l\u2019adre\u00e7a MAC). Corregeix-la i torna-ho a provar.",
+ "invalid_port": "El port ha d'estar compr\u00e8s entre 1024 i 65535. Corregeix-lo i torna-ho a provar."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "name": "Nom",
+ "port": "Port"
+ },
+ "description": "Configuraci\u00f3 d'una inst\u00e0ncia de servidor de Minecraft per poder monitoritzar-lo.",
+ "title": "Enlla\u00e7 del servidor de Minecraft"
+ }
+ },
+ "title": "Servidor de Minecraft"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/da.json b/homeassistant/components/minecraft_server/.translations/da.json
new file mode 100644
index 00000000000..bf930f2f277
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/da.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "V\u00e6rten er allerede konfigureret."
+ },
+ "error": {
+ "cannot_connect": "Det lykkedes ikke at oprette forbindelse til serveren. Kontroller v\u00e6rten og porten, og pr\u00f8v igen. S\u00f8rg ogs\u00e5 for, at du k\u00f8rer mindst Minecraft version 1.7 p\u00e5 din server.",
+ "invalid_ip": "IP-adressen er ugyldig (MAC-adressen kunne ikke bestemmes). Ret den, og pr\u00f8v igen.",
+ "invalid_port": "Porten skal v\u00e6re i intervallet fra 1024 til 65535. Ret den, og pr\u00f8v igen."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e6rt",
+ "name": "Navn",
+ "port": "Port"
+ },
+ "description": "Konfigurer din Minecraft-server-instans for at tillade overv\u00e5gning.",
+ "title": "Forbind din Minecraft-server"
+ }
+ },
+ "title": "Minecraft-server"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/de.json b/homeassistant/components/minecraft_server/.translations/de.json
new file mode 100644
index 00000000000..00426308239
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/de.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Der Host ist bereits konfiguriert."
+ },
+ "error": {
+ "cannot_connect": "Verbindung zum Server fehlgeschlagen. Bitte \u00fcberpr\u00fcfe den Host und den Port und versuche es erneut. Stelle au\u00dferdem sicher, dass Du mindestens Minecraft Version 1.7 auf Deinem Server ausf\u00fchrst.",
+ "invalid_ip": "IP-Adresse ist ung\u00fcltig (MAC-Adresse konnte nicht ermittelt werden). Bitte korrigieren und erneut versuchen.",
+ "invalid_port": "Der Port muss im Bereich von 1024 bis 65535 liegen. Bitte korrigieren und erneut versuchen."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Name",
+ "port": "Port"
+ },
+ "description": "Richte deine Minecraft Server-Instanz ein, um es \u00fcberwachen zu k\u00f6nnen.",
+ "title": "Verkn\u00fcpfe deinen Minecraft Server"
+ }
+ },
+ "title": "Minecraft Server"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/en.json b/homeassistant/components/minecraft_server/.translations/en.json
new file mode 100644
index 00000000000..d0f7a5d6300
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/en.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Host is already configured."
+ },
+ "error": {
+ "cannot_connect": "Failed to connect to server. Please check the host and port and try again. Also ensure that you are running at least Minecraft version 1.7 on your server.",
+ "invalid_ip": "IP address is invalid (MAC address could not be determined). Please correct it and try again.",
+ "invalid_port": "Port must be in range from 1024 to 65535. Please correct it and try again."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Name",
+ "port": "Port"
+ },
+ "description": "Set up your Minecraft Server instance to allow monitoring.",
+ "title": "Link your Minecraft Server"
+ }
+ },
+ "title": "Minecraft Server"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/es.json b/homeassistant/components/minecraft_server/.translations/es.json
new file mode 100644
index 00000000000..14831ef45e1
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/es.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El host ya est\u00e1 configurado."
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar al servidor. Compruebe el host y el puerto e int\u00e9ntelo de nuevo. Tambi\u00e9n aseg\u00farese de que est\u00e1 ejecutando al menos Minecraft versi\u00f3n 1.7 en su servidor.",
+ "invalid_ip": "La direcci\u00f3n IP no es valida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo.",
+ "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nombre",
+ "port": "Puerto"
+ },
+ "description": "Configura tu instancia de Minecraft Server para permitir la supervisi\u00f3n.",
+ "title": "Enlace su servidor Minecraft"
+ }
+ },
+ "title": "Servidor Minecraft"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/fr.json b/homeassistant/components/minecraft_server/.translations/fr.json
new file mode 100644
index 00000000000..bf87c6f3d73
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/fr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "name": "Nom",
+ "port": "Port"
+ },
+ "title": "Reliez votre serveur Minecraft"
+ }
+ },
+ "title": "Serveur Minecraft"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/hu.json b/homeassistant/components/minecraft_server/.translations/hu.json
new file mode 100644
index 00000000000..9341bdbe4d1
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kiszolg\u00e1l\u00f3 m\u00e1r konfigur\u00e1lva van."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Kiszolg\u00e1l\u00f3",
+ "name": "N\u00e9v",
+ "port": "Port"
+ },
+ "title": "Kapcsolja \u00f6ssze a Minecraft szervert"
+ }
+ },
+ "title": "Minecraft szerver"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/it.json b/homeassistant/components/minecraft_server/.translations/it.json
new file mode 100644
index 00000000000..5861eebcc9a
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/it.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'host \u00e8 gi\u00e0 configurato."
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi al server. Controllare l'host e la porta e riprovare. Assicurarsi inoltre che si esegue almeno Minecraft versione 1.7 sul server.",
+ "invalid_ip": "L'indirizzo IP non \u00e8 valido (non \u00e8 stato possibile determinare l'indirizzo MAC). Correggilo e riprova.",
+ "invalid_port": "La porta deve essere compresa tra 1024 e 65535. Correggila e riprova."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nome",
+ "port": "Porta"
+ },
+ "description": "Configurare l'istanza del Server Minecraft per consentire il monitoraggio.",
+ "title": "Collega il tuo Server Minecraft"
+ }
+ },
+ "title": "Server Minecraft"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/ko.json b/homeassistant/components/minecraft_server/.translations/ko.json
new file mode 100644
index 00000000000..66b281cc5d9
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/ko.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\ub97c \ud655\uc778\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \ub610\ud55c \uc11c\ubc84\uc5d0\uc11c Minecraft \ubc84\uc804 1.7 \uc774\uc0c1\uc744 \uc2e4\ud589 \uc911\uc778\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "invalid_ip": "IP \uc8fc\uc18c\uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (MAC \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4). \uc218\uc815 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "invalid_port": "\ud3ec\ud2b8\ub294 1024-65535 \ubc94\uc704\uc5d0 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud3ec\ud2b8\ub97c \uc218\uc815\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "name": "\uc774\ub984",
+ "port": "\ud3ec\ud2b8"
+ },
+ "description": "\ubaa8\ub2c8\ud130\ub9c1\uc774 \uac00\ub2a5\ud558\ub3c4\ub85d Minecraft \uc11c\ubc84 \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.",
+ "title": "Minecraft \uc11c\ubc84 \uc5f0\uacb0"
+ }
+ },
+ "title": "Minecraft \uc11c\ubc84"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/lb.json b/homeassistant/components/minecraft_server/.translations/lb.json
new file mode 100644
index 00000000000..f95dd062005
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/lb.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen mam Server. Iwwerpr\u00e9if den Numm a Port a prob\u00e9ier nach emol. G\u00e9i och s\u00e9cher dass op d'mannst Minecraft Versioun 1.7 um Server leeft.",
+ "invalid_ip": "IP Adress ass ong\u00eblteg (MAC Adress konnt net best\u00ebmmt ginn). Korrig\u00e9iert et a prob\u00e9iert et nach eng K\u00e9ier w.e.g.",
+ "invalid_port": "Port muss zw\u00ebscht 1024 a 65535 sinn. Korrig\u00e9iert et a prob\u00e9iert et nach eng K\u00e9ier w.e.g."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Apparat",
+ "name": "Numm",
+ "port": "Port"
+ },
+ "description": "Riicht deng Minecraft Server Instanz a fir d'Iwwerwaachung z'erlaben",
+ "title": "Verbann d\u00e4in Minecraft Server"
+ }
+ },
+ "title": "Minecraft Server"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/nl.json b/homeassistant/components/minecraft_server/.translations/nl.json
new file mode 100644
index 00000000000..75e19bc2550
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/nl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Host is al geconfigureerd."
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken met de server. Controleer de host en de poort en probeer het opnieuw. Zorg er ook voor dat u minimaal Minecraft versie 1.7 op uw server uitvoert.",
+ "invalid_ip": "IP-adres is ongeldig (MAC-adres kon niet worden bepaald). Corrigeer het en probeer het opnieuw.",
+ "invalid_port": "Poort moet tussen 1024 en 65535 liggen. Corrigeer dit en probeer het opnieuw."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Naam",
+ "port": "Poort"
+ },
+ "description": "Stel uw Minecraft server in om monitoring toe te staan.",
+ "title": "Koppel uw Minecraft server"
+ }
+ },
+ "title": "Minecraft server"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/no.json b/homeassistant/components/minecraft_server/.translations/no.json
new file mode 100644
index 00000000000..f7be289d48c
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/no.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Verten er allerede konfigurert."
+ },
+ "error": {
+ "cannot_connect": "Kan ikke koble til serveren. Kontroller verten og porten, og pr\u00f8v p\u00e5 nytt. S\u00f8rg ogs\u00e5 for at du kj\u00f8rer minst Minecraft versjon 1.7 p\u00e5 serveren din.",
+ "invalid_ip": "IP-adressen er ugyldig (MAC-adressen kan ikke fastsl\u00e5s). Vennligst korriger den og pr\u00f8v p\u00e5 nytt.",
+ "invalid_port": "Porten m\u00e5 v\u00e6re i omr\u00e5det 1024 til 65535. Vennligst korriger den og pr\u00f8v p\u00e5 nytt."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "name": "Navn",
+ "port": "Port"
+ },
+ "description": "Konfigurer Minecraft Server-forekomsten slik at den kan overv\u00e5kes.",
+ "title": "Link din Minecraft Server"
+ }
+ },
+ "title": "Minecraft Server"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/pl.json b/homeassistant/components/minecraft_server/.translations/pl.json
new file mode 100644
index 00000000000..f9c4a515566
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/pl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Host jest ju\u017c skonfigurowany."
+ },
+ "error": {
+ "cannot_connect": "B\u0142\u0105d po\u0142\u0105czenia z serwerem. Sprawd\u017a adres hosta i port i spr\u00f3buj ponownie. Upewnij si\u0119 tak\u017ce, \u017ce na serwerze dzia\u0142a Minecraft w wersji przynajmniej 1.7.",
+ "invalid_ip": "Adres IP jest nieprawid\u0142owy (nie mo\u017cna ustali\u0107 adresu MAC). Popraw to i spr\u00f3buj ponownie.",
+ "invalid_port": "Port musi znajdowa\u0107 si\u0119 w zakresie od 1024 do 65535. Popraw go i spr\u00f3buj ponownie."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nazwa",
+ "port": "Port"
+ },
+ "description": "Skonfiguruj instancj\u0119 serwera Minecraft, aby umo\u017cliwi\u0107 monitorowanie.",
+ "title": "Po\u0142\u0105cz sw\u00f3j serwer Minecraft"
+ }
+ },
+ "title": "Serwer Minecraft"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/ru.json b/homeassistant/components/minecraft_server/.translations/ru.json
new file mode 100644
index 00000000000..916b342ee4a
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/ru.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0422\u0430\u043a\u0436\u0435 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d Minecraft \u0432\u0435\u0440\u0441\u0438\u0438 1.7, \u0438\u043b\u0438 \u0432\u044b\u0448\u0435.",
+ "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 (\u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c MAC-\u0430\u0434\u0440\u0435\u0441).",
+ "invalid_port": "\u041f\u043e\u0440\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0435 \u043e\u0442 1024 \u0434\u043e 65535."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Minecraft.",
+ "title": "Minecraft Server"
+ }
+ },
+ "title": "Minecraft Server"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/sl.json b/homeassistant/components/minecraft_server/.translations/sl.json
new file mode 100644
index 00000000000..cf8a8af54ee
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/sl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Gostitelj je \u017ee konfiguriran."
+ },
+ "error": {
+ "cannot_connect": "Povezava s stre\u017enikom ni uspela. Preverite gostitelja in vrata in poskusite znova. Zagotovite tudi, da na stre\u017eniku izvajate vsaj Minecraft razli\u010dice 1.7.",
+ "invalid_ip": "IP naslov ni veljaven (MAC naslova ni mogo\u010de dolo\u010diti). Popravite ga in poskusite znova.",
+ "invalid_port": "Vrata morajo biti v razponu od 1024 do 65535. Prosimo, popravite in poskusite znova."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Gostitelj",
+ "name": "Ime",
+ "port": "Vrata"
+ },
+ "description": "Nastavite svoj Minecraft stre\u017enik, da omogo\u010dite spremljanje.",
+ "title": "Pove\u017eite svoj Minecraft stre\u017enik"
+ }
+ },
+ "title": "Minecraft stre\u017enik"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/sv.json b/homeassistant/components/minecraft_server/.translations/sv.json
new file mode 100644
index 00000000000..acf941878dd
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/sv.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "V\u00e4rden \u00e4r redan konfigurerad."
+ },
+ "error": {
+ "cannot_connect": "Misslyckades med att ansluta till servern. Kontrollera v\u00e4rden och porten och f\u00f6rs\u00f6k igen. Se ocks\u00e5 till att du k\u00f6r minst Minecraft version 1.7 p\u00e5 din server.",
+ "invalid_ip": "IP-adressen \u00e4r ogiltig (MAC-adressen kunde inte fastst\u00e4llas). Korrigera det och f\u00f6rs\u00f6k igen.",
+ "invalid_port": "Porten m\u00e5ste ligga inom intervallet 1024 till 65535. Korrigera den och f\u00f6rs\u00f6k igen."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e4rd",
+ "name": "Namn",
+ "port": "Port"
+ },
+ "description": "St\u00e4ll in din Minecraft Server-instans f\u00f6r att till\u00e5ta \u00f6vervakning.",
+ "title": "L\u00e4nka din Minecraft-server"
+ }
+ },
+ "title": "Minecraft-server"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/tr.json b/homeassistant/components/minecraft_server/.translations/tr.json
new file mode 100644
index 00000000000..595c1686982
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/tr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Host zaten ayarlanm\u0131\u015f."
+ },
+ "error": {
+ "cannot_connect": "Server ile ba\u011flant\u0131 kurulamad\u0131. L\u00fctfen host ve port ayarlar\u0131n\u0131 kontrol et ve tekrar dene. Ayr\u0131ca, serverda en az Minecraft s\u00fcr\u00fcm 1.7 \u00e7al\u0131\u015ft\u0131rd\u0131\u011f\u0131ndan emin ol.",
+ "invalid_ip": "IP adresi ge\u00e7ersiz (MAC adresi belirlenemedi). L\u00fctfen d\u00fczelt ve tekrar dene.",
+ "invalid_port": "Port 1024 ile 65535 aral\u0131\u011f\u0131nda olmal\u0131d\u0131r. L\u00fctfen d\u00fczelt ve yeniden dene."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Ad",
+ "port": "Port"
+ },
+ "description": "G\u00f6zetmeye izin vermek i\u00e7in Minecraft server nesnesini ayarla.",
+ "title": "Minecraft Servern\u0131 ba\u011fla"
+ }
+ },
+ "title": "Minecraft Server"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/.translations/zh-Hant.json b/homeassistant/components/minecraft_server/.translations/zh-Hant.json
new file mode 100644
index 00000000000..c451ad71065
--- /dev/null
+++ b/homeassistant/components/minecraft_server/.translations/zh-Hant.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u5f8c\u518d\u8a66\u4e00\u6b21\u3002\u53e6\u8acb\u78ba\u8a8d\u65bc\u4f3a\u670d\u5668\u4e0a\u57f7\u884c\u6700\u65b0\u7248\u672c Minecraft 1.7 \u7248\u3002",
+ "invalid_ip": "IP \u4f4d\u5740\u7121\u6548\uff08MAC \u4f4d\u5740\u7121\u6cd5\u78ba\u8a8d\uff09\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002",
+ "invalid_port": "\u901a\u8a0a\u57e0\u7bc4\u570d\u4ecb\u65bc 1024 \u81f3 65535\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "name": "\u540d\u7a31",
+ "port": "\u901a\u8a0a\u57e0"
+ },
+ "description": "\u8a2d\u5b9a Minecraft \u4f3a\u670d\u5668\u4ee5\u9032\u884c\u76e3\u63a7\u3002",
+ "title": "\u9023\u7d50 Minecraft \u4f3a\u670d\u5668"
+ }
+ },
+ "title": "Minecraft \u4f3a\u670d\u5668"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py
new file mode 100644
index 00000000000..789e4d8f1b8
--- /dev/null
+++ b/homeassistant/components/minecraft_server/__init__.py
@@ -0,0 +1,273 @@
+"""The Minecraft Server integration."""
+
+import asyncio
+from datetime import datetime, timedelta
+import logging
+from typing import Any, Dict
+
+from mcstatus.server import MinecraftServer as MCStatus
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX
+
+PLATFORMS = ["binary_sensor", "sensor"]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+ """Set up the Minecraft Server component."""
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool:
+ """Set up Minecraft Server from a config entry."""
+ domain_data = hass.data.setdefault(DOMAIN, {})
+
+ # Create and store server instance.
+ unique_id = config_entry.unique_id
+ _LOGGER.debug(
+ "Creating server instance for '%s' (host='%s', port=%s)",
+ config_entry.data[CONF_NAME],
+ config_entry.data[CONF_HOST],
+ config_entry.data[CONF_PORT],
+ )
+ server = MinecraftServer(hass, unique_id, config_entry.data)
+ domain_data[unique_id] = server
+ await server.async_update()
+ server.start_periodic_update()
+
+ # Set up platform(s).
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry
+) -> bool:
+ """Unload Minecraft Server config entry."""
+ unique_id = config_entry.unique_id
+ server = hass.data[DOMAIN][unique_id]
+
+ # Unload platforms.
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+
+ # Clean up.
+ server.stop_periodic_update()
+ hass.data[DOMAIN].pop(unique_id)
+
+ return True
+
+
+class MinecraftServer:
+ """Representation of a Minecraft server."""
+
+ # Private constants
+ _MAX_RETRIES_PING = 3
+ _MAX_RETRIES_STATUS = 3
+
+ def __init__(
+ self, hass: HomeAssistantType, unique_id: str, config_data: ConfigType
+ ) -> None:
+ """Initialize server instance."""
+ self._hass = hass
+
+ # Server data
+ self.unique_id = unique_id
+ self.name = config_data[CONF_NAME]
+ self.host = config_data[CONF_HOST]
+ self.port = config_data[CONF_PORT]
+ self.online = False
+ self._last_status_request_failed = False
+
+ # 3rd party library instance
+ self._mc_status = MCStatus(self.host, self.port)
+
+ # Data provided by 3rd party library
+ self.description = None
+ self.version = None
+ self.protocol_version = None
+ self.latency_time = None
+ self.players_online = None
+ self.players_max = None
+ self.players_list = None
+
+ # Dispatcher signal name
+ self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}"
+
+ # Callback for stopping periodic update.
+ self._stop_periodic_update = None
+
+ def start_periodic_update(self) -> None:
+ """Start periodic execution of update method."""
+ self._stop_periodic_update = async_track_time_interval(
+ self._hass, self.async_update, timedelta(seconds=SCAN_INTERVAL)
+ )
+
+ def stop_periodic_update(self) -> None:
+ """Stop periodic execution of update method."""
+ self._stop_periodic_update()
+
+ async def async_check_connection(self) -> None:
+ """Check server connection using a 'ping' request and store result."""
+ try:
+ await self._hass.async_add_executor_job(
+ self._mc_status.ping, self._MAX_RETRIES_PING
+ )
+ self.online = True
+ except OSError as error:
+ _LOGGER.debug(
+ "Error occurred while trying to ping the server - OSError: %s", error
+ )
+ self.online = False
+
+ async def async_update(self, now: datetime = None) -> None:
+ """Get server data from 3rd party library and update properties."""
+ # Check connection status.
+ server_online_old = self.online
+ await self.async_check_connection()
+ server_online = self.online
+
+ # Inform user once about connection state changes if necessary.
+ if server_online_old and not server_online:
+ _LOGGER.warning("Connection to server lost")
+ elif not server_online_old and server_online:
+ _LOGGER.info("Connection to server (re-)established")
+
+ # Update the server properties if server is online.
+ if server_online:
+ await self._async_status_request()
+
+ # Notify sensors about new data.
+ async_dispatcher_send(self._hass, self.signal_name)
+
+ async def _async_status_request(self) -> None:
+ """Request server status and update properties."""
+ try:
+ status_response = await self._hass.async_add_executor_job(
+ self._mc_status.status, self._MAX_RETRIES_STATUS
+ )
+
+ # Got answer to request, update properties.
+ self.description = status_response.description["text"]
+ self.version = status_response.version.name
+ self.protocol_version = status_response.version.protocol
+ self.players_online = status_response.players.online
+ self.players_max = status_response.players.max
+ self.latency_time = status_response.latency
+ self.players_list = []
+ if status_response.players.sample is not None:
+ for player in status_response.players.sample:
+ self.players_list.append(player.name)
+
+ # Inform user once about successful update if necessary.
+ if self._last_status_request_failed:
+ _LOGGER.info("Updating the server properties succeeded again")
+ self._last_status_request_failed = False
+ except OSError as error:
+ # No answer to request, set all properties to unknown.
+ self.description = None
+ self.version = None
+ self.protocol_version = None
+ self.players_online = None
+ self.players_max = None
+ self.latency_time = None
+ self.players_list = None
+
+ # Inform user once about failed update if necessary.
+ if not self._last_status_request_failed:
+ _LOGGER.warning(
+ "Updating the server properties failed - OSError: %s", error,
+ )
+ self._last_status_request_failed = True
+
+
+class MinecraftServerEntity(Entity):
+ """Representation of a Minecraft Server base entity."""
+
+ def __init__(
+ self, server: MinecraftServer, type_name: str, icon: str, device_class: str
+ ) -> None:
+ """Initialize base entity."""
+ self._server = server
+ self._name = f"{server.name} {type_name}"
+ self._icon = icon
+ self._unique_id = f"{self._server.unique_id}-{type_name}"
+ self._device_info = {
+ "identifiers": {(DOMAIN, self._server.unique_id)},
+ "name": self._server.name,
+ "manufacturer": MANUFACTURER,
+ "model": f"Minecraft Server ({self._server.version})",
+ "sw_version": self._server.protocol_version,
+ }
+ self._device_class = device_class
+ self._device_state_attributes = None
+ self._disconnect_dispatcher = None
+
+ @property
+ def name(self) -> str:
+ """Return name."""
+ return self._name
+
+ @property
+ def unique_id(self) -> str:
+ """Return unique ID."""
+ return self._unique_id
+
+ @property
+ def device_info(self) -> Dict[str, Any]:
+ """Return device information."""
+ return self._device_info
+
+ @property
+ def device_class(self) -> str:
+ """Return device class."""
+ return self._device_class
+
+ @property
+ def icon(self) -> str:
+ """Return icon."""
+ return self._icon
+
+ @property
+ def should_poll(self) -> bool:
+ """Disable polling."""
+ return False
+
+ async def async_update(self) -> None:
+ """Fetch data from the server."""
+ raise NotImplementedError()
+
+ async def async_added_to_hass(self) -> None:
+ """Connect dispatcher to signal from server."""
+ self._disconnect_dispatcher = async_dispatcher_connect(
+ self.hass, self._server.signal_name, self._update_callback
+ )
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Disconnect dispatcher before removal."""
+ self._disconnect_dispatcher()
+
+ @callback
+ def _update_callback(self) -> None:
+ """Triggers update of properties after receiving signal from server."""
+ self.async_schedule_update_ha_state(force_refresh=True)
diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py
new file mode 100644
index 00000000000..cde2a414900
--- /dev/null
+++ b/homeassistant/components/minecraft_server/binary_sensor.py
@@ -0,0 +1,47 @@
+"""The Minecraft Server binary sensor platform."""
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ BinarySensorDevice,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import MinecraftServer, MinecraftServerEntity
+from .const import DOMAIN, ICON_STATUS, NAME_STATUS
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Minecraft Server binary sensor platform."""
+ server = hass.data[DOMAIN][config_entry.unique_id]
+
+ # Create entities list.
+ entities = [MinecraftServerStatusBinarySensor(server)]
+
+ # Add binary sensor entities.
+ async_add_entities(entities, True)
+
+
+class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorDevice):
+ """Representation of a Minecraft Server status binary sensor."""
+
+ def __init__(self, server: MinecraftServer) -> None:
+ """Initialize status binary sensor."""
+ super().__init__(
+ server=server,
+ type_name=NAME_STATUS,
+ icon=ICON_STATUS,
+ device_class=DEVICE_CLASS_CONNECTIVITY,
+ )
+ self._is_on = False
+
+ @property
+ def is_on(self) -> bool:
+ """Return binary state."""
+ return self._is_on
+
+ async def async_update(self) -> None:
+ """Update status."""
+ self._is_on = self._server.online
diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py
new file mode 100644
index 00000000000..8c6049a2c1b
--- /dev/null
+++ b/homeassistant/components/minecraft_server/config_flow.py
@@ -0,0 +1,116 @@
+"""Config flow for Minecraft Server integration."""
+from functools import partial
+import ipaddress
+
+import getmac
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.config_entries import ConfigFlow
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
+
+from . import MinecraftServer
+from .const import ( # pylint: disable=unused-import
+ DEFAULT_HOST,
+ DEFAULT_NAME,
+ DEFAULT_PORT,
+ DOMAIN,
+)
+
+
+class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Minecraft Server."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+
+ if user_input is not None:
+ # User inputs.
+ host = user_input[CONF_HOST]
+ port = user_input[CONF_PORT]
+
+ unique_id = ""
+
+ # Check if 'host' is a valid IP address and if so, get the MAC address.
+ ip_address = None
+ mac_address = None
+ try:
+ ip_address = ipaddress.ip_address(host)
+ except ValueError:
+ # Host is not a valid IP address.
+ pass
+ else:
+ # Host is a valid IP address.
+ if ip_address.version == 4:
+ # Address type is IPv4.
+ params = {"ip": host}
+ else:
+ # Address type is IPv6.
+ params = {"ip6": host}
+ mac_address = await self.hass.async_add_executor_job(
+ partial(getmac.get_mac_address, **params)
+ )
+
+ # Validate IP address via valid MAC address.
+ if ip_address is not None and mac_address is None:
+ errors["base"] = "invalid_ip"
+ # Validate port configuration (limit to user and dynamic port range).
+ elif (port < 1024) or (port > 65535):
+ errors["base"] = "invalid_port"
+ # Validate host and port via ping request to server.
+ else:
+ # Build unique_id.
+ if ip_address is not None:
+ # Since IP addresses can change and therefore are not allowed in a
+ # unique_id, fall back to the MAC address.
+ unique_id = f"{mac_address}-{port}"
+ else:
+ # Use host name in unique_id (host names should not change).
+ unique_id = f"{host}-{port}"
+
+ # Abort in case the host was already configured before.
+ await self.async_set_unique_id(unique_id)
+ self._abort_if_unique_id_configured()
+
+ # Create server instance with configuration data and try pinging the server.
+ server = MinecraftServer(self.hass, unique_id, user_input)
+ await server.async_check_connection()
+ if not server.online:
+ # Host or port invalid or server not reachable.
+ errors["base"] = "cannot_connect"
+ else:
+ # Configuration data are available and no error was detected, create configuration entry.
+ return self.async_create_entry(
+ title=f"{host}:{port}", data=user_input
+ )
+
+ # Show configuration form (default form in case of no user_input,
+ # form filled with user_input and eventually with errors otherwise).
+ return self._show_config_form(user_input, errors)
+
+ def _show_config_form(self, user_input=None, errors=None):
+ """Show the setup form to the user."""
+ if user_input is None:
+ user_input = {}
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)
+ ): str,
+ vol.Required(
+ CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST)
+ ): vol.All(str, vol.Lower),
+ vol.Optional(
+ CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT)
+ ): int,
+ }
+ ),
+ errors=errors,
+ )
diff --git a/homeassistant/components/minecraft_server/const.py b/homeassistant/components/minecraft_server/const.py
new file mode 100644
index 00000000000..c3ab6615481
--- /dev/null
+++ b/homeassistant/components/minecraft_server/const.py
@@ -0,0 +1,37 @@
+"""Constants for the Minecraft Server integration."""
+
+ATTR_PLAYERS_LIST = "players_list"
+
+DEFAULT_HOST = "localhost"
+DEFAULT_NAME = "Minecraft Server"
+DEFAULT_PORT = 25565
+
+DOMAIN = "minecraft_server"
+
+ICON_LATENCY_TIME = "mdi:signal"
+ICON_PLAYERS_MAX = "mdi:account-multiple"
+ICON_PLAYERS_ONLINE = "mdi:account-multiple"
+ICON_PROTOCOL_VERSION = "mdi:numeric"
+ICON_STATUS = "mdi:lan"
+ICON_VERSION = "mdi:numeric"
+
+KEY_SERVERS = "servers"
+
+MANUFACTURER = "Mojang AB"
+
+NAME_LATENCY_TIME = "Latency Time"
+NAME_PLAYERS_MAX = "Players Max"
+NAME_PLAYERS_ONLINE = "Players Online"
+NAME_PROTOCOL_VERSION = "Protocol Version"
+NAME_STATUS = "Status"
+NAME_VERSION = "Version"
+
+SCAN_INTERVAL = 60
+
+SIGNAL_NAME_PREFIX = f"signal_{DOMAIN}"
+
+UNIT_LATENCY_TIME = "ms"
+UNIT_PLAYERS_MAX = "players"
+UNIT_PLAYERS_ONLINE = "players"
+UNIT_PROTOCOL_VERSION = None
+UNIT_VERSION = None
diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json
new file mode 100644
index 00000000000..1dda76dee77
--- /dev/null
+++ b/homeassistant/components/minecraft_server/manifest.json
@@ -0,0 +1,10 @@
+{
+ "domain": "minecraft_server",
+ "name": "Minecraft Server",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/minecraft_server",
+ "requirements": ["getmac==0.8.1", "mcstatus==2.3.0"],
+ "dependencies": [],
+ "codeowners": ["@elmurato"],
+ "quality_scale": "silver"
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py
new file mode 100644
index 00000000000..0b37a7d979b
--- /dev/null
+++ b/homeassistant/components/minecraft_server/sensor.py
@@ -0,0 +1,177 @@
+"""The Minecraft Server sensor platform."""
+
+import logging
+from typing import Any, Dict
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import MinecraftServer, MinecraftServerEntity
+from .const import (
+ ATTR_PLAYERS_LIST,
+ DOMAIN,
+ ICON_LATENCY_TIME,
+ ICON_PLAYERS_MAX,
+ ICON_PLAYERS_ONLINE,
+ ICON_PROTOCOL_VERSION,
+ ICON_VERSION,
+ NAME_LATENCY_TIME,
+ NAME_PLAYERS_MAX,
+ NAME_PLAYERS_ONLINE,
+ NAME_PROTOCOL_VERSION,
+ NAME_VERSION,
+ UNIT_LATENCY_TIME,
+ UNIT_PLAYERS_MAX,
+ UNIT_PLAYERS_ONLINE,
+ UNIT_PROTOCOL_VERSION,
+ UNIT_VERSION,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Minecraft Server sensor platform."""
+ server = hass.data[DOMAIN][config_entry.unique_id]
+
+ # Create entities list.
+ entities = [
+ MinecraftServerVersionSensor(server),
+ MinecraftServerProtocolVersionSensor(server),
+ MinecraftServerLatencyTimeSensor(server),
+ MinecraftServerPlayersOnlineSensor(server),
+ MinecraftServerPlayersMaxSensor(server),
+ ]
+
+ # Add sensor entities.
+ async_add_entities(entities, True)
+
+
+class MinecraftServerSensorEntity(MinecraftServerEntity):
+ """Representation of a Minecraft Server sensor base entity."""
+
+ def __init__(
+ self,
+ server: MinecraftServer,
+ type_name: str,
+ icon: str = None,
+ unit: str = None,
+ device_class: str = None,
+ ) -> None:
+ """Initialize sensor base entity."""
+ super().__init__(server, type_name, icon, device_class)
+ self._state = None
+ self._unit = unit
+
+ @property
+ def available(self) -> bool:
+ """Return sensor availability."""
+ return self._server.online
+
+ @property
+ def state(self) -> Any:
+ """Return sensor state."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return sensor measurement unit."""
+ return self._unit
+
+
+class MinecraftServerVersionSensor(MinecraftServerSensorEntity):
+ """Representation of a Minecraft Server version sensor."""
+
+ def __init__(self, server: MinecraftServer) -> None:
+ """Initialize version sensor."""
+ super().__init__(
+ server=server, type_name=NAME_VERSION, icon=ICON_VERSION, unit=UNIT_VERSION
+ )
+
+ async def async_update(self) -> None:
+ """Update version."""
+ self._state = self._server.version
+
+
+class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity):
+ """Representation of a Minecraft Server protocol version sensor."""
+
+ def __init__(self, server: MinecraftServer) -> None:
+ """Initialize protocol version sensor."""
+ super().__init__(
+ server=server,
+ type_name=NAME_PROTOCOL_VERSION,
+ icon=ICON_PROTOCOL_VERSION,
+ unit=UNIT_PROTOCOL_VERSION,
+ )
+
+ async def async_update(self) -> None:
+ """Update protocol version."""
+ self._state = self._server.protocol_version
+
+
+class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity):
+ """Representation of a Minecraft Server latency time sensor."""
+
+ def __init__(self, server: MinecraftServer) -> None:
+ """Initialize latency time sensor."""
+ super().__init__(
+ server=server,
+ type_name=NAME_LATENCY_TIME,
+ icon=ICON_LATENCY_TIME,
+ unit=UNIT_LATENCY_TIME,
+ )
+
+ async def async_update(self) -> None:
+ """Update latency time."""
+ self._state = self._server.latency_time
+
+
+class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity):
+ """Representation of a Minecraft Server online players sensor."""
+
+ def __init__(self, server: MinecraftServer) -> None:
+ """Initialize online players sensor."""
+ super().__init__(
+ server=server,
+ type_name=NAME_PLAYERS_ONLINE,
+ icon=ICON_PLAYERS_ONLINE,
+ unit=UNIT_PLAYERS_ONLINE,
+ )
+
+ async def async_update(self) -> None:
+ """Update online players state and device state attributes."""
+ self._state = self._server.players_online
+
+ device_state_attributes = None
+ players_list = self._server.players_list
+
+ if players_list is not None:
+ if len(players_list) != 0:
+ device_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list}
+
+ self._device_state_attributes = device_state_attributes
+
+ @property
+ def device_state_attributes(self) -> Dict[str, Any]:
+ """Return players list in device state attributes."""
+ return self._device_state_attributes
+
+
+class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity):
+ """Representation of a Minecraft Server maximum number of players sensor."""
+
+ def __init__(self, server: MinecraftServer) -> None:
+ """Initialize maximum number of players sensor."""
+ super().__init__(
+ server=server,
+ type_name=NAME_PLAYERS_MAX,
+ icon=ICON_PLAYERS_MAX,
+ unit=UNIT_PLAYERS_MAX,
+ )
+
+ async def async_update(self) -> None:
+ """Update maximum number of players."""
+ self._state = self._server.players_max
diff --git a/homeassistant/components/minecraft_server/strings.json b/homeassistant/components/minecraft_server/strings.json
new file mode 100644
index 00000000000..7743d940be6
--- /dev/null
+++ b/homeassistant/components/minecraft_server/strings.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "title": "Minecraft Server",
+ "step": {
+ "user": {
+ "title": "Link your Minecraft Server",
+ "description": "Set up your Minecraft Server instance to allow monitoring.",
+ "data": {
+ "name": "Name",
+ "host": "Host",
+ "port": "Port"
+ }
+ }
+ },
+ "error": {
+ "invalid_port": "Port must be in range from 1024 to 65535. Please correct it and try again.",
+ "cannot_connect": "Failed to connect to server. Please check the host and port and try again. Also ensure that you are running at least Minecraft version 1.7 on your server.",
+ "invalid_ip": "IP address is invalid (MAC address could not be determined). Please correct it and try again."
+ },
+ "abort": {
+ "already_configured": "Host is already configured."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py
index 720cf7106e7..f43f1c88396 100644
--- a/homeassistant/components/mobile_app/const.py
+++ b/homeassistant/components/mobile_app/const.py
@@ -52,6 +52,8 @@ ATTR_WEBHOOK_ENCRYPTED = "encrypted"
ATTR_WEBHOOK_ENCRYPTED_DATA = "encrypted_data"
ATTR_WEBHOOK_TYPE = "type"
+ERR_ENCRYPTION_ALREADY_ENABLED = "encryption_already_enabled"
+ERR_ENCRYPTION_NOT_AVAILABLE = "encryption_not_available"
ERR_ENCRYPTION_REQUIRED = "encryption_required"
ERR_SENSOR_NOT_REGISTERED = "not_registered"
ERR_SENSOR_DUPLICATE_UNIQUE_ID = "duplicate_unique_id"
diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py
index 27cb9934b18..5200c6b0c12 100644
--- a/homeassistant/components/mobile_app/entity.py
+++ b/homeassistant/components/mobile_app/entity.py
@@ -7,6 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import (
+ ATTR_DEVICE_NAME,
ATTR_SENSOR_ATTRIBUTES,
ATTR_SENSOR_DEVICE_CLASS,
ATTR_SENSOR_ICON,
@@ -38,6 +39,7 @@ class MobileAppEntity(Entity):
)
self._entity_type = config[ATTR_SENSOR_TYPE]
self.unsub_dispatcher = None
+ self._name = f"{entry.data[ATTR_DEVICE_NAME]} {config[ATTR_SENSOR_NAME]}"
async def async_added_to_hass(self):
"""Register callbacks."""
@@ -58,7 +60,7 @@ class MobileAppEntity(Entity):
@property
def name(self):
"""Return the name of the mobile app sensor."""
- return self._config[ATTR_SENSOR_NAME]
+ return self._name
@property
def device_class(self):
diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py
index 3a477d89925..c47f38986a1 100644
--- a/homeassistant/components/mobile_app/webhook.py
+++ b/homeassistant/components/mobile_app/webhook.py
@@ -1,8 +1,10 @@
"""Webhook handlers for mobile_app."""
from functools import wraps
import logging
+import secrets
-from aiohttp.web import HTTPBadRequest, Request, Response
+from aiohttp.web import HTTPBadRequest, Request, Response, json_response
+from nacl.secret import SecretBox
import voluptuous as vol
from homeassistant.components.binary_sensor import (
@@ -71,6 +73,8 @@ from .const import (
DATA_DELETED_IDS,
DATA_STORE,
DOMAIN,
+ ERR_ENCRYPTION_ALREADY_ENABLED,
+ ERR_ENCRYPTION_NOT_AVAILABLE,
ERR_ENCRYPTION_REQUIRED,
ERR_SENSOR_DUPLICATE_UNIQUE_ID,
ERR_SENSOR_NOT_REGISTERED,
@@ -84,6 +88,7 @@ from .helpers import (
registration_context,
safe_registration,
savable_state,
+ supports_encryption,
webhook_response,
)
@@ -307,6 +312,34 @@ async def webhook_update_registration(hass, config_entry, data):
)
+@WEBHOOK_COMMANDS.register("enable_encryption")
+async def webhook_enable_encryption(hass, config_entry, data):
+ """Handle a encryption enable webhook."""
+ if config_entry.data[ATTR_SUPPORTS_ENCRYPTION]:
+ _LOGGER.warning(
+ "Refusing to enable encryption for %s because it is already enabled!",
+ config_entry.data[ATTR_DEVICE_NAME],
+ )
+ return error_response(
+ ERR_ENCRYPTION_ALREADY_ENABLED, "Encryption already enabled"
+ )
+
+ if not supports_encryption():
+ _LOGGER.warning(
+ "Unable to enable encryption for %s because libsodium is unavailable!",
+ config_entry.data[ATTR_DEVICE_NAME],
+ )
+ return error_response(ERR_ENCRYPTION_NOT_AVAILABLE, "Encryption is unavailable")
+
+ secret = secrets.token_hex(SecretBox.KEY_SIZE)
+
+ data = {**config_entry.data, ATTR_SUPPORTS_ENCRYPTION: True, CONF_SECRET: secret}
+
+ hass.config_entries.async_update_entry(config_entry, data=data)
+
+ return json_response({"secret": secret})
+
+
@WEBHOOK_COMMANDS.register("register_sensor")
@validate_schema(
{
diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py
index 823703ac4c9..218d3d3baa9 100644
--- a/homeassistant/components/modbus/__init__.py
+++ b/homeassistant/components/modbus/__init__.py
@@ -159,10 +159,10 @@ def setup(hass, config):
def write_register(service):
"""Write Modbus registers."""
- unit = int(float(service.data.get(ATTR_UNIT)))
- address = int(float(service.data.get(ATTR_ADDRESS)))
- value = service.data.get(ATTR_VALUE)
- client_name = service.data.get(ATTR_HUB)
+ unit = int(float(service.data[ATTR_UNIT]))
+ address = int(float(service.data[ATTR_ADDRESS]))
+ value = service.data[ATTR_VALUE]
+ client_name = service.data[ATTR_HUB]
if isinstance(value, list):
hub_collect[client_name].write_registers(
unit, address, [int(float(i)) for i in value]
@@ -172,10 +172,10 @@ def setup(hass, config):
def write_coil(service):
"""Write Modbus coil."""
- unit = service.data.get(ATTR_UNIT)
- address = service.data.get(ATTR_ADDRESS)
- state = service.data.get(ATTR_STATE)
- client_name = service.data.get(ATTR_HUB)
+ unit = service.data[ATTR_UNIT]
+ address = service.data[ATTR_ADDRESS]
+ state = service.data[ATTR_STATE]
+ client_name = service.data[ATTR_HUB]
hub_collect[client_name].write_coil(unit, address, state)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
@@ -213,6 +213,12 @@ class ModbusHub:
kwargs = {"unit": unit} if unit else {}
return self._client.read_coils(address, count, **kwargs)
+ def read_discrete_inputs(self, unit, address, count):
+ """Read discrete inputs."""
+ with self._lock:
+ kwargs = {"unit": unit} if unit else {}
+ return self._client.read_discrete_inputs(address, count, **kwargs)
+
def read_input_registers(self, unit, address, count):
"""Read input registers."""
with self._lock:
diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py
index 9a431d24b0c..8ea6e2dbfa6 100644
--- a/homeassistant/components/modbus/binary_sensor.py
+++ b/homeassistant/components/modbus/binary_sensor.py
@@ -1,7 +1,9 @@
-"""Support for Modbus Coil sensors."""
+"""Support for Modbus Coil and Discrete Input sensors."""
import logging
from typing import Optional
+from pymodbus.exceptions import ConnectionException, ModbusException
+from pymodbus.pdu import ExceptionResponse
import voluptuous as vol
from homeassistant.components.binary_sensor import (
@@ -16,53 +18,76 @@ from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN
_LOGGER = logging.getLogger(__name__)
-CONF_COIL = "coil"
-CONF_COILS = "coils"
+CONF_DEPRECATED_COIL = "coil"
+CONF_DEPRECATED_COILS = "coils"
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_COILS): [
- {
- vol.Required(CONF_COIL): cv.positive_int,
- vol.Required(CONF_NAME): cv.string,
- vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
- vol.Optional(CONF_SLAVE): cv.positive_int,
- }
- ]
- }
+CONF_INPUTS = "inputs"
+CONF_INPUT_TYPE = "input_type"
+CONF_ADDRESS = "address"
+
+DEFAULT_INPUT_TYPE_COIL = "coil"
+DEFAULT_INPUT_TYPE_DISCRETE = "discrete_input"
+
+PLATFORM_SCHEMA = vol.All(
+ cv.deprecated(CONF_DEPRECATED_COILS, CONF_INPUTS),
+ PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_INPUTS): [
+ vol.All(
+ cv.deprecated(CONF_DEPRECATED_COIL, CONF_ADDRESS),
+ vol.Schema(
+ {
+ vol.Required(CONF_ADDRESS): cv.positive_int,
+ vol.Required(CONF_NAME): cv.string,
+ vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
+ vol.Optional(CONF_SLAVE): cv.positive_int,
+ vol.Optional(
+ CONF_INPUT_TYPE, default=DEFAULT_INPUT_TYPE_COIL
+ ): vol.In(
+ [DEFAULT_INPUT_TYPE_COIL, DEFAULT_INPUT_TYPE_DISCRETE]
+ ),
+ }
+ ),
+ )
+ ]
+ }
+ ),
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Modbus binary sensors."""
sensors = []
- for coil in config.get(CONF_COILS):
- hub = hass.data[MODBUS_DOMAIN][coil.get(CONF_HUB)]
+ for entry in config[CONF_INPUTS]:
+ hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]]
sensors.append(
- ModbusCoilSensor(
+ ModbusBinarySensor(
hub,
- coil.get(CONF_NAME),
- coil.get(CONF_SLAVE),
- coil.get(CONF_COIL),
- coil.get(CONF_DEVICE_CLASS),
+ entry[CONF_NAME],
+ entry.get(CONF_SLAVE),
+ entry[CONF_ADDRESS],
+ entry.get(CONF_DEVICE_CLASS),
+ entry[CONF_INPUT_TYPE],
)
)
add_entities(sensors)
-class ModbusCoilSensor(BinarySensorDevice):
- """Modbus coil sensor."""
+class ModbusBinarySensor(BinarySensorDevice):
+ """Modbus binary sensor."""
- def __init__(self, hub, name, slave, coil, device_class):
- """Initialize the Modbus coil sensor."""
+ def __init__(self, hub, name, slave, address, device_class, input_type):
+ """Initialize the Modbus binary sensor."""
self._hub = hub
self._name = name
self._slave = int(slave) if slave else None
- self._coil = int(coil)
+ self._address = int(address)
self._device_class = device_class
+ self._input_type = input_type
self._value = None
+ self._available = True
@property
def name(self):
@@ -79,15 +104,38 @@ class ModbusCoilSensor(BinarySensorDevice):
"""Return the device class of the sensor."""
return self._device_class
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
+
def update(self):
"""Update the state of the sensor."""
- result = self._hub.read_coils(self._slave, self._coil, 1)
try:
- self._value = result.bits[0]
- except AttributeError:
- _LOGGER.error(
- "No response from hub %s, slave %s, coil %s",
- self._hub.name,
- self._slave,
- self._coil,
- )
+ if self._input_type == DEFAULT_INPUT_TYPE_COIL:
+ result = self._hub.read_coils(self._slave, self._address, 1)
+ else:
+ result = self._hub.read_discrete_inputs(self._slave, self._address, 1)
+ except ConnectionException:
+ self._set_unavailable()
+ return
+
+ if isinstance(result, (ModbusException, ExceptionResponse)):
+ self._set_unavailable()
+ return
+
+ self._value = result.bits[0]
+ self._available = True
+
+ def _set_unavailable(self):
+ """Set unavailable state and log it as an error."""
+ if not self._available:
+ return
+
+ _LOGGER.error(
+ "No response from hub %s, slave %s, address %s",
+ self._hub.name,
+ self._slave,
+ self._address,
+ )
+ self._available = False
diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py
index 99ea686543d..c0423849418 100644
--- a/homeassistant/components/modbus/climate.py
+++ b/homeassistant/components/modbus/climate.py
@@ -1,7 +1,10 @@
"""Support for Generic Modbus Thermostats."""
import logging
import struct
+from typing import Optional
+from pymodbus.exceptions import ConnectionException, ModbusException
+from pymodbus.pdu import ExceptionResponse
import voluptuous as vol
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
@@ -24,6 +27,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_TARGET_TEMP = "target_temp_register"
CONF_CURRENT_TEMP = "current_temp_register"
+CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type"
CONF_DATA_TYPE = "data_type"
CONF_COUNT = "data_count"
CONF_PRECISION = "precision"
@@ -39,6 +43,9 @@ CONF_STEP = "temp_step"
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
HVAC_MODES = [HVAC_MODE_AUTO]
+DEFAULT_REGISTER_TYPE_HOLDING = "holding"
+DEFAULT_REGISTER_TYPE_INPUT = "input"
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
@@ -46,6 +53,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_SLAVE): cv.positive_int,
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
vol.Optional(CONF_COUNT, default=2): cv.positive_int,
+ vol.Optional(
+ CONF_CURRENT_TEMP_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING
+ ): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]),
vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In(
[DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]
),
@@ -53,8 +63,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_PRECISION, default=1): cv.positive_int,
vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
- vol.Optional(CONF_MAX_TEMP, default=5): cv.positive_int,
- vol.Optional(CONF_MIN_TEMP, default=35): cv.positive_int,
+ vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int,
+ vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int,
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
vol.Optional(CONF_UNIT, default="C"): cv.string,
}
@@ -63,20 +73,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Modbus Thermostat Platform."""
- name = config.get(CONF_NAME)
- modbus_slave = config.get(CONF_SLAVE)
- target_temp_register = config.get(CONF_TARGET_TEMP)
- current_temp_register = config.get(CONF_CURRENT_TEMP)
- data_type = config.get(CONF_DATA_TYPE)
- count = config.get(CONF_COUNT)
- precision = config.get(CONF_PRECISION)
- scale = config.get(CONF_SCALE)
- offset = config.get(CONF_OFFSET)
- unit = config.get(CONF_UNIT)
- max_temp = config.get(CONF_MAX_TEMP)
- min_temp = config.get(CONF_MIN_TEMP)
- temp_step = config.get(CONF_STEP)
- hub_name = config.get(CONF_HUB)
+ name = config[CONF_NAME]
+ modbus_slave = config[CONF_SLAVE]
+ target_temp_register = config[CONF_TARGET_TEMP]
+ current_temp_register = config[CONF_CURRENT_TEMP]
+ current_temp_register_type = config[CONF_CURRENT_TEMP_REGISTER_TYPE]
+ data_type = config[CONF_DATA_TYPE]
+ count = config[CONF_COUNT]
+ precision = config[CONF_PRECISION]
+ scale = config[CONF_SCALE]
+ offset = config[CONF_OFFSET]
+ unit = config[CONF_UNIT]
+ max_temp = config[CONF_MAX_TEMP]
+ min_temp = config[CONF_MIN_TEMP]
+ temp_step = config[CONF_STEP]
+ hub_name = config[CONF_HUB]
hub = hass.data[MODBUS_DOMAIN][hub_name]
add_entities(
@@ -87,6 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
modbus_slave,
target_temp_register,
current_temp_register,
+ current_temp_register_type,
data_type,
count,
precision,
@@ -112,6 +124,7 @@ class ModbusThermostat(ClimateDevice):
modbus_slave,
target_temp_register,
current_temp_register,
+ current_temp_register_type,
data_type,
count,
precision,
@@ -128,6 +141,7 @@ class ModbusThermostat(ClimateDevice):
self._slave = modbus_slave
self._target_temperature_register = target_temp_register
self._current_temperature_register = current_temp_register
+ self._current_temperature_register_type = current_temp_register_type
self._target_temperature = None
self._current_temperature = None
self._data_type = data_type
@@ -140,6 +154,7 @@ class ModbusThermostat(ClimateDevice):
self._min_temp = min_temp
self._temp_step = temp_step
self._structure = ">f"
+ self._available = True
data_types = {
DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"},
@@ -156,9 +171,11 @@ class ModbusThermostat(ClimateDevice):
def update(self):
"""Update Target & Current Temperature."""
- self._target_temperature = self.read_register(self._target_temperature_register)
- self._current_temperature = self.read_register(
- self._current_temperature_register
+ self._target_temperature = self._read_register(
+ DEFAULT_REGISTER_TYPE_HOLDING, self._target_temperature_register
+ )
+ self._current_temperature = self._read_register(
+ self._current_temperature_register_type, self._current_temperature_register
)
@property
@@ -215,20 +232,32 @@ class ModbusThermostat(ClimateDevice):
return
byte_string = struct.pack(self._structure, target_temperature)
register_value = struct.unpack(">h", byte_string[0:2])[0]
+ self._write_register(self._target_temperature_register, register_value)
- try:
- self.write_register(self._target_temperature_register, register_value)
- except AttributeError as ex:
- _LOGGER.error(ex)
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
- def read_register(self, register):
- """Read holding register using the Modbus hub slave."""
+ def _read_register(self, register_type, register) -> Optional[float]:
+ """Read register using the Modbus hub slave."""
try:
- result = self._hub.read_holding_registers(
- self._slave, register, self._count
- )
- except AttributeError as ex:
- _LOGGER.error(ex)
+ if register_type == DEFAULT_REGISTER_TYPE_INPUT:
+ result = self._hub.read_input_registers(
+ self._slave, register, self._count
+ )
+ else:
+ result = self._hub.read_holding_registers(
+ self._slave, register, self._count
+ )
+ except ConnectionException:
+ self._set_unavailable(register)
+ return
+
+ if isinstance(result, (ModbusException, ExceptionResponse)):
+ self._set_unavailable(register)
+ return
+
byte_string = b"".join(
[x.to_bytes(2, byteorder="big") for x in result.registers]
)
@@ -237,8 +266,29 @@ class ModbusThermostat(ClimateDevice):
(self._scale * val) + self._offset, f".{self._precision}f"
)
register_value = float(register_value)
+ self._available = True
+
return register_value
- def write_register(self, register, value):
- """Write register using the Modbus hub slave."""
- self._hub.write_registers(self._slave, register, [value, 0])
+ def _write_register(self, register, value):
+ """Write holding register using the Modbus hub slave."""
+ try:
+ self._hub.write_registers(self._slave, register, [value, 0])
+ except ConnectionException:
+ self._set_unavailable(register)
+ return
+
+ self._available = True
+
+ def _set_unavailable(self, register):
+ """Set unavailable state and log it as an error."""
+ if not self._available:
+ return
+
+ _LOGGER.error(
+ "No response from hub %s, slave %s, register %s",
+ self._hub.name,
+ self._slave,
+ register,
+ )
+ self._available = False
diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py
index 484382983ac..716cb5299b7 100644
--- a/homeassistant/components/modbus/sensor.py
+++ b/homeassistant/components/modbus/sensor.py
@@ -3,6 +3,8 @@ import logging
import struct
from typing import Any, Optional, Union
+from pymodbus.exceptions import ConnectionException, ModbusException
+from pymodbus.pdu import ExceptionResponse
import voluptuous as vol
from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA
@@ -35,8 +37,8 @@ DATA_TYPE_FLOAT = "float"
DATA_TYPE_INT = "int"
DATA_TYPE_UINT = "uint"
-REGISTER_TYPE_HOLDING = "holding"
-REGISTER_TYPE_INPUT = "input"
+DEFAULT_REGISTER_TYPE_HOLDING = "holding"
+DEFAULT_REGISTER_TYPE_INPUT = "input"
def number(value: Any) -> Union[int, float]:
@@ -72,9 +74,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
vol.Optional(CONF_OFFSET, default=0): number,
vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
- vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In(
- [REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]
- ),
+ vol.Optional(
+ CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING
+ ): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]),
vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
vol.Optional(CONF_SCALE, default=1): number,
vol.Optional(CONF_SLAVE): cv.positive_int,
@@ -93,17 +95,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data_types[DATA_TYPE_UINT] = {1: "H", 2: "I", 4: "Q"}
data_types[DATA_TYPE_FLOAT] = {1: "e", 2: "f", 4: "d"}
- for register in config.get(CONF_REGISTERS):
+ for register in config[CONF_REGISTERS]:
structure = ">i"
- if register.get(CONF_DATA_TYPE) != DATA_TYPE_CUSTOM:
+ if register[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM:
try:
structure = ">{}".format(
- data_types[register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)]
+ data_types[register[CONF_DATA_TYPE]][register[CONF_COUNT]]
)
except KeyError:
_LOGGER.error(
"Unable to detect data type for %s sensor, try a custom type",
- register.get(CONF_NAME),
+ register[CONF_NAME],
)
continue
else:
@@ -112,35 +114,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
try:
size = struct.calcsize(structure)
except struct.error as err:
- _LOGGER.error(
- "Error in sensor %s structure: %s", register.get(CONF_NAME), err
- )
+ _LOGGER.error("Error in sensor %s structure: %s", register[CONF_NAME], err)
continue
- if register.get(CONF_COUNT) * 2 != size:
+ if register[CONF_COUNT] * 2 != size:
_LOGGER.error(
"Structure size (%d bytes) mismatch registers count (%d words)",
size,
- register.get(CONF_COUNT),
+ register[CONF_COUNT],
)
continue
- hub_name = register.get(CONF_HUB)
+ hub_name = register[CONF_HUB]
hub = hass.data[MODBUS_DOMAIN][hub_name]
sensors.append(
ModbusRegisterSensor(
hub,
- register.get(CONF_NAME),
+ register[CONF_NAME],
register.get(CONF_SLAVE),
- register.get(CONF_REGISTER),
- register.get(CONF_REGISTER_TYPE),
+ register[CONF_REGISTER],
+ register[CONF_REGISTER_TYPE],
register.get(CONF_UNIT_OF_MEASUREMENT),
- register.get(CONF_COUNT),
- register.get(CONF_REVERSE_ORDER),
- register.get(CONF_SCALE),
- register.get(CONF_OFFSET),
+ register[CONF_COUNT],
+ register[CONF_REVERSE_ORDER],
+ register[CONF_SCALE],
+ register[CONF_OFFSET],
structure,
- register.get(CONF_PRECISION),
+ register[CONF_PRECISION],
register.get(CONF_DEVICE_CLASS),
)
)
@@ -184,6 +184,7 @@ class ModbusRegisterSensor(RestoreEntity):
self._structure = structure
self._device_class = device_class
self._value = None
+ self._available = True
async def async_added_to_hass(self):
"""Handle entity which will be added."""
@@ -212,30 +213,34 @@ class ModbusRegisterSensor(RestoreEntity):
"""Return the device class of the sensor."""
return self._device_class
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
+
def update(self):
"""Update the state of the sensor."""
- if self._register_type == REGISTER_TYPE_INPUT:
- result = self._hub.read_input_registers(
- self._slave, self._register, self._count
- )
- else:
- result = self._hub.read_holding_registers(
- self._slave, self._register, self._count
- )
- val = 0
-
try:
- registers = result.registers
- if self._reverse_order:
- registers.reverse()
- except AttributeError:
- _LOGGER.error(
- "No response from hub %s, slave %s, register %s",
- self._hub.name,
- self._slave,
- self._register,
- )
+ if self._register_type == DEFAULT_REGISTER_TYPE_INPUT:
+ result = self._hub.read_input_registers(
+ self._slave, self._register, self._count
+ )
+ else:
+ result = self._hub.read_holding_registers(
+ self._slave, self._register, self._count
+ )
+ except ConnectionException:
+ self._set_unavailable()
return
+
+ if isinstance(result, (ModbusException, ExceptionResponse)):
+ self._set_unavailable()
+ return
+
+ registers = result.registers
+ if self._reverse_order:
+ registers.reverse()
+
byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers])
val = struct.unpack(self._structure, byte_string)[0]
val = self._scale * val + self._offset
@@ -245,3 +250,18 @@ class ModbusRegisterSensor(RestoreEntity):
self._value += "." + "0" * self._precision
else:
self._value = f"{val:.{self._precision}f}"
+
+ self._available = True
+
+ def _set_unavailable(self):
+ """Set unavailable state and log it as an error."""
+ if not self._available:
+ return
+
+ _LOGGER.error(
+ "No response from hub %s, slave %s, address %s",
+ self._hub.name,
+ self._slave,
+ self._register,
+ )
+ self._available = False
diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml
index 2158528814f..8c11209570b 100644
--- a/homeassistant/components/modbus/services.yaml
+++ b/homeassistant/components/modbus/services.yaml
@@ -4,9 +4,11 @@ write_coil:
address: {description: Address of the register to write to., example: 0}
state: {description: State to write., example: false}
unit: {description: Address of the modbus unit., example: 21}
+ hub: {description: Optional Modbus hub name. A hub with the name 'default' is used if not specified., example: "hub1"}
write_register:
description: Write to a modbus holding register.
fields:
address: {description: Address of the holding register to write to., example: 0}
unit: {description: Address of the modbus unit., example: 21}
value: {description: Value (single value or array) to write., example: "0 or [4,0]"}
+ hub: {description: Optional Modbus hub name. A hub with the name 'default' is used if not specified., example: "hub1"}
diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py
index 0ed33dedb57..d4f52622538 100644
--- a/homeassistant/components/modbus/switch.py
+++ b/homeassistant/components/modbus/switch.py
@@ -1,6 +1,9 @@
"""Support for Modbus switches."""
import logging
+from typing import Optional
+from pymodbus.exceptions import ConnectionException, ModbusException
+from pymodbus.pdu import ExceptionResponse
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA
@@ -29,8 +32,8 @@ CONF_STATE_ON = "state_on"
CONF_VERIFY_REGISTER = "verify_register"
CONF_VERIFY_STATE = "verify_state"
-REGISTER_TYPE_HOLDING = "holding"
-REGISTER_TYPE_INPUT = "input"
+DEFAULT_REGISTER_TYPE_HOLDING = "holding"
+DEFAULT_REGISTER_TYPE_INPUT = "input"
REGISTERS_SCHEMA = vol.Schema(
{
@@ -39,8 +42,8 @@ REGISTERS_SCHEMA = vol.Schema(
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_REGISTER): cv.positive_int,
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
- vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In(
- [REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]
+ vol.Optional(CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING): vol.In(
+ [DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]
),
vol.Optional(CONF_SLAVE): cv.positive_int,
vol.Optional(CONF_STATE_OFF): cv.positive_int,
@@ -74,30 +77,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Read configuration and create Modbus devices."""
switches = []
if CONF_COILS in config:
- for coil in config.get(CONF_COILS):
- hub_name = coil.get(CONF_HUB)
+ for coil in config[CONF_COILS]:
+ hub_name = coil[CONF_HUB]
hub = hass.data[MODBUS_DOMAIN][hub_name]
switches.append(
ModbusCoilSwitch(
- hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL)
+ hub, coil[CONF_NAME], coil[CONF_SLAVE], coil[CONF_COIL]
)
)
if CONF_REGISTERS in config:
- for register in config.get(CONF_REGISTERS):
- hub_name = register.get(CONF_HUB)
+ for register in config[CONF_REGISTERS]:
+ hub_name = register[CONF_HUB]
hub = hass.data[MODBUS_DOMAIN][hub_name]
switches.append(
ModbusRegisterSwitch(
hub,
- register.get(CONF_NAME),
+ register[CONF_NAME],
register.get(CONF_SLAVE),
- register.get(CONF_REGISTER),
- register.get(CONF_COMMAND_ON),
- register.get(CONF_COMMAND_OFF),
- register.get(CONF_VERIFY_STATE),
+ register[CONF_REGISTER],
+ register[CONF_COMMAND_ON],
+ register[CONF_COMMAND_OFF],
+ register[CONF_VERIFY_STATE],
register.get(CONF_VERIFY_REGISTER),
- register.get(CONF_REGISTER_TYPE),
+ register[CONF_REGISTER_TYPE],
register.get(CONF_STATE_ON),
register.get(CONF_STATE_OFF),
)
@@ -116,6 +119,7 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
self._slave = int(slave) if slave else None
self._coil = int(coil)
self._is_on = None
+ self._available = True
async def async_added_to_hass(self):
"""Handle entity which will be added."""
@@ -134,26 +138,62 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
"""Return the name of the switch."""
return self._name
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
+
def turn_on(self, **kwargs):
"""Set switch on."""
- self._hub.write_coil(self._slave, self._coil, True)
+ self._write_coil(self._coil, True)
def turn_off(self, **kwargs):
"""Set switch off."""
- self._hub.write_coil(self._slave, self._coil, False)
+ self._write_coil(self._coil, False)
def update(self):
"""Update the state of the switch."""
- result = self._hub.read_coils(self._slave, self._coil, 1)
+ self._is_on = self._read_coil(self._coil)
+
+ def _read_coil(self, coil) -> Optional[bool]:
+ """Read coil using the Modbus hub slave."""
try:
- self._is_on = bool(result.bits[0])
- except AttributeError:
- _LOGGER.error(
- "No response from hub %s, slave %s, coil %s",
- self._hub.name,
- self._slave,
- self._coil,
- )
+ result = self._hub.read_coils(self._slave, coil, 1)
+ except ConnectionException:
+ self._set_unavailable()
+ return
+
+ if isinstance(result, (ModbusException, ExceptionResponse)):
+ self._set_unavailable()
+ return
+
+ value = bool(result.bits[0])
+ self._available = True
+
+ return value
+
+ def _write_coil(self, coil, value):
+ """Write coil using the Modbus hub slave."""
+ try:
+ self._hub.write_coil(self._slave, coil, value)
+ except ConnectionException:
+ self._set_unavailable()
+ return
+
+ self._available = True
+
+ def _set_unavailable(self):
+ """Set unavailable state and log it as an error."""
+ if not self._available:
+ return
+
+ _LOGGER.error(
+ "No response from hub %s, slave %s, coil %s",
+ self._hub.name,
+ self._slave,
+ self._coil,
+ )
+ self._available = False
class ModbusRegisterSwitch(ModbusCoilSwitch):
@@ -184,6 +224,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
self._verify_state = verify_state
self._verify_register = verify_register if verify_register else self._register
self._register_type = register_type
+ self._available = True
if state_on is not None:
self._state_on = state_on
@@ -199,46 +240,86 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
def turn_on(self, **kwargs):
"""Set switch on."""
- self._hub.write_register(self._slave, self._register, self._command_on)
- if not self._verify_state:
- self._is_on = True
+
+ # Only holding register is writable
+ if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING:
+ self._write_register(self._command_on)
+ if not self._verify_state:
+ self._is_on = True
def turn_off(self, **kwargs):
"""Set switch off."""
- self._hub.write_register(self._slave, self._register, self._command_off)
- if not self._verify_state:
- self._is_on = False
+
+ # Only holding register is writable
+ if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING:
+ self._write_register(self._command_off)
+ if not self._verify_state:
+ self._is_on = False
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
def update(self):
"""Update the state of the switch."""
if not self._verify_state:
return
- value = 0
- if self._register_type == REGISTER_TYPE_INPUT:
- result = self._hub.read_input_registers(self._slave, self._register, 1)
- else:
- result = self._hub.read_holding_registers(self._slave, self._register, 1)
-
- try:
- value = int(result.registers[0])
- except AttributeError:
- _LOGGER.error(
- "No response from hub %s, slave %s, register %s",
- self._hub.name,
- self._slave,
- self._verify_register,
- )
-
+ value = self._read_register()
if value == self._state_on:
self._is_on = True
elif value == self._state_off:
self._is_on = False
- else:
+ elif value is not None:
_LOGGER.error(
"Unexpected response from hub %s, slave %s register %s, got 0x%2x",
self._hub.name,
self._slave,
- self._verify_register,
+ self._register,
value,
)
+
+ def _read_register(self) -> Optional[int]:
+ try:
+ if self._register_type == DEFAULT_REGISTER_TYPE_INPUT:
+ result = self._hub.read_input_registers(self._slave, self._register, 1)
+ else:
+ result = self._hub.read_holding_registers(
+ self._slave, self._register, 1
+ )
+ except ConnectionException:
+ self._set_unavailable()
+ return
+
+ if isinstance(result, (ModbusException, ExceptionResponse)):
+ self._set_unavailable()
+ return
+
+ value = int(result.registers[0])
+ self._available = True
+
+ return value
+
+ def _write_register(self, value):
+ """Write holding register using the Modbus hub slave."""
+ try:
+ self._hub.write_register(self._slave, self._register, value)
+ except ConnectionException:
+ self._set_unavailable()
+ return
+
+ self._available = True
+
+ def _set_unavailable(self):
+ """Set unavailable state and log it as an error."""
+ if not self._available:
+ return
+
+ _LOGGER.error(
+ "No response from hub %s, slave %s, register %s",
+ self._hub.name,
+ self._slave,
+ self._register,
+ )
+ self._available = False
diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json
index 47dc4d344bc..b8cce4bd808 100644
--- a/homeassistant/components/mqtt/.translations/ca.json
+++ b/homeassistant/components/mqtt/.translations/ca.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Primer bot\u00f3",
+ "button_2": "Segon bot\u00f3",
+ "button_3": "Tercer bot\u00f3",
+ "button_4": "Quart bot\u00f3",
+ "button_5": "Cinqu\u00e8 bot\u00f3",
+ "button_6": "Sis\u00e8 bot\u00f3",
+ "turn_off": "Desactiva",
+ "turn_on": "Activa"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" clicat dues vegades",
+ "button_long_press": "\"{subtype}\" premut cont\u00ednuament",
+ "button_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut",
+ "button_quadruple_press": "\"{subtype}\" clicat quatre vegades",
+ "button_quintuple_press": "\"{subtype}\" clicat cinc vegades",
+ "button_short_press": "\"{subtype}\" premut",
+ "button_short_release": "\"{subtype}\" alliberat",
+ "button_triple_press": "\"{subtype}\" clicat tres vegades"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/da.json b/homeassistant/components/mqtt/.translations/da.json
index 93ea57d49ea..e018ab7aa14 100644
--- a/homeassistant/components/mqtt/.translations/da.json
+++ b/homeassistant/components/mqtt/.translations/da.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "F\u00f8rste knap",
+ "button_2": "Anden knap",
+ "button_3": "Tredje knap",
+ "button_4": "Fjerde knap",
+ "button_5": "Femte knap",
+ "button_6": "Sjette knap",
+ "turn_off": "Sluk",
+ "turn_on": "T\u00e6nd"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" dobbeltklikket",
+ "button_long_press": "\"{subtype}\" trykket p\u00e5 konstant",
+ "button_long_release": "\"{subtype}\" sluppet efter langt tryk",
+ "button_quadruple_press": "\"{subtype}\" firedobbelt-klikket",
+ "button_quintuple_press": "\"{subtype}\" femdobbelt-klikket",
+ "button_short_press": "\"{subtype}\" trykket p\u00e5",
+ "button_short_release": "\"{subtype}\" sluppet",
+ "button_triple_press": "\"{subtype}\" tredobbeltklikket"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json
index d95c43cc618..87c6a989f52 100644
--- a/homeassistant/components/mqtt/.translations/de.json
+++ b/homeassistant/components/mqtt/.translations/de.json
@@ -22,10 +22,32 @@
"data": {
"discovery": "Suche aktivieren"
},
- "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?",
+ "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Hass.io Add-on {addon} bereitgestellt wird?",
"title": "MQTT Broker per Hass.io add-on"
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "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",
+ "turn_off": "Ausschalten",
+ "turn_on": "Einschalten"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" doppelt angeklickt",
+ "button_long_press": "\"{subtype}\" kontinuierlich gedr\u00fcckt",
+ "button_long_release": "\"{subtype}\" nach langem Dr\u00fccken freigegeben",
+ "button_quadruple_press": "\"{subtype}\" Vierfach geklickt",
+ "button_quintuple_press": "\"{subtype}\" f\u00fcnffach geklickt",
+ "button_short_press": "\"{subtype}\" gedr\u00fcckt",
+ "button_short_release": "\"{subtype}\" freigegeben",
+ "button_triple_press": "\"{subtype}\" dreifach geklickt"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/en.json b/homeassistant/components/mqtt/.translations/en.json
index ad18951a9d7..55baf3b7f0e 100644
--- a/homeassistant/components/mqtt/.translations/en.json
+++ b/homeassistant/components/mqtt/.translations/en.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "First button",
+ "button_2": "Second button",
+ "button_3": "Third button",
+ "button_4": "Fourth button",
+ "button_5": "Fifth button",
+ "button_6": "Sixth button",
+ "turn_off": "Turn off",
+ "turn_on": "Turn on"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" double clicked",
+ "button_long_press": "\"{subtype}\" continuously pressed",
+ "button_long_release": "\"{subtype}\" released after long press",
+ "button_quadruple_press": "\"{subtype}\" quadruple clicked",
+ "button_quintuple_press": "\"{subtype}\" quintuple clicked",
+ "button_short_press": "\"{subtype}\" pressed",
+ "button_short_release": "\"{subtype}\" released",
+ "button_triple_press": "\"{subtype}\" triple clicked"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/es.json b/homeassistant/components/mqtt/.translations/es.json
index e0c94ac621a..a705a885494 100644
--- a/homeassistant/components/mqtt/.translations/es.json
+++ b/homeassistant/components/mqtt/.translations/es.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Primer bot\u00f3n",
+ "button_2": "Segundo bot\u00f3n",
+ "button_3": "Tercer bot\u00f3n",
+ "button_4": "Cuarto bot\u00f3n",
+ "button_5": "Quinto bot\u00f3n",
+ "button_6": "Sexto bot\u00f3n",
+ "turn_off": "Apagar",
+ "turn_on": "Encender"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" doble pulsaci\u00f3n",
+ "button_long_press": "\"{subtype}\" pulsado continuamente",
+ "button_long_release": "\"{subtype}\" soltado despu\u00e9s de pulsaci\u00f3n larga",
+ "button_quadruple_press": "\"{subtype}\" cu\u00e1druple pulsaci\u00f3n",
+ "button_quintuple_press": "\"{subtype}\" quintuple pulsaci\u00f3n",
+ "button_short_press": "\"{subtype}\" pulsado",
+ "button_short_release": "\"{subtype}\" soltado",
+ "button_triple_press": "\"{subtype}\" triple pulsaci\u00f3n"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/it.json b/homeassistant/components/mqtt/.translations/it.json
index cf2b3ddf7d5..45f7f8dcdb5 100644
--- a/homeassistant/components/mqtt/.translations/it.json
+++ b/homeassistant/components/mqtt/.translations/it.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Primo pulsante",
+ "button_2": "Secondo pulsante",
+ "button_3": "Terzo pulsante",
+ "button_4": "Quarto pulsante",
+ "button_5": "Quinto pulsante",
+ "button_6": "Sesto pulsante",
+ "turn_off": "Spegni",
+ "turn_on": "Accendi"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" cliccato due volte",
+ "button_long_press": "\"{subtype}\" premuto continuamente",
+ "button_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione",
+ "button_quadruple_press": "\"{subtype}\" cliccato quattro volte",
+ "button_quintuple_press": "\"{subtype}\" cliccato cinque volte",
+ "button_short_press": "\"{subtype}\" premuto",
+ "button_short_release": "\"{subtype}\" rilasciato",
+ "button_triple_press": "\"{subtype}\" cliccato tre volte"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json
index 307a6aaadeb..8a0243013d9 100644
--- a/homeassistant/components/mqtt/.translations/ko.json
+++ b/homeassistant/components/mqtt/.translations/ko.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\uccab \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_2": "\ub450 \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_3": "\uc138 \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_4": "\ub124 \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_5": "\ub2e4\uc12f \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_6": "\uc5ec\uc12f \ubc88\uc9f8 \ubc84\ud2bc",
+ "turn_off": "\ub044\uae30",
+ "turn_on": "\ucf1c\uae30"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" \uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c",
+ "button_long_press": "\"{subtype}\" \uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c",
+ "button_long_release": "\"{subtype}\" \uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c",
+ "button_quadruple_press": "\"{subtype}\" \uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c",
+ "button_quintuple_press": "\"{subtype}\" \uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c",
+ "button_short_press": "\"{subtype}\" \uc774 \ub20c\ub9b4 \ub54c",
+ "button_short_release": "\"{subtype}\" \uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c",
+ "button_triple_press": "\"{subtype}\" \uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/lb.json b/homeassistant/components/mqtt/.translations/lb.json
index 9dcd9c58a3a..9467ab8a9a7 100644
--- a/homeassistant/components/mqtt/.translations/lb.json
+++ b/homeassistant/components/mqtt/.translations/lb.json
@@ -27,5 +27,17 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u00c9ischte Kn\u00e4ppchen",
+ "button_2": "Zweete Kn\u00e4ppchen",
+ "button_3": "Dr\u00ebtte Kn\u00e4ppchen",
+ "button_4": "V\u00e9ierte Kn\u00e4ppchen",
+ "button_5": "F\u00ebnnefte Kn\u00e4ppchen",
+ "button_6": "Sechste Kn\u00e4ppchen",
+ "turn_off": "Ausschalten",
+ "turn_on": "Uschalten"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json
index 8dcc0bded9f..27a77a25226 100644
--- a/homeassistant/components/mqtt/.translations/no.json
+++ b/homeassistant/components/mqtt/.translations/no.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "F\u00f8rste knapp",
+ "button_2": "Andre knapp",
+ "button_3": "Tredje knapp",
+ "button_4": "Fjerde knapp",
+ "button_5": "Femte knapp",
+ "button_6": "Sjette knapp",
+ "turn_off": "Skru av",
+ "turn_on": "Sl\u00e5 p\u00e5"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" dobbeltklikket",
+ "button_long_press": "{subtype}\" trykket kontinuerlig",
+ "button_long_release": "\"{subtype}\" utgitt etter lang trykk",
+ "button_quadruple_press": "\"{subtype}\" firedoblet klikket",
+ "button_quintuple_press": "\"{subtype}\" quintuple klikket",
+ "button_short_press": "{subtype}\u00bb trykket",
+ "button_short_release": "\"{subtype}\" utgitt",
+ "button_triple_press": "\"{subtype}\" trippel klikket"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/pl.json b/homeassistant/components/mqtt/.translations/pl.json
index 24cdeb0f12e..86561f89d2b 100644
--- a/homeassistant/components/mqtt/.translations/pl.json
+++ b/homeassistant/components/mqtt/.translations/pl.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "pierwszy przycisk",
+ "button_2": "drugi przycisk",
+ "button_3": "trzeci przycisk",
+ "button_4": "czwarty przycisk",
+ "button_5": "pi\u0105ty przycisk",
+ "button_6": "sz\u00f3sty przycisk",
+ "turn_off": "nast\u0105pi wy\u0142\u0105czenie",
+ "turn_on": "nast\u0105pi w\u0142\u0105czenie"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty",
+ "button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
+ "button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
+ "button_quadruple_press": "\"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty",
+ "button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty",
+ "button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty",
+ "button_short_release": "\"{subtype}\" zostanie zwolniony",
+ "button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/ro.json b/homeassistant/components/mqtt/.translations/ro.json
index bcd150e3063..0bbf39315d9 100644
--- a/homeassistant/components/mqtt/.translations/ro.json
+++ b/homeassistant/components/mqtt/.translations/ro.json
@@ -22,7 +22,7 @@
"data": {
"discovery": "Activa\u021bi descoperirea"
},
- "description": "Dori\u021bi s\u0103 configura\u021bi Home Assistant pentru a v\u0103 conecta la brokerul MQTT furnizat de addon-ul {addon} ?",
+ "description": "Dori\u021bi s\u0103 configura\u021bi Home Assistant pentru a se conecta la brokerul MQTT furnizat de addon-ul {addon} ?",
"title": "MQTT Broker, prin intermediul Hass.io add-on"
}
},
diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json
index 925b8cf5ab4..3559fcc6b2b 100644
--- a/homeassistant/components/mqtt/.translations/ru.json
+++ b/homeassistant/components/mqtt/.translations/ru.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "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",
+ "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": {
+ "button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430",
+ "button_long_press": "\"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430",
+ "button_long_release": "\"{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",
+ "button_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430",
+ "button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437",
+ "button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430",
+ "button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430",
+ "button_triple_press": "\"{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/mqtt/.translations/zh-Hans.json b/homeassistant/components/mqtt/.translations/zh-Hans.json
index f30e1bf10b4..c12004236bd 100644
--- a/homeassistant/components/mqtt/.translations/zh-Hans.json
+++ b/homeassistant/components/mqtt/.translations/zh-Hans.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u7b2c\u4e00\u4e2a\u6309\u94ae",
+ "button_2": "\u7b2c\u4e8c\u4e2a\u6309\u94ae",
+ "button_3": "\u7b2c\u4e09\u4e2a\u6309\u94ae",
+ "button_4": "\u7b2c\u56db\u4e2a\u6309\u94ae",
+ "button_5": "\u7b2c\u4e94\u4e2a\u6309\u94ae",
+ "button_6": "\u7b2c\u516d\u4e2a\u6309\u94ae",
+ "turn_off": "\u5173\u95ed",
+ "turn_on": "\u6253\u5f00"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" \u53cc\u51fb",
+ "button_long_press": "\"{subtype}\" \u6301\u7eed\u6309\u4e0b",
+ "button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u91ca\u653e",
+ "button_quadruple_press": "\"{subtype}\" \u56db\u8fde\u51fb",
+ "button_quintuple_press": "\"{subtype}\" \u4e94\u8fde\u51fb",
+ "button_short_press": "\"{subtype}\" \u6309\u4e0b",
+ "button_short_release": "\"{subtype}\" \u91ca\u653e",
+ "button_triple_press": "\"{subtype}\" \u4e09\u8fde\u51fb"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/zh-Hant.json b/homeassistant/components/mqtt/.translations/zh-Hant.json
index 09f2f44a902..3c57e7b6bb0 100644
--- a/homeassistant/components/mqtt/.translations/zh-Hant.json
+++ b/homeassistant/components/mqtt/.translations/zh-Hant.json
@@ -27,5 +27,27 @@
}
},
"title": "MQTT"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u7b2c\u4e00\u500b\u6309\u9215",
+ "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215",
+ "button_3": "\u7b2c\u4e09\u500b\u6309\u9215",
+ "button_4": "\u7b2c\u56db\u500b\u6309\u9215",
+ "button_5": "\u7b2c\u4e94\u500b\u6309\u9215",
+ "button_6": "\u7b2c\u516d\u500b\u6309\u9215",
+ "turn_off": "\u95dc\u9589",
+ "turn_on": "\u958b\u555f"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" \u96d9\u64ca",
+ "button_long_press": "\"{subtype}\" \u6301\u7e8c\u6309\u4e0b",
+ "button_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e",
+ "button_quadruple_press": "\"{subtype}\" \u56db\u9023\u64ca",
+ "button_quintuple_press": "\"{subtype}\" \u4e94\u9023\u64ca",
+ "button_short_press": "\"{subtype}\" \u6309\u4e0b",
+ "button_short_release": "\"{subtype}\" \u91cb\u653e",
+ "button_triple_press": "\"{subtype}\" \u4e09\u9023\u64ca"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index f64c643f0f4..540d09d7c9f 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -568,7 +568,7 @@ async def async_setup_entry(hass, entry):
conf = CONFIG_SCHEMA({DOMAIN: entry.data})[DOMAIN]
elif any(key in conf for key in entry.data):
_LOGGER.warning(
- "Data in your config entry is going to override your "
+ "Data in your configuration entry is going to override your "
"configuration.yaml: %s",
entry.data,
)
@@ -1194,6 +1194,34 @@ class MqttDiscoveryUpdate(Entity):
)
+def device_info_from_config(config):
+ """Return a device description for device registry."""
+ if not config:
+ return None
+
+ info = {
+ "identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]},
+ "connections": {tuple(x) for x in config[CONF_CONNECTIONS]},
+ }
+
+ if CONF_MANUFACTURER in config:
+ info["manufacturer"] = config[CONF_MANUFACTURER]
+
+ if CONF_MODEL in config:
+ info["model"] = config[CONF_MODEL]
+
+ if CONF_NAME in config:
+ info["name"] = config[CONF_NAME]
+
+ if CONF_SW_VERSION in config:
+ info["sw_version"] = config[CONF_SW_VERSION]
+
+ if CONF_VIA_DEVICE in config:
+ info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE])
+
+ return info
+
+
class MqttEntityDeviceInfo(Entity):
"""Mixin used for mqtt platforms that support the device registry."""
@@ -1216,32 +1244,7 @@ class MqttEntityDeviceInfo(Entity):
@property
def device_info(self):
"""Return a device description for device registry."""
- if not self._device_config:
- return None
-
- info = {
- "identifiers": {
- (DOMAIN, id_) for id_ in self._device_config[CONF_IDENTIFIERS]
- },
- "connections": {tuple(x) for x in self._device_config[CONF_CONNECTIONS]},
- }
-
- if CONF_MANUFACTURER in self._device_config:
- info["manufacturer"] = self._device_config[CONF_MANUFACTURER]
-
- if CONF_MODEL in self._device_config:
- info["model"] = self._device_config[CONF_MODEL]
-
- if CONF_NAME in self._device_config:
- info["name"] = self._device_config[CONF_NAME]
-
- if CONF_SW_VERSION in self._device_config:
- info["sw_version"] = self._device_config[CONF_SW_VERSION]
-
- if CONF_VIA_DEVICE in self._device_config:
- info["via_device"] = (DOMAIN, self._device_config[CONF_VIA_DEVICE])
-
- return info
+ return device_info_from_config(self._device_config)
@websocket_api.async_response
diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py
index 6f9b1720102..6cfab66c3f1 100644
--- a/homeassistant/components/mqtt/abbreviations.py
+++ b/homeassistant/components/mqtt/abbreviations.py
@@ -3,6 +3,7 @@
ABBREVIATIONS = {
"act_t": "action_topic",
"act_tpl": "action_template",
+ "atype": "automation_type",
"aux_cmd_t": "aux_command_topic",
"aux_stat_tpl": "aux_state_template",
"aux_stat_t": "aux_state_topic",
@@ -80,6 +81,7 @@ ABBREVIATIONS = {
"osc_cmd_t": "oscillation_command_topic",
"osc_stat_t": "oscillation_state_topic",
"osc_val_tpl": "oscillation_value_template",
+ "pl": "payload",
"pl_arm_away": "payload_arm_away",
"pl_arm_home": "payload_arm_home",
"pl_arm_nite": "payload_arm_night",
@@ -132,14 +134,17 @@ ABBREVIATIONS = {
"spds": "speeds",
"src_type": "source_type",
"stat_clsd": "state_closed",
+ "stat_closing": "state_closing",
"stat_off": "state_off",
"stat_on": "state_on",
"stat_open": "state_open",
+ "stat_opening": "state_opening",
"stat_locked": "state_locked",
"stat_unlocked": "state_unlocked",
"stat_t": "state_topic",
"stat_tpl": "state_template",
"stat_val_tpl": "state_value_template",
+ "stype": "subtype",
"sup_feat": "supported_features",
"swing_mode_cmd_t": "swing_mode_command_topic",
"swing_mode_stat_tpl": "swing_mode_state_template",
diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py
index e6cfab90c26..885343b7090 100644
--- a/homeassistant/components/mqtt/cover.py
+++ b/homeassistant/components/mqtt/cover.py
@@ -25,7 +25,9 @@ from homeassistant.const import (
CONF_OPTIMISTIC,
CONF_VALUE_TEMPLATE,
STATE_CLOSED,
+ STATE_CLOSING,
STATE_OPEN,
+ STATE_OPENING,
STATE_UNKNOWN,
)
from homeassistant.core import callback
@@ -64,7 +66,9 @@ CONF_PAYLOAD_STOP = "payload_stop"
CONF_POSITION_CLOSED = "position_closed"
CONF_POSITION_OPEN = "position_open"
CONF_STATE_CLOSED = "state_closed"
+CONF_STATE_CLOSING = "state_closing"
CONF_STATE_OPEN = "state_open"
+CONF_STATE_OPENING = "state_opening"
CONF_TILT_CLOSED_POSITION = "tilt_closed_value"
CONF_TILT_INVERT_STATE = "tilt_invert_state"
CONF_TILT_MAX = "tilt_max"
@@ -131,7 +135,9 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_SET_POSITION_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
+ vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string,
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
+ vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(
CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION
@@ -172,15 +178,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def async_discover(discovery_payload):
"""Discover and add an MQTT cover."""
+ discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH)
try:
- discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH)
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_hash
)
except Exception:
- if discovery_hash:
- clear_discovery_hash(hass, discovery_hash)
+ clear_discovery_hash(hass, discovery_hash)
raise
async_dispatcher_connect(
@@ -289,12 +294,20 @@ class MqttCover(
payload = template.async_render_with_possible_json_value(payload)
if payload == self._config[CONF_STATE_OPEN]:
- self._state = False
+ self._state = STATE_OPEN
+ elif payload == self._config[CONF_STATE_OPENING]:
+ self._state = STATE_OPENING
elif payload == self._config[CONF_STATE_CLOSED]:
- self._state = True
+ self._state = STATE_CLOSED
+ elif payload == self._config[CONF_STATE_CLOSING]:
+ self._state = STATE_CLOSING
else:
- _LOGGER.warning("Payload is not True or False: %s", payload)
+ _LOGGER.warning(
+ "Payload is not supported (e.g. open, closed, opening, closing): %s",
+ payload,
+ )
return
+
self.async_write_ha_state()
@callback
@@ -309,7 +322,11 @@ class MqttCover(
float(payload), COVER_PAYLOAD
)
self._position = percentage_payload
- self._state = percentage_payload == DEFAULT_POSITION_CLOSED
+ self._state = (
+ STATE_CLOSED
+ if percentage_payload == DEFAULT_POSITION_CLOSED
+ else STATE_OPEN
+ )
else:
_LOGGER.warning("Payload is not integer within range: %s", payload)
return
@@ -370,8 +387,21 @@ class MqttCover(
@property
def is_closed(self):
- """Return if the cover is closed."""
- return self._state
+ """Return true if the cover is closed or None if the status is unknown."""
+ if self._state is None:
+ return None
+
+ return self._state == STATE_CLOSED
+
+ @property
+ def is_opening(self):
+ """Return true if the cover is actively opening."""
+ return self._state == STATE_OPENING
+
+ @property
+ def is_closing(self):
+ """Return true if the cover is actively closing."""
+ return self._state == STATE_CLOSING
@property
def current_cover_position(self):
@@ -423,7 +453,7 @@ class MqttCover(
)
if self._optimistic:
# Optimistically assume that cover has changed state.
- self._state = False
+ self._state = STATE_OPEN
if self._config.get(CONF_GET_POSITION_TOPIC):
self._position = self.find_percentage_in_range(
self._config[CONF_POSITION_OPEN], COVER_PAYLOAD
@@ -444,7 +474,7 @@ class MqttCover(
)
if self._optimistic:
# Optimistically assume that cover has changed state.
- self._state = True
+ self._state = STATE_CLOSED
if self._config.get(CONF_GET_POSITION_TOPIC):
self._position = self.find_percentage_in_range(
self._config[CONF_POSITION_CLOSED], COVER_PAYLOAD
@@ -538,7 +568,11 @@ class MqttCover(
self._config[CONF_RETAIN],
)
if self._optimistic:
- self._state = percentage_position == self._config[CONF_POSITION_CLOSED]
+ self._state = (
+ STATE_CLOSED
+ if percentage_position == self._config[CONF_POSITION_CLOSED]
+ else STATE_OPEN
+ )
self._position = percentage_position
self.async_write_ha_state()
diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py
new file mode 100644
index 00000000000..3f0889875d0
--- /dev/null
+++ b/homeassistant/components/mqtt/device_automation.py
@@ -0,0 +1,44 @@
+"""Provides device automations for MQTT."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components import mqtt
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from . import ATTR_DISCOVERY_HASH, device_trigger
+from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash
+
+_LOGGER = logging.getLogger(__name__)
+
+AUTOMATION_TYPE_TRIGGER = "trigger"
+AUTOMATION_TYPES = [AUTOMATION_TYPE_TRIGGER]
+AUTOMATION_TYPES_SCHEMA = vol.In(AUTOMATION_TYPES)
+CONF_AUTOMATION_TYPE = "automation_type"
+
+PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
+ {vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA},
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up MQTT device automation dynamically through MQTT discovery."""
+
+ async def async_discover(discovery_payload):
+ """Discover and add an MQTT device automation."""
+ discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH)
+ try:
+ config = PLATFORM_SCHEMA(discovery_payload)
+ if config[CONF_AUTOMATION_TYPE] == AUTOMATION_TYPE_TRIGGER:
+ await device_trigger.async_setup_trigger(
+ hass, config, config_entry, discovery_hash
+ )
+ except Exception:
+ if discovery_hash:
+ clear_discovery_hash(hass, discovery_hash)
+ raise
+
+ async_dispatcher_connect(
+ hass, MQTT_DISCOVERY_NEW.format("device_automation", "mqtt"), async_discover
+ )
diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py
new file mode 100644
index 00000000000..2149024266d
--- /dev/null
+++ b/homeassistant/components/mqtt/device_trigger.py
@@ -0,0 +1,273 @@
+"""Provides device automations for MQTT."""
+import logging
+from typing import List
+
+import attr
+import voluptuous as vol
+
+from homeassistant.components import mqtt
+from homeassistant.components.automation import AutomationActionType
+import homeassistant.components.automation.mqtt as automation_mqtt
+from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from . import (
+ ATTR_DISCOVERY_HASH,
+ CONF_CONNECTIONS,
+ CONF_DEVICE,
+ CONF_IDENTIFIERS,
+ CONF_PAYLOAD,
+ CONF_QOS,
+ DOMAIN,
+)
+from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_AUTOMATION_TYPE = "automation_type"
+CONF_DISCOVERY_ID = "discovery_id"
+CONF_SUBTYPE = "subtype"
+CONF_TOPIC = "topic"
+DEFAULT_ENCODING = "utf-8"
+DEVICE = "device"
+
+MQTT_TRIGGER_BASE = {
+ # Trigger when MQTT message is received
+ CONF_PLATFORM: DEVICE,
+ CONF_DOMAIN: DOMAIN,
+}
+
+TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_PLATFORM): DEVICE,
+ vol.Required(CONF_DOMAIN): DOMAIN,
+ vol.Required(CONF_DEVICE_ID): str,
+ vol.Required(CONF_DISCOVERY_ID): str,
+ vol.Required(CONF_TYPE): cv.string,
+ vol.Required(CONF_SUBTYPE): cv.string,
+ }
+)
+
+TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_AUTOMATION_TYPE): str,
+ vol.Required(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
+ vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string),
+ vol.Required(CONF_TYPE): cv.string,
+ vol.Required(CONF_SUBTYPE): cv.string,
+ },
+ mqtt.validate_device_has_at_least_one_identifier,
+)
+
+DEVICE_TRIGGERS = "mqtt_device_triggers"
+
+
+@attr.s(slots=True)
+class TriggerInstance:
+ """Attached trigger settings."""
+
+ action = attr.ib(type=AutomationActionType)
+ automation_info = attr.ib(type=dict)
+ trigger = attr.ib(type="Trigger")
+ remove = attr.ib(type=CALLBACK_TYPE, default=None)
+
+ async def async_attach_trigger(self):
+ """Attach MQTT trigger."""
+ mqtt_config = {
+ automation_mqtt.CONF_TOPIC: self.trigger.topic,
+ automation_mqtt.CONF_ENCODING: DEFAULT_ENCODING,
+ automation_mqtt.CONF_QOS: self.trigger.qos,
+ }
+ if self.trigger.payload:
+ mqtt_config[CONF_PAYLOAD] = self.trigger.payload
+
+ if self.remove:
+ self.remove()
+ self.remove = await automation_mqtt.async_attach_trigger(
+ self.trigger.hass, mqtt_config, self.action, self.automation_info,
+ )
+
+
+@attr.s(slots=True)
+class Trigger:
+ """Device trigger settings."""
+
+ device_id = attr.ib(type=str)
+ hass = attr.ib(type=HomeAssistantType)
+ payload = attr.ib(type=str)
+ qos = attr.ib(type=int)
+ subtype = attr.ib(type=str)
+ topic = attr.ib(type=str)
+ type = attr.ib(type=str)
+ trigger_instances = attr.ib(type=[TriggerInstance], default=attr.Factory(list))
+
+ async def add_trigger(self, action, automation_info):
+ """Add MQTT trigger."""
+ instance = TriggerInstance(action, automation_info, self)
+ self.trigger_instances.append(instance)
+
+ if self.topic is not None:
+ # If we know about the trigger, subscribe to MQTT topic
+ await instance.async_attach_trigger()
+
+ @callback
+ def async_remove() -> None:
+ """Remove trigger."""
+ if instance not in self.trigger_instances:
+ raise HomeAssistantError("Can't remove trigger twice")
+
+ if instance.remove:
+ instance.remove()
+ self.trigger_instances.remove(instance)
+
+ return async_remove
+
+ async def update_trigger(self, config):
+ """Update MQTT device trigger."""
+ self.type = config[CONF_TYPE]
+ self.subtype = config[CONF_SUBTYPE]
+ self.topic = config[CONF_TOPIC]
+ self.payload = config[CONF_PAYLOAD]
+ self.qos = config[CONF_QOS]
+
+ # Unsubscribe+subscribe if this trigger is in use
+ for trig in self.trigger_instances:
+ await trig.async_attach_trigger()
+
+ def detach_trigger(self):
+ """Remove MQTT device trigger."""
+ # Mark trigger as unknown
+
+ self.topic = None
+ # Unsubscribe if this trigger is in use
+ for trig in self.trigger_instances:
+ if trig.remove:
+ trig.remove()
+ trig.remove = None
+
+
+async def _update_device(hass, config_entry, config):
+ """Update device registry."""
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ config_entry_id = config_entry.entry_id
+ device_info = mqtt.device_info_from_config(config[CONF_DEVICE])
+
+ if config_entry_id is not None and device_info is not None:
+ device_info["config_entry_id"] = config_entry_id
+ device_registry.async_get_or_create(**device_info)
+
+
+async def async_setup_trigger(hass, config, config_entry, discovery_hash):
+ """Set up the MQTT device trigger."""
+ config = TRIGGER_DISCOVERY_SCHEMA(config)
+ discovery_id = discovery_hash[1]
+ remove_signal = None
+
+ async def discovery_update(payload):
+ """Handle discovery update."""
+ _LOGGER.info(
+ "Got update for trigger with hash: %s '%s'", discovery_hash, payload
+ )
+ if not payload:
+ # Empty payload: Remove trigger
+ _LOGGER.info("Removing trigger: %s", discovery_hash)
+ if discovery_id in hass.data[DEVICE_TRIGGERS]:
+ device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id]
+ device_trigger.detach_trigger()
+ clear_discovery_hash(hass, discovery_hash)
+ remove_signal()
+ else:
+ # Non-empty payload: Update trigger
+ _LOGGER.info("Updating trigger: %s", discovery_hash)
+ payload.pop(ATTR_DISCOVERY_HASH)
+ config = TRIGGER_DISCOVERY_SCHEMA(payload)
+ await _update_device(hass, config_entry, config)
+ device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id]
+ await device_trigger.update_trigger(config)
+
+ remove_signal = async_dispatcher_connect(
+ hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_update
+ )
+
+ await _update_device(hass, config_entry, config)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_device(
+ {(DOMAIN, id_) for id_ in config[CONF_DEVICE][CONF_IDENTIFIERS]},
+ {tuple(x) for x in config[CONF_DEVICE][CONF_CONNECTIONS]},
+ )
+
+ if device is None:
+ return
+
+ if DEVICE_TRIGGERS not in hass.data:
+ hass.data[DEVICE_TRIGGERS] = {}
+ if discovery_id not in hass.data[DEVICE_TRIGGERS]:
+ hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
+ hass=hass,
+ device_id=device.id,
+ type=config[CONF_TYPE],
+ subtype=config[CONF_SUBTYPE],
+ topic=config[CONF_TOPIC],
+ payload=config[CONF_PAYLOAD],
+ qos=config[CONF_QOS],
+ )
+ else:
+ await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger(config)
+
+
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+ """List device triggers for MQTT devices."""
+ triggers = []
+
+ if DEVICE_TRIGGERS not in hass.data:
+ return triggers
+
+ for discovery_id, trig in hass.data[DEVICE_TRIGGERS].items():
+ if trig.device_id != device_id or trig.topic is None:
+ continue
+
+ trigger = {
+ **MQTT_TRIGGER_BASE,
+ "device_id": device_id,
+ "type": trig.type,
+ "subtype": trig.subtype,
+ "discovery_id": discovery_id,
+ }
+ triggers.append(trigger)
+
+ return triggers
+
+
+async def async_attach_trigger(
+ hass: HomeAssistant,
+ config: ConfigType,
+ action: AutomationActionType,
+ automation_info: dict,
+) -> CALLBACK_TYPE:
+ """Attach a trigger."""
+ if DEVICE_TRIGGERS not in hass.data:
+ hass.data[DEVICE_TRIGGERS] = {}
+ config = TRIGGER_SCHEMA(config)
+ device_id = config[CONF_DEVICE_ID]
+ discovery_id = config[CONF_DISCOVERY_ID]
+
+ if discovery_id not in hass.data[DEVICE_TRIGGERS]:
+ hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
+ hass=hass,
+ device_id=device_id,
+ type=config[CONF_TYPE],
+ subtype=config[CONF_SUBTYPE],
+ topic=None,
+ payload=None,
+ qos=None,
+ )
+ return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger(
+ action, automation_info
+ )
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index f393c315793..418f648564d 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -26,6 +26,7 @@ SUPPORTED_COMPONENTS = [
"camera",
"climate",
"cover",
+ "device_automation",
"fan",
"light",
"lock",
@@ -40,6 +41,7 @@ CONFIG_ENTRY_COMPONENTS = [
"camera",
"climate",
"cover",
+ "device_automation",
"fan",
"light",
"lock",
@@ -197,9 +199,15 @@ async def async_start(
config_entries_key = "{}.{}".format(component, "mqtt")
async with hass.data[DATA_CONFIG_ENTRY_LOCK]:
if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]:
- await hass.config_entries.async_forward_entry_setup(
- config_entry, component
- )
+ if component == "device_automation":
+ # Local import to avoid circular dependencies
+ from . import device_automation
+
+ await device_automation.async_setup_entry(hass, config_entry)
+ else:
+ await hass.config_entries.async_forward_entry_setup(
+ config_entry, component
+ )
hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)
async_dispatcher_send(
diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json
index 8bacfa530bd..f0a38bcbc55 100644
--- a/homeassistant/components/mqtt/strings.json
+++ b/homeassistant/components/mqtt/strings.json
@@ -27,5 +27,27 @@
"error": {
"cannot_connect": "Unable to connect to the broker."
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "button_short_press": "\"{subtype}\" pressed",
+ "button_short_release": "\"{subtype}\" released",
+ "button_long_press": "\"{subtype}\" continuously pressed",
+ "button_long_release": "\"{subtype}\" released after long press",
+ "button_double_press": "\"{subtype}\" double clicked",
+ "button_triple_press": "\"{subtype}\" triple clicked",
+ "button_quadruple_press": "\"{subtype}\" quadruple clicked",
+ "button_quintuple_press": "\"{subtype}\" quintuple clicked"
+ },
+ "trigger_subtype": {
+ "turn_on": "Turn on",
+ "turn_off": "Turn off",
+ "button_1": "First button",
+ "button_2": "Second button",
+ "button_3": "Third button",
+ "button_4": "Fourth button",
+ "button_5": "Fifth button",
+ "button_6": "Sixth button"
+ }
}
}
diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py
index 19eb8e9e92c..45da4a77d5f 100644
--- a/homeassistant/components/mysensors/light.py
+++ b/homeassistant/components/mysensors/light.py
@@ -11,6 +11,7 @@ from homeassistant.components.light import (
Light,
)
from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.core import callback
import homeassistant.util.color as color_util
from homeassistant.util.color import rgb_hex_to_rgb_list
@@ -150,11 +151,13 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light):
self._values[value_type] = STATE_OFF
self.async_schedule_update_ha_state()
+ @callback
def _async_update_light(self):
"""Update the controller with values from light child."""
value_type = self.gateway.const.SetReq.V_LIGHT
self._state = self._values[value_type] == STATE_ON
+ @callback
def _async_update_dimmer(self):
"""Update the controller with values from dimmer child."""
value_type = self.gateway.const.SetReq.V_DIMMER
@@ -163,6 +166,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light):
if self._brightness == 0:
self._state = False
+ @callback
def _async_update_rgb_or_w(self):
"""Update the controller with values from RGB or RGBW child."""
value = self._values[self.value_type]
diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py
index 3a045e0391d..ca766810a3d 100644
--- a/homeassistant/components/mystrom/switch.py
+++ b/homeassistant/components/mystrom/switch.py
@@ -5,6 +5,7 @@ import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
DEFAULT_NAME = "myStrom Switch"
@@ -30,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
MyStromPlug(host).get_status()
except exceptions.MyStromConnectionError:
_LOGGER.error("No route to device: %s", host)
- return
+ raise PlatformNotReady()
add_entities([MyStromSwitch(name, host)])
@@ -46,7 +47,7 @@ class MyStromSwitch(SwitchDevice):
self._resource = resource
self.data = {}
self.plug = MyStromPlug(self._resource)
- self.update()
+ self._available = True
@property
def name(self):
@@ -63,6 +64,11 @@ class MyStromSwitch(SwitchDevice):
"""Return the current power consumption in W."""
return round(self.data["power"], 2)
+ @property
+ def available(self):
+ """Could the device be accessed during the last update call."""
+ return self._available
+
def turn_on(self, **kwargs):
"""Turn the switch on."""
from pymystrom import exceptions
@@ -87,6 +93,8 @@ class MyStromSwitch(SwitchDevice):
try:
self.data = self.plug.get_status()
+ self._available = True
except exceptions.MyStromConnectionError:
self.data = {"power": 0, "relay": False}
+ self._available = False
_LOGGER.error("No route to device: %s", self._resource)
diff --git a/homeassistant/components/neato/.translations/hu.json b/homeassistant/components/neato/.translations/hu.json
new file mode 100644
index 00000000000..50fa4b5866f
--- /dev/null
+++ b/homeassistant/components/neato/.translations/hu.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "M\u00e1r konfigur\u00e1lva van",
+ "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok"
+ },
+ "create_entry": {
+ "default": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} )."
+ },
+ "error": {
+ "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok",
+ "unexpected_error": "V\u00e1ratlan hiba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v",
+ "vendor": "Sz\u00e1ll\u00edt\u00f3"
+ },
+ "description": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} ).",
+ "title": "Neato Fi\u00f3kinform\u00e1ci\u00f3"
+ }
+ },
+ "title": "Neato"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/.translations/pl.json b/homeassistant/components/neato/.translations/pl.json
index caea115b7d5..e6b55b12c53 100644
--- a/homeassistant/components/neato/.translations/pl.json
+++ b/homeassistant/components/neato/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "already_configured": "Konto jest ju\u017c skonfigurowane.",
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce"
},
"create_entry": {
diff --git a/homeassistant/components/neato/.translations/sv.json b/homeassistant/components/neato/.translations/sv.json
new file mode 100644
index 00000000000..64edf9e93ce
--- /dev/null
+++ b/homeassistant/components/neato/.translations/sv.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Redan konfigurerad",
+ "invalid_credentials": "Ogiltiga autentiseringsuppgifter"
+ },
+ "create_entry": {
+ "default": "Se [Neato-dokumentation]({docs_url})."
+ },
+ "error": {
+ "invalid_credentials": "Ogiltiga autentiseringsuppgifter",
+ "unexpected_error": "Ov\u00e4ntat fel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "L\u00f6senord",
+ "username": "Anv\u00e4ndarnamn",
+ "vendor": "Leverant\u00f6r"
+ },
+ "description": "Se [Neato-dokumentation] ({docs_url}).",
+ "title": "Neato-kontoinfo"
+ }
+ },
+ "title": "Neato"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json
index 8718843d73d..c6025abe0b5 100644
--- a/homeassistant/components/nederlandse_spoorwegen/manifest.json
+++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json
@@ -2,7 +2,7 @@
"domain": "nederlandse_spoorwegen",
"name": "Nederlandse Spoorwegen (NS)",
"documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen",
- "requirements": ["nsapi==3.0.2"],
+ "requirements": ["nsapi==3.0.3"],
"dependencies": [],
"codeowners": ["@YarmoM"]
}
diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py
index df37fad2aa3..45413d4a15a 100644
--- a/homeassistant/components/nederlandse_spoorwegen/sensor.py
+++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py
@@ -20,6 +20,7 @@ CONF_ROUTES = "routes"
CONF_FROM = "from"
CONF_TO = "to"
CONF_VIA = "via"
+CONF_TIME = "time"
ICON = "mdi:train"
@@ -31,6 +32,7 @@ ROUTE_SCHEMA = vol.Schema(
vol.Required(CONF_FROM): cv.string,
vol.Required(CONF_TO): cv.string,
vol.Optional(CONF_VIA): cv.string,
+ vol.Optional(CONF_TIME): cv.time,
}
)
@@ -68,6 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
departure.get(CONF_FROM),
departure.get(CONF_TO),
departure.get(CONF_VIA),
+ departure.get(CONF_TIME),
)
)
if sensors:
@@ -88,13 +91,14 @@ def valid_stations(stations, given_stations):
class NSDepartureSensor(Entity):
"""Implementation of a NS Departure Sensor."""
- def __init__(self, nsapi, name, departure, heading, via):
+ def __init__(self, nsapi, name, departure, heading, via, time):
"""Initialize the sensor."""
self._nsapi = nsapi
self._name = name
self._departure = departure
self._via = via
self._heading = heading
+ self._time = time
self._state = None
self._trips = None
@@ -138,6 +142,7 @@ class NSDepartureSensor(Entity):
"arrival_platform_planned": self._trips[0].arrival_platform_planned,
"arrival_platform_actual": self._trips[0].arrival_platform_actual,
"next": None,
+ "punctuality": None,
"status": self._trips[0].status.lower(),
"transfers": self._trips[0].nr_transfers,
"route": route,
@@ -186,26 +191,49 @@ class NSDepartureSensor(Entity):
):
attributes["arrival_delay"] = True
+ # Punctuality attributes
+ if self._trips[0].punctuality is not None:
+ attributes["punctuality"] = self._trips[0].punctuality
+
# Next attributes
- if self._trips[1].departure_time_actual is not None:
- attributes["next"] = self._trips[1].departure_time_actual.strftime("%H:%M")
- elif self._trips[1].departure_time_planned is not None:
- attributes["next"] = self._trips[1].departure_time_planned.strftime("%H:%M")
+ if len(self._trips) > 1:
+ if self._trips[1].departure_time_actual is not None:
+ attributes["next"] = self._trips[1].departure_time_actual.strftime(
+ "%H:%M"
+ )
+ elif self._trips[1].departure_time_planned is not None:
+ attributes["next"] = self._trips[1].departure_time_planned.strftime(
+ "%H:%M"
+ )
return attributes
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the trip information."""
+
+ # If looking for a specific trip time, update around that trip time only.
+ if self._time and (
+ (datetime.now() + timedelta(minutes=30)).time() < self._time
+ or (datetime.now() - timedelta(minutes=30)).time() > self._time
+ ):
+ self._state = None
+ self._trips = None
+ return
+
+ # Set the search parameter to search from a specific trip time or to just search for next trip.
+ if self._time:
+ trip_time = (
+ datetime.today()
+ .replace(hour=self._time.hour, minute=self._time.minute)
+ .strftime("%d-%m-%Y %H:%M")
+ )
+ else:
+ trip_time = datetime.now().strftime("%d-%m-%Y %H:%M")
+
try:
self._trips = self._nsapi.get_trips(
- datetime.now().strftime("%d-%m-%Y %H:%M"),
- self._departure,
- self._via,
- self._heading,
- True,
- 0,
- 2,
+ trip_time, self._departure, self._via, self._heading, True, 0, 2
)
if self._trips:
if self._trips[0].departure_time_actual is None:
diff --git a/homeassistant/components/nest/.translations/zh-Hans.json b/homeassistant/components/nest/.translations/zh-Hans.json
index 0b5cbc989fd..0825fdfdc79 100644
--- a/homeassistant/components/nest/.translations/zh-Hans.json
+++ b/homeassistant/components/nest/.translations/zh-Hans.json
@@ -8,14 +8,14 @@
},
"error": {
"internal_error": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u5185\u90e8\u9519\u8bef",
- "invalid_code": "\u65e0\u6548\u4ee3\u7801",
- "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6",
- "unknown": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef"
+ "invalid_code": "\u9a8c\u8bc1\u7801\u65e0\u6548",
+ "timeout": "\u9a8c\u8bc1\u7801\u8d85\u65f6",
+ "unknown": "\u9a8c\u8bc1\u7801\u672a\u77e5\u9519\u8bef"
},
"step": {
"init": {
"data": {
- "flow_impl": "\u63d0\u4f9b\u8005"
+ "flow_impl": "\u8ba4\u8bc1\u63d0\u4f9b\u8005"
},
"description": "\u9009\u62e9\u60a8\u60f3\u901a\u8fc7\u54ea\u4e2a\u6388\u6743\u63d0\u4f9b\u8005\u4e0e Nest \u8fdb\u884c\u6388\u6743\u3002",
"title": "\u6388\u6743\u63d0\u4f9b\u8005"
diff --git a/homeassistant/components/netatmo/.translations/hu.json b/homeassistant/components/netatmo/.translations/hu.json
new file mode 100644
index 00000000000..9994e527f01
--- /dev/null
+++ b/homeassistant/components/netatmo/.translations/hu.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Csak egy Netatmo-fi\u00f3kot \u00e1ll\u00edthatsz be.",
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n."
+ },
+ "create_entry": {
+ "default": "A Netatmo sikeresen hiteles\u00edtett."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
+ }
+ },
+ "title": "Netatmo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/.translations/nl.json b/homeassistant/components/netatmo/.translations/nl.json
index d9062850f2a..5f5fe375117 100644
--- a/homeassistant/components/netatmo/.translations/nl.json
+++ b/homeassistant/components/netatmo/.translations/nl.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_setup": "U kunt slechts \u00e9\u00e9n Netatmo account configureren.",
"authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
"missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen."
},
diff --git a/homeassistant/components/netatmo/.translations/sl.json b/homeassistant/components/netatmo/.translations/sl.json
new file mode 100644
index 00000000000..5288c84e44b
--- /dev/null
+++ b/homeassistant/components/netatmo/.translations/sl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Konfigurirate lahko samo en ra\u010dun Netatmo.",
+ "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.",
+ "missing_configuration": "Komponenta Netatmo ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo."
+ },
+ "create_entry": {
+ "default": "Uspe\u0161no overjeno z Netatmo."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Izberite na\u010din preverjanja pristnosti"
+ }
+ },
+ "title": "Netatmo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json
index 14ec2e61b9c..6fe084cc885 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==3.2.2"
+ "pyatmo==3.2.4"
],
"dependencies": [
"webhook"
diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py
index afdb7c053f3..818662ee69c 100644
--- a/homeassistant/components/netatmo/sensor.py
+++ b/homeassistant/components/netatmo/sensor.py
@@ -1,7 +1,6 @@
"""Support for the Netatmo Weather Service."""
from datetime import timedelta
import logging
-from time import time
import pyatmo
@@ -519,7 +518,6 @@ class NetatmoData:
"""Initialize the data object."""
self.data = {}
self.station_data = station_data
- self._next_update = time()
self.auth = auth
def get_module_infos(self):
diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py
index 23b1034a5b3..5f18553ba62 100644
--- a/homeassistant/components/netgear/device_tracker.py
+++ b/homeassistant/components/netgear/device_tracker.py
@@ -1,5 +1,6 @@
"""Support for Netgear routers."""
import logging
+from pprint import pformat
from pynetgear import Netgear
import voluptuous as vol
@@ -30,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Optional(CONF_USERNAME, default=""): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_PORT, default=None): vol.Any(None, cv.port),
+ vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_APS, default=[]): vol.All(cv.ensure_list, [cv.string]),
@@ -41,55 +42,43 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_scanner(hass, config):
"""Validate the configuration and returns a Netgear scanner."""
info = config[DOMAIN]
- host = info.get(CONF_HOST)
- ssl = info.get(CONF_SSL)
- username = info.get(CONF_USERNAME)
- password = info.get(CONF_PASSWORD)
+ host = info[CONF_HOST]
+ ssl = info[CONF_SSL]
+ username = info[CONF_USERNAME]
+ password = info[CONF_PASSWORD]
port = info.get(CONF_PORT)
- devices = info.get(CONF_DEVICES)
- excluded_devices = info.get(CONF_EXCLUDE)
- accesspoints = info.get(CONF_APS)
+ devices = info[CONF_DEVICES]
+ excluded_devices = info[CONF_EXCLUDE]
+ accesspoints = info[CONF_APS]
- scanner = NetgearDeviceScanner(
- host, ssl, username, password, port, devices, excluded_devices, accesspoints
- )
+ api = Netgear(password, host, username, port, ssl)
+ scanner = NetgearDeviceScanner(api, devices, excluded_devices, accesspoints)
- return scanner if scanner.success_init else None
+ _LOGGER.debug("Logging in")
+
+ results = scanner.get_attached_devices()
+
+ if results is not None:
+ scanner.last_results = results
+ else:
+ _LOGGER.error("Failed to Login")
+ return None
+
+ return scanner
class NetgearDeviceScanner(DeviceScanner):
"""Queries a Netgear wireless router using the SOAP-API."""
def __init__(
- self,
- host,
- ssl,
- username,
- password,
- port,
- devices,
- excluded_devices,
- accesspoints,
+ self, api, devices, excluded_devices, accesspoints,
):
"""Initialize the scanner."""
-
self.tracked_devices = devices
self.excluded_devices = excluded_devices
self.tracked_accesspoints = accesspoints
-
self.last_results = []
- self._api = Netgear(password, host, username, port, ssl)
-
- _LOGGER.info("Logging in")
-
- results = self.get_attached_devices()
-
- self.success_init = results is not None
-
- if self.success_init:
- self.last_results = results
- else:
- _LOGGER.error("Failed to Login")
+ self._api = api
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
@@ -110,10 +99,7 @@ class NetgearDeviceScanner(DeviceScanner):
or dev.name in self.excluded_devices
)
)
-
- # when link_rate is None this means the router still knows about
- # the device, but it is not in range.
- if tracked and dev.link_rate is not None:
+ if tracked:
devices.append(dev.mac)
if (
self.tracked_accesspoints
@@ -156,21 +142,20 @@ class NetgearDeviceScanner(DeviceScanner):
Returns boolean if scanning successful.
"""
- if not self.success_init:
- return
-
- _LOGGER.info("Scanning")
+ _LOGGER.debug("Scanning")
results = self.get_attached_devices()
+ if _LOGGER.isEnabledFor(logging.DEBUG):
+ _LOGGER.debug("Scan result: \n%s", pformat(results))
+
if results is None:
_LOGGER.warning("Error scanning devices")
self.last_results = results or []
def get_attached_devices(self):
- """
- List attached devices with pynetgear.
+ """List attached devices with pynetgear.
The v2 method takes more time and is more heavy on the router
so we only use it if we need connected AP info.
diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json
index 4cfffab7d73..c5685411045 100644
--- a/homeassistant/components/netgear/manifest.json
+++ b/homeassistant/components/netgear/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "netgear",
- "name": "Netgear",
+ "name": "NETGEAR",
"documentation": "https://www.home-assistant.io/integrations/netgear",
"requirements": ["pynetgear==0.6.1"],
"dependencies": [],
diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json
index 0be2ca146b1..43cf6e34480 100644
--- a/homeassistant/components/netgear_lte/manifest.json
+++ b/homeassistant/components/netgear_lte/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "netgear_lte",
- "name": "Netgear LTE",
+ "name": "NETGEAR LTE",
"documentation": "https://www.home-assistant.io/integrations/netgear_lte",
"requirements": ["eternalegypt==0.0.11"],
"dependencies": [],
diff --git a/homeassistant/components/netgear_lte/sensor_types.py b/homeassistant/components/netgear_lte/sensor_types.py
index e1a9d1a23d2..a744937dacd 100644
--- a/homeassistant/components/netgear_lte/sensor_types.py
+++ b/homeassistant/components/netgear_lte/sensor_types.py
@@ -1,6 +1,7 @@
"""Define possible sensor types."""
from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY
+from homeassistant.const import DATA_MEBIBYTES
SENSOR_SMS = "sms"
SENSOR_SMS_TOTAL = "sms_total"
@@ -9,7 +10,7 @@ SENSOR_USAGE = "usage"
SENSOR_UNITS = {
SENSOR_SMS: "unread",
SENSOR_SMS_TOTAL: "messages",
- SENSOR_USAGE: "MiB",
+ SENSOR_USAGE: DATA_MEBIBYTES,
"radio_quality": "%",
"rx_level": "dBm",
"tx_level": "dBm",
diff --git a/homeassistant/components/notion/.translations/ca.json b/homeassistant/components/notion/.translations/ca.json
index 0b6a24626be..09f598ef5d1 100644
--- a/homeassistant/components/notion/.translations/ca.json
+++ b/homeassistant/components/notion/.translations/ca.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Aquest nom d'usuari ja est\u00e0 en \u00fas."
+ },
"error": {
"identifier_exists": "Nom d'usuari ja registrat",
"invalid_credentials": "Nom d'usuari o contrasenya incorrectes",
diff --git a/homeassistant/components/notion/.translations/de.json b/homeassistant/components/notion/.translations/de.json
index e9c735001e9..e11a16458c9 100644
--- a/homeassistant/components/notion/.translations/de.json
+++ b/homeassistant/components/notion/.translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Dieser Benutzername wird bereits benutzt."
+ },
"error": {
"identifier_exists": "Benutzername bereits registriert",
"invalid_credentials": "Ung\u00fcltiger Benutzername oder Passwort",
diff --git a/homeassistant/components/notion/.translations/en.json b/homeassistant/components/notion/.translations/en.json
index b05f613a73f..2476293a216 100644
--- a/homeassistant/components/notion/.translations/en.json
+++ b/homeassistant/components/notion/.translations/en.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "This username is already in use."
+ },
"error": {
"identifier_exists": "Username already registered",
"invalid_credentials": "Invalid username or password",
diff --git a/homeassistant/components/notion/.translations/ko.json b/homeassistant/components/notion/.translations/ko.json
index 76dc91cf46b..52c7b6339cb 100644
--- a/homeassistant/components/notion/.translations/ko.json
+++ b/homeassistant/components/notion/.translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uc774 \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4."
+ },
"error": {
"identifier_exists": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
diff --git a/homeassistant/components/notion/.translations/no.json b/homeassistant/components/notion/.translations/no.json
index 2798db1cbc3..16105e680c5 100644
--- a/homeassistant/components/notion/.translations/no.json
+++ b/homeassistant/components/notion/.translations/no.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Dette brukernavnet er allerede i bruk."
+ },
"error": {
"identifier_exists": "Brukernavn er allerede registrert",
"invalid_credentials": "Ugyldig brukernavn eller passord",
diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json
index 380d4ad151e..07facb21e93 100644
--- a/homeassistant/components/notion/.translations/pl.json
+++ b/homeassistant/components/notion/.translations/pl.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ta nazwa u\u017cytkownika jest ju\u017c w u\u017cyciu."
+ },
"error": {
- "identifier_exists": "Nazwa u\u017cytkownika ju\u017c zarejestrowana",
+ "identifier_exists": "Nazwa u\u017cytkownika jest ju\u017c zarejestrowana.",
"invalid_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o",
"no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie"
},
@@ -9,11 +12,11 @@
"user": {
"data": {
"password": "Has\u0142o",
- "username": "Nazwa u\u017cytkownika / adres e-mail"
+ "username": "Nazwa u\u017cytkownika/adres e-mail"
},
"title": "Wprowad\u017a dane"
}
},
- "title": "Poj\u0119cie"
+ "title": "Notion"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/notion/.translations/zh-Hant.json b/homeassistant/components/notion/.translations/zh-Hant.json
index f672f519f40..c426dfa3265 100644
--- a/homeassistant/components/notion/.translations/zh-Hant.json
+++ b/homeassistant/components/notion/.translations/zh-Hant.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u6b64\u4f7f\u7528\u8005\u540d\u7a31\u5df2\u88ab\u4f7f\u7528\u3002"
+ },
"error": {
"identifier_exists": "\u4f7f\u7528\u8005\u540d\u7a31\u5df2\u8a3b\u518a",
"invalid_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u7121\u6548",
diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py
index 2f81cb72ac0..6ce5c4e5bc7 100644
--- a/homeassistant/components/notion/const.py
+++ b/homeassistant/components/notion/const.py
@@ -7,7 +7,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
DATA_CLIENT = "client"
-TOPIC_DATA_UPDATE = "data_update"
+TOPIC_DATA_UPDATE = f"{DOMAIN}_data_update"
TYPE_BINARY_SENSOR = "binary_sensor"
TYPE_SENSOR = "sensor"
diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py
index a04d2bd69b2..f0d8c901387 100644
--- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py
+++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py
@@ -18,13 +18,13 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import callback
-from homeassistant.helpers import ConfigType, aiohttp_client, config_validation as cv
+from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.event import async_track_time_interval
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json
index b2dcfe10cf6..1c2aa268ca2 100644
--- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json
+++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json
@@ -2,7 +2,7 @@
"domain": "nsw_rural_fire_service_feed",
"name": "NSW Rural Fire Service Incidents",
"documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed",
- "requirements": ["aio_geojson_nsw_rfs_incidents==0.1"],
+ "requirements": ["aio_geojson_nsw_rfs_incidents==0.3"],
"dependencies": [],
"codeowners": ["@exxamalte"]
}
diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py
index 3556c88a6da..24ef18ab985 100644
--- a/homeassistant/components/nzbget/sensor.py
+++ b/homeassistant/components/nzbget/sensor.py
@@ -1,6 +1,7 @@
"""Monitor the NZBGet API."""
import logging
+from homeassistant.const import DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
@@ -12,15 +13,19 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "NZBGet"
SENSOR_TYPES = {
- "article_cache": ["ArticleCacheMB", "Article Cache", "MB"],
- "average_download_rate": ["AverageDownloadRate", "Average Speed", "MB/s"],
+ "article_cache": ["ArticleCacheMB", "Article Cache", DATA_MEGABYTES],
+ "average_download_rate": [
+ "AverageDownloadRate",
+ "Average Speed",
+ DATA_RATE_MEGABYTES_PER_SECOND,
+ ],
"download_paused": ["DownloadPaused", "Download Paused", None],
- "download_rate": ["DownloadRate", "Speed", "MB/s"],
- "download_size": ["DownloadedSizeMB", "Size", "MB"],
- "free_disk_space": ["FreeDiskSpaceMB", "Disk Free", "MB"],
+ "download_rate": ["DownloadRate", "Speed", DATA_RATE_MEGABYTES_PER_SECOND],
+ "download_size": ["DownloadedSizeMB", "Size", DATA_MEGABYTES],
+ "free_disk_space": ["FreeDiskSpaceMB", "Disk Free", DATA_MEGABYTES],
"post_job_count": ["PostJobCount", "Post Processing Jobs", "Jobs"],
"post_paused": ["PostPaused", "Post Processing Paused", None],
- "remaining_size": ["RemainingSizeMB", "Queue Size", "MB"],
+ "remaining_size": ["RemainingSizeMB", "Queue Size", DATA_MEGABYTES],
"uptime": ["UpTimeSec", "Uptime", "min"],
}
diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml
index 84e4af15b5d..88a6267860e 100644
--- a/homeassistant/components/nzbget/services.yaml
+++ b/homeassistant/components/nzbget/services.yaml
@@ -10,5 +10,5 @@ set_speed:
description: Set download speed limit
fields:
speed:
- description: Speed limit in KB/s. 0 is unlimited.
- example: 1000
\ No newline at end of file
+ description: Speed limit in kB/s. 0 is unlimited.
+ example: 1000
diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json
index d63a543f227..98e7c320a60 100644
--- a/homeassistant/components/octoprint/manifest.json
+++ b/homeassistant/components/octoprint/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/octoprint",
"requirements": [],
"dependencies": [],
+ "after_dependencies": ["discovery"],
"codeowners": []
}
diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py
index d21aac9ff65..98d878fc2ea 100644
--- a/homeassistant/components/octoprint/sensor.py
+++ b/homeassistant/components/octoprint/sensor.py
@@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"If you do not want to have your printer on
"
" at all times, and you would like to monitor
"
"temperatures, please add
"
- "bed and/or number_of_tools to your config
"
+ "bed and/or number_of_tools to your configuration
"
"and restart.",
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py
index 936bf9f751b..6a7f282ac87 100644
--- a/homeassistant/components/onewire/sensor.py
+++ b/homeassistant/components/onewire/sensor.py
@@ -19,6 +19,7 @@ CONF_NAMES = "names"
DEFAULT_MOUNT_DIR = "/sys/bus/w1/devices/"
DEVICE_SENSORS = {
+ # Family : { SensorType: owfs path }
"10": {"temperature": "temperature"},
"12": {"temperature": "TAI8570/temperature", "pressure": "TAI8570/pressure"},
"22": {"temperature": "temperature"},
@@ -27,6 +28,9 @@ DEVICE_SENSORS = {
"humidity": "humidity",
"pressure": "B1-R1-A/pressure",
"illuminance": "S3-R1-A/illuminance",
+ "voltage_VAD": "VAD",
+ "voltage_VDD": "VDD",
+ "current": "IAD",
},
"28": {"temperature": "temperature"},
"3B": {"temperature": "temperature"},
@@ -54,6 +58,7 @@ HOBBYBOARD_EF = {
}
SENSOR_TYPES = {
+ # SensorType: [ Measured unit, Unit ]
"temperature": ["temperature", TEMP_CELSIUS],
"humidity": ["humidity", "%"],
"humidity_raw": ["humidity", "%"],
@@ -70,6 +75,10 @@ SENSOR_TYPES = {
"counter_a": ["counter", "count"],
"counter_b": ["counter", "count"],
"HobbyBoard": ["none", "none"],
+ "voltage": ["voltage", "V"],
+ "voltage_VAD": ["voltage", "V"],
+ "voltage_VDD": ["voltage", "V"],
+ "current": ["current", "A"],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -95,11 +104,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
base_dir = config[CONF_MOUNT_DIR]
owport = config[CONF_PORT]
owhost = config.get(CONF_HOST)
+ if owhost:
+ _LOGGER.debug("Initializing using %s:%s", owhost, owport)
+ else:
+ _LOGGER.debug("Initializing using %s", base_dir)
+
devs = []
device_names = {}
- if "names" in config:
- if isinstance(config["names"], dict):
- device_names = config["names"]
+ if CONF_NAMES in config:
+ if isinstance(config[CONF_NAMES], dict):
+ device_names = config[CONF_NAMES]
# We have an owserver on a remote(or local) host/port
if owhost:
@@ -112,7 +126,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
devices = []
for device in devices:
- _LOGGER.debug("found device: %s", device)
+ _LOGGER.debug("Found device: %s", device)
family = owproxy.read(f"{device}family").decode()
dev_type = "std"
if "EF" in family:
@@ -200,6 +214,7 @@ class OneWire(Entity):
self._device_file = device_file
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self._state = None
+ self._value_raw = None
def _read_value_raw(self):
"""Read the value as it is returned by the sensor."""
@@ -224,6 +239,16 @@ class OneWire(Entity):
"""Return the unit the value is expressed in."""
return self._unit_of_measurement
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the sensor."""
+ return {"device_file": self._device_file, "raw_value": self._value_raw}
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ return self._device_file
+
class OneWireProxy(OneWire):
"""Implementation of a One wire Sensor through owserver."""
@@ -249,6 +274,7 @@ class OneWireProxy(OneWire):
_LOGGER.error("Owserver failure in read(), got: %s", exc)
if value_read:
value = round(float(value_read), 1)
+ self._value_raw = float(value_read)
self._state = value
@@ -267,6 +293,7 @@ class OneWireDirect(OneWire):
if equals_pos != -1:
value_string = lines[1][equals_pos + 2 :]
value = round(float(value_string) / 1000.0, 1)
+ self._value_raw = float(value_string)
self._state = value
@@ -280,6 +307,7 @@ class OneWireOWFS(OneWire):
value_read = self._read_value_raw()
if len(value_read) == 1:
value = round(float(value_read[0]), 1)
+ self._value_raw = float(value_read[0])
except ValueError:
_LOGGER.warning("Invalid value read from %s", self._device_file)
except FileNotFoundError:
diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json
index 3d13db3ead3..40ab3a8a7ed 100644
--- a/homeassistant/components/opencv/manifest.json
+++ b/homeassistant/components/opencv/manifest.json
@@ -2,7 +2,7 @@
"domain": "opencv",
"name": "OpenCV",
"documentation": "https://www.home-assistant.io/integrations/opencv",
- "requirements": ["numpy==1.17.4", "opencv-python-headless==4.1.2.30"],
+ "requirements": ["numpy==1.18.1", "opencv-python-headless==4.1.2.30"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/opentherm_gw/.translations/hu.json b/homeassistant/components/opentherm_gw/.translations/hu.json
new file mode 100644
index 00000000000..8a0780581fd
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/.translations/hu.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Az \u00e1tj\u00e1r\u00f3 m\u00e1r konfigur\u00e1lva van",
+ "id_exists": "Az \u00e1tj\u00e1r\u00f3 azonos\u00edt\u00f3ja m\u00e1r l\u00e9tezik",
+ "serial_error": "Hiba t\u00f6rt\u00e9nt az eszk\u00f6zh\u00f6z val\u00f3 csatlakoz\u00e1skor",
+ "timeout": "A csatlakoz\u00e1si k\u00eds\u00e9rletre sz\u00e1nt id\u0151 lej\u00e1rt"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "device": "El\u00e9r\u00e9si \u00fat vagy URL",
+ "floor_temperature": "Padl\u00f3 kl\u00edma h\u0151m\u00e9rs\u00e9klete",
+ "id": "ID",
+ "name": "N\u00e9v",
+ "precision": "Kl\u00edma h\u0151m\u00e9rs\u00e9klet pontoss\u00e1ga"
+ },
+ "title": "OpenTherm \u00e1tj\u00e1r\u00f3"
+ }
+ },
+ "title": "OpenTherm \u00e1tj\u00e1r\u00f3"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Padl\u00f3 h\u0151m\u00e9rs\u00e9klete",
+ "precision": "Pontoss\u00e1g"
+ }
+ }
+ }
+ }
+}
\ 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 fe8ccfc8975..88791781e3f 100644
--- a/homeassistant/components/opentherm_gw/.translations/pl.json
+++ b/homeassistant/components/opentherm_gw/.translations/pl.json
@@ -1,8 +1,8 @@
{
"config": {
"error": {
- "already_configured": "Bramka jest ju\u017c skonfigurowana",
- "id_exists": "Identyfikator bramki ju\u017c istnieje",
+ "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": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia."
},
diff --git a/homeassistant/components/opentherm_gw/.translations/sv.json b/homeassistant/components/opentherm_gw/.translations/sv.json
new file mode 100644
index 00000000000..89ce4d75674
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/.translations/sv.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Gateway redan konfigurerad",
+ "id_exists": "Gateway-id finns redan",
+ "serial_error": "Fel vid anslutning till enheten",
+ "timeout": "Anslutningsf\u00f6rs\u00f6ket avbr\u00f6ts"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "device": "S\u00f6kv\u00e4g eller URL",
+ "floor_temperature": "Golvtemperatur",
+ "id": "ID",
+ "name": "Namn",
+ "precision": "Klimatemperaturprecision"
+ },
+ "title": "OpenTherm Gateway"
+ }
+ },
+ "title": "OpenTherm Gateway"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Golvetemperatur",
+ "precision": "Precision"
+ },
+ "description": "Alternativ f\u00f6r OpenTherm Gateway"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/.translations/de.json b/homeassistant/components/openuv/.translations/de.json
index 7f8121dd96b..cc8ee92df4b 100644
--- a/homeassistant/components/openuv/.translations/de.json
+++ b/homeassistant/components/openuv/.translations/de.json
@@ -12,7 +12,7 @@
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad"
},
- "title": "Gebe deine Informationen ein"
+ "title": "Gib deine Informationen ein"
}
},
"title": "OpenUV"
diff --git a/homeassistant/components/openuv/.translations/pl.json b/homeassistant/components/openuv/.translations/pl.json
index ee3875c2903..ff6d1b21055 100644
--- a/homeassistant/components/openuv/.translations/pl.json
+++ b/homeassistant/components/openuv/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "identifier_exists": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane",
+ "identifier_exists": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane.",
"invalid_api_key": "Nieprawid\u0142owy klucz API"
},
"step": {
diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py
index 417903c46e0..07f697a5a46 100644
--- a/homeassistant/components/pandora/media_player.py
+++ b/homeassistant/components/pandora/media_player.py
@@ -119,8 +119,8 @@ class PandoraMediaPlayer(MediaPlayerDevice):
elif mode == 2:
_LOGGER.warning(
"The pianobar client is not configured to log in. "
- "Please create a config file for it as described at "
- "https://home-assistant.io/components/media_player.pandora/"
+ "Please create a configuration file for it as described at "
+ "https://home-assistant.io/integrations/pandora/"
)
# pass through the email/password prompts to quit cleanly
self._pianobar.sendcontrol("m")
diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py
index ed6144af47e..5791d17f6dd 100644
--- a/homeassistant/components/pi_hole/__init__.py
+++ b/homeassistant/components/pi_hole/__init__.py
@@ -91,7 +91,7 @@ async def async_setup(hass, config):
"""Set up the pi_hole integration."""
def get_data():
- """Retrive component data."""
+ """Retrieve component data."""
return hass.data[DOMAIN]
def ensure_api_token(call_data):
diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py
index 0ae62b31865..ca4eea32bd6 100644
--- a/homeassistant/components/pi_hole/const.py
+++ b/homeassistant/components/pi_hole/const.py
@@ -1,4 +1,4 @@
-"""Constants for the pi_hole intergration."""
+"""Constants for the pi_hole integration."""
from datetime import timedelta
DOMAIN = "pi_hole"
diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py
index c4d88f6061c..c0effda7a55 100644
--- a/homeassistant/components/ping/device_tracker.py
+++ b/homeassistant/components/ping/device_tracker.py
@@ -21,7 +21,7 @@ CONF_PING_COUNT = "count"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
- vol.Required(const.CONF_HOSTS): {cv.string: cv.string},
+ vol.Required(const.CONF_HOSTS): {cv.slug: cv.string},
vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int,
}
)
diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py
index 3e71b54c9fa..b834a8e6829 100644
--- a/homeassistant/components/pioneer/media_player.py
+++ b/homeassistant/components/pioneer/media_player.py
@@ -13,6 +13,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
+ SUPPORT_VOLUME_STEP,
)
from homeassistant.const import (
CONF_HOST,
@@ -36,6 +37,7 @@ DEFAULT_SOURCES = {}
SUPPORT_PIONEER = (
SUPPORT_PAUSE
| SUPPORT_VOLUME_SET
+ | SUPPORT_VOLUME_STEP
| SUPPORT_VOLUME_MUTE
| SUPPORT_TURN_ON
| SUPPORT_TURN_OFF
diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json
index 63cf65b8d6c..d562d62b602 100644
--- a/homeassistant/components/plex/.translations/ca.json
+++ b/homeassistant/components/plex/.translations/ca.json
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "Ignora els nous usuaris gestionats/compartits",
+ "monitored_users": "Usuaris monitoritzats",
"show_all_controls": "Mostra tots els controls",
"use_episode_art": "Utilitza imatges de l'episodi"
},
diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json
index 18dbbb840c3..9b80373727d 100644
--- a/homeassistant/components/plex/.translations/da.json
+++ b/homeassistant/components/plex/.translations/da.json
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "Ignorer nye administrerede/delte brugere",
+ "monitored_users": "Monitorerede brugere",
"show_all_controls": "Vis alle kontrolelementer",
"use_episode_art": "Brug episodekunst"
},
diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json
index aa8c5e08dd6..ea8f4b60de4 100644
--- a/homeassistant/components/plex/.translations/de.json
+++ b/homeassistant/components/plex/.translations/de.json
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "Ignorieren neuer verwalteter/freigegebener Benutzer",
+ "monitored_users": "\u00dcberwachte Benutzer",
"show_all_controls": "Alle Steuerelemente anzeigen",
"use_episode_art": "Episode-Bilder verwenden"
},
diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json
index 31211182f47..4567171af77 100644
--- a/homeassistant/components/plex/.translations/en.json
+++ b/homeassistant/components/plex/.translations/en.json
@@ -4,7 +4,7 @@
"all_configured": "All linked servers already configured",
"already_configured": "This Plex server is already configured",
"already_in_progress": "Plex is being configured",
- "discovery_no_file": "No legacy config file found",
+ "discovery_no_file": "No legacy configuration file found",
"invalid_import": "Imported configuration is invalid",
"non-interactive": "Non-interactive import",
"token_request_timeout": "Timed out obtaining token",
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "Ignore new managed/shared users",
+ "monitored_users": "Monitored users",
"show_all_controls": "Show all controls",
"use_episode_art": "Use episode art"
},
diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json
index 53dd3228288..24127a7332c 100644
--- a/homeassistant/components/plex/.translations/es.json
+++ b/homeassistant/components/plex/.translations/es.json
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "Ignorar nuevos usuarios administrados/compartidos",
+ "monitored_users": "Usuarios monitorizados",
"show_all_controls": "Mostrar todos los controles",
"use_episode_art": "Usar el arte de episodios"
},
diff --git a/homeassistant/components/plex/.translations/hu.json b/homeassistant/components/plex/.translations/hu.json
new file mode 100644
index 00000000000..4712fb37b55
--- /dev/null
+++ b/homeassistant/components/plex/.translations/hu.json
@@ -0,0 +1,57 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Az \u00f6sszes \u00f6sszekapcsolt szerver m\u00e1r konfigur\u00e1lva van",
+ "already_configured": "Ez a Plex szerver m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A Plex konfigur\u00e1l\u00e1sa folyamatban van",
+ "discovery_no_file": "Nem tal\u00e1lhat\u00f3 r\u00e9gi konfigur\u00e1ci\u00f3s f\u00e1jl",
+ "invalid_import": "Az import\u00e1lt konfigur\u00e1ci\u00f3 \u00e9rv\u00e9nytelen",
+ "non-interactive": "Nem interakt\u00edv import\u00e1l\u00e1s",
+ "token_request_timeout": "Token k\u00e9r\u00e9sre sz\u00e1nt id\u0151 lej\u00e1rt",
+ "unknown": "Ismeretlen okb\u00f3l nem siker\u00fclt"
+ },
+ "error": {
+ "faulty_credentials": "A hiteles\u00edt\u00e9s sikertelen",
+ "no_servers": "Nincs szerver csatlakoztatva a fi\u00f3khoz",
+ "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3"
+ },
+ "step": {
+ "manual_setup": {
+ "data": {
+ "host": "Kiszolg\u00e1l\u00f3",
+ "port": "Port"
+ }
+ },
+ "select_server": {
+ "data": {
+ "server": "szerver"
+ },
+ "description": "T\u00f6bb szerver el\u00e9rhet\u0151, v\u00e1lasszon egyet:",
+ "title": "Plex-kiszolg\u00e1l\u00f3 kiv\u00e1laszt\u00e1sa"
+ },
+ "start_website_auth": {
+ "description": "Folytassa az enged\u00e9lyez\u00e9st a plex.tv webhelyen.",
+ "title": "Plex-kiszolg\u00e1l\u00f3 csatlakoztat\u00e1sa"
+ },
+ "user": {
+ "data": {
+ "token": "Plex token"
+ },
+ "description": "Folytassa az enged\u00e9lyez\u00e9st a plex.tv webhelyen, vagy manu\u00e1lisan konfigur\u00e1lja a szervert.",
+ "title": "Plex-kiszolg\u00e1l\u00f3 csatlakoztat\u00e1sa"
+ }
+ },
+ "title": "Plex"
+ },
+ "options": {
+ "step": {
+ "plex_mp_settings": {
+ "data": {
+ "show_all_controls": "Az \u00f6sszes vez\u00e9rl\u0151 megjelen\u00edt\u00e9se",
+ "use_episode_art": "Haszn\u00e1lja az epiz\u00f3d bor\u00edt\u00f3j\u00e1t"
+ },
+ "description": "Plex media lej\u00e1tsz\u00f3k be\u00e1ll\u00edt\u00e1sai"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json
index 06c20660fef..e5ff4e01dc0 100644
--- a/homeassistant/components/plex/.translations/it.json
+++ b/homeassistant/components/plex/.translations/it.json
@@ -4,7 +4,7 @@
"all_configured": "Tutti i server collegati sono gi\u00e0 configurati",
"already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato",
"already_in_progress": "Plex \u00e8 in fase di configurazione",
- "discovery_no_file": "Nessun file di configurazione legacy trovato",
+ "discovery_no_file": "Non \u00e8 stato trovato nessun file di configurazione da sostituire",
"invalid_import": "La configurazione importata non \u00e8 valida",
"non-interactive": "Importazione non interattiva",
"token_request_timeout": "Timeout per l'ottenimento del token",
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "Ignora nuovi utenti gestiti/condivisi",
+ "monitored_users": "Utenti monitorati",
"show_all_controls": "Mostra tutti i controlli",
"use_episode_art": "Usa la grafica dell'episodio"
},
diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json
index cf5a7946b9d..3292fab0a8e 100644
--- a/homeassistant/components/plex/.translations/ko.json
+++ b/homeassistant/components/plex/.translations/ko.json
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "\uc0c8\ub85c\uc6b4 \uad00\ub9ac/\uacf5\uc720 \uc0ac\uc6a9\uc790 \ubb34\uc2dc",
+ "monitored_users": "\ubaa8\ub2c8\ud130\ub9c1\ub418\ub294 \uc0ac\uc6a9\uc790",
"show_all_controls": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864 \ud45c\uc2dc\ud558\uae30",
"use_episode_art": "\uc5d0\ud53c\uc18c\ub4dc \uc544\ud2b8 \uc0ac\uc6a9"
},
diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json
index c6fcabc40d7..6ed9d372fc1 100644
--- a/homeassistant/components/plex/.translations/lb.json
+++ b/homeassistant/components/plex/.translations/lb.json
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "Nei verwalt / gedeelt Benotzer ignor\u00e9ieren",
+ "monitored_users": "Iwwerwaachte Benotzer",
"show_all_controls": "Weis all Kontrollen",
"use_episode_art": "Benotz Biller vun der Episode"
},
diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json
index cc6dac8a35b..c80ba5f2e06 100644
--- a/homeassistant/components/plex/.translations/no.json
+++ b/homeassistant/components/plex/.translations/no.json
@@ -4,7 +4,7 @@
"all_configured": "Alle knyttet servere som allerede er konfigurert",
"already_configured": "Denne Plex-serveren er allerede konfigurert",
"already_in_progress": "Plex blir konfigurert",
- "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet",
+ "discovery_no_file": "Ingen eldre konfigurasjonsfil funnet",
"invalid_import": "Den importerte konfigurasjonen er ugyldig",
"non-interactive": "Ikke-interaktiv import",
"token_request_timeout": "Tidsavbrudd ved innhenting av token",
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "Ignorer nye administrerte/delte brukere",
+ "monitored_users": "Overv\u00e5kede brukere",
"show_all_controls": "Vis alle kontroller",
"use_episode_art": "Bruk episode bilde"
},
diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json
index d752899b9f0..6531b552000 100644
--- a/homeassistant/components/plex/.translations/pl.json
+++ b/homeassistant/components/plex/.translations/pl.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.",
- "already_configured": "Serwer Plex jest ju\u017c skonfigurowany",
+ "already_configured": "Ten 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",
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "Ignoruj nowych zarz\u0105dzanych/wsp\u00f3\u0142dzielonych u\u017cytkownik\u00f3w",
+ "monitored_users": "Monitorowani u\u017cytkownicy",
"show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce",
"use_episode_art": "U\u017cyj grafiki odcinka"
},
diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json
index 334a4e353d4..2da10b1e8c4 100644
--- a/homeassistant/components/plex/.translations/ru.json
+++ b/homeassistant/components/plex/.translations/ru.json
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0445 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0445/\u043e\u0431\u0449\u0438\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439",
+ "monitored_users": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438",
"show_all_controls": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
"use_episode_art": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u043e\u0436\u043a\u0438 \u044d\u043f\u0438\u0437\u043e\u0434\u043e\u0432"
},
diff --git a/homeassistant/components/plex/.translations/sv.json b/homeassistant/components/plex/.translations/sv.json
index 702cec128c0..25152e9dc81 100644
--- a/homeassistant/components/plex/.translations/sv.json
+++ b/homeassistant/components/plex/.translations/sv.json
@@ -1,10 +1,62 @@
{
+ "config": {
+ "abort": {
+ "all_configured": "Alla l\u00e4nkade servrar har redan konfigurerats",
+ "already_configured": "Denna Plex-server \u00e4r redan konfigurerad",
+ "already_in_progress": "Plex konfigureras",
+ "discovery_no_file": "Ingen \u00e4ldre konfigurationsfil hittades",
+ "invalid_import": "Importerad konfiguration \u00e4r ogiltig",
+ "non-interactive": "Icke-interaktiv import",
+ "token_request_timeout": "Timeout att erh\u00e5lla token",
+ "unknown": "Misslyckades av ok\u00e4nd anledning"
+ },
+ "error": {
+ "faulty_credentials": "Auktoriseringen misslyckades",
+ "no_servers": "Inga servrar l\u00e4nkade till konto",
+ "no_token": "Ange en token eller v\u00e4lj manuell inst\u00e4llning",
+ "not_found": "Plex-server hittades inte"
+ },
+ "step": {
+ "manual_setup": {
+ "data": {
+ "host": "V\u00e4rd",
+ "port": "Port",
+ "ssl": "Anv\u00e4nd SSL",
+ "token": "Token (om det beh\u00f6vs)",
+ "verify_ssl": "Verifiera SSL-certifikat"
+ },
+ "title": "Plex-server"
+ },
+ "select_server": {
+ "data": {
+ "server": "Server"
+ },
+ "description": "V\u00e4lj flera servrar tillg\u00e4ngliga, v\u00e4lj en:",
+ "title": "V\u00e4lj Plex-server"
+ },
+ "start_website_auth": {
+ "description": "Forts\u00e4tt att auktorisera p\u00e5 plex.tv.",
+ "title": "Anslut Plex-servern"
+ },
+ "user": {
+ "data": {
+ "manual_setup": "Manuell inst\u00e4llning",
+ "token": "Plex-token"
+ },
+ "description": "Forts\u00e4tt att auktorisera p\u00e5 plex.tv eller konfigurera en server manuellt.",
+ "title": "Anslut Plex-servern"
+ }
+ },
+ "title": "Plex"
+ },
"options": {
"step": {
"plex_mp_settings": {
"data": {
- "show_all_controls": "Visa alla kontroller"
- }
+ "show_all_controls": "Visa alla kontroller",
+ "use_episode_art": "Anv\u00e4nd avsnittsbild"
+ },
+ "description": "Alternativ f\u00f6r Plex-mediaspelare"
}
}
}
diff --git a/homeassistant/components/plex/.translations/zh-Hans.json b/homeassistant/components/plex/.translations/zh-Hans.json
new file mode 100644
index 00000000000..614f83e3cc0
--- /dev/null
+++ b/homeassistant/components/plex/.translations/zh-Hans.json
@@ -0,0 +1,12 @@
+{
+ "options": {
+ "step": {
+ "plex_mp_settings": {
+ "data": {
+ "ignore_new_shared_users": "\u5ffd\u7565\u65b0\u589e\u7ba1\u7406/\u5171\u4eab\u4f7f\u7528\u8005",
+ "monitored_users": "\u53d7\u76d1\u89c6\u7684\u7528\u6237"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json
index 5c05d2104f9..436333b0a79 100644
--- a/homeassistant/components/plex/.translations/zh-Hant.json
+++ b/homeassistant/components/plex/.translations/zh-Hant.json
@@ -53,6 +53,8 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_new_shared_users": "\u5ffd\u7565\u65b0\u589e\u7ba1\u7406/\u5206\u4eab\u4f7f\u7528\u8005",
+ "monitored_users": "\u5df2\u76e3\u63a7\u4f7f\u7528\u8005",
"show_all_controls": "\u986f\u793a\u6240\u6709\u63a7\u5236",
"use_episode_art": "\u4f7f\u7528\u5f71\u96c6\u5287\u7167"
},
diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py
index 89659769192..c9b120f75f6 100644
--- a/homeassistant/components/plex/__init__.py
+++ b/homeassistant/components/plex/__init__.py
@@ -19,6 +19,7 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP,
)
+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.dispatcher import (
@@ -27,6 +28,7 @@ from homeassistant.helpers.dispatcher import (
)
from .const import (
+ CONF_IGNORE_NEW_SHARED_USERS,
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
CONF_SHOW_ALL_CONTROLS,
@@ -50,6 +52,7 @@ MEDIA_PLAYER_SCHEMA = vol.Schema(
{
vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean,
vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean,
+ vol.Optional(CONF_IGNORE_NEW_SHARED_USERS, default=False): cv.boolean,
}
)
@@ -127,7 +130,7 @@ async def async_setup_entry(hass, entry):
server_config[CONF_URL],
error,
)
- return False
+ raise ConfigEntryNotReady
except (
plexapi.exceptions.BadRequest,
plexapi.exceptions.Unauthorized,
diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py
index d38d13c847e..19cec6dfb8b 100644
--- a/homeassistant/components/plex/config_flow.py
+++ b/homeassistant/components/plex/config_flow.py
@@ -14,12 +14,15 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.const import CONF_SSL, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json
from .const import ( # pylint: disable=unused-import
AUTH_CALLBACK_NAME,
AUTH_CALLBACK_PATH,
CONF_CLIENT_IDENTIFIER,
+ CONF_IGNORE_NEW_SHARED_USERS,
+ CONF_MONITORED_USERS,
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
CONF_SHOW_ALL_CONTROLS,
@@ -28,6 +31,7 @@ from .const import ( # pylint: disable=unused-import
DOMAIN,
PLEX_CONFIG_FILE,
PLEX_SERVER_CONFIG,
+ SERVERS,
X_PLEX_DEVICE_NAME,
X_PLEX_PLATFORM,
X_PLEX_PRODUCT,
@@ -52,7 +56,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Plex config flow."""
VERSION = 1
- CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
@staticmethod
@callback
@@ -254,6 +258,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry):
"""Initialize Plex options flow."""
self.options = copy.deepcopy(config_entry.options)
+ self.server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
async def async_step_init(self, user_input=None):
"""Manage the Plex options."""
@@ -261,6 +266,8 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_plex_mp_settings(self, user_input=None):
"""Manage the Plex media_player options."""
+ plex_server = self.hass.data[DOMAIN][SERVERS][self.server_id]
+
if user_input is not None:
self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] = user_input[
CONF_USE_EPISODE_ART
@@ -268,19 +275,56 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] = user_input[
CONF_SHOW_ALL_CONTROLS
]
+ self.options[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = user_input[
+ CONF_IGNORE_NEW_SHARED_USERS
+ ]
+
+ account_data = {
+ user: {"enabled": bool(user in user_input[CONF_MONITORED_USERS])}
+ for user in plex_server.accounts
+ }
+
+ self.options[MP_DOMAIN][CONF_MONITORED_USERS] = account_data
+
return self.async_create_entry(title="", data=self.options)
+ available_accounts = {name: name for name in plex_server.accounts}
+ available_accounts[plex_server.owner] += " [Owner]"
+
+ default_accounts = plex_server.accounts
+ known_accounts = set(plex_server.option_monitored_users)
+ if known_accounts:
+ default_accounts = {
+ user
+ for user in plex_server.option_monitored_users
+ if plex_server.option_monitored_users[user]["enabled"]
+ }
+ for user in plex_server.accounts:
+ if user not in known_accounts:
+ available_accounts[user] += " [New]"
+
+ if not plex_server.option_ignore_new_shared_users:
+ for new_user in plex_server.accounts - known_accounts:
+ default_accounts.add(new_user)
+
return self.async_show_form(
step_id="plex_mp_settings",
data_schema=vol.Schema(
{
vol.Required(
CONF_USE_EPISODE_ART,
- default=self.options[MP_DOMAIN][CONF_USE_EPISODE_ART],
+ default=plex_server.option_use_episode_art,
): bool,
vol.Required(
CONF_SHOW_ALL_CONTROLS,
- default=self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS],
+ default=plex_server.option_show_all_controls,
+ ): bool,
+ vol.Optional(
+ CONF_MONITORED_USERS, default=default_accounts
+ ): cv.multi_select(available_accounts),
+ vol.Required(
+ CONF_IGNORE_NEW_SHARED_USERS,
+ default=plex_server.option_ignore_new_shared_users,
): bool,
}
),
diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py
index ad62bade1fd..7d6812674ca 100644
--- a/homeassistant/components/plex/const.py
+++ b/homeassistant/components/plex/const.py
@@ -3,6 +3,7 @@ from homeassistant.const import __version__
DOMAIN = "plex"
NAME_FORMAT = "Plex ({})"
+COMMON_PLAYERS = ["Plex Web"]
DEFAULT_PORT = 32400
DEFAULT_SSL = False
@@ -28,6 +29,8 @@ CONF_SERVER = "server"
CONF_SERVER_IDENTIFIER = "server_id"
CONF_USE_EPISODE_ART = "use_episode_art"
CONF_SHOW_ALL_CONTROLS = "show_all_controls"
+CONF_IGNORE_NEW_SHARED_USERS = "ignore_new_shared_users"
+CONF_MONITORED_USERS = "monitored_users"
AUTH_CALLBACK_PATH = "/auth/plex/callback"
AUTH_CALLBACK_NAME = "auth:plex:callback"
diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py
index d8155d1a43b..47e5ba6104f 100644
--- a/homeassistant/components/plex/media_player.py
+++ b/homeassistant/components/plex/media_player.py
@@ -21,19 +21,14 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
)
-from homeassistant.const import (
- DEVICE_DEFAULT_NAME,
- STATE_IDLE,
- STATE_OFF,
- STATE_PAUSED,
- STATE_PLAYING,
-)
+from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_registry import async_get_registry
from homeassistant.util import dt as dt_util
from .const import (
+ COMMON_PLAYERS,
CONF_SERVER_IDENTIFIER,
DISPATCHERS,
DOMAIN as PLEX_DOMAIN,
@@ -114,6 +109,10 @@ class PlexMediaPlayer(MediaPlayerDevice):
self._is_player_active = False
self._machine_identifier = device.machineIdentifier
self._make = ""
+ self._device_platform = None
+ self._device_product = None
+ self._device_title = None
+ self._device_version = None
self._name = None
self._player_state = "idle"
self._previous_volume_level = 1 # Used in fake muting
@@ -128,6 +127,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
self._media_content_type = None
self._media_duration = None
self._media_image_url = None
+ self._media_summary = None
self._media_title = None
self._media_position = None
self._media_position_updated_at = None
@@ -169,6 +169,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
self._media_content_type = None
self._media_duration = None
self._media_image_url = None
+ self._media_summary = None
self._media_title = None
# Music
self._media_album_artist = None
@@ -188,7 +189,6 @@ class PlexMediaPlayer(MediaPlayerDevice):
self._clear_media_details()
self._available = self.device or self.session
- name_base = None
if self.device:
try:
@@ -197,7 +197,10 @@ class PlexMediaPlayer(MediaPlayerDevice):
device_url = "127.0.0.1"
if "127.0.0.1" in device_url:
self.device.proxyThroughServer()
- name_base = self.device.title or self.device.product
+ self._device_platform = self.device.platform
+ self._device_product = self.device.product
+ self._device_title = self.device.title
+ self._device_version = self.device.version
self._device_protocol_capabilities = self.device.protocolCapabilities
self._player_state = self.device.state
@@ -215,11 +218,15 @@ class PlexMediaPlayer(MediaPlayerDevice):
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
+ self._device_platform = self._device_platform or session_device.platform
+ self._device_product = self._device_product or session_device.product
+ self._device_title = self._device_title or session_device.title
+ self._device_version = self._device_version or session_device.version
else:
_LOGGER.warning("No player associated with active session")
- self._session_username = self.session.usernames[0]
+ if self.session.usernames:
+ self._session_username = self.session.usernames[0]
# Calculate throttled position for proper progress display.
position = int(self.session.viewOffset / 1000)
@@ -237,13 +244,21 @@ class PlexMediaPlayer(MediaPlayerDevice):
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)
+ name_parts = [self._device_product, self._device_title or self._device_platform]
+ if (self._device_product in COMMON_PLAYERS) and self.make:
+ # Add more context in name for likely duplicates
+ name_parts.append(self.make)
+ if self.username and self.username != self.plex_server.owner:
+ # Prepend username for shared/managed clients
+ name_parts.insert(0, self.username)
+ self._name = NAME_FORMAT.format(" - ".join(name_parts))
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)
# title (movie name, tv episode name, music song name)
+ self._media_summary = self.session.summary
self._media_title = self.session.title
# media type
self._set_media_type()
@@ -260,7 +275,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
thumb_url = self.session.thumbUrl
if (
self.media_content_type is MEDIA_TYPE_TVSHOW
- and not self.plex_server.use_episode_art
+ and not self.plex_server.option_use_episode_art
):
thumb_url = self.session.url(self.session.grandparentThumb)
@@ -348,6 +363,11 @@ class PlexMediaPlayer(MediaPlayerDevice):
"""Return the name of the device."""
return self._name
+ @property
+ def username(self):
+ """Return the username of the client owner."""
+ return self._session_username
+
@property
def app_name(self):
"""Return the library name of playing media."""
@@ -428,6 +448,11 @@ class PlexMediaPlayer(MediaPlayerDevice):
"""Return the image URL of current playing media."""
return self._media_image_url
+ @property
+ def media_summary(self):
+ """Return the summary of current playing media."""
+ return self._media_summary
+
@property
def media_title(self):
"""Return the title of current playing media."""
@@ -457,7 +482,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
def supported_features(self):
"""Flag media player features that are supported."""
# force show all controls
- if self.plex_server.show_all_controls:
+ if self.plex_server.option_show_all_controls:
return (
SUPPORT_PAUSE
| SUPPORT_PREVIOUS_TRACK
@@ -699,8 +724,24 @@ class PlexMediaPlayer(MediaPlayerDevice):
"""Return the scene state attributes."""
attr = {
"media_content_rating": self._media_content_rating,
- "session_username": self._session_username,
+ "session_username": self.username,
"media_library_name": self._app_name,
+ "summary": self.media_summary,
}
return attr
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ if self.machine_identifier is None:
+ return None
+
+ return {
+ "identifiers": {(PLEX_DOMAIN, self.machine_identifier)},
+ "manufacturer": self._device_platform or "Plex",
+ "model": self._device_product or self.make,
+ "name": self.name,
+ "sw_version": self._device_version,
+ "via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier),
+ }
diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py
index 2aed57946eb..b1e93aec8c0 100644
--- a/homeassistant/components/plex/sensor.py
+++ b/homeassistant/components/plex/sensor.py
@@ -101,23 +101,24 @@ class PlexSensor(Entity):
_LOGGER.debug("Refreshing sensor [%s]", self.unique_id)
now_playing = []
for sess in self.sessions:
+ if sess.TYPE == "photo":
+ _LOGGER.debug("Photo session detected, skipping: %s", sess)
+ continue
user = sess.usernames[0]
device = sess.players[0].title
now_playing_user = f"{user} - {device}"
now_playing_title = ""
- if sess.TYPE == "episode":
+ if sess.TYPE in ["clip", "episode"]:
# example:
- # "Supernatural (2005) - S01 · E13 - Route 666"
+ # "Supernatural (2005) - s01e13 - Route 666"
season_title = sess.grandparentTitle
if sess.show().year is not None:
- season_title += " ({0})".format(sess.show().year)
- season_episode = "S{0}".format(sess.parentIndex)
- if sess.index is not None:
- season_episode += f" · E{sess.index}"
+ season_title += f" ({sess.show().year!s})"
+ season_episode = sess.seasonEpisode
episode_title = sess.title
- now_playing_title = "{0} - {1} - {2}".format(
- season_title, season_episode, episode_title
+ now_playing_title = (
+ f"{season_title} - {season_episode} - {episode_title}"
)
elif sess.TYPE == "track":
# example:
@@ -125,9 +126,7 @@ class PlexSensor(Entity):
track_artist = sess.grandparentTitle
track_album = sess.parentTitle
track_title = sess.title
- now_playing_title = "{0} - {1} - {2}".format(
- track_artist, track_album, track_title
- )
+ now_playing_title = f"{track_artist} - {track_album} - {track_title}"
else:
# example:
# "picture_of_last_summer_camp (2015)"
@@ -139,3 +138,17 @@ class PlexSensor(Entity):
now_playing.append((now_playing_user, now_playing_title))
self._state = len(self.sessions)
self._now_playing = now_playing
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ if self.unique_id is None:
+ return None
+
+ return {
+ "identifiers": {(PLEX_DOMAIN, self._server.machine_identifier)},
+ "manufacturer": "Plex",
+ "model": "Plex Media Server",
+ "name": "Activity Sensor",
+ "sw_version": self._server.version,
+ }
diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py
index 46602cf6552..5532362b87a 100644
--- a/homeassistant/components/plex/server.py
+++ b/homeassistant/components/plex/server.py
@@ -13,6 +13,8 @@ from homeassistant.helpers.dispatcher import dispatcher_send
from .const import (
CONF_CLIENT_IDENTIFIER,
+ CONF_IGNORE_NEW_SHARED_USERS,
+ CONF_MONITORED_USERS,
CONF_SERVER,
CONF_SHOW_ALL_CONTROLS,
CONF_USE_EPISODE_ART,
@@ -51,6 +53,9 @@ class PlexServer:
self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
self.options = options
self.server_choice = None
+ self._accounts = []
+ self._owner_username = None
+ self._version = None
# Header conditionally added as it is not available in config entry v1
if CONF_CLIENT_IDENTIFIER in server_config:
@@ -93,6 +98,22 @@ class PlexServer:
else:
_connect_with_token()
+ self._accounts = [
+ account.name
+ for account in self._plex_server.systemAccounts()
+ if account.name
+ ]
+
+ owner_account = [
+ account.name
+ for account in self._plex_server.systemAccounts()
+ if account.accountID == 1
+ ]
+ if owner_account:
+ self._owner_username = owner_account[0]
+
+ self._version = self._plex_server.version
+
def refresh_entity(self, machine_identifier, device, session):
"""Forward refresh dispatch to media_player."""
unique_id = f"{self.machine_identifier}:{machine_identifier}"
@@ -109,8 +130,22 @@ class PlexServer:
_LOGGER.debug("Updating devices")
available_clients = {}
+ ignored_clients = set()
new_clients = set()
+ monitored_users = self.accounts
+ known_accounts = set(self.option_monitored_users)
+ if known_accounts:
+ monitored_users = {
+ user
+ for user in self.option_monitored_users
+ if self.option_monitored_users[user]["enabled"]
+ }
+
+ if not self.option_ignore_new_shared_users:
+ for new_user in self.accounts - known_accounts:
+ monitored_users.add(new_user)
+
try:
devices = self._plex_server.clients()
sessions = self._plex_server.sessions()
@@ -132,7 +167,15 @@ class PlexServer:
_LOGGER.debug("New device: %s", device.machineIdentifier)
for session in sessions:
+ if session.TYPE == "photo":
+ _LOGGER.debug("Photo session detected, skipping: %s", session)
+ continue
+ session_username = session.usernames[0]
for player in session.players:
+ if session_username not in monitored_users:
+ ignored_clients.add(player.machineIdentifier)
+ _LOGGER.debug("Ignoring Plex client owned by %s", session_username)
+ continue
self._known_idle.discard(player.machineIdentifier)
available_clients.setdefault(
player.machineIdentifier, {"device": player}
@@ -145,6 +188,8 @@ class PlexServer:
new_entity_configs = []
for client_id, client_data in available_clients.items():
+ if client_id in ignored_clients:
+ continue
if client_id in new_clients:
new_entity_configs.append(client_data)
else:
@@ -152,11 +197,11 @@ class PlexServer:
client_id, client_data["device"], client_data.get("session")
)
- self._known_clients.update(new_clients)
+ self._known_clients.update(new_clients | ignored_clients)
- idle_clients = (self._known_clients - self._known_idle).difference(
- available_clients
- )
+ idle_clients = (
+ self._known_clients - self._known_idle - ignored_clients
+ ).difference(available_clients)
for client_id in idle_clients:
self.refresh_entity(client_id, None, None)
self._known_idle.add(client_id)
@@ -179,6 +224,21 @@ class PlexServer:
"""Return the plexapi PlexServer instance."""
return self._plex_server
+ @property
+ def accounts(self):
+ """Return accounts associated with the Plex server."""
+ return set(self._accounts)
+
+ @property
+ def owner(self):
+ """Return the Plex server owner username."""
+ return self._owner_username
+
+ @property
+ def version(self):
+ """Return the version of the Plex server."""
+ return self._version
+
@property
def friendly_name(self):
"""Return name of connected Plex server."""
@@ -195,15 +255,25 @@ class PlexServer:
return self._plex_server._baseurl # pylint: disable=protected-access
@property
- def use_episode_art(self):
+ def option_ignore_new_shared_users(self):
+ """Return ignore_new_shared_users option."""
+ return self.options[MP_DOMAIN].get(CONF_IGNORE_NEW_SHARED_USERS, False)
+
+ @property
+ def option_use_episode_art(self):
"""Return use_episode_art option."""
return self.options[MP_DOMAIN][CONF_USE_EPISODE_ART]
@property
- def show_all_controls(self):
+ def option_show_all_controls(self):
"""Return show_all_controls option."""
return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS]
+ @property
+ def option_monitored_users(self):
+ """Return dict of monitored users option."""
+ return self.options[MP_DOMAIN].get(CONF_MONITORED_USERS, {})
+
@property
def library(self):
"""Return library attribute from server object."""
diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json
index b6491db350c..1f99e28df8b 100644
--- a/homeassistant/components/plex/strings.json
+++ b/homeassistant/components/plex/strings.json
@@ -23,7 +23,7 @@
"all_configured": "All linked servers already configured",
"already_configured": "This Plex server is already configured",
"already_in_progress": "Plex is being configured",
- "discovery_no_file": "No legacy config file found",
+ "discovery_no_file": "No legacy configuration file found",
"invalid_import": "Imported configuration is invalid",
"non-interactive": "Non-interactive import",
"token_request_timeout": "Timed out obtaining token",
@@ -36,7 +36,9 @@
"description": "Options for Plex Media Players",
"data": {
"use_episode_art": "Use episode art",
- "show_all_controls": "Show all controls"
+ "show_all_controls": "Show all controls",
+ "ignore_new_shared_users": "Ignore new managed/shared users",
+ "monitored_users": "Monitored users"
}
}
}
diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json
index 5fc3d189b69..601f017d42f 100644
--- a/homeassistant/components/plugwise/manifest.json
+++ b/homeassistant/components/plugwise/manifest.json
@@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/integrations/plugwise",
"dependencies": [],
"codeowners": ["@laetificat", "@CoMPaTech", "@bouwew"],
- "requirements": ["haanna==0.14.1"]
+ "requirements": ["haanna==0.14.3"]
}
diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py
index c20296a2c18..d77cb4f56da 100644
--- a/homeassistant/components/prometheus/__init__.py
+++ b/homeassistant/components/prometheus/__init__.py
@@ -334,7 +334,7 @@ class PrometheusMetrics:
@staticmethod
def _sensor_fallback_metric(state, unit):
- """Get metric from fallback logic for compatability."""
+ """Get metric from fallback logic for compatibility."""
if unit in (None, ""):
_LOGGER.debug("Unsupported sensor: %s", state.entity_id)
return None
diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py
index 58cb50ee304..315fb8b1c91 100644
--- a/homeassistant/components/proxmoxve/__init__.py
+++ b/homeassistant/components/proxmoxve/__init__.py
@@ -147,12 +147,12 @@ class ProxmoxClient:
verify_ssl=self._verify_ssl,
)
- self._connection_start_time = time.time()
+ self._connection_start_time = time.monotonic()
def get_api_client(self):
"""Return the ProxmoxAPI client and rebuild it if necessary."""
- connection_age = time.time() - self._connection_start_time
+ connection_age = time.monotonic() - self._connection_start_time
# Workaround for the Proxmoxer bug where the connection stops working after some time
if connection_age > 30 * 60:
diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json
index 06498e51ad3..d12fbe2d3d7 100644
--- a/homeassistant/components/proxy/manifest.json
+++ b/homeassistant/components/proxy/manifest.json
@@ -2,7 +2,9 @@
"domain": "proxy",
"name": "Camera Proxy",
"documentation": "https://www.home-assistant.io/integrations/proxy",
- "requirements": ["pillow==6.2.1"],
+ "requirements": [
+ "pillow==7.0.0"
+ ],
"dependencies": [],
"codeowners": []
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/ps4/.translations/de.json b/homeassistant/components/ps4/.translations/de.json
index 6f4962a305d..66eaecbb548 100644
--- a/homeassistant/components/ps4/.translations/de.json
+++ b/homeassistant/components/ps4/.translations/de.json
@@ -25,7 +25,7 @@
"name": "Name",
"region": "Region"
},
- "description": "Geben deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.",
+ "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.",
"title": "PlayStation 4"
},
"mode": {
diff --git a/homeassistant/components/ps4/.translations/hu.json b/homeassistant/components/ps4/.translations/hu.json
index 77b13f33a51..7a8623b9030 100644
--- a/homeassistant/components/ps4/.translations/hu.json
+++ b/homeassistant/components/ps4/.translations/hu.json
@@ -6,6 +6,7 @@
},
"link": {
"data": {
+ "code": "PIN",
"ip_address": "IP-c\u00edm",
"name": "N\u00e9v",
"region": "R\u00e9gi\u00f3"
diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json
index 8551b3da3e6..80c12cc746c 100644
--- a/homeassistant/components/ps4/manifest.json
+++ b/homeassistant/components/ps4/manifest.json
@@ -3,7 +3,7 @@
"name": "Sony PlayStation 4",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ps4",
- "requirements": ["pyps4-2ndscreen==1.0.6"],
+ "requirements": ["pyps4-2ndscreen==1.0.7"],
"dependencies": [],
"codeowners": ["@ktnrg45"]
}
diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py
index 33b5c556c7d..28d201d78cd 100644
--- a/homeassistant/components/ps4/media_player.py
+++ b/homeassistant/components/ps4/media_player.py
@@ -97,11 +97,7 @@ class PS4Device(MediaPlayerDevice):
def status_callback(self):
"""Handle status callback. Parse status."""
self._parse_status()
-
- @callback
- def schedule_update(self):
- """Schedules update with HA."""
- self.async_schedule_update_ha_state()
+ self.async_write_ha_state()
@callback
def subscribe_to_protocol(self):
@@ -184,7 +180,6 @@ class PS4Device(MediaPlayerDevice):
self._media_content_id = title_id
if self._use_saved():
_LOGGER.debug("Using saved data for media: %s", title_id)
- self.schedule_update()
return
self._media_title = name
@@ -223,13 +218,11 @@ class PS4Device(MediaPlayerDevice):
"""Set states for state idle."""
self.reset_title()
self._state = STATE_IDLE
- self.schedule_update()
def state_standby(self):
"""Set states for state standby."""
self.reset_title()
self._state = STATE_STANDBY
- self.schedule_update()
def state_unknown(self):
"""Set states for state unknown."""
@@ -286,8 +279,8 @@ class PS4Device(MediaPlayerDevice):
self._media_image = art or None
self._media_type = media_type
- self.update_list()
- self.schedule_update()
+ await self.hass.async_add_executor_job(self.update_list)
+ self.async_write_ha_state()
def update_list(self):
"""Update Game List, Correct data if different."""
diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json
index 3428e429b8c..9bdd1bb53f9 100644
--- a/homeassistant/components/pushover/manifest.json
+++ b/homeassistant/components/pushover/manifest.json
@@ -2,7 +2,7 @@
"domain": "pushover",
"name": "Pushover",
"documentation": "https://www.home-assistant.io/integrations/pushover",
- "requirements": ["python-pushover==0.4"],
+ "requirements": ["pushover_complete==1.1.1"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py
index 1930ff66f2e..bc44cbeddb7 100644
--- a/homeassistant/components/pushover/notify.py
+++ b/homeassistant/components/pushover/notify.py
@@ -1,8 +1,7 @@
"""Pushover platform for notify component."""
import logging
-from pushover import Client, InitError, RequestError
-import requests
+from pushover_complete import PushoverAPI
import voluptuous as vol
from homeassistant.components.notify import (
@@ -19,6 +18,15 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
ATTR_ATTACHMENT = "attachment"
+ATTR_URL = "url"
+ATTR_URL_TITLE = "url_title"
+ATTR_PRIORITY = "priority"
+ATTR_RETRY = "retry"
+ATTR_SOUND = "sound"
+ATTR_HTML = "html"
+ATTR_CALLBACK_URL = "callback_url"
+ATTR_EXPIRE = "expire"
+ATTR_TIMESTAMP = "timestamp"
CONF_USER_KEY = "user_key"
@@ -29,13 +37,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the Pushover notification service."""
- try:
- return PushoverNotificationService(
- hass, config[CONF_USER_KEY], config[CONF_API_KEY]
- )
- except InitError:
- _LOGGER.error("Wrong API key supplied")
- return None
+ return PushoverNotificationService(
+ hass, config[CONF_USER_KEY], config[CONF_API_KEY]
+ )
class PushoverNotificationService(BaseNotificationService):
@@ -46,54 +50,42 @@ class PushoverNotificationService(BaseNotificationService):
self._hass = hass
self._user_key = user_key
self._api_token = api_token
- self.pushover = Client(self._user_key, api_token=self._api_token)
+ self.pushover = PushoverAPI(self._api_token)
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
- # Make a copy and use empty dict if necessary
+
+ # Extract params from data dict
+ title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = dict(kwargs.get(ATTR_DATA) or {})
+ url = data.get(ATTR_URL, None)
+ url_title = data.get(ATTR_URL_TITLE, None)
+ priority = data.get(ATTR_PRIORITY, None)
+ retry = data.get(ATTR_PRIORITY, None)
+ expire = data.get(ATTR_EXPIRE, None)
+ callback_url = data.get(ATTR_CALLBACK_URL, None)
+ timestamp = data.get(ATTR_TIMESTAMP, None)
+ sound = data.get(ATTR_SOUND, None)
+ html = 1 if data.get(ATTR_HTML, False) else 0
- data["title"] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
-
- # Check for attachment.
- if ATTR_ATTACHMENT in data:
- # If attachment is a URL, use requests to open it as a stream.
- if data[ATTR_ATTACHMENT].startswith("http"):
+ image = data.get(ATTR_ATTACHMENT, None)
+ # Check for attachment
+ if image is not None:
+ # Only allow attachments from whitelisted paths, check valid path
+ if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]):
+ # try to open it as a normal file.
try:
- response = requests.get(
- data[ATTR_ATTACHMENT], stream=True, timeout=5
- )
- if response.status_code == 200:
- # Replace the attachment identifier with file object.
- data[ATTR_ATTACHMENT] = response.content
- else:
- _LOGGER.error(
- "Failed to download image %s, response code: %d",
- data[ATTR_ATTACHMENT],
- response.status_code,
- )
- # Remove attachment key to send without attachment.
- del data[ATTR_ATTACHMENT]
- except requests.exceptions.RequestException as ex_val:
+ file_handle = open(data[ATTR_ATTACHMENT], "rb")
+ # Replace the attachment identifier with file object.
+ image = file_handle
+ except OSError as ex_val:
_LOGGER.error(ex_val)
- # Remove attachment key to try sending without attachment
- del data[ATTR_ATTACHMENT]
- else:
- # Not a URL, check valid path first
- if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]):
- # try to open it as a normal file.
- try:
- file_handle = open(data[ATTR_ATTACHMENT], "rb")
- # Replace the attachment identifier with file object.
- data[ATTR_ATTACHMENT] = file_handle
- except OSError as ex_val:
- _LOGGER.error(ex_val)
- # Remove attachment key to send without attachment.
- del data[ATTR_ATTACHMENT]
- else:
- _LOGGER.error("Path is not whitelisted")
# Remove attachment key to send without attachment.
- del data[ATTR_ATTACHMENT]
+ image = None
+ else:
+ _LOGGER.error("Path is not whitelisted")
+ # Remove attachment key to send without attachment.
+ image = None
targets = kwargs.get(ATTR_TARGET)
@@ -101,12 +93,22 @@ class PushoverNotificationService(BaseNotificationService):
targets = [targets]
for target in targets:
- if target is not None:
- data["device"] = target
-
try:
- self.pushover.send_message(message, **data)
+ self.pushover.send_message(
+ self._user_key,
+ message,
+ target,
+ title,
+ url,
+ url_title,
+ image,
+ priority,
+ retry,
+ expire,
+ callback_url,
+ timestamp,
+ sound,
+ html,
+ )
except ValueError as val_err:
_LOGGER.error(val_err)
- except RequestError:
- _LOGGER.exception("Could not send pushover notification")
diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py
index fd4461e3e1b..579919821a3 100644
--- a/homeassistant/components/pyload/sensor.py
+++ b/homeassistant/components/pyload/sensor.py
@@ -16,6 +16,7 @@ from homeassistant.const import (
CONF_SSL,
CONF_USERNAME,
CONTENT_TYPE_JSON,
+ DATA_RATE_MEGABYTES_PER_SECOND,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -29,7 +30,7 @@ DEFAULT_PORT = 8000
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15)
-SENSOR_TYPES = {"speed": ["speed", "Speed", "MB/s"]}
+SENSOR_TYPES = {"speed": ["speed", "Speed", DATA_RATE_MEGABYTES_PER_SECOND]}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py
index 9544d74b1cd..46f82e99a62 100644
--- a/homeassistant/components/qbittorrent/sensor.py
+++ b/homeassistant/components/qbittorrent/sensor.py
@@ -11,6 +11,7 @@ from homeassistant.const import (
CONF_PASSWORD,
CONF_URL,
CONF_USERNAME,
+ DATA_RATE_KILOBYTES_PER_SECOND,
STATE_IDLE,
)
from homeassistant.exceptions import PlatformNotReady
@@ -27,8 +28,8 @@ DEFAULT_NAME = "qBittorrent"
SENSOR_TYPES = {
SENSOR_TYPE_CURRENT_STATUS: ["Status", None],
- SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", "kB/s"],
- SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", "kB/s"],
+ SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND],
+ SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py
index c3863bd0077..1ad53f4db48 100644
--- a/homeassistant/components/qnap/sensor.py
+++ b/homeassistant/components/qnap/sensor.py
@@ -16,6 +16,8 @@ from homeassistant.const import (
CONF_TIMEOUT,
CONF_USERNAME,
CONF_VERIFY_SSL,
+ DATA_GIBIBYTES,
+ DATA_RATE_MEBIBYTES_PER_SECOND,
TEMP_CELSIUS,
)
from homeassistant.exceptions import PlatformNotReady
@@ -62,22 +64,22 @@ _CPU_MON_COND = {
"cpu_usage": ["CPU Usage", "%", "mdi:chip"],
}
_MEMORY_MON_COND = {
- "memory_free": ["Memory Available", "GB", "mdi:memory"],
- "memory_used": ["Memory Used", "GB", "mdi:memory"],
+ "memory_free": ["Memory Available", DATA_GIBIBYTES, "mdi:memory"],
+ "memory_used": ["Memory Used", DATA_GIBIBYTES, "mdi:memory"],
"memory_percent_used": ["Memory Usage", "%", "mdi:memory"],
}
_NETWORK_MON_COND = {
"network_link_status": ["Network Link", None, "mdi:checkbox-marked-circle-outline"],
- "network_tx": ["Network Up", "MB/s", "mdi:upload"],
- "network_rx": ["Network Down", "MB/s", "mdi:download"],
+ "network_tx": ["Network Up", DATA_RATE_MEBIBYTES_PER_SECOND, "mdi:upload"],
+ "network_rx": ["Network Down", DATA_RATE_MEBIBYTES_PER_SECOND, "mdi:download"],
}
_DRIVE_MON_COND = {
"drive_smart_status": ["SMART Status", None, "mdi:checkbox-marked-circle-outline"],
"drive_temp": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"],
}
_VOLUME_MON_COND = {
- "volume_size_used": ["Used Space", "GB", "mdi:chart-pie"],
- "volume_size_free": ["Free Space", "GB", "mdi:chart-pie"],
+ "volume_size_used": ["Used Space", DATA_GIBIBYTES, "mdi:chart-pie"],
+ "volume_size_free": ["Free Space", DATA_GIBIBYTES, "mdi:chart-pie"],
"volume_percentage_used": ["Volume Used", "%", "mdi:chart-pie"],
}
@@ -270,7 +272,7 @@ class QNAPMemorySensor(QNAPSensor):
if self._api.data:
data = self._api.data["system_stats"]["memory"]
size = round_nicely(float(data["total"]) / 1024)
- return {ATTR_MEMORY_SIZE: f"{size} GB"}
+ return {ATTR_MEMORY_SIZE: f"{size} {DATA_GIBIBYTES}"}
class QNAPNetworkSensor(QNAPSensor):
@@ -399,4 +401,6 @@ class QNAPVolumeSensor(QNAPSensor):
data = self._api.data["volumes"][self.monitor_device]
total_gb = int(data["total_size"]) / 1024 / 1024 / 1024
- return {ATTR_VOLUME_SIZE: "{} GB".format(round_nicely(total_gb))}
+ return {
+ ATTR_VOLUME_SIZE: "{} {}".format(round_nicely(total_gb), DATA_GIBIBYTES)
+ }
diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json
index 6ea6db621fb..cc2cde26aa5 100644
--- a/homeassistant/components/qrcode/manifest.json
+++ b/homeassistant/components/qrcode/manifest.json
@@ -2,7 +2,10 @@
"domain": "qrcode",
"name": "QR Code",
"documentation": "https://www.home-assistant.io/integrations/qrcode",
- "requirements": ["pillow==6.2.1", "pyzbar==0.1.7"],
+ "requirements": [
+ "pillow==7.0.0",
+ "pyzbar==0.1.7"
+ ],
"dependencies": [],
"codeowners": []
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py
index 79e45ffd9a8..6cfdd53653d 100644
--- a/homeassistant/components/radarr/sensor.py
+++ b/homeassistant/components/radarr/sensor.py
@@ -14,6 +14,15 @@ from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_PORT,
CONF_SSL,
+ DATA_BYTES,
+ DATA_EXABYTES,
+ DATA_GIGABYTES,
+ DATA_KILOBYTES,
+ DATA_MEGABYTES,
+ DATA_PETABYTES,
+ DATA_TERABYTES,
+ DATA_YOTTABYTES,
+ DATA_ZETTABYTES,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -29,12 +38,12 @@ DEFAULT_HOST = "localhost"
DEFAULT_PORT = 7878
DEFAULT_URLBASE = ""
DEFAULT_DAYS = "1"
-DEFAULT_UNIT = "GB"
+DEFAULT_UNIT = DATA_GIGABYTES
SCAN_INTERVAL = timedelta(minutes=10)
SENSOR_TYPES = {
- "diskspace": ["Disk Space", "GB", "mdi:harddisk"],
+ "diskspace": ["Disk Space", DATA_GIGABYTES, "mdi:harddisk"],
"upcoming": ["Upcoming", "Movies", "mdi:television"],
"wanted": ["Wanted", "Movies", "mdi:television"],
"movies": ["Movies", "Movies", "mdi:television"],
@@ -51,7 +60,17 @@ ENDPOINTS = {
}
# Support to Yottabytes for the future, why not
-BYTE_SIZES = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
+BYTE_SIZES = [
+ DATA_BYTES,
+ DATA_KILOBYTES,
+ DATA_MEGABYTES,
+ DATA_GIGABYTES,
+ DATA_TERABYTES,
+ DATA_PETABYTES,
+ DATA_EXABYTES,
+ DATA_ZETTABYTES,
+ DATA_YOTTABYTES,
+]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json
index 59e4947f9bb..cb8e95df42f 100644
--- a/homeassistant/components/rainforest_eagle/manifest.json
+++ b/homeassistant/components/rainforest_eagle/manifest.json
@@ -2,7 +2,11 @@
"domain": "rainforest_eagle",
"name": "Rainforest Eagle-200",
"documentation": "https://www.home-assistant.io/integrations/rainforest_eagle",
- "requirements": ["eagle200_reader==0.2.1"],
+ "requirements": [
+ "eagle200_reader==0.2.1",
+ "uEagle==0.0.1"
+ ],
"dependencies": [],
- "codeowners": ["@gtdiehl"]
+ "codeowners": ["@gtdiehl",
+ "@jcalbert"]
}
diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py
index 315bbdff51b..99751e63f5b 100644
--- a/homeassistant/components/rainforest_eagle/sensor.py
+++ b/homeassistant/components/rainforest_eagle/sensor.py
@@ -4,6 +4,7 @@ import logging
from eagle200_reader import EagleReader
from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout
+from uEagle import Eagle as LegacyReader
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -49,6 +50,25 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
+def hwtest(cloud_id, install_code, ip_address):
+ """Try API call 'get_network_info' to see if target device is Legacy or Eagle-200."""
+ reader = LeagleReader(cloud_id, install_code, ip_address)
+ response = reader.get_network_info()
+
+ # Branch to test if target is Legacy Model
+ if "NetworkInfo" in response:
+ if response["NetworkInfo"].get("ModelId", None) == "Z109-EAGLE":
+ return reader
+
+ # Branch to test if target is Eagle-200 Model
+ if "Response" in response:
+ if response["Response"].get("Command", None) == "get_network_info":
+ return EagleReader(ip_address, cloud_id, install_code)
+
+ # Catch-all if hardware ID tests fail
+ raise ValueError("Couldn't determine device model.")
+
+
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the Eagle-200 sensor."""
ip_address = config[CONF_IP_ADDRESS]
@@ -56,7 +76,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
install_code = config[CONF_INSTALL_CODE]
try:
- eagle_reader = EagleReader(ip_address, cloud_id, install_code)
+ eagle_reader = hwtest(cloud_id, install_code, ip_address)
except (ConnectError, HTTPError, Timeout, ValueError) as error:
_LOGGER.error("Failed to connect during setup: %s", error)
return
@@ -138,3 +158,21 @@ class EagleData:
state = self.data.get(sensor_type)
_LOGGER.debug("Updating: %s - %s", sensor_type, state)
return state
+
+
+class LeagleReader(LegacyReader):
+ """Wraps uEagle to make it behave like eagle_reader, offering update()."""
+
+ def update(self):
+ """Fetch and return the four sensor values in a dict."""
+ out = {}
+
+ resp = self.get_instantaneous_demand()["InstantaneousDemand"]
+ out["instantanous_demand"] = resp["Demand"]
+
+ resp = self.get_current_summation()["CurrentSummation"]
+ out["summation_delivered"] = resp["SummationDelivered"]
+ out["summation_received"] = resp["SummationReceived"]
+ out["summation_total"] = out["summation_delivered"] - out["summation_received"]
+
+ return out
diff --git a/homeassistant/components/rainmachine/.translations/ca.json b/homeassistant/components/rainmachine/.translations/ca.json
index 60458f1469e..494b1ecc69c 100644
--- a/homeassistant/components/rainmachine/.translations/ca.json
+++ b/homeassistant/components/rainmachine/.translations/ca.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Aquest controlador RainMachine ja est\u00e0 configurat."
+ },
"error": {
"identifier_exists": "Aquest compte ja est\u00e0 registrat",
"invalid_credentials": "Credencials inv\u00e0lides"
diff --git a/homeassistant/components/rainmachine/.translations/da.json b/homeassistant/components/rainmachine/.translations/da.json
index 34f4fff4ed0..fe53a86993d 100644
--- a/homeassistant/components/rainmachine/.translations/da.json
+++ b/homeassistant/components/rainmachine/.translations/da.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Denne RainMachine-controller er allerede konfigureret."
+ },
"error": {
"identifier_exists": "Konto er allerede registreret",
"invalid_credentials": "Ugyldige legitimationsoplysninger"
diff --git a/homeassistant/components/rainmachine/.translations/de.json b/homeassistant/components/rainmachine/.translations/de.json
index c262fa5a652..257a0908c6a 100644
--- a/homeassistant/components/rainmachine/.translations/de.json
+++ b/homeassistant/components/rainmachine/.translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Dieser RainMachine-Kontroller ist bereits konfiguriert."
+ },
"error": {
"identifier_exists": "Konto bereits registriert",
"invalid_credentials": "Ung\u00fcltige Anmeldeinformationen"
diff --git a/homeassistant/components/rainmachine/.translations/en.json b/homeassistant/components/rainmachine/.translations/en.json
index 54b67066f2b..4ad5bfd7c0d 100644
--- a/homeassistant/components/rainmachine/.translations/en.json
+++ b/homeassistant/components/rainmachine/.translations/en.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "This RainMachine controller is already configured."
+ },
"error": {
"identifier_exists": "Account already registered",
"invalid_credentials": "Invalid credentials"
diff --git a/homeassistant/components/rainmachine/.translations/ko.json b/homeassistant/components/rainmachine/.translations/ko.json
index 4e2df2ca217..66d6cb0b740 100644
--- a/homeassistant/components/rainmachine/.translations/ko.json
+++ b/homeassistant/components/rainmachine/.translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uc774 RainMachine \ucee8\ud2b8\ub864\ub7ec\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
"error": {
"identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_credentials": "\ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
diff --git a/homeassistant/components/rainmachine/.translations/no.json b/homeassistant/components/rainmachine/.translations/no.json
index 5ec4e5fdc34..980c2c693ce 100644
--- a/homeassistant/components/rainmachine/.translations/no.json
+++ b/homeassistant/components/rainmachine/.translations/no.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Denne RainMachine-kontrolleren er allerede konfigurert."
+ },
"error": {
"identifier_exists": "Konto er allerede registrert",
"invalid_credentials": "Ugyldig legitimasjon"
diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json
index cf842efe9f6..5e813243f13 100644
--- a/homeassistant/components/rainmachine/.translations/pl.json
+++ b/homeassistant/components/rainmachine/.translations/pl.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ten kontroler RainMachine jest ju\u017c skonfigurowany."
+ },
"error": {
- "identifier_exists": "Konto jest ju\u017c zarejestrowane",
+ "identifier_exists": "Konto jest ju\u017c zarejestrowane.",
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce"
},
"step": {
diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json
index ca535663f54..e1bce5874e3 100644
--- a/homeassistant/components/rainmachine/.translations/ru.json
+++ b/homeassistant/components/rainmachine/.translations/ru.json
@@ -1,5 +1,8 @@
{
"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": {
"identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
diff --git a/homeassistant/components/rainmachine/.translations/zh-Hant.json b/homeassistant/components/rainmachine/.translations/zh-Hant.json
index 518cc54192f..3d9663a9a79 100644
--- a/homeassistant/components/rainmachine/.translations/zh-Hant.json
+++ b/homeassistant/components/rainmachine/.translations/zh-Hant.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u6b64 RainMachine \u63a7\u5236\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ },
"error": {
"identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a",
"invalid_credentials": "\u6191\u8b49\u7121\u6548"
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index ab56a5fc33b..af34d4dd9f6 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -59,14 +59,16 @@ SERVICE_PURGE_SCHEMA = vol.Schema(
DEFAULT_URL = "sqlite:///{hass_config_path}"
DEFAULT_DB_FILE = "home-assistant_v2.db"
+DEFAULT_DB_MAX_RETRIES = 10
+DEFAULT_DB_RETRY_WAIT = 3
CONF_DB_URL = "db_url"
+CONF_DB_MAX_RETRIES = "db_max_retries"
+CONF_DB_RETRY_WAIT = "db_retry_wait"
CONF_PURGE_KEEP_DAYS = "purge_keep_days"
CONF_PURGE_INTERVAL = "purge_interval"
CONF_EVENT_TYPES = "event_types"
-CONNECT_RETRY_WAIT = 3
-
FILTER_SCHEMA = vol.Schema(
{
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema(
@@ -96,6 +98,12 @@ CONFIG_SCHEMA = vol.Schema(
vol.Coerce(int), vol.Range(min=0)
),
vol.Optional(CONF_DB_URL): cv.string,
+ vol.Optional(
+ CONF_DB_MAX_RETRIES, default=DEFAULT_DB_MAX_RETRIES
+ ): cv.positive_int,
+ vol.Optional(
+ CONF_DB_RETRY_WAIT, default=DEFAULT_DB_RETRY_WAIT
+ ): cv.positive_int,
}
)
},
@@ -133,6 +141,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
conf = config[DOMAIN]
keep_days = conf.get(CONF_PURGE_KEEP_DAYS)
purge_interval = conf.get(CONF_PURGE_INTERVAL)
+ db_max_retries = conf[CONF_DB_MAX_RETRIES]
+ db_retry_wait = conf[CONF_DB_RETRY_WAIT]
db_url = conf.get(CONF_DB_URL, None)
if not db_url:
@@ -145,6 +155,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
keep_days=keep_days,
purge_interval=purge_interval,
uri=db_url,
+ db_max_retries=db_max_retries,
+ db_retry_wait=db_retry_wait,
include=include,
exclude=exclude,
)
@@ -174,6 +186,8 @@ class Recorder(threading.Thread):
keep_days: int,
purge_interval: int,
uri: str,
+ db_max_retries: int,
+ db_retry_wait: int,
include: Dict,
exclude: Dict,
) -> None:
@@ -186,6 +200,8 @@ class Recorder(threading.Thread):
self.queue: Any = queue.Queue()
self.recording_start = dt_util.utcnow()
self.db_url = uri
+ self.db_max_retries = db_max_retries
+ self.db_retry_wait = db_retry_wait
self.async_db_ready = asyncio.Future()
self.engine: Any = None
self.run_info: Any = None
@@ -217,9 +233,9 @@ class Recorder(threading.Thread):
tries = 1
connected = False
- while not connected and tries <= 10:
+ while not connected and tries <= self.db_max_retries:
if tries != 1:
- time.sleep(CONNECT_RETRY_WAIT)
+ time.sleep(self.db_retry_wait)
try:
self._setup_connection()
migration.migrate_schema(self)
@@ -230,7 +246,7 @@ class Recorder(threading.Thread):
_LOGGER.error(
"Error during connection setup: %s (retrying in %s seconds)",
err,
- CONNECT_RETRY_WAIT,
+ self.db_retry_wait,
)
tries += 1
@@ -337,9 +353,9 @@ class Recorder(threading.Thread):
tries = 1
updated = False
- while not updated and tries <= 10:
+ while not updated and tries <= self.db_max_retries:
if tries != 1:
- time.sleep(CONNECT_RETRY_WAIT)
+ time.sleep(self.db_retry_wait)
try:
with session_scope(session=self.get_session()) as session:
try:
@@ -367,7 +383,7 @@ class Recorder(threading.Thread):
"Error in database connectivity: %s. "
"(retrying in %s seconds)",
err,
- CONNECT_RETRY_WAIT,
+ self.db_retry_wait,
)
tries += 1
diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json
index 1c58366f6b5..f1687d73e04 100644
--- a/homeassistant/components/reddit/manifest.json
+++ b/homeassistant/components/reddit/manifest.json
@@ -2,7 +2,7 @@
"domain": "reddit",
"name": "Reddit",
"documentation": "https://www.home-assistant.io/integrations/reddit",
- "requirements": ["praw==6.5.0"],
+ "requirements": ["praw==6.5.1"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py
index a3feccaf10c..3f8bded6a85 100644
--- a/homeassistant/components/remote/__init__.py
+++ b/homeassistant/components/remote/__init__.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import functools as ft
import logging
+from typing import Any, Iterable
import voluptuous as vol
@@ -19,9 +20,10 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.loader import bind_hass
-# mypy: allow-untyped-defs, no-check-untyped-defs
+# mypy: allow-untyped-calls
_LOGGER = logging.getLogger(__name__)
@@ -57,12 +59,12 @@ REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema(
@bind_hass
-def is_on(hass, entity_id):
+def is_on(hass: HomeAssistantType, entity_id: str) -> bool:
"""Return if the remote is on based on the statemachine."""
return hass.states.is_state(entity_id, STATE_ON)
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Track states and offer events for remotes."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config)
@@ -111,24 +113,26 @@ class RemoteDevice(ToggleEntity):
"""Representation of a remote."""
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag supported features."""
return 0
- def send_command(self, command, **kwargs):
- """Send a command to a device."""
+ def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
+ """Send commands to a device."""
raise NotImplementedError()
- async def async_send_command(self, command, **kwargs):
- """Send a command to a device."""
+ async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
+ """Send commands to a device."""
+ assert self.hass is not None
await self.hass.async_add_executor_job(
ft.partial(self.send_command, command, **kwargs)
)
- def learn_command(self, **kwargs):
+ def learn_command(self, **kwargs: Any) -> None:
"""Learn a command from a device."""
raise NotImplementedError()
- async def async_learn_command(self, **kwargs):
+ async def async_learn_command(self, **kwargs: Any) -> None:
"""Learn a command from a device."""
+ assert self.hass is not None
await self.hass.async_add_executor_job(ft.partial(self.learn_command, **kwargs))
diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json
index 8c8b7f39609..fd7eea12f7e 100644
--- a/homeassistant/components/rest/manifest.json
+++ b/homeassistant/components/rest/manifest.json
@@ -2,7 +2,7 @@
"domain": "rest",
"name": "RESTful",
"documentation": "https://www.home-assistant.io/integrations/rest",
- "requirements": [],
+ "requirements": ["jsonpath==0.82", "xmltodict==0.12.0"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py
index 51120cb350c..70424325241 100644
--- a/homeassistant/components/rest/sensor.py
+++ b/homeassistant/components/rest/sensor.py
@@ -1,10 +1,13 @@
"""Support for RESTful API sensors."""
import json
import logging
+from xml.parsers.expat import ExpatError
+from jsonpath import jsonpath
import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol
+import xmltodict
from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA
from homeassistant.const import (
@@ -38,7 +41,9 @@ DEFAULT_VERIFY_SSL = True
DEFAULT_FORCE_UPDATE = False
DEFAULT_TIMEOUT = 10
+
CONF_JSON_ATTRS = "json_attributes"
+CONF_JSON_ATTRS_PATH = "json_attributes_path"
METHODS = ["POST", "GET"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -57,6 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_JSON_ATTRS_PATH): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
@@ -84,6 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
device_class = config.get(CONF_DEVICE_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE)
json_attrs = config.get(CONF_JSON_ATTRS)
+ json_attrs_path = config.get(CONF_JSON_ATTRS_PATH)
force_update = config.get(CONF_FORCE_UPDATE)
timeout = config.get(CONF_TIMEOUT)
@@ -120,6 +127,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
json_attrs,
force_update,
resource_template,
+ json_attrs_path,
)
],
True,
@@ -140,6 +148,7 @@ class RestSensor(Entity):
json_attrs,
force_update,
resource_template,
+ json_attrs_path,
):
"""Initialize the REST sensor."""
self._hass = hass
@@ -153,6 +162,7 @@ class RestSensor(Entity):
self._attributes = None
self._force_update = force_update
self._resource_template = resource_template
+ self._json_attrs_path = json_attrs_path
@property
def name(self):
@@ -191,12 +201,29 @@ class RestSensor(Entity):
self.rest.update()
value = self.rest.data
+ _LOGGER.debug("Data fetched from resource: %s", value)
+ content_type = self.rest.headers.get("content-type")
+
+ if content_type and content_type.startswith("text/xml"):
+ try:
+ value = json.dumps(xmltodict.parse(value))
+ _LOGGER.debug("JSON converted from XML: %s", value)
+ except ExpatError:
+ _LOGGER.warning(
+ "REST xml result could not be parsed and converted to JSON."
+ )
+ _LOGGER.debug("Erroneous XML: %s", value)
if self._json_attrs:
self._attributes = {}
if value:
try:
json_dict = json.loads(value)
+ if self._json_attrs_path is not None:
+ json_dict = jsonpath(json_dict, self._json_attrs_path)
+ # jsonpath will always store the result in json_dict[0]
+ # so the next line happens to work exactly as needed to
+ # find the result
if isinstance(json_dict, list):
json_dict = json_dict[0]
if isinstance(json_dict, dict):
@@ -240,6 +267,7 @@ class RestData:
self._verify_ssl = verify_ssl
self._timeout = timeout
self.data = None
+ self.headers = None
def set_url(self, url):
"""Set url."""
@@ -259,6 +287,8 @@ class RestData:
verify=self._verify_ssl,
)
self.data = response.text
+ self.headers = response.headers
except requests.exceptions.RequestException as ex:
_LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex)
self.data = None
+ self.headers = None
diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py
index 5db82a1d4e8..794542cb9d4 100644
--- a/homeassistant/components/rflink/cover.py
+++ b/homeassistant/components/rflink/cover.py
@@ -23,6 +23,8 @@ from . import (
_LOGGER = logging.getLogger(__name__)
+PARALLEL_UPDATES = 0
+
TYPE_STANDARD = "standard"
TYPE_INVERTED = "inverted"
diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py
index db616b92fc4..1ed19569585 100644
--- a/homeassistant/components/rflink/light.py
+++ b/homeassistant/components/rflink/light.py
@@ -31,6 +31,8 @@ from . import (
_LOGGER = logging.getLogger(__name__)
+PARALLEL_UPDATES = 0
+
TYPE_DIMMABLE = "dimmable"
TYPE_SWITCHABLE = "switchable"
TYPE_HYBRID = "hybrid"
diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py
index 8e0ce9a0c8e..990d76101cc 100644
--- a/homeassistant/components/rflink/switch.py
+++ b/homeassistant/components/rflink/switch.py
@@ -22,6 +22,8 @@ from . import (
_LOGGER = logging.getLogger(__name__)
+PARALLEL_UPDATES = 0
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(
diff --git a/homeassistant/components/ring/.translations/hu.json b/homeassistant/components/ring/.translations/hu.json
new file mode 100644
index 00000000000..578399c8152
--- /dev/null
+++ b/homeassistant/components/ring/.translations/hu.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.",
+ "unknown": "V\u00e1ratlan hiba"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "K\u00e9tfaktoros k\u00f3d"
+ },
+ "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s"
+ },
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "Bejelentkez\u00e9s a Ring fi\u00f3kkal"
+ }
+ },
+ "title": "Ring"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ring/.translations/nl.json b/homeassistant/components/ring/.translations/nl.json
index 1bb012bd25e..70736b15a9c 100644
--- a/homeassistant/components/ring/.translations/nl.json
+++ b/homeassistant/components/ring/.translations/nl.json
@@ -9,6 +9,9 @@
},
"step": {
"2fa": {
+ "data": {
+ "2fa": "Twee-factor code"
+ },
"title": "Tweestapsverificatie"
},
"user": {
diff --git a/homeassistant/components/ring/.translations/pl.json b/homeassistant/components/ring/.translations/pl.json
index f34903ff7d1..e592522c43b 100644
--- a/homeassistant/components/ring/.translations/pl.json
+++ b/homeassistant/components/ring/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
},
"error": {
"invalid_auth": "Niepoprawne uwierzytelnienie",
diff --git a/homeassistant/components/ring/.translations/sl.json b/homeassistant/components/ring/.translations/sl.json
new file mode 100644
index 00000000000..58e86634312
--- /dev/null
+++ b/homeassistant/components/ring/.translations/sl.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Naprava je \u017ee konfigurirana"
+ },
+ "error": {
+ "invalid_auth": "Neveljavna avtentikacija",
+ "unknown": "Nepri\u010dakovana napaka"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Dvofaktorska koda"
+ },
+ "title": "Dvofaktorska avtentikacija"
+ },
+ "user": {
+ "data": {
+ "password": "Geslo",
+ "username": "Uporabni\u0161ko ime"
+ },
+ "title": "Prijava s ra\u010dunom Ring"
+ }
+ },
+ "title": "Ring"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ring/.translations/sv.json b/homeassistant/components/ring/.translations/sv.json
index 54e9f5200f2..e92790740fb 100644
--- a/homeassistant/components/ring/.translations/sv.json
+++ b/homeassistant/components/ring/.translations/sv.json
@@ -4,6 +4,7 @@
"already_configured": "Enheten \u00e4r redan konfigurerad"
},
"error": {
+ "invalid_auth": "Ogiltig autentisering",
"unknown": "Ov\u00e4ntat fel"
},
"step": {
diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py
index 34aa9f6b0ec..0d54db5993f 100644
--- a/homeassistant/components/ring/__init__.py
+++ b/homeassistant/components/ring/__init__.py
@@ -9,12 +9,9 @@ from typing import Optional
from oauthlib.oauth2 import AccessDeniedError
import requests
from ring_doorbell import Auth, Ring
-import voluptuous as vol
-from homeassistant import config_entries
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__
+from homeassistant.const import __version__
from homeassistant.core import HomeAssistant, callback
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util.async_ import run_callback_threadsafe
@@ -30,18 +27,6 @@ DEFAULT_ENTITY_NAMESPACE = "ring"
PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera")
-CONFIG_SCHEMA = vol.Schema(
- {
- vol.Optional(DOMAIN): vol.Schema(
- {
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- }
- )
- },
- extra=vol.ALLOW_EXTRA,
-)
-
async def async_setup(hass, config):
"""Set up the Ring component."""
@@ -56,16 +41,6 @@ async def async_setup(hass, config):
await hass.async_add_executor_job(legacy_cleanup)
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_IMPORT},
- data={
- "username": config[DOMAIN]["username"],
- "password": config[DOMAIN]["password"],
- },
- )
- )
return True
diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py
index a25e0283753..fd9dbe0a17e 100644
--- a/homeassistant/components/ring/config_flow.py
+++ b/homeassistant/components/ring/config_flow.py
@@ -75,13 +75,6 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="2fa", data_schema=vol.Schema({"2fa": str}),
)
- async def async_step_import(self, user_input):
- """Handle import."""
- if self._async_current_entries():
- return self.async_abort(reason="already_configured")
-
- return await self.async_step_user(user_input)
-
class Require2FA(exceptions.HomeAssistantError):
"""Error to indicate we require 2FA."""
diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py
index 8df1191a420..507e3c133cc 100644
--- a/homeassistant/components/rmvtransport/sensor.py
+++ b/homeassistant/components/rmvtransport/sensor.py
@@ -233,7 +233,7 @@ class RMVDepartureData:
)
except RMVtransportApiConnectionError:
self.departures = []
- _LOGGER.warning("Could not retrive data from rmv.de")
+ _LOGGER.warning("Could not retrieve data from rmv.de")
return
self.station = _data.get("station")
_deps = []
diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json
index b92a95af9d7..ba67f61b2ee 100644
--- a/homeassistant/components/roku/manifest.json
+++ b/homeassistant/components/roku/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["roku==4.0.0"],
"dependencies": [],
+ "after_dependencies": ["discovery"],
"codeowners": []
}
diff --git a/homeassistant/components/rpi_gpio_pwm/light.py b/homeassistant/components/rpi_gpio_pwm/light.py
index aededbc676c..96ac3c6f2ed 100644
--- a/homeassistant/components/rpi_gpio_pwm/light.py
+++ b/homeassistant/components/rpi_gpio_pwm/light.py
@@ -19,7 +19,7 @@ from homeassistant.components.light import (
SUPPORT_TRANSITION,
Light,
)
-from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE, STATE_ON
+from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_TYPE, STATE_ON
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.color as color_util
@@ -58,6 +58,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES),
vol.Optional(CONF_FREQUENCY): cv.positive_int,
vol.Optional(CONF_ADDRESS): cv.byte,
+ vol.Optional(CONF_HOST): cv.string,
}
],
)
@@ -76,6 +77,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if CONF_FREQUENCY in led_conf:
opt_args["freq"] = led_conf[CONF_FREQUENCY]
if driver_type == CONF_DRIVER_GPIO:
+ if CONF_HOST in led_conf:
+ opt_args["host"] = led_conf[CONF_HOST]
driver = GpioDriver(pins, **opt_args)
elif driver_type == CONF_DRIVER_PCA9685:
if CONF_ADDRESS in led_conf:
diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json
index 688cad8324e..46fe96a6426 100644
--- a/homeassistant/components/rpi_gpio_pwm/manifest.json
+++ b/homeassistant/components/rpi_gpio_pwm/manifest.json
@@ -2,7 +2,7 @@
"domain": "rpi_gpio_pwm",
"name": "pigpio Daemon PWM LED",
"documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm",
- "requirements": ["pwmled==1.4.1"],
+ "requirements": ["pwmled==1.5.0"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py
index 4ae272ca9bd..c6833fcfda0 100644
--- a/homeassistant/components/rtorrent/sensor.py
+++ b/homeassistant/components/rtorrent/sensor.py
@@ -9,6 +9,7 @@ from homeassistant.const import (
CONF_MONITORED_VARIABLES,
CONF_NAME,
CONF_URL,
+ DATA_RATE_KILOBYTES_PER_SECOND,
STATE_IDLE,
)
from homeassistant.exceptions import PlatformNotReady
@@ -24,8 +25,8 @@ SENSOR_TYPE_UPLOAD_SPEED = "upload_speed"
DEFAULT_NAME = "rtorrent"
SENSOR_TYPES = {
SENSOR_TYPE_CURRENT_STATUS: ["Status", None],
- SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", "kB/s"],
- SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", "kB/s"],
+ SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND],
+ SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py
index f436bcb8a72..b36abbedb48 100644
--- a/homeassistant/components/sabnzbd/__init__.py
+++ b/homeassistant/components/sabnzbd/__init__.py
@@ -14,6 +14,9 @@ from homeassistant.const import (
CONF_PORT,
CONF_SENSORS,
CONF_SSL,
+ DATA_GIGABYTES,
+ DATA_MEGABYTES,
+ DATA_RATE_MEGABYTES_PER_SECOND,
)
from homeassistant.core import callback
from homeassistant.helpers import discovery
@@ -49,16 +52,16 @@ SIGNAL_SABNZBD_UPDATED = "sabnzbd_updated"
SENSOR_TYPES = {
"current_status": ["Status", None, "status"],
- "speed": ["Speed", "MB/s", "kbpersec"],
- "queue_size": ["Queue", "MB", "mb"],
- "queue_remaining": ["Left", "MB", "mbleft"],
- "disk_size": ["Disk", "GB", "diskspacetotal1"],
- "disk_free": ["Disk Free", "GB", "diskspace1"],
+ "speed": ["Speed", DATA_RATE_MEGABYTES_PER_SECOND, "kbpersec"],
+ "queue_size": ["Queue", DATA_MEGABYTES, "mb"],
+ "queue_remaining": ["Left", DATA_MEGABYTES, "mbleft"],
+ "disk_size": ["Disk", DATA_GIGABYTES, "diskspacetotal1"],
+ "disk_free": ["Disk Free", DATA_GIGABYTES, "diskspace1"],
"queue_count": ["Queue Count", None, "noofslots_total"],
- "day_size": ["Daily Total", "GB", "day_size"],
- "week_size": ["Weekly Total", "GB", "week_size"],
- "month_size": ["Monthly Total", "GB", "month_size"],
- "total_size": ["Total", "GB", "total_size"],
+ "day_size": ["Daily Total", DATA_GIGABYTES, "day_size"],
+ "week_size": ["Weekly Total", DATA_GIGABYTES, "week_size"],
+ "month_size": ["Monthly Total", DATA_GIGABYTES, "month_size"],
+ "total_size": ["Total", DATA_GIGABYTES, "total_size"],
}
SPEED_LIMIT_SCHEMA = vol.Schema(
diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json
index 78cfd4aa1f0..6fec5c008b3 100644
--- a/homeassistant/components/sabnzbd/manifest.json
+++ b/homeassistant/components/sabnzbd/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/sabnzbd",
"requirements": ["pysabnzbd==1.1.0"],
"dependencies": ["configurator"],
+ "after_dependencies": ["discovery"],
"codeowners": []
}
diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py
index 30399640088..797780d562a 100644
--- a/homeassistant/components/saj/sensor.py
+++ b/homeassistant/components/saj/sensor.py
@@ -217,7 +217,7 @@ class SAJsensor(Entity):
@property
def per_total_basis(self) -> bool:
- """Return if the sensors value is cummulative or not."""
+ """Return if the sensors value is cumulative or not."""
return self._sensor.per_total_basis
@property
diff --git a/homeassistant/components/salt/__init__.py b/homeassistant/components/salt/__init__.py
new file mode 100644
index 00000000000..29c371ece52
--- /dev/null
+++ b/homeassistant/components/salt/__init__.py
@@ -0,0 +1 @@
+"""The salt component."""
diff --git a/homeassistant/components/salt/device_tracker.py b/homeassistant/components/salt/device_tracker.py
new file mode 100644
index 00000000000..7c03403622a
--- /dev/null
+++ b/homeassistant/components/salt/device_tracker.py
@@ -0,0 +1,71 @@
+"""Support for Salt Fiber Box routers."""
+import logging
+
+from saltbox import RouterLoginException, RouterNotReachableException, SaltBox
+import voluptuous as vol
+
+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__)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ }
+)
+
+
+def get_scanner(hass, config):
+ """Return the Salt device scanner."""
+ scanner = SaltDeviceScanner(config[DOMAIN])
+
+ # Test whether the router is accessible.
+ data = scanner.get_salt_data()
+ return scanner if data is not None else None
+
+
+class SaltDeviceScanner(DeviceScanner):
+ """This class queries a Salt Fiber Box router."""
+
+ def __init__(self, config):
+ """Initialize the scanner."""
+ host = config[CONF_HOST]
+ username = config[CONF_USERNAME]
+ password = config[CONF_PASSWORD]
+ self.saltbox = SaltBox(f"http://{host}", username, password)
+ self.online_clients = []
+
+ def scan_devices(self):
+ """Scan for new devices and return a list with found device IDs."""
+ self._update_info()
+ return [client["mac"] for client in self.online_clients]
+
+ def get_device_name(self, device):
+ """Return the name of the given device or None if we don't know."""
+ for client in self.online_clients:
+ if client["mac"] == device:
+ return client["name"]
+ return None
+
+ def get_salt_data(self):
+ """Retrieve data from Salt router and return parsed result."""
+ try:
+ clients = self.saltbox.get_online_clients()
+ return clients
+ except (RouterLoginException, RouterNotReachableException) as error:
+ _LOGGER.warning(error)
+ return None
+
+ def _update_info(self):
+ """Pull the current information from the Salt router."""
+ _LOGGER.debug("Loading data from Salt Fiber Box")
+ data = self.get_salt_data()
+ self.online_clients = data or []
diff --git a/homeassistant/components/salt/manifest.json b/homeassistant/components/salt/manifest.json
new file mode 100644
index 00000000000..019fdf9ae5f
--- /dev/null
+++ b/homeassistant/components/salt/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "salt",
+ "name": "Salt Fiber Box",
+ "documentation": "https://www.home-assistant.io/integrations/salt",
+ "requirements": ["saltbox==0.1.3"],
+ "dependencies": [],
+ "codeowners": ["@bjornorri"]
+}
diff --git a/homeassistant/components/samsungtv/.translations/ca.json b/homeassistant/components/samsungtv/.translations/ca.json
index d938373797e..7ca5879a5c0 100644
--- a/homeassistant/components/samsungtv/.translations/ca.json
+++ b/homeassistant/components/samsungtv/.translations/ca.json
@@ -5,8 +5,10 @@
"already_in_progress": "La configuraci\u00f3 de la Samsung TV ja est\u00e0 en curs.",
"auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquesta Samsung TV.",
"not_found": "No s'han trobat Samsung TV's compatibles a la xarxa.",
+ "not_successful": "No s'ha pogut connectar amb el dispositiu Samsung TV.",
"not_supported": "Actualment aquest dispositiu Samsung TV no \u00e9s compatible."
},
+ "flow_title": "Samsung TV: {model}",
"step": {
"confirm": {
"description": "Vols configurar la Samsung TV {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la TV demanant autenticaci\u00f3. Les configuracuons manuals d'aquesta TV es sobreescriuran.",
diff --git a/homeassistant/components/samsungtv/.translations/da.json b/homeassistant/components/samsungtv/.translations/da.json
index 117069eb016..379fd5d8b6d 100644
--- a/homeassistant/components/samsungtv/.translations/da.json
+++ b/homeassistant/components/samsungtv/.translations/da.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "Dette Samsung-tv er allerede konfigureret.",
"already_in_progress": "Samsung-tv-konfiguration er allerede i gang.",
- "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv.",
+ "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv. Tjek dit tvs indstillinger for at godkende Home Assistant.",
"not_found": "Der blev ikke fundet nogen underst\u00f8ttede Samsung-tv-enheder p\u00e5 netv\u00e6rket.",
"not_successful": "Kan ikke oprette forbindelse til denne Samsung tv-enhed.",
"not_supported": "Dette Samsung TV underst\u00f8ttes i \u00f8jeblikket ikke."
diff --git a/homeassistant/components/samsungtv/.translations/de.json b/homeassistant/components/samsungtv/.translations/de.json
index 60372837ffc..27b9ecc37df 100644
--- a/homeassistant/components/samsungtv/.translations/de.json
+++ b/homeassistant/components/samsungtv/.translations/de.json
@@ -3,10 +3,12 @@
"abort": {
"already_configured": "Dieser Samsung TV ist bereits konfiguriert",
"already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.",
- "auth_missing": "Home Assistant ist nicht authentifiziert, um eine Verbindung zu diesem Samsung TV herzustellen.",
+ "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.",
"not_found": "Keine unterst\u00fctzten Samsung TV-Ger\u00e4te im Netzwerk gefunden.",
+ "not_successful": "Es kann keine Verbindung zu diesem Samsung-Fernsehger\u00e4t hergestellt werden.",
"not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt."
},
+ "flow_title": "Samsung TV: {model}",
"step": {
"confirm": {
"description": "M\u00f6chtest du Samsung TV {model} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.",
@@ -17,7 +19,7 @@
"host": "Host oder IP-Adresse",
"name": "Name"
},
- "description": "Gebe deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt.",
+ "description": "Gib deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt.",
"title": "Samsung TV"
}
},
diff --git a/homeassistant/components/samsungtv/.translations/es.json b/homeassistant/components/samsungtv/.translations/es.json
index 3535d4bc65f..4466b329a2a 100644
--- a/homeassistant/components/samsungtv/.translations/es.json
+++ b/homeassistant/components/samsungtv/.translations/es.json
@@ -5,8 +5,10 @@
"already_in_progress": "La configuraci\u00f3n del televisor Samsung ya est\u00e1 en progreso.",
"auth_missing": "Home Assistant no est\u00e1 autenticado para conectarse a este televisor Samsung.",
"not_found": "No se encontraron televisiones Samsung compatibles en la red.",
+ "not_successful": "No se puede conectar a este dispositivo Samsung TV.",
"not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible."
},
+ "flow_title": "Televisor Samsung: {model}",
"step": {
"confirm": {
"description": "\u00bfDesea configurar el televisor Samsung {model} ? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.",
diff --git a/homeassistant/components/samsungtv/.translations/fr.json b/homeassistant/components/samsungtv/.translations/fr.json
index b880e41e5df..e381660a3e2 100644
--- a/homeassistant/components/samsungtv/.translations/fr.json
+++ b/homeassistant/components/samsungtv/.translations/fr.json
@@ -5,8 +5,10 @@
"already_in_progress": "La configuration du t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 en cours.",
"auth_missing": "Home Assistant n'est pas authentifi\u00e9 pour se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung.",
"not_found": "Aucun t\u00e9l\u00e9viseur Samsung pris en charge trouv\u00e9 sur le r\u00e9seau.",
+ "not_successful": "Impossible de se connecter \u00e0 cet appareil Samsung TV.",
"not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge."
},
+ "flow_title": "Samsung TV: {model}",
"step": {
"confirm": {
"description": "Voulez vous installer la TV {model} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.",
diff --git a/homeassistant/components/samsungtv/.translations/hu.json b/homeassistant/components/samsungtv/.translations/hu.json
index 6d816ecb95a..c7a046428bc 100644
--- a/homeassistant/components/samsungtv/.translations/hu.json
+++ b/homeassistant/components/samsungtv/.translations/hu.json
@@ -1,14 +1,28 @@
{
"config": {
"abort": {
+ "already_configured": "Ez a Samsung TV m\u00e1r konfigur\u00e1lva van.",
+ "already_in_progress": "A Samsung TV konfigur\u00e1l\u00e1sa m\u00e1r folyamatban van.",
+ "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV-k\u00e9sz\u00fcl\u00e9k\u00e9ben a Home Assistant enged\u00e9lyez\u00e9si be\u00e1ll\u00edt\u00e1sait.",
"not_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 t\u00e1mogatott Samsung TV-eszk\u00f6z.",
+ "not_successful": "Nem lehet csatlakozni ehhez a Samsung TV k\u00e9sz\u00fcl\u00e9khez.",
"not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott."
},
+ "flow_title": "Samsung TV: {model}",
"step": {
"confirm": {
"description": "Be\u00e1ll\u00edtja a Samsung TV {model} k\u00e9sz\u00fcl\u00e9ket? Ha soha nem csatlakozott home assistant-hez ezel\u0151tt, meg kell jelennie egy felugr\u00f3 ablaknak a TV-ben, ahol hiteles\u00edt\u00e9st k\u00e9r. A tv-k\u00e9sz\u00fcl\u00e9k manu\u00e1lis konfigur\u00e1ci\u00f3i fel\u00fcl\u00edr\u00f3dnak.",
"title": "Samsung TV"
+ },
+ "user": {
+ "data": {
+ "host": "Hosztn\u00e9v vagy IP c\u00edm",
+ "name": "N\u00e9v"
+ },
+ "description": "\u00cdrja be a Samsung TV adatait. Ha soha nem csatlakoztatta a Home Assistant alkalmaz\u00e1st ezel\u0151tt, l\u00e1tnia kell a t\u00e9v\u00e9ben egy felugr\u00f3 ablakot, amely enged\u00e9lyt k\u00e9r.",
+ "title": "Samsung TV"
}
- }
+ },
+ "title": "Samsung TV"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/samsungtv/.translations/it.json b/homeassistant/components/samsungtv/.translations/it.json
index c783db24720..3d2d4dd8e11 100644
--- a/homeassistant/components/samsungtv/.translations/it.json
+++ b/homeassistant/components/samsungtv/.translations/it.json
@@ -3,13 +3,15 @@
"abort": {
"already_configured": "Questo Samsung TV \u00e8 gi\u00e0 configurato.",
"already_in_progress": "La configurazione di Samsung TV \u00e8 gi\u00e0 in corso.",
- "auth_missing": "Home Assistant non \u00e8 autenticato per connettersi a questo Samsung TV.",
+ "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo Samsung TV. Controlla le impostazioni del tuo TV per autorizzare Home Assistant.",
"not_found": "Nessun dispositivo Samsung TV supportato trovato sulla rete.",
+ "not_successful": "Impossibile connettersi a questo dispositivo Samsung TV.",
"not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato."
},
+ "flow_title": "Samsung TV: {model}",
"step": {
"confirm": {
- "description": "Vuoi configurare Samsung TV {model} ? Se non hai mai collegato Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione. Le configurazioni manuali per questo televisore verranno sovrascritte.",
+ "description": "Vuoi configurare Samsung TV {model}? Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul tuo TV in cui \u00e8 richiesta l'autorizzazione. Le configurazioni manuali per questo TV verranno sovrascritte.",
"title": "Samsung TV"
},
"user": {
@@ -17,7 +19,7 @@
"host": "Host o indirizzo IP",
"name": "Nome"
},
- "description": "Inserisci le informazioni del tuo Samsung TV. Se non hai mai connesso Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione.",
+ "description": "Inserisci le informazioni del tuo Samsung TV. Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul TV in cui \u00e8 richiesta l'autorizzazione.",
"title": "Samsung TV"
}
},
diff --git a/homeassistant/components/samsungtv/.translations/ko.json b/homeassistant/components/samsungtv/.translations/ko.json
index f7656eb9035..0226fd52dc0 100644
--- a/homeassistant/components/samsungtv/.translations/ko.json
+++ b/homeassistant/components/samsungtv/.translations/ko.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "\uc774 \uc0bc\uc131 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.",
- "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
+ "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant \ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.",
"not_found": "\uc9c0\uc6d0\ub418\ub294 \uc0bc\uc131 TV \ubaa8\ub378\uc774 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
"not_successful": "\uc0bc\uc131 TV \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
diff --git a/homeassistant/components/samsungtv/.translations/nl.json b/homeassistant/components/samsungtv/.translations/nl.json
index 93bb5953e31..09c0bba05a3 100644
--- a/homeassistant/components/samsungtv/.translations/nl.json
+++ b/homeassistant/components/samsungtv/.translations/nl.json
@@ -1,12 +1,17 @@
{
"config": {
"abort": {
- "auth_missing": "Home Assistant is niet geverifieerd om verbinding te maken met deze Samsung TV.",
+ "already_configured": "Deze Samsung TV is al geconfigureerd.",
+ "already_in_progress": "Samsung TV configuratie is al in uitvoering.",
+ "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV.",
"not_found": "Geen ondersteunde Samsung TV-apparaten gevonden op het netwerk.",
- "not_supported": "Deze Samsung TV-apparaten wordt momenteel niet ondersteund."
+ "not_successful": "Niet in staat om verbinding te maken met dit Samsung TV toestel.",
+ "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund."
},
+ "flow_title": "Samsung TV: {model}",
"step": {
"confirm": {
+ "description": "Wilt u Samsung TV {model} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven",
"title": "Samsung TV"
},
"user": {
@@ -14,6 +19,7 @@
"host": "Hostnaam of IP-adres",
"name": "Naam"
},
+ "description": "Voer uw Samsung TV informatie in. Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt.",
"title": "Samsung TV"
}
},
diff --git a/homeassistant/components/samsungtv/.translations/sl.json b/homeassistant/components/samsungtv/.translations/sl.json
new file mode 100644
index 00000000000..95286476ed0
--- /dev/null
+++ b/homeassistant/components/samsungtv/.translations/sl.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ta televizor Samsung je \u017ee konfiguriran.",
+ "already_in_progress": "Konfiguracija Samsung TV je \u017ee v teku.",
+ "auth_missing": "Home Assistant nima dovoljenja za povezavo s tem televizorjem Samsung. Preverite nastavitve televizorja, da ga pooblastite.",
+ "not_found": "V omre\u017eju ni bilo najdenih nobenih podprtih naprav Samsung TV.",
+ "not_successful": "Povezave s to napravo Samsung TV ni mogo\u010de vzpostaviti.",
+ "not_supported": "Ta naprava Samsung TV trenutno ni podprta."
+ },
+ "flow_title": "Samsung TV: {model}",
+ "step": {
+ "confirm": {
+ "description": "Vnesite podatke o televizorju Samsung. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje. Ro\u010dna konfiguracija za ta TV bo prepisana.",
+ "title": "Samsung TV"
+ },
+ "user": {
+ "data": {
+ "host": "Gostitelj ali IP naslov",
+ "name": "Ime"
+ },
+ "description": "Vnesite podatke o televizorju Samsung. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje.",
+ "title": "Samsung TV"
+ }
+ },
+ "title": "Samsung TV"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/samsungtv/.translations/tr.json b/homeassistant/components/samsungtv/.translations/tr.json
new file mode 100644
index 00000000000..3cf1f135e1f
--- /dev/null
+++ b/homeassistant/components/samsungtv/.translations/tr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bu Samsung TV zaten ayarlanm\u0131\u015f.",
+ "already_in_progress": "Samsung TV ayar\u0131 zaten s\u00fcr\u00fcyor.",
+ "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.",
+ "not_found": "A\u011fda desteklenen Samsung TV cihaz\u0131 bulunamad\u0131.",
+ "not_successful": "Bu Samsung TV cihaz\u0131na ba\u011flan\u0131lam\u0131yor.",
+ "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor."
+ },
+ "flow_title": "Samsung TV: {model}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host veya IP adresi",
+ "name": "Ad"
+ },
+ "description": "Samsung TV bilgilerini gir. Daha \u00f6nce hi\u00e7 Home Assistant'a ba\u011flamad\u0131ysan, TV'nde izin isteyen bir pencere g\u00f6receksindir.",
+ "title": "Samsung TV"
+ }
+ },
+ "title": "Samsung TV"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py
index debe7349b6c..e52123297ab 100644
--- a/homeassistant/components/samsungtv/config_flow.py
+++ b/homeassistant/components/samsungtv/config_flow.py
@@ -5,6 +5,7 @@ from urllib.parse import urlparse
from samsungctl import Remote
from samsungctl.exceptions import AccessDenied, UnhandledResponse
import voluptuous as vol
+from websocket import WebSocketException
from homeassistant import config_entries
from homeassistant.components.ssdp import (
@@ -23,7 +24,7 @@ from homeassistant.const import (
)
# pylint:disable=unused-import
-from .const import CONF_MANUFACTURER, CONF_MODEL, DOMAIN, LOGGER, METHODS
+from .const import CONF_MANUFACTURER, CONF_MODEL, DOMAIN, LOGGER
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str})
@@ -32,6 +33,12 @@ RESULT_SUCCESS = "success"
RESULT_NOT_SUCCESSFUL = "not_successful"
RESULT_NOT_SUPPORTED = "not_supported"
+SUPPORTED_METHODS = (
+ {"method": "websocket", "timeout": 1},
+ # We need this high timeout because waiting for auth popup is just an open socket
+ {"method": "legacy", "timeout": 31},
+)
+
def _get_ip(host):
if host is None:
@@ -76,27 +83,25 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def _try_connect(self):
"""Try to connect and check auth."""
- for method in METHODS:
+ for cfg in SUPPORTED_METHODS:
config = {
"name": "HomeAssistant",
"description": "HomeAssistant",
"id": "ha.component.samsung",
"host": self._host,
- "method": method,
"port": self._port,
- # We need this high timeout because waiting for auth popup is just an open socket
- "timeout": 31,
}
+ config.update(cfg)
try:
LOGGER.debug("Try config: %s", config)
with Remote(config.copy()):
LOGGER.debug("Working config: %s", config)
- self._method = method
+ self._method = cfg["method"]
return RESULT_SUCCESS
except AccessDenied:
LOGGER.debug("Working but denied config: %s", config)
return RESULT_AUTH_MISSING
- except UnhandledResponse:
+ except (UnhandledResponse, WebSocketException):
LOGGER.debug("Working but unsupported config: %s", config)
return RESULT_NOT_SUPPORTED
except OSError as err:
diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py
index ea893390a5b..46f6fb59a8c 100644
--- a/homeassistant/components/samsungtv/const.py
+++ b/homeassistant/components/samsungtv/const.py
@@ -9,5 +9,3 @@ DEFAULT_NAME = "Samsung TV"
CONF_MANUFACTURER = "manufacturer"
CONF_MODEL = "model"
CONF_ON_ACTION = "turn_on_action"
-
-METHODS = ("websocket", "legacy")
diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json
index e0800cdef27..90352bbd108 100644
--- a/homeassistant/components/scrape/manifest.json
+++ b/homeassistant/components/scrape/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/scrape",
"requirements": ["beautifulsoup4==4.8.2"],
"dependencies": [],
+ "after_dependencies": ["rest"],
"codeowners": ["@fabaff"]
}
diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py
index 44684656372..0a7b8596248 100644
--- a/homeassistant/components/script/__init__.py
+++ b/homeassistant/components/script/__init__.py
@@ -9,6 +9,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_NAME,
CONF_ALIAS,
+ CONF_ICON,
EVENT_SCRIPT_STARTED,
SERVICE_RELOAD,
SERVICE_TOGGLE,
@@ -42,7 +43,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCRIPT_ENTRY_SCHEMA = vol.Schema(
{
- CONF_ALIAS: cv.string,
+ vol.Optional(CONF_ALIAS): cv.string,
+ vol.Optional(CONF_ICON): cv.icon,
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DESCRIPTION, default=""): cv.string,
vol.Optional(CONF_FIELDS, default={}): {
@@ -207,9 +209,15 @@ async def _async_process_config(hass, config, component):
scripts = []
for object_id, cfg in config.get(DOMAIN, {}).items():
- alias = cfg.get(CONF_ALIAS, object_id)
- script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE])
- scripts.append(script)
+ scripts.append(
+ ScriptEntity(
+ hass,
+ object_id,
+ cfg.get(CONF_ALIAS, object_id),
+ cfg.get(CONF_ICON),
+ cfg[CONF_SEQUENCE],
+ )
+ )
hass.services.async_register(
DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA
)
@@ -227,9 +235,12 @@ async def _async_process_config(hass, config, component):
class ScriptEntity(ToggleEntity):
"""Representation of a script entity."""
- def __init__(self, hass, object_id, name, sequence):
+ icon = None
+
+ def __init__(self, hass, object_id, name, icon, sequence):
"""Initialize the script."""
self.object_id = object_id
+ self.icon = icon
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self.script = Script(hass, sequence, name, self.async_update_ha_state)
diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json
index 8a87205d4b7..900fe9252b4 100644
--- a/homeassistant/components/sendgrid/manifest.json
+++ b/homeassistant/components/sendgrid/manifest.json
@@ -2,7 +2,7 @@
"domain": "sendgrid",
"name": "SendGrid",
"documentation": "https://www.home-assistant.io/integrations/sendgrid",
- "requirements": ["sendgrid==6.1.0"],
+ "requirements": ["sendgrid==6.1.1"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/sensor/.translations/sv.json b/homeassistant/components/sensor/.translations/sv.json
new file mode 100644
index 00000000000..90001148f12
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/sv.json
@@ -0,0 +1,26 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_battery_level": "Aktuell {entity_name} batteriniv\u00e5",
+ "is_humidity": "Aktuell {entity_name} fuktighet",
+ "is_illuminance": "Aktuell {entity_name} belysning",
+ "is_power": "Aktuell {entity_name} str\u00f6m",
+ "is_pressure": "Aktuellt {entity_name} tryck",
+ "is_signal_strength": "Aktuell {entity_name} signalstyrka",
+ "is_temperature": "Aktuell {entity_name} temperatur",
+ "is_timestamp": "Aktuell {entity_name} tidsst\u00e4mpel",
+ "is_value": "Aktuellt {entity_name} v\u00e4rde"
+ },
+ "trigger_type": {
+ "battery_level": "{entity_name} batteriniv\u00e5 \u00e4ndras",
+ "humidity": "{entity_name} fuktighet \u00e4ndras",
+ "illuminance": "{entity_name} belysning \u00e4ndras",
+ "power": "{entity_name} str\u00f6mf\u00f6r\u00e4ndringar",
+ "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar",
+ "signal_strength": "{entity_name} signalstyrka \u00e4ndras",
+ "temperature": "{entity_name} temperaturf\u00f6r\u00e4ndringar",
+ "timestamp": "{entity_name} tidst\u00e4mpel \u00e4ndras",
+ "value": "{entity_name} v\u00e4rde \u00e4ndras"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/.translations/af.json b/homeassistant/components/sentry/.translations/af.json
new file mode 100644
index 00000000000..61ef8f8d389
--- /dev/null
+++ b/homeassistant/components/sentry/.translations/af.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Sentry"
+ }
+ },
+ "title": "Sentry"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/.translations/de.json b/homeassistant/components/sentry/.translations/de.json
index ea1e3f674ae..db71d8818bc 100644
--- a/homeassistant/components/sentry/.translations/de.json
+++ b/homeassistant/components/sentry/.translations/de.json
@@ -9,7 +9,7 @@
},
"step": {
"user": {
- "description": "Gebe deine Sentry-DSN ein",
+ "description": "Gib deine Sentry-DSN ein",
"title": "Sentry"
}
},
diff --git a/homeassistant/components/sentry/.translations/hu.json b/homeassistant/components/sentry/.translations/hu.json
new file mode 100644
index 00000000000..64318828e6d
--- /dev/null
+++ b/homeassistant/components/sentry/.translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az Sentry m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "bad_dsn": "\u00c9rv\u00e9nytelen DSN",
+ "unknown": "V\u00e1ratlan hiba"
+ },
+ "step": {
+ "user": {
+ "description": "Add meg a Sentry DSN-t",
+ "title": "Sentry"
+ }
+ },
+ "title": "Sentry"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/.translations/nl.json b/homeassistant/components/sentry/.translations/nl.json
index 7e198e836d7..67bd1ea54e2 100644
--- a/homeassistant/components/sentry/.translations/nl.json
+++ b/homeassistant/components/sentry/.translations/nl.json
@@ -1,7 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "Sentry is al geconfigureerd"
+ },
"error": {
+ "bad_dsn": "Ongeldige DSN",
"unknown": "Onverwachte fout"
- }
+ },
+ "step": {
+ "user": {
+ "description": "Voer uw Sentry DSN in",
+ "title": "Sentry"
+ }
+ },
+ "title": "Sentry"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/.translations/pl.json b/homeassistant/components/sentry/.translations/pl.json
index 4bb7abbc328..d97fa159a87 100644
--- a/homeassistant/components/sentry/.translations/pl.json
+++ b/homeassistant/components/sentry/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Sentry jest ju\u017c skonfigurowane"
+ "already_configured": "Sentry jest ju\u017c skonfigurowane."
},
"error": {
"bad_dsn": "Nieprawid\u0142owy DSN",
diff --git a/homeassistant/components/sentry/.translations/sv.json b/homeassistant/components/sentry/.translations/sv.json
new file mode 100644
index 00000000000..7f0968e7dbe
--- /dev/null
+++ b/homeassistant/components/sentry/.translations/sv.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Sentry har redan konfigurerats"
+ },
+ "error": {
+ "bad_dsn": "Ogiltig DSN",
+ "unknown": "Ov\u00e4ntat fel"
+ },
+ "step": {
+ "user": {
+ "description": "Ange din Sentry DSN",
+ "title": "Sentry"
+ }
+ },
+ "title": "Sentry"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json
index 672679b7254..eba33e75f71 100644
--- a/homeassistant/components/seven_segments/manifest.json
+++ b/homeassistant/components/seven_segments/manifest.json
@@ -2,7 +2,9 @@
"domain": "seven_segments",
"name": "Seven Segments OCR",
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
- "requirements": ["pillow==6.2.1"],
+ "requirements": [
+ "pillow==7.0.0"
+ ],
"dependencies": [],
"codeowners": []
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py
index 27e2fe9b563..da07290f422 100644
--- a/homeassistant/components/sigfox/sensor.py
+++ b/homeassistant/components/sigfox/sensor.py
@@ -100,7 +100,7 @@ class SigfoxAPI:
@property
def auth(self):
- """Return the API authentification."""
+ """Return the API authentication."""
return self._auth
@property
diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json
index 98a7b4e59a6..3efa1c33e85 100644
--- a/homeassistant/components/signal_messenger/manifest.json
+++ b/homeassistant/components/signal_messenger/manifest.json
@@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/integrations/signal_messenger",
"dependencies": [],
"codeowners": ["@bbernhard"],
- "requirements": ["pysignalclirestapi==0.1.4"]
+ "requirements": ["pysignalclirestapi==0.2.4"]
}
diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py
index 8fbf9c70873..cee871fb17e 100644
--- a/homeassistant/components/signal_messenger/notify.py
+++ b/homeassistant/components/signal_messenger/notify.py
@@ -17,6 +17,7 @@ CONF_SENDER_NR = "number"
CONF_RECP_NR = "recipients"
CONF_SIGNAL_CLI_REST_API = "url"
ATTR_FILENAME = "attachment"
+ATTR_FILENAMES = "attachments"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -34,9 +35,7 @@ def get_service(hass, config, discovery_info=None):
recp_nrs = config[CONF_RECP_NR]
signal_cli_rest_api_url = config[CONF_SIGNAL_CLI_REST_API]
- signal_cli_rest_api = SignalCliRestApi(
- signal_cli_rest_api_url, sender_nr, api_version=1
- )
+ signal_cli_rest_api = SignalCliRestApi(signal_cli_rest_api_url, sender_nr)
return SignalNotificationService(recp_nrs, signal_cli_rest_api)
@@ -60,12 +59,21 @@ class SignalNotificationService(BaseNotificationService):
data = kwargs.get(ATTR_DATA)
- filename = None
- if data is not None and ATTR_FILENAME in data:
- filename = data[ATTR_FILENAME]
+ filenames = None
+ if data is not None:
+ if ATTR_FILENAMES in data:
+ filenames = data[ATTR_FILENAMES]
+ if ATTR_FILENAME in data:
+ _LOGGER.warning(
+ "The 'attachment' option is deprecated, please replace it with 'attachments'. This option will become invalid in version 0.108."
+ )
+ if filenames is None:
+ filenames = [data[ATTR_FILENAME]]
+ else:
+ filenames.append(data[ATTR_FILENAME])
try:
- self._signal_cli_rest_api.send_message(message, self._recp_nrs, filename)
+ self._signal_cli_rest_api.send_message(message, self._recp_nrs, filenames)
except SignalCliRestApiError as ex:
_LOGGER.error("%s", ex)
raise ex
diff --git a/homeassistant/components/simplisafe/.translations/ca.json b/homeassistant/components/simplisafe/.translations/ca.json
index a02c3a5e28e..a89e4c753cb 100644
--- a/homeassistant/components/simplisafe/.translations/ca.json
+++ b/homeassistant/components/simplisafe/.translations/ca.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Aquest compte SimpliSafe ja est\u00e0 en \u00fas."
+ },
"error": {
"identifier_exists": "Aquest compte ja est\u00e0 registrat",
"invalid_credentials": "Credencials inv\u00e0lides"
diff --git a/homeassistant/components/simplisafe/.translations/da.json b/homeassistant/components/simplisafe/.translations/da.json
index 0d3970eeba5..ccd82979520 100644
--- a/homeassistant/components/simplisafe/.translations/da.json
+++ b/homeassistant/components/simplisafe/.translations/da.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Denne SimpliSafe-konto er allerede i brug."
+ },
"error": {
"identifier_exists": "Konto er allerede registreret",
"invalid_credentials": "Ugyldige legitimationsoplysninger"
diff --git a/homeassistant/components/simplisafe/.translations/de.json b/homeassistant/components/simplisafe/.translations/de.json
index ee7eaecc852..4d5eefc480b 100644
--- a/homeassistant/components/simplisafe/.translations/de.json
+++ b/homeassistant/components/simplisafe/.translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet."
+ },
"error": {
"identifier_exists": "Konto bereits registriert",
"invalid_credentials": "Ung\u00fcltige Anmeldeinformationen"
@@ -11,7 +14,7 @@
"password": "Passwort",
"username": "E-Mail-Adresse"
},
- "title": "Gebe deine Informationen ein"
+ "title": "Gib deine Informationen ein"
}
},
"title": "SimpliSafe"
diff --git a/homeassistant/components/simplisafe/.translations/en.json b/homeassistant/components/simplisafe/.translations/en.json
index b000335af8f..7e9c26291f7 100644
--- a/homeassistant/components/simplisafe/.translations/en.json
+++ b/homeassistant/components/simplisafe/.translations/en.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "This SimpliSafe account is already in use."
+ },
"error": {
"identifier_exists": "Account already registered",
"invalid_credentials": "Invalid credentials"
diff --git a/homeassistant/components/simplisafe/.translations/ko.json b/homeassistant/components/simplisafe/.translations/ko.json
index 5cbe233a05e..3327ddf9ab1 100644
--- a/homeassistant/components/simplisafe/.translations/ko.json
+++ b/homeassistant/components/simplisafe/.translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uc774 SimpliSafe \uacc4\uc815\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4."
+ },
"error": {
"identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_credentials": "\ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
diff --git a/homeassistant/components/simplisafe/.translations/no.json b/homeassistant/components/simplisafe/.translations/no.json
index 7c28209514e..4c25893791b 100644
--- a/homeassistant/components/simplisafe/.translations/no.json
+++ b/homeassistant/components/simplisafe/.translations/no.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk."
+ },
"error": {
"identifier_exists": "Konto er allerede registrert",
"invalid_credentials": "Ugyldig legitimasjon"
diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json
index ad8a15d06b7..3a9c160a0c5 100644
--- a/homeassistant/components/simplisafe/.translations/pl.json
+++ b/homeassistant/components/simplisafe/.translations/pl.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu."
+ },
"error": {
- "identifier_exists": "Konto jest ju\u017c zarejestrowane",
+ "identifier_exists": "Konto jest ju\u017c zarejestrowane.",
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce"
},
"step": {
diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json
index 301eed6d1c1..2d8b63c4bab 100644
--- a/homeassistant/components/simplisafe/.translations/ru.json
+++ b/homeassistant/components/simplisafe/.translations/ru.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430."
+ },
"error": {
"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\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
diff --git a/homeassistant/components/simplisafe/.translations/zh-Hant.json b/homeassistant/components/simplisafe/.translations/zh-Hant.json
index bd0b2c6f3d6..b456bde33c7 100644
--- a/homeassistant/components/simplisafe/.translations/zh-Hant.json
+++ b/homeassistant/components/simplisafe/.translations/zh-Hant.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u6b64 SimpliSafe \u5e33\u865f\u5df2\u88ab\u4f7f\u7528\u3002"
+ },
"error": {
"identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a",
"invalid_credentials": "\u6191\u8b49\u7121\u6548"
diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py
index b3d3baff16f..09004189820 100644
--- a/homeassistant/components/simplisafe/__init__.py
+++ b/homeassistant/components/simplisafe/__init__.py
@@ -3,12 +3,25 @@ import asyncio
import logging
from simplipy import API
-from simplipy.errors import InvalidCredentialsError, SimplipyError
-from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF
+from simplipy.errors import InvalidCredentialsError, SimplipyError, WebsocketError
+from simplipy.websocket import (
+ EVENT_CAMERA_MOTION_DETECTED,
+ EVENT_DOORBELL_DETECTED,
+ EVENT_ENTRY_DETECTED,
+ EVENT_LOCK_LOCKED,
+ EVENT_LOCK_UNLOCKED,
+ EVENT_MOTION_DETECTED,
+)
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
-from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
+from homeassistant.const import (
+ ATTR_CODE,
+ CONF_CODE,
+ CONF_PASSWORD,
+ CONF_TOKEN,
+ CONF_USERNAME,
+)
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
@@ -21,36 +34,65 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.event import async_call_later, async_track_time_interval
from homeassistant.helpers.service import (
async_register_admin_service,
verify_domain_control,
)
from .config_flow import configured_instances
-from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE
+from .const import (
+ ATTR_ALARM_DURATION,
+ ATTR_ALARM_VOLUME,
+ ATTR_CHIME_VOLUME,
+ ATTR_ENTRY_DELAY_AWAY,
+ ATTR_ENTRY_DELAY_HOME,
+ ATTR_EXIT_DELAY_AWAY,
+ ATTR_EXIT_DELAY_HOME,
+ ATTR_LIGHT,
+ ATTR_VOICE_PROMPT_VOLUME,
+ DATA_CLIENT,
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+ VOLUMES,
+)
_LOGGER = logging.getLogger(__name__)
CONF_ACCOUNTS = "accounts"
DATA_LISTENER = "listener"
+TOPIC_UPDATE = "simplisafe_update_data_{0}"
-ATTR_ALARM_DURATION = "alarm_duration"
-ATTR_ALARM_VOLUME = "alarm_volume"
-ATTR_CHIME_VOLUME = "chime_volume"
-ATTR_ENTRY_DELAY_AWAY = "entry_delay_away"
-ATTR_ENTRY_DELAY_HOME = "entry_delay_home"
-ATTR_EXIT_DELAY_AWAY = "exit_delay_away"
-ATTR_EXIT_DELAY_HOME = "exit_delay_home"
-ATTR_LIGHT = "light"
+EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
+EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION"
+
+DEFAULT_SOCKET_MIN_RETRY = 15
+DEFAULT_WATCHDOG_SECONDS = 5 * 60
+
+WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED]
+WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT = [
+ EVENT_CAMERA_MOTION_DETECTED,
+ EVENT_DOORBELL_DETECTED,
+ EVENT_ENTRY_DETECTED,
+ EVENT_MOTION_DETECTED,
+]
+
+ATTR_CATEGORY = "category"
+ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by"
+ATTR_LAST_EVENT_INFO = "last_event_info"
+ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name"
+ATTR_LAST_EVENT_SENSOR_SERIAL = "last_event_sensor_serial"
+ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type"
+ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp"
+ATTR_LAST_EVENT_TYPE = "last_event_type"
+ATTR_LAST_EVENT_TYPE = "last_event_type"
+ATTR_MESSAGE = "message"
ATTR_PIN_LABEL = "label"
ATTR_PIN_LABEL_OR_VALUE = "label_or_pin"
ATTR_PIN_VALUE = "pin"
ATTR_SYSTEM_ID = "system_id"
-ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume"
-
-VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH]
+ATTR_TIMESTAMP = "timestamp"
SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int})
@@ -176,9 +218,8 @@ async def async_setup_entry(hass, config_entry):
_async_save_refresh_token(hass, config_entry, api.refresh_token)
- systems = await api.get_systems()
- simplisafe = SimpliSafe(hass, api, systems, config_entry)
- await simplisafe.async_update()
+ simplisafe = SimpliSafe(hass, api, config_entry)
+ await simplisafe.async_init()
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe
for component in ("alarm_control_panel", "lock"):
@@ -186,22 +227,6 @@ async def async_setup_entry(hass, config_entry):
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
- async def refresh(event_time):
- """Refresh data from the SimpliSafe account."""
- await simplisafe.async_update()
- _LOGGER.debug("Updated data for all SimpliSafe systems")
- async_dispatcher_send(hass, TOPIC_UPDATE)
-
- hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval(
- hass, refresh, DEFAULT_SCAN_INTERVAL
- )
-
- # Register the base station for each system:
- for system in systems.values():
- hass.async_create_task(
- async_register_base_station(hass, system, config_entry.entry_id)
- )
-
@callback
def verify_system_exists(coro):
"""Log an error if a service call uses an invalid system ID."""
@@ -209,7 +234,7 @@ async def async_setup_entry(hass, config_entry):
async def decorator(call):
"""Decorate."""
system_id = int(call.data[ATTR_SYSTEM_ID])
- if system_id not in systems:
+ if system_id not in simplisafe.systems:
_LOGGER.error("Unknown system ID in service call: %s", system_id)
return
await coro(call)
@@ -222,7 +247,7 @@ async def async_setup_entry(hass, config_entry):
async def decorator(call):
"""Decorate."""
- system = systems[int(call.data[ATTR_SYSTEM_ID])]
+ system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])]
if system.version != 3:
_LOGGER.error("Service only available on V3 systems")
return
@@ -234,7 +259,7 @@ async def async_setup_entry(hass, config_entry):
@_verify_domain_control
async def remove_pin(call):
"""Remove a PIN."""
- system = systems[call.data[ATTR_SYSTEM_ID]]
+ system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE])
except SimplipyError as err:
@@ -245,7 +270,7 @@ async def async_setup_entry(hass, config_entry):
@_verify_domain_control
async def set_pin(call):
"""Set a PIN."""
- system = systems[call.data[ATTR_SYSTEM_ID]]
+ system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE])
except SimplipyError as err:
@@ -257,7 +282,7 @@ async def async_setup_entry(hass, config_entry):
@_verify_domain_control
async def set_system_properties(call):
"""Set one or more system parameters."""
- system = systems[call.data[ATTR_SYSTEM_ID]]
+ system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.set_properties(
{
@@ -300,90 +325,278 @@ async def async_unload_entry(hass, entry):
return True
-class SimpliSafe:
- """Define a SimpliSafe API object."""
+class SimpliSafeWebsocket:
+ """Define a SimpliSafe websocket "manager" object."""
- def __init__(self, hass, api, systems, config_entry):
+ def __init__(self, hass, websocket):
+ """Initialize."""
+ self._hass = hass
+ self._websocket = websocket
+ self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
+ self._websocket_reconnect_underway = False
+ self._websocket_watchdog_listener = None
+ self.last_events = {}
+
+ async def _async_attempt_websocket_connect(self):
+ """Attempt to connect to the websocket (retrying later on fail)."""
+ self._websocket_reconnect_underway = True
+
+ try:
+ await self._websocket.async_connect()
+ except WebsocketError as err:
+ _LOGGER.error("Error with the websocket connection: %s", err)
+ self._websocket_reconnect_delay = min(
+ 2 * self._websocket_reconnect_delay, 480
+ )
+ async_call_later(
+ self._hass,
+ self._websocket_reconnect_delay,
+ self.async_websocket_connect,
+ )
+ else:
+ self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
+ self._websocket_reconnect_underway = False
+
+ async def _async_websocket_reconnect(self, event_time):
+ """Forcibly disconnect from and reconnect to the websocket."""
+ _LOGGER.debug("Websocket watchdog expired; forcing socket reconnection")
+ await self.async_websocket_disconnect()
+ await self._async_attempt_websocket_connect()
+
+ def _on_connect(self):
+ """Define a handler to fire when the websocket is connected."""
+ _LOGGER.info("Connected to websocket")
+ _LOGGER.debug("Websocket watchdog starting")
+ if self._websocket_watchdog_listener is not None:
+ self._websocket_watchdog_listener()
+ self._websocket_watchdog_listener = async_call_later(
+ self._hass, DEFAULT_WATCHDOG_SECONDS, self._async_websocket_reconnect
+ )
+
+ @staticmethod
+ def _on_disconnect():
+ """Define a handler to fire when the websocket is disconnected."""
+ _LOGGER.info("Disconnected from websocket")
+
+ def _on_event(self, event):
+ """Define a handler to fire when a new SimpliSafe event arrives."""
+ _LOGGER.debug("New websocket event: %s", event)
+ self.last_events[event.system_id] = event
+ async_dispatcher_send(self._hass, TOPIC_UPDATE.format(event.system_id))
+
+ _LOGGER.debug("Resetting websocket watchdog")
+ self._websocket_watchdog_listener()
+ self._websocket_watchdog_listener = async_call_later(
+ self._hass, DEFAULT_WATCHDOG_SECONDS, self._async_websocket_reconnect
+ )
+ self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
+
+ if event.event_type not in WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT:
+ return
+
+ if event.sensor_type:
+ sensor_type = event.sensor_type.name
+ else:
+ sensor_type = None
+
+ self._hass.bus.async_fire(
+ EVENT_SIMPLISAFE_EVENT,
+ event_data={
+ ATTR_LAST_EVENT_CHANGED_BY: event.changed_by,
+ ATTR_LAST_EVENT_TYPE: event.event_type,
+ ATTR_LAST_EVENT_INFO: event.info,
+ ATTR_LAST_EVENT_SENSOR_NAME: event.sensor_name,
+ ATTR_LAST_EVENT_SENSOR_SERIAL: event.sensor_serial,
+ ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type,
+ ATTR_SYSTEM_ID: event.system_id,
+ ATTR_LAST_EVENT_TIMESTAMP: event.timestamp,
+ },
+ )
+
+ async def async_websocket_connect(self):
+ """Register handlers and connect to the websocket."""
+ if self._websocket_reconnect_underway:
+ return
+
+ self._websocket.on_connect(self._on_connect)
+ self._websocket.on_disconnect(self._on_disconnect)
+ self._websocket.on_event(self._on_event)
+
+ await self._async_attempt_websocket_connect()
+
+ async def async_websocket_disconnect(self):
+ """Disconnect from the websocket."""
+ await self._websocket.async_disconnect()
+
+
+class SimpliSafe:
+ """Define a SimpliSafe data object."""
+
+ def __init__(self, hass, api, config_entry):
"""Initialize."""
self._api = api
self._config_entry = config_entry
self._emergency_refresh_token_used = False
self._hass = hass
- self.last_event_data = {}
- self.systems = systems
+ self._system_notifications = {}
+ self.initial_event_to_use = {}
+ self.systems = {}
+ self.websocket = SimpliSafeWebsocket(hass, api.websocket)
- async def _update_system(self, system):
- """Update a system."""
- try:
+ @callback
+ def _async_process_new_notifications(self, system):
+ """Act on any new system notifications."""
+ old_notifications = self._system_notifications.get(system.system_id, [])
+ latest_notifications = system.notifications
+
+ # Save the latest notifications:
+ self._system_notifications[system.system_id] = latest_notifications
+
+ # Process any notifications that are new:
+ to_add = set(latest_notifications) - set(old_notifications)
+
+ if not to_add:
+ return
+
+ _LOGGER.debug("New system notifications: %s", to_add)
+
+ for notification in to_add:
+ text = notification.text
+ if notification.link:
+ text = f"{text} For more information: {notification.link}"
+
+ self._hass.bus.async_fire(
+ EVENT_SIMPLISAFE_NOTIFICATION,
+ event_data={
+ ATTR_CATEGORY: notification.category,
+ ATTR_CODE: notification.code,
+ ATTR_MESSAGE: text,
+ ATTR_TIMESTAMP: notification.timestamp,
+ },
+ )
+
+ async def async_init(self):
+ """Initialize the data class."""
+ asyncio.create_task(self.websocket.async_websocket_connect())
+
+ self.systems = await self._api.get_systems()
+ for system in self.systems.values():
+ self._hass.async_create_task(
+ async_register_base_station(
+ self._hass, system, self._config_entry.entry_id
+ )
+ )
+
+ # Future events will come from the websocket, but since subscription to the
+ # websocket doesn't provide the most recent event, we grab it from the REST
+ # API to ensure event-related attributes aren't empty on startup:
+ try:
+ self.initial_event_to_use[
+ system.system_id
+ ] = await system.get_latest_event()
+ except SimplipyError as err:
+ _LOGGER.error("Error while fetching initial event: %s", err)
+ self.initial_event_to_use[system.system_id] = {}
+
+ async def refresh(event_time):
+ """Refresh data from the SimpliSafe account."""
+ await self.async_update()
+
+ self._hass.data[DOMAIN][DATA_LISTENER][
+ self._config_entry.entry_id
+ ] = async_track_time_interval(self._hass, refresh, DEFAULT_SCAN_INTERVAL)
+
+ await self.async_update()
+
+ async def async_update(self):
+ """Get updated data from SimpliSafe."""
+
+ async def update_system(system):
+ """Update a system."""
await system.update()
- except InvalidCredentialsError:
- # SimpliSafe's cloud is a little shaky. At times, a 500 or 502 will
- # seemingly harm simplisafe-python's existing access token _and_ refresh
- # token, thus preventing the integration from recovering. However, the
- # refresh token stored in the config entry escapes unscathed (again,
- # apparently); so, if we detect that we're in such a situation, try a last-
- # ditch effort by re-authenticating with the stored token:
- if self._emergency_refresh_token_used:
- # If we've already tried this, log the error, suggest a HASS restart,
- # and stop the time tracker:
- _LOGGER.error(
- "SimpliSafe authentication disconnected. Please restart HASS."
+ self._async_process_new_notifications(system)
+ _LOGGER.debug('Updated REST API data for "%s"', system.address)
+ async_dispatcher_send(self._hass, TOPIC_UPDATE.format(system.system_id))
+
+ tasks = [update_system(system) for system in self.systems.values()]
+
+ results = await asyncio.gather(*tasks, return_exceptions=True)
+ for result in results:
+ if isinstance(result, InvalidCredentialsError):
+ if self._emergency_refresh_token_used:
+ _LOGGER.error(
+ "SimpliSafe authentication disconnected. Please restart HASS."
+ )
+ remove_listener = self._hass.data[DOMAIN][DATA_LISTENER].pop(
+ self._config_entry.entry_id
+ )
+ remove_listener()
+ return
+
+ _LOGGER.warning("SimpliSafe cloud error; trying stored refresh token")
+ self._emergency_refresh_token_used = True
+ return await self._api.refresh_access_token(
+ self._config_entry.data[CONF_TOKEN]
)
- remove_listener = self._hass.data[DOMAIN][DATA_LISTENER].pop(
- self._config_entry.entry_id
- )
- remove_listener()
+
+ if isinstance(result, SimplipyError):
+ _LOGGER.error("SimpliSafe error while updating: %s", result)
return
- _LOGGER.warning("SimpliSafe cloud error; trying stored refresh token")
- self._emergency_refresh_token_used = True
- return await self._api.refresh_access_token(
- self._config_entry.data[CONF_TOKEN]
- )
- except SimplipyError as err:
- _LOGGER.error(
- 'SimpliSafe error while updating "%s": %s', system.address, err
- )
- return
- except Exception as err: # pylint: disable=broad-except
- _LOGGER.error('Unknown error while updating "%s": %s', system.address, err)
- return
+ if isinstance(result, SimplipyError):
+ _LOGGER.error("Unknown error while updating: %s", result)
+ return
- self.last_event_data[system.system_id] = await system.get_latest_event()
+ if self._api.refresh_token_dirty:
+ # Reconnect the websocket:
+ await self._api.websocket.async_disconnect()
+ await self._api.websocket.async_connect()
+
+ # Save the new refresh token:
+ _async_save_refresh_token(
+ self._hass, self._config_entry, self._api.refresh_token
+ )
# If we've reached this point using an emergency refresh token, we're in the
# clear and we can discard it:
if self._emergency_refresh_token_used:
self._emergency_refresh_token_used = False
- async def async_update(self):
- """Get updated data from SimpliSafe."""
- tasks = [self._update_system(system) for system in self.systems.values()]
-
- await asyncio.gather(*tasks)
-
- if self._api.refresh_token_dirty:
- _async_save_refresh_token(
- self._hass, self._config_entry, self._api.refresh_token
- )
-
class SimpliSafeEntity(Entity):
"""Define a base SimpliSafe entity."""
- def __init__(self, system, name, *, serial=None):
+ def __init__(self, simplisafe, system, name, *, serial=None):
"""Initialize."""
self._async_unsub_dispatcher_connect = None
- self._attrs = {ATTR_SYSTEM_ID: system.system_id}
+ self._last_processed_websocket_event = None
self._name = name
self._online = True
+ self._simplisafe = simplisafe
self._system = system
+ self.websocket_events_to_listen_for = []
if serial:
self._serial = serial
else:
self._serial = system.serial
+ self._attrs = {
+ ATTR_LAST_EVENT_INFO: simplisafe.initial_event_to_use[system.system_id].get(
+ "info"
+ ),
+ ATTR_LAST_EVENT_SENSOR_NAME: simplisafe.initial_event_to_use[
+ system.system_id
+ ].get("sensorName"),
+ ATTR_LAST_EVENT_SENSOR_TYPE: simplisafe.initial_event_to_use[
+ system.system_id
+ ].get("sensorType"),
+ ATTR_LAST_EVENT_TIMESTAMP: simplisafe.initial_event_to_use[
+ system.system_id
+ ].get("eventTimestamp"),
+ ATTR_SYSTEM_ID: system.system_id,
+ }
+
@property
def available(self):
"""Return whether the entity is available."""
@@ -420,6 +633,36 @@ class SimpliSafeEntity(Entity):
"""Return the unique ID of the entity."""
return self._serial
+ @callback
+ def _async_should_ignore_websocket_event(self, event):
+ """Return whether this entity should ignore a particular websocket event.
+
+ Note that we can't check for a final condition – whether the event belongs to
+ a particular entity, like a lock – because some events (like arming the system
+ from a keypad _or_ from the website) should impact the same entity.
+ """
+ # We've already processed this event:
+ if self._last_processed_websocket_event == event:
+ return True
+
+ # This is an event for a system other than the one this entity belongs to:
+ if event.system_id != self._system.system_id:
+ return True
+
+ # This isn't an event that this entity cares about:
+ if event.event_type not in self.websocket_events_to_listen_for:
+ return True
+
+ # This event is targeted at a specific entity whose serial number is different
+ # from this one's:
+ if (
+ event.event_type in WEBSOCKET_EVENTS_REQUIRING_SERIAL
+ and event.sensor_serial != self._serial
+ ):
+ return True
+
+ return False
+
async def async_added_to_hass(self):
"""Register callbacks."""
@@ -429,9 +672,47 @@ class SimpliSafeEntity(Entity):
self.async_schedule_update_ha_state(True)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
- self.hass, TOPIC_UPDATE, update
+ self.hass, TOPIC_UPDATE.format(self._system.system_id), update
)
+ async def async_update(self):
+ """Update the entity."""
+ self.async_update_from_rest_api()
+
+ last_websocket_event = self._simplisafe.websocket.last_events.get(
+ self._system.system_id
+ )
+
+ if self._async_should_ignore_websocket_event(last_websocket_event):
+ return
+
+ self._last_processed_websocket_event = last_websocket_event
+
+ if last_websocket_event.sensor_type:
+ sensor_type = last_websocket_event.sensor_type.name
+ else:
+ sensor_type = None
+
+ self._attrs.update(
+ {
+ ATTR_LAST_EVENT_INFO: last_websocket_event.info,
+ ATTR_LAST_EVENT_SENSOR_NAME: last_websocket_event.sensor_name,
+ ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type,
+ ATTR_LAST_EVENT_TIMESTAMP: last_websocket_event.timestamp,
+ }
+ )
+ self.async_update_from_websocket_event(last_websocket_event)
+
+ @callback
+ def async_update_from_rest_api(self):
+ """Update the entity with the provided REST API data."""
+ pass
+
+ @callback
+ def async_update_from_websocket_event(self, event):
+ """Update the entity with the provided websocket API data."""
+ pass
+
async def async_will_remove_from_hass(self) -> None:
"""Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py
index 362c0244749..c675f9c2748 100644
--- a/homeassistant/components/simplisafe/alarm_control_panel.py
+++ b/homeassistant/components/simplisafe/alarm_control_panel.py
@@ -2,9 +2,21 @@
import logging
import re
-from simplipy.entity import EntityTypes
+from simplipy.errors import SimplipyError
from simplipy.system import SystemStates
-from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF
+from simplipy.websocket import (
+ EVENT_ALARM_CANCELED,
+ EVENT_ALARM_TRIGGERED,
+ EVENT_ARMED_AWAY,
+ EVENT_ARMED_AWAY_BY_KEYPAD,
+ EVENT_ARMED_AWAY_BY_REMOTE,
+ EVENT_ARMED_HOME,
+ EVENT_AWAY_EXIT_DELAY_BY_KEYPAD,
+ EVENT_AWAY_EXIT_DELAY_BY_REMOTE,
+ EVENT_DISARMED_BY_MASTER_PIN,
+ EVENT_DISARMED_BY_REMOTE,
+ EVENT_HOME_EXIT_DELAY,
+)
from homeassistant.components.alarm_control_panel import (
FORMAT_NUMBER,
@@ -23,40 +35,33 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
-from homeassistant.util.dt import utc_from_timestamp
+from homeassistant.core import callback
from . import SimpliSafeEntity
-from .const import DATA_CLIENT, DOMAIN
+from .const import (
+ ATTR_ALARM_DURATION,
+ ATTR_ALARM_VOLUME,
+ ATTR_CHIME_VOLUME,
+ ATTR_ENTRY_DELAY_AWAY,
+ ATTR_ENTRY_DELAY_HOME,
+ ATTR_EXIT_DELAY_AWAY,
+ ATTR_EXIT_DELAY_HOME,
+ ATTR_LIGHT,
+ ATTR_VOICE_PROMPT_VOLUME,
+ DATA_CLIENT,
+ DOMAIN,
+ VOLUME_STRING_MAP,
+)
_LOGGER = logging.getLogger(__name__)
-ATTR_ALARM_DURATION = "alarm_duration"
-ATTR_ALARM_VOLUME = "alarm_volume"
ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level"
-ATTR_CHIME_VOLUME = "chime_volume"
-ATTR_ENTRY_DELAY_AWAY = "entry_delay_away"
-ATTR_ENTRY_DELAY_HOME = "entry_delay_home"
-ATTR_EXIT_DELAY_AWAY = "exit_delay_away"
-ATTR_EXIT_DELAY_HOME = "exit_delay_home"
ATTR_GSM_STRENGTH = "gsm_strength"
-ATTR_LAST_EVENT_INFO = "last_event_info"
-ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name"
-ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type"
-ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp"
-ATTR_LAST_EVENT_TYPE = "last_event_type"
-ATTR_LIGHT = "light"
+ATTR_PIN_NAME = "pin_name"
ATTR_RF_JAMMING = "rf_jamming"
-ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume"
ATTR_WALL_POWER_LEVEL = "wall_power_level"
ATTR_WIFI_STRENGTH = "wifi_strength"
-VOLUME_STRING_MAP = {
- VOLUME_HIGH: "high",
- VOLUME_LOW: "low",
- VOLUME_MEDIUM: "medium",
- VOLUME_OFF: "off",
-}
-
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up a SimpliSafe alarm control panel based on a config entry."""
@@ -75,33 +80,42 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
def __init__(self, simplisafe, system, code):
"""Initialize the SimpliSafe alarm."""
- super().__init__(system, "Alarm Control Panel")
+ super().__init__(simplisafe, system, "Alarm Control Panel")
self._changed_by = None
self._code = code
- self._simplisafe = simplisafe
- self._state = None
+ self._last_event = None
- if self._system.version == 3:
- self._attrs.update(
- {
- ATTR_ALARM_DURATION: self._system.alarm_duration,
- ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume],
- ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level,
- ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume],
- ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away,
- ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home,
- ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away,
- ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home,
- ATTR_GSM_STRENGTH: self._system.gsm_strength,
- ATTR_LIGHT: self._system.light,
- ATTR_RF_JAMMING: self._system.rf_jamming,
- ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[
- self._system.voice_prompt_volume
- ],
- ATTR_WALL_POWER_LEVEL: self._system.wall_power_level,
- ATTR_WIFI_STRENGTH: self._system.wifi_strength,
- }
- )
+ if system.alarm_going_off:
+ self._state = STATE_ALARM_TRIGGERED
+ elif system.state == SystemStates.away:
+ self._state = STATE_ALARM_ARMED_AWAY
+ elif system.state in (
+ SystemStates.away_count,
+ SystemStates.exit_delay,
+ SystemStates.home_count,
+ ):
+ self._state = STATE_ALARM_ARMING
+ elif system.state == SystemStates.home:
+ self._state = STATE_ALARM_ARMED_HOME
+ elif system.state == SystemStates.off:
+ self._state = STATE_ALARM_DISARMED
+ else:
+ self._state = None
+
+ for event_type in (
+ EVENT_ALARM_CANCELED,
+ EVENT_ALARM_TRIGGERED,
+ EVENT_ARMED_AWAY,
+ EVENT_ARMED_AWAY_BY_KEYPAD,
+ EVENT_ARMED_AWAY_BY_REMOTE,
+ EVENT_ARMED_HOME,
+ EVENT_AWAY_EXIT_DELAY_BY_KEYPAD,
+ EVENT_AWAY_EXIT_DELAY_BY_REMOTE,
+ EVENT_DISARMED_BY_MASTER_PIN,
+ EVENT_DISARMED_BY_REMOTE,
+ EVENT_HOME_EXIT_DELAY,
+ ):
+ self.websocket_events_to_listen_for.append(event_type)
@property
def changed_by(self):
@@ -139,71 +153,96 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
if not self._validate_code(code, "disarming"):
return
- await self._system.set_off()
+ try:
+ await self._system.set_off()
+ except SimplipyError as err:
+ _LOGGER.error('Error while disarming "%s": %s', self._system.name, err)
+ return
+
+ self._state = STATE_ALARM_DISARMED
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
if not self._validate_code(code, "arming home"):
return
- await self._system.set_home()
+ try:
+ await self._system.set_home()
+ except SimplipyError as err:
+ _LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err)
+ return
+
+ self._state = STATE_ALARM_ARMED_HOME
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
if not self._validate_code(code, "arming away"):
return
- await self._system.set_away()
+ try:
+ await self._system.set_away()
+ except SimplipyError as err:
+ _LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err)
+ return
- async def async_update(self):
- """Update alarm status."""
- last_event = self._simplisafe.last_event_data[self._system.system_id]
-
- if last_event.get("pinName"):
- self._changed_by = last_event["pinName"]
+ self._state = STATE_ALARM_ARMING
+ @callback
+ def async_update_from_rest_api(self):
+ """Update the entity with the provided REST API data."""
if self._system.state == SystemStates.error:
self._online = False
return
-
self._online = True
- if self._system.alarm_going_off:
+ if self._system.version == 3:
+ self._attrs.update(
+ {
+ ATTR_ALARM_DURATION: self._system.alarm_duration,
+ ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume],
+ ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level,
+ ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume],
+ ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away,
+ ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home,
+ ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away,
+ ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home,
+ ATTR_GSM_STRENGTH: self._system.gsm_strength,
+ ATTR_LIGHT: self._system.light,
+ ATTR_RF_JAMMING: self._system.rf_jamming,
+ ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[
+ self._system.voice_prompt_volume
+ ],
+ ATTR_WALL_POWER_LEVEL: self._system.wall_power_level,
+ ATTR_WIFI_STRENGTH: self._system.wifi_strength,
+ }
+ )
+
+ @callback
+ def async_update_from_websocket_event(self, event):
+ """Update the entity with the provided websocket API event data."""
+ if event.event_type in (
+ EVENT_ALARM_CANCELED,
+ EVENT_DISARMED_BY_MASTER_PIN,
+ EVENT_DISARMED_BY_REMOTE,
+ ):
+ self._state = STATE_ALARM_DISARMED
+ elif event.event_type == EVENT_ALARM_TRIGGERED:
self._state = STATE_ALARM_TRIGGERED
- elif self._system.state == SystemStates.away:
+ elif event.event_type in (
+ EVENT_ARMED_AWAY,
+ EVENT_ARMED_AWAY_BY_KEYPAD,
+ EVENT_ARMED_AWAY_BY_REMOTE,
+ ):
self._state = STATE_ALARM_ARMED_AWAY
- elif self._system.state in (
- SystemStates.away_count,
- SystemStates.exit_delay,
- SystemStates.home_count,
+ elif event.event_type == EVENT_ARMED_HOME:
+ self._state = STATE_ALARM_ARMED_HOME
+ elif event.event_type in (
+ EVENT_AWAY_EXIT_DELAY_BY_KEYPAD,
+ EVENT_AWAY_EXIT_DELAY_BY_REMOTE,
+ EVENT_HOME_EXIT_DELAY,
):
self._state = STATE_ALARM_ARMING
- elif self._system.state == SystemStates.home:
- self._state = STATE_ALARM_ARMED_HOME
- elif self._system.state == SystemStates.off:
- self._state = STATE_ALARM_DISARMED
else:
self._state = None
- try:
- last_event_sensor_type = EntityTypes(last_event["sensorType"]).name
- except ValueError:
- _LOGGER.warning(
- 'Encountered unknown entity type: %s ("%s"). Please report it at'
- "https://github.com/home-assistant/home-assistant/issues.",
- last_event["sensorType"],
- last_event["sensorName"],
- )
- last_event_sensor_type = None
-
- self._attrs.update(
- {
- ATTR_LAST_EVENT_INFO: last_event["info"],
- ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"],
- ATTR_LAST_EVENT_SENSOR_TYPE: last_event_sensor_type,
- ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp(
- last_event["eventTimestamp"]
- ),
- ATTR_LAST_EVENT_TYPE: last_event["eventType"],
- }
- )
+ self._changed_by = event.changed_by
diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py
index 4dfef39de46..6ca5f8323a7 100644
--- a/homeassistant/components/simplisafe/const.py
+++ b/homeassistant/components/simplisafe/const.py
@@ -1,10 +1,28 @@
"""Define constants for the SimpliSafe component."""
from datetime import timedelta
+from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF
+
DOMAIN = "simplisafe"
DATA_CLIENT = "client"
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
-TOPIC_UPDATE = "update"
+ATTR_ALARM_DURATION = "alarm_duration"
+ATTR_ALARM_VOLUME = "alarm_volume"
+ATTR_CHIME_VOLUME = "chime_volume"
+ATTR_ENTRY_DELAY_AWAY = "entry_delay_away"
+ATTR_ENTRY_DELAY_HOME = "entry_delay_home"
+ATTR_EXIT_DELAY_AWAY = "exit_delay_away"
+ATTR_EXIT_DELAY_HOME = "exit_delay_home"
+ATTR_LIGHT = "light"
+ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume"
+
+VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH]
+VOLUME_STRING_MAP = {
+ VOLUME_HIGH: "high",
+ VOLUME_LOW: "low",
+ VOLUME_MEDIUM: "medium",
+ VOLUME_OFF: "off",
+}
diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py
index 10c5d310e73..58448ec4599 100644
--- a/homeassistant/components/simplisafe/lock.py
+++ b/homeassistant/components/simplisafe/lock.py
@@ -1,10 +1,12 @@
"""Support for SimpliSafe locks."""
import logging
+from simplipy.errors import SimplipyError
from simplipy.lock import LockStates
+from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED
from homeassistant.components.lock import LockDevice
-from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED
+from homeassistant.core import callback
from . import SimpliSafeEntity
from .const import DATA_CLIENT, DOMAIN
@@ -15,19 +17,13 @@ ATTR_LOCK_LOW_BATTERY = "lock_low_battery"
ATTR_JAMMED = "jammed"
ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery"
-STATE_MAP = {
- LockStates.locked: STATE_LOCKED,
- LockStates.unknown: STATE_UNKNOWN,
- LockStates.unlocked: STATE_UNLOCKED,
-}
-
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up SimpliSafe locks based on a config entry."""
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
async_add_entities(
[
- SimpliSafeLock(system, lock)
+ SimpliSafeLock(simplisafe, system, lock)
for system in simplisafe.systems.values()
for lock in system.locks.values()
]
@@ -37,32 +33,48 @@ async def async_setup_entry(hass, entry, async_add_entities):
class SimpliSafeLock(SimpliSafeEntity, LockDevice):
"""Define a SimpliSafe lock."""
- def __init__(self, system, lock):
+ def __init__(self, simplisafe, system, lock):
"""Initialize."""
- super().__init__(system, lock.name, serial=lock.serial)
+ super().__init__(simplisafe, system, lock.name, serial=lock.serial)
+ self._is_locked = False
self._lock = lock
+ for event_type in (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED):
+ self.websocket_events_to_listen_for.append(event_type)
+
@property
def is_locked(self):
"""Return true if the lock is locked."""
- return STATE_MAP.get(self._lock.state) == STATE_LOCKED
+ return self._is_locked
async def async_lock(self, **kwargs):
"""Lock the lock."""
- await self._lock.lock()
+ try:
+ await self._lock.lock()
+ except SimplipyError as err:
+ _LOGGER.error('Error while locking "%s": %s', self._lock.name, err)
+ return
+
+ self._is_locked = True
async def async_unlock(self, **kwargs):
"""Unlock the lock."""
- await self._lock.unlock()
+ try:
+ await self._lock.unlock()
+ except SimplipyError as err:
+ _LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err)
+ return
- async def async_update(self):
- """Update lock status."""
+ self._is_locked = False
+
+ @callback
+ def async_update_from_rest_api(self):
+ """Update the entity with the provided REST API data."""
if self._lock.offline or self._lock.disabled:
self._online = False
return
self._online = True
-
self._attrs.update(
{
ATTR_LOCK_LOW_BATTERY: self._lock.lock_low_battery,
@@ -70,3 +82,11 @@ class SimpliSafeLock(SimpliSafeEntity, LockDevice):
ATTR_PIN_PAD_LOW_BATTERY: self._lock.pin_pad_low_battery,
}
)
+
+ @callback
+ def async_update_from_websocket_event(self, event):
+ """Update the entity with the provided websocket event data."""
+ if event.event_type == EVENT_LOCK_LOCKED:
+ self._is_locked = True
+ else:
+ self._is_locked = False
diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index f95db72d45a..e44f39265cb 100644
--- a/homeassistant/components/simplisafe/manifest.json
+++ b/homeassistant/components/simplisafe/manifest.json
@@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
- "requirements": ["simplisafe-python==6.1.0"],
+ "requirements": ["simplisafe-python==8.1.1"],
"dependencies": [],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py
index d34653e60e7..c31ab97cb95 100644
--- a/homeassistant/components/smappee/__init__.py
+++ b/homeassistant/components/smappee/__init__.py
@@ -199,7 +199,7 @@ class Smappee:
try:
return self._smappy.get_consumption(location_id, start, end, aggregation)
except RequestException as error:
- _LOGGER.error("Error getting comsumption from Smappee cloud. (%s)", error)
+ _LOGGER.error("Error getting consumption from Smappee cloud. (%s)", error)
def get_sensor_consumption(self, location_id, sensor_id, aggregation, delta):
"""Update data from Smappee."""
@@ -221,7 +221,7 @@ class Smappee:
location_id, sensor_id, start, end, aggregation
)
except RequestException as error:
- _LOGGER.error("Error getting comsumption from Smappee cloud. (%s)", error)
+ _LOGGER.error("Error getting consumption from Smappee cloud. (%s)", error)
def actuator_on(self, location_id, actuator_id, is_remote_switch, duration=None):
"""Turn on actuator."""
diff --git a/homeassistant/components/smartthings/.translations/sv.json b/homeassistant/components/smartthings/.translations/sv.json
index 6da4624fa39..725957682ad 100644
--- a/homeassistant/components/smartthings/.translations/sv.json
+++ b/homeassistant/components/smartthings/.translations/sv.json
@@ -13,7 +13,7 @@
"step": {
"user": {
"data": {
- "access_token": "\u00c5tkomsttoken"
+ "access_token": "\u00c5tkomstnyckel"
},
"description": "V\u00e4nligen ange en [personlig \u00e5tkomsttoken]({token_url}) f\u00f6r SmartThings som har skapats enligt [instruktionerna]({component_url}).",
"title": "Ange personlig \u00e5tkomsttoken"
diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py
index 33f9558023d..1539fa076e4 100644
--- a/homeassistant/components/smartthings/__init__.py
+++ b/homeassistant/components/smartthings/__init__.py
@@ -146,7 +146,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
except ClientResponseError as ex:
if ex.status in (401, 403):
_LOGGER.exception(
- "Unable to setup config entry '%s' - please reconfigure the integration",
+ "Unable to setup configuration entry '%s' - please reconfigure the integration",
entry.title,
)
remove_entry = True
@@ -183,7 +183,7 @@ async def async_get_entry_scenes(entry: ConfigEntry, api):
except ClientResponseError as ex:
if ex.status == 403:
_LOGGER.exception(
- "Unable to load scenes for config entry '%s' because the access token does not have the required access",
+ "Unable to load scenes for configuration entry '%s' because the access token does not have the required access",
entry.title,
)
else:
@@ -230,7 +230,7 @@ async def async_remove_entry(hass: HomeAssistantType, entry: ConfigEntry) -> Non
app_count = sum(1 for entry in all_entries if entry.data[CONF_APP_ID] == app_id)
if app_count > 1:
_LOGGER.debug(
- "App %s was not removed because it is in use by other config entries",
+ "App %s was not removed because it is in use by other configuration entries",
app_id,
)
return
diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py
index 19a9e20cd6b..232540ee47b 100644
--- a/homeassistant/components/smartthings/climate.py
+++ b/homeassistant/components/smartthings/climate.py
@@ -406,7 +406,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice):
self._device.device_id,
mode,
)
- self._hvac_modes = modes
+ self._hvac_modes = list(modes)
@property
def current_temperature(self):
diff --git a/homeassistant/components/smhi/.translations/pl.json b/homeassistant/components/smhi/.translations/pl.json
index 21973cd54b6..818f27853ff 100644
--- a/homeassistant/components/smhi/.translations/pl.json
+++ b/homeassistant/components/smhi/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "Nazwa ju\u017c istnieje",
+ "name_exists": "Nazwa ju\u017c istnieje.",
"wrong_location": "Lokalizacja w Szwecji"
},
"step": {
diff --git a/homeassistant/components/socialblade/manifest.json b/homeassistant/components/socialblade/manifest.json
index 2ce7fbabf0f..540febe7f2e 100644
--- a/homeassistant/components/socialblade/manifest.json
+++ b/homeassistant/components/socialblade/manifest.json
@@ -2,7 +2,7 @@
"domain": "socialblade",
"name": "Social Blade",
"documentation": "https://www.home-assistant.io/integrations/socialblade",
- "requirements": ["socialbladeclient==0.2"],
+ "requirements": ["socialbladeclient==0.5"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/solaredge/.translations/hu.json b/homeassistant/components/solaredge/.translations/hu.json
new file mode 100644
index 00000000000..ae8f51983ea
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/hu.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "Ennek az install\u00e1ci\u00f3nak a neve"
+ },
+ "title": "Az API param\u00e9terek megad\u00e1sa ehhez a telep\u00edt\u00e9shez"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/pl.json b/homeassistant/components/solaredge/.translations/pl.json
index 376a81219b0..5e80c1563f4 100644
--- a/homeassistant/components/solaredge/.translations/pl.json
+++ b/homeassistant/components/solaredge/.translations/pl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "site_exists": "Ten site_id jest ju\u017c skonfigurowany"
+ "site_exists": "To site_id jest ju\u017c skonfigurowane."
},
"error": {
- "site_exists": "Ten site_id jest ju\u017c skonfigurowany"
+ "site_exists": "To site_id jest ju\u017c skonfigurowane."
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/.translations/sv.json b/homeassistant/components/solaredge/.translations/sv.json
new file mode 100644
index 00000000000..25bb0f325a1
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/sv.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Denna site_id \u00e4r redan konfigurerad"
+ },
+ "error": {
+ "site_exists": "Denna site_id \u00e4r redan konfigurerad"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-nyckeln f\u00f6r den h\u00e4r webbplatsen",
+ "name": "Namnet p\u00e5 den h\u00e4r installationen",
+ "site_id": "SolarEdge webbplats-id"
+ },
+ "title": "Definiera API-parametrarna f\u00f6r den h\u00e4r installationen"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py
index 7c8c9380522..62bf99ab383 100644
--- a/homeassistant/components/solaredge/config_flow.py
+++ b/homeassistant/components/solaredge/config_flow.py
@@ -54,7 +54,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return True
async def async_step_user(self, user_input=None):
- """Step when user intializes a integration."""
+ """Step when user initializes a integration."""
self._errors = {}
if user_input is not None:
name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME))
diff --git a/homeassistant/components/solarlog/.translations/hu.json b/homeassistant/components/solarlog/.translations/hu.json
new file mode 100644
index 00000000000..e52cebefda6
--- /dev/null
+++ b/homeassistant/components/solarlog/.translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/.translations/pl.json b/homeassistant/components/solarlog/.translations/pl.json
index 251d183b361..fdbf21feb92 100644
--- a/homeassistant/components/solarlog/.translations/pl.json
+++ b/homeassistant/components/solarlog/.translations/pl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
},
"error": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a adres hosta"
},
"step": {
diff --git a/homeassistant/components/solarlog/.translations/ru.json b/homeassistant/components/solarlog/.translations/ru.json
index b64496c4591..3333d5c0d5f 100644
--- a/homeassistant/components/solarlog/.translations/ru.json
+++ b/homeassistant/components/solarlog/.translations/ru.json
@@ -11,7 +11,7 @@
"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"
+ "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 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 Solar-Log"
},
"title": "Solar-Log"
}
diff --git a/homeassistant/components/solarlog/.translations/sv.json b/homeassistant/components/solarlog/.translations/sv.json
new file mode 100644
index 00000000000..981bd9fb167
--- /dev/null
+++ b/homeassistant/components/solarlog/.translations/sv.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten \u00e4r redan konfigurerad"
+ },
+ "error": {
+ "already_configured": "Enheten \u00e4r redan konfigurerad",
+ "cannot_connect": "Det gick inte att ansluta, kontrollera v\u00e4rdadressen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e4rdnamnet eller ip-adressen f\u00f6r din Solar-Log-enhet",
+ "name": "Prefixet som ska anv\u00e4ndas f\u00f6r dina Solar-Log sensorer"
+ },
+ "title": "Definiera din Solar-Log-anslutning"
+ }
+ },
+ "title": "Solar-Log"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py
index 5cb2d5deec1..111155b27b6 100644
--- a/homeassistant/components/solarlog/config_flow.py
+++ b/homeassistant/components/solarlog/config_flow.py
@@ -54,7 +54,7 @@ class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return False
async def async_step_user(self, user_input=None):
- """Step when user intializes a integration."""
+ """Step when user initializes a integration."""
self._errors = {}
if user_input is not None:
# set some defaults in case we need to return to the form
diff --git a/homeassistant/components/soma/.translations/hu.json b/homeassistant/components/soma/.translations/hu.json
new file mode 100644
index 00000000000..797cfa1b2d8
--- /dev/null
+++ b/homeassistant/components/soma/.translations/hu.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "connection_error": "Nem siker\u00fclt csatlakozni a SOMA Connecthez.",
+ "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.",
+ "result_error": "A SOMA Connect hiba\u00e1llapottal v\u00e1laszolt."
+ },
+ "create_entry": {
+ "default": "Soma sikeresen hiteles\u00edtett."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Kiszolg\u00e1l\u00f3",
+ "port": "Port"
+ },
+ "description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.",
+ "title": "SOMA csatlakoz\u00e1s"
+ }
+ },
+ "title": "Soma"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/soma/.translations/sv.json b/homeassistant/components/soma/.translations/sv.json
new file mode 100644
index 00000000000..bb3ce895fd5
--- /dev/null
+++ b/homeassistant/components/soma/.translations/sv.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Du kan bara konfigurera ett Soma-konto.",
+ "authorize_url_timeout": "Timeout vid generering av auktoriserings-url.",
+ "connection_error": "Det gick inte att ansluta till SOMA Connect.",
+ "missing_configuration": "Soma-komponenten \u00e4r inte konfigurerad. F\u00f6lj dokumentationen.",
+ "result_error": "SOMA Connect svarade med felstatus."
+ },
+ "create_entry": {
+ "default": "Lyckad autentisering med Soma."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e4rd",
+ "port": "Port"
+ },
+ "description": "Ange anslutningsinst\u00e4llningar f\u00f6r din SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ },
+ "title": "Soma"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/.translations/hu.json b/homeassistant/components/somfy/.translations/hu.json
new file mode 100644
index 00000000000..3df2fb30477
--- /dev/null
+++ b/homeassistant/components/somfy/.translations/hu.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "pick_implementation": {
+ "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/.translations/sv.json b/homeassistant/components/somfy/.translations/sv.json
index 390cd1f4d80..982b32a90a1 100644
--- a/homeassistant/components/somfy/.translations/sv.json
+++ b/homeassistant/components/somfy/.translations/sv.json
@@ -8,6 +8,11 @@
"create_entry": {
"default": "Lyckad autentisering med Somfy."
},
+ "step": {
+ "pick_implementation": {
+ "title": "V\u00e4lj autentiseringsmetod"
+ }
+ },
"title": "Somfy"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py
index 82bcdad6ef4..c0781b37603 100644
--- a/homeassistant/components/sonarr/sensor.py
+++ b/homeassistant/components/sonarr/sensor.py
@@ -14,6 +14,15 @@ from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_PORT,
CONF_SSL,
+ DATA_BYTES,
+ DATA_EXABYTES,
+ DATA_GIGABYTES,
+ DATA_KILOBYTES,
+ DATA_MEGABYTES,
+ DATA_PETABYTES,
+ DATA_TERABYTES,
+ DATA_YOTTABYTES,
+ DATA_ZETTABYTES,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -29,10 +38,10 @@ DEFAULT_HOST = "localhost"
DEFAULT_PORT = 8989
DEFAULT_URLBASE = ""
DEFAULT_DAYS = "1"
-DEFAULT_UNIT = "GB"
+DEFAULT_UNIT = DATA_GIGABYTES
SENSOR_TYPES = {
- "diskspace": ["Disk Space", "GB", "mdi:harddisk"],
+ "diskspace": ["Disk Space", DATA_GIGABYTES, "mdi:harddisk"],
"queue": ["Queue", "Episodes", "mdi:download"],
"upcoming": ["Upcoming", "Episodes", "mdi:television"],
"wanted": ["Wanted", "Episodes", "mdi:television"],
@@ -52,7 +61,17 @@ ENDPOINTS = {
}
# Support to Yottabytes for the future, why not
-BYTE_SIZES = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
+BYTE_SIZES = [
+ DATA_BYTES,
+ DATA_KILOBYTES,
+ DATA_MEGABYTES,
+ DATA_GIGABYTES,
+ DATA_TERABYTES,
+ DATA_PETABYTES,
+ DATA_EXABYTES,
+ DATA_ZETTABYTES,
+ DATA_YOTTABYTES,
+]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
diff --git a/homeassistant/components/sonos/.translations/zh-Hans.json b/homeassistant/components/sonos/.translations/zh-Hans.json
index 17c1e78d3e8..de2609f4a71 100644
--- a/homeassistant/components/sonos/.translations/zh-Hans.json
+++ b/homeassistant/components/sonos/.translations/zh-Hans.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Sonos \u8bbe\u5907\u3002",
- "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Sonos \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002"
+ "single_instance_allowed": "\u53ea\u9700\u8bbe\u7f6e\u4e00\u6b21 Sonos \u5373\u53ef\u3002"
},
"step": {
"confirm": {
diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py
index 97d03e2116e..37b479a90b1 100644
--- a/homeassistant/components/sonos/media_player.py
+++ b/homeassistant/components/sonos/media_player.py
@@ -107,7 +107,7 @@ class SonosData:
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Sonos platform. Obsolete."""
_LOGGER.error(
- "Loading Sonos by media_player platform config is no longer supported"
+ "Loading Sonos by media_player platform configuration is no longer supported"
)
@@ -762,6 +762,7 @@ class SonosEntity(MediaPlayerDevice):
return await self.hass.async_add_executor_job(_get_soco_group)
+ @callback
def _async_regroup(group):
"""Rebuild internal group layout."""
sonos_group = []
diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py
index 72677995a9d..71592e92c17 100644
--- a/homeassistant/components/soundtouch/media_player.py
+++ b/homeassistant/components/soundtouch/media_player.py
@@ -241,57 +241,46 @@ class SoundTouchDevice(MediaPlayerDevice):
def turn_off(self):
"""Turn off media player."""
self._device.power_off()
- self._status = self._device.status()
def turn_on(self):
"""Turn on media player."""
self._device.power_on()
- self._status = self._device.status()
def volume_up(self):
"""Volume up the media player."""
self._device.volume_up()
- self._volume = self._device.volume()
def volume_down(self):
"""Volume down media player."""
self._device.volume_down()
- self._volume = self._device.volume()
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._device.set_volume(int(volume * 100))
- self._volume = self._device.volume()
def mute_volume(self, mute):
"""Send mute command."""
self._device.mute()
- self._volume = self._device.volume()
def media_play_pause(self):
"""Simulate play pause media player."""
self._device.play_pause()
- self._status = self._device.status()
def media_play(self):
"""Send play command."""
self._device.play()
- self._status = self._device.status()
def media_pause(self):
"""Send media pause command to media player."""
self._device.pause()
- self._status = self._device.status()
def media_next_track(self):
"""Send next track command."""
self._device.next_track()
- self._status = self._device.status()
def media_previous_track(self):
"""Send the previous track command."""
self._device.previous_track()
- self._status = self._device.status()
@property
def media_image_url(self):
diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py
index b5ff14ce01d..34689c4dccf 100644
--- a/homeassistant/components/spc/binary_sensor.py
+++ b/homeassistant/components/spc/binary_sensor.py
@@ -19,6 +19,7 @@ def _get_device_class(zone_type):
ZoneType.ALARM: "motion",
ZoneType.ENTRY_EXIT: "opening",
ZoneType.FIRE: "smoke",
+ ZoneType.TECHNICAL: "power",
}.get(zone_type)
diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py
index 69aadb7ac6c..a08c9421c76 100644
--- a/homeassistant/components/speedtestdotnet/const.py
+++ b/homeassistant/components/speedtestdotnet/const.py
@@ -1,10 +1,12 @@
"""Consts used by Speedtest.net."""
+from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND
+
DOMAIN = "speedtestdotnet"
DATA_UPDATED = f"{DOMAIN}_data_updated"
SENSOR_TYPES = {
"ping": ["Ping", "ms"],
- "download": ["Download", "Mbit/s"],
- "upload": ["Upload", "Mbit/s"],
+ "download": ["Download", DATA_RATE_MEGABITS_PER_SECOND],
+ "upload": ["Upload", DATA_RATE_MEGABITS_PER_SECOND],
}
diff --git a/homeassistant/components/spotify/.translations/nl.json b/homeassistant/components/spotify/.translations/nl.json
new file mode 100644
index 00000000000..abe59854044
--- /dev/null
+++ b/homeassistant/components/spotify/.translations/nl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "U kunt slechts \u00e9\u00e9n Spotify-account configureren.",
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "missing_configuration": "De Spotify integratie is niet geconfigureerd. Gelieve de documentatie te volgen."
+ },
+ "create_entry": {
+ "default": "Succesvol geauthenticeerd met Spotify."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Kies Authenticatiemethode"
+ }
+ },
+ "title": "Spotify"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/.translations/sl.json b/homeassistant/components/spotify/.translations/sl.json
new file mode 100644
index 00000000000..6ab0b0a40a6
--- /dev/null
+++ b/homeassistant/components/spotify/.translations/sl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Konfigurirate lahko samo en ra\u010dun Spotify.",
+ "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.",
+ "missing_configuration": "Integracija Spotify ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo."
+ },
+ "create_entry": {
+ "default": "Uspe\u0161no overjena s Spotify."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Izberite na\u010din preverjanja pristnosti"
+ }
+ },
+ "title": "Spotify"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/.translations/tr.json b/homeassistant/components/spotify/.translations/tr.json
new file mode 100644
index 00000000000..88755b800f4
--- /dev/null
+++ b/homeassistant/components/spotify/.translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Yaln\u0131zca bir Spotify hesab\u0131 ayarlayabilirsin.",
+ "authorize_url_timeout": "Kimlik do\u011frulama URL'sini olu\u015ftururken zaman a\u015f\u0131m\u0131 ger\u00e7ekle\u015fti.",
+ "missing_configuration": "Spotify entegrasyonu ayarlanmam\u0131\u015f. L\u00fctfen dok\u00fcmentasyonu takip et."
+ },
+ "create_entry": {
+ "default": "Spotify ile kimlik ba\u015far\u0131yla do\u011fruland\u0131."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7"
+ }
+ },
+ "title": "Spotify"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/starline/.translations/hu.json b/homeassistant/components/starline/.translations/hu.json
index c45d9ac871e..ccc5b7983d0 100644
--- a/homeassistant/components/starline/.translations/hu.json
+++ b/homeassistant/components/starline/.translations/hu.json
@@ -8,9 +8,35 @@
"step": {
"auth_app": {
"data": {
- "app_id": "App ID"
- }
+ "app_id": "App ID",
+ "app_secret": "Titok"
+ },
+ "description": "Alkalmaz\u00e1s azonos\u00edt\u00f3ja \u00e9s titkos k\u00f3dja a StarLine fejleszt\u0151i fi\u00f3kb\u00f3l ",
+ "title": "Alkalmaz\u00e1si hiteles\u00edt\u0151 adatok"
+ },
+ "auth_captcha": {
+ "data": {
+ "captcha_code": "K\u00f3d a k\u00e9pr\u0151l"
+ },
+ "description": "{captcha_img}",
+ "title": "Captcha"
+ },
+ "auth_mfa": {
+ "data": {
+ "mfa_code": "SMS k\u00f3d"
+ },
+ "description": "Adja meg a {phone_number} telefonra k\u00fcld\u00f6tt k\u00f3dot.",
+ "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s"
+ },
+ "auth_user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "description": "A StarLine fi\u00f3k e-mail c\u00edme \u00e9s jelszava",
+ "title": "Felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok"
}
- }
+ },
+ "title": "Starline"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/starline/.translations/sv.json b/homeassistant/components/starline/.translations/sv.json
index 42d01b56753..83f2300892d 100644
--- a/homeassistant/components/starline/.translations/sv.json
+++ b/homeassistant/components/starline/.translations/sv.json
@@ -1,11 +1,42 @@
{
"config": {
+ "error": {
+ "error_auth_app": "Fel applikations-id eller hemlighet",
+ "error_auth_mfa": "Felaktig kod",
+ "error_auth_user": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord"
+ },
"step": {
"auth_app": {
"data": {
+ "app_id": "App-ID",
"app_secret": "Hemlighet"
- }
+ },
+ "description": "Applikations-ID och hemlig kod fr\u00e5n StarLine-utvecklarkonto",
+ "title": "Autentiseringsuppgifter f\u00f6r applikation"
+ },
+ "auth_captcha": {
+ "data": {
+ "captcha_code": "Kod fr\u00e5n bild"
+ },
+ "description": "{captcha_img}",
+ "title": "Captcha"
+ },
+ "auth_mfa": {
+ "data": {
+ "mfa_code": "SMS-kod"
+ },
+ "description": "Ange koden som skickas till telefonen {phone_number}",
+ "title": "Tv\u00e5faktorautentisering"
+ },
+ "auth_user": {
+ "data": {
+ "password": "L\u00f6senord",
+ "username": "Anv\u00e4ndarnamn"
+ },
+ "description": "StarLine-kontots e-postadress och l\u00f6senord",
+ "title": "Anv\u00e4ndaruppgifter"
}
- }
+ },
+ "title": "StarLine"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py
index fa559f62913..34415e9dca4 100644
--- a/homeassistant/components/starline/config_flow.py
+++ b/homeassistant/components/starline/config_flow.py
@@ -4,7 +4,7 @@ from typing import Optional
from starline import StarlineAuth
import voluptuous as vol
-from homeassistant import config_entries
+from homeassistant import config_entries, core
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import ( # pylint: disable=unused-import
@@ -85,6 +85,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self._async_authenticate_user(error)
return self._async_form_auth_captcha(error)
+ @core.callback
def _async_form_auth_app(self, error=None):
"""Authenticate application form."""
errors = {}
@@ -106,6 +107,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
+ @core.callback
def _async_form_auth_user(self, error=None):
"""Authenticate user form."""
errors = {}
@@ -127,6 +129,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
+ @core.callback
def _async_form_auth_mfa(self, error=None):
"""Authenticate mfa form."""
errors = {}
@@ -146,6 +149,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders={"phone_number": self._phone_number},
)
+ @core.callback
def _async_form_auth_captcha(self, error=None):
"""Captcha verification form."""
errors = {}
diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py
index e07f21e5d60..82106c2da57 100644
--- a/homeassistant/components/startca/sensor.py
+++ b/homeassistant/components/startca/sensor.py
@@ -8,7 +8,12 @@ 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
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_MONITORED_VARIABLES,
+ CONF_NAME,
+ DATA_GIGABYTES,
+)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -19,7 +24,6 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Start.ca"
CONF_TOTAL_BANDWIDTH = "total_bandwidth"
-GIGABYTES = "GB"
PERCENT = "%"
MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
@@ -27,17 +31,17 @@ REQUEST_TIMEOUT = 5 # seconds
SENSOR_TYPES = {
"usage": ["Usage Ratio", PERCENT, "mdi:percent"],
- "usage_gb": ["Usage", GIGABYTES, "mdi:download"],
- "limit": ["Data limit", GIGABYTES, "mdi:download"],
- "used_download": ["Used Download", GIGABYTES, "mdi:download"],
- "used_upload": ["Used Upload", GIGABYTES, "mdi:upload"],
- "used_total": ["Used Total", GIGABYTES, "mdi:download"],
- "grace_download": ["Grace Download", GIGABYTES, "mdi:download"],
- "grace_upload": ["Grace Upload", GIGABYTES, "mdi:upload"],
- "grace_total": ["Grace Total", GIGABYTES, "mdi:download"],
- "total_download": ["Total Download", GIGABYTES, "mdi:download"],
- "total_upload": ["Total Upload", GIGABYTES, "mdi:download"],
- "used_remaining": ["Remaining", GIGABYTES, "mdi:download"],
+ "usage_gb": ["Usage", DATA_GIGABYTES, "mdi:download"],
+ "limit": ["Data limit", DATA_GIGABYTES, "mdi:download"],
+ "used_download": ["Used Download", DATA_GIGABYTES, "mdi:download"],
+ "used_upload": ["Used Upload", DATA_GIGABYTES, "mdi:upload"],
+ "used_total": ["Used Total", DATA_GIGABYTES, "mdi:download"],
+ "grace_download": ["Grace Download", DATA_GIGABYTES, "mdi:download"],
+ "grace_upload": ["Grace Upload", DATA_GIGABYTES, "mdi:upload"],
+ "grace_total": ["Grace Total", DATA_GIGABYTES, "mdi:download"],
+ "total_download": ["Total Download", DATA_GIGABYTES, "mdi:download"],
+ "total_upload": ["Total Upload", DATA_GIGABYTES, "mdi:download"],
+ "used_remaining": ["Remaining", DATA_GIGABYTES, "mdi:download"],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py
index 865fda93a3e..d85b6b079ae 100644
--- a/homeassistant/components/statistics/sensor.py
+++ b/homeassistant/components/statistics/sensor.py
@@ -294,6 +294,7 @@ class StatisticsSensor(Entity):
"""Timer callback for sensor update."""
_LOGGER.debug("%s: executing scheduled update", self.entity_id)
self.async_schedule_update_ha_state(True)
+ self._update_listener = None
self._update_listener = async_track_point_in_utc_time(
self.hass, _scheduled_update, next_to_purge_timestamp
diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py
index 99ffd833eb3..6cd07c7f926 100644
--- a/homeassistant/components/stream/worker.py
+++ b/homeassistant/components/stream/worker.py
@@ -95,7 +95,7 @@ def stream_worker(hass, stream, quit_event):
if packet.is_keyframe:
# Calculate the segment duration by multiplying the presentation
# timestamp by the time base, which gets us total seconds.
- # By then dividing by the seqence, we can calculate how long
+ # By then dividing by the sequence, we can calculate how long
# each segment is, assuming the stream starts from 0.
segment_duration = (packet.pts * packet.time_base) / sequence
# Save segment to outputs
diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py
index 450d7eb9a15..a22ba4a1335 100644
--- a/homeassistant/components/surepetcare/__init__.py
+++ b/homeassistant/components/surepetcare/__init__.py
@@ -1,17 +1,17 @@
"""Support for Sure Petcare cat/pet flaps."""
import logging
+from typing import Any, Dict, List
from surepy import (
SurePetcare,
SurePetcareAuthenticationError,
SurePetcareError,
- SureThingID,
+ SureProductID,
)
import voluptuous as vol
from homeassistant.const import (
CONF_ID,
- CONF_NAME,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_TYPE,
@@ -23,9 +23,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from .const import (
+ CONF_FEEDERS,
CONF_FLAPS,
- CONF_HOUSEHOLD_ID,
+ CONF_PARENT,
CONF_PETS,
+ CONF_PRODUCT_ID,
DATA_SURE_PETCARE,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
@@ -36,23 +38,19 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-FLAP_SCHEMA = vol.Schema(
- {vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string}
-)
-
-PET_SCHEMA = vol.Schema(
- {vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string}
-)
-
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_HOUSEHOLD_ID): cv.positive_int,
- vol.Required(CONF_FLAPS): vol.All(cv.ensure_list, [FLAP_SCHEMA]),
- vol.Required(CONF_PETS): vol.All(cv.ensure_list, [PET_SCHEMA]),
+ vol.Optional(CONF_FEEDERS, default=[]): vol.All(
+ cv.ensure_list, [cv.positive_int]
+ ),
+ vol.Optional(CONF_FLAPS, default=[]): vol.All(
+ cv.ensure_list, [cv.positive_int]
+ ),
+ vol.Optional(CONF_PETS): vol.All(cv.ensure_list, [cv.positive_int]),
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.time_period,
@@ -63,7 +61,7 @@ CONFIG_SCHEMA = vol.Schema(
)
-async def async_setup(hass, config):
+async def async_setup(hass, config) -> bool:
"""Initialize the Sure Petcare component."""
conf = config[DOMAIN]
@@ -78,11 +76,10 @@ async def async_setup(hass, config):
surepy = SurePetcare(
conf[CONF_USERNAME],
conf[CONF_PASSWORD],
- conf[CONF_HOUSEHOLD_ID],
hass.loop,
async_get_clientsession(hass),
)
- await surepy.refresh_token()
+ await surepy.get_data()
except SurePetcareAuthenticationError:
_LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!")
return False
@@ -90,32 +87,44 @@ async def async_setup(hass, config):
_LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error)
return False
- # add flaps
+ # add feeders
things = [
- {
- CONF_NAME: flap[CONF_NAME],
- CONF_ID: flap[CONF_ID],
- CONF_TYPE: SureThingID.FLAP.name,
- }
- for flap in conf[CONF_FLAPS]
+ {CONF_ID: feeder, CONF_TYPE: SureProductID.FEEDER}
+ for feeder in conf[CONF_FEEDERS]
]
- # add pets
+ # add flaps (don't differentiate between CAT and PET for now)
things.extend(
[
- {
- CONF_NAME: pet[CONF_NAME],
- CONF_ID: pet[CONF_ID],
- CONF_TYPE: SureThingID.PET.name,
- }
- for pet in conf[CONF_PETS]
+ {CONF_ID: flap, CONF_TYPE: SureProductID.PET_FLAP}
+ for flap in conf[CONF_FLAPS]
]
)
- spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI(
- hass, surepy, things, conf[CONF_HOUSEHOLD_ID]
+ # discover hubs the flaps/feeders are connected to
+ for device in things.copy():
+ device_data = await surepy.device(device[CONF_ID])
+ if (
+ CONF_PARENT in device_data
+ and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SureProductID.HUB
+ and device_data[CONF_PARENT][CONF_ID] not in things
+ ):
+ things.append(
+ {
+ CONF_ID: device_data[CONF_PARENT][CONF_ID],
+ CONF_TYPE: SureProductID.HUB,
+ }
+ )
+
+ # add pets
+ things.extend(
+ [{CONF_ID: pet, CONF_TYPE: SureProductID.PET} for pet in conf[CONF_PETS]]
)
+ _LOGGER.debug("Devices and Pets to setup: %s", things)
+
+ spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI(hass, surepy, things)
+
# initial update
await spc.async_update()
@@ -135,16 +144,18 @@ async def async_setup(hass, config):
class SurePetcareAPI:
"""Define a generic Sure Petcare object."""
- def __init__(self, hass, surepy, ids, household_id):
+ def __init__(self, hass, surepy: SurePetcare, ids: List[Dict[str, Any]]) -> None:
"""Initialize the Sure Petcare object."""
self.hass = hass
self.surepy = surepy
- self.household_id = household_id
self.ids = ids
- self.states = {}
+ self.states: Dict[str, Any] = {}
- async def async_update(self, args=None):
+ async def async_update(self, arg: Any = None) -> None:
"""Refresh Sure Petcare data."""
+
+ await self.surepy.get_data()
+
for thing in self.ids:
sure_id = thing[CONF_ID]
sure_type = thing[CONF_TYPE]
@@ -152,10 +163,15 @@ class SurePetcareAPI:
try:
type_state = self.states.setdefault(sure_type, {})
- if sure_type == SureThingID.FLAP.name:
- type_state[sure_id] = await self.surepy.get_flap_data(sure_id)
- elif sure_type == SureThingID.PET.name:
- type_state[sure_id] = await self.surepy.get_pet_data(sure_id)
+ if sure_type in [
+ SureProductID.CAT_FLAP,
+ SureProductID.PET_FLAP,
+ SureProductID.FEEDER,
+ SureProductID.HUB,
+ ]:
+ type_state[sure_id] = await self.surepy.device(sure_id)
+ elif sure_type == SureProductID.PET:
+ type_state[sure_id] = await self.surepy.pet(sure_id)
except SurePetcareError as error:
_LOGGER.error("Unable to retrieve data from surepetcare.io: %s", error)
diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py
index 100da5cb790..5b3ac492137 100644
--- a/homeassistant/components/surepetcare/binary_sensor.py
+++ b/homeassistant/components/surepetcare/binary_sensor.py
@@ -1,23 +1,28 @@
"""Support for Sure PetCare Flaps/Pets binary sensors."""
+from datetime import datetime
import logging
+from typing import Any, Dict, Optional
-from surepy import SureLocationID, SureLockStateID, SureThingID
+from surepy import SureLocationID, SureProductID
from homeassistant.components.binary_sensor import (
- DEVICE_CLASS_LOCK,
+ DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_PRESENCE,
BinarySensorDevice,
)
-from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
+from homeassistant.const import CONF_ID, CONF_TYPE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from .const import DATA_SURE_PETCARE, DEFAULT_DEVICE_CLASS, SPC, TOPIC_UPDATE
+from . import SurePetcareAPI
+from .const import DATA_SURE_PETCARE, SPC, TOPIC_UPDATE
_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_entities, discovery_info=None
+) -> None:
"""Set up Sure PetCare Flaps sensors based on a config entry."""
if discovery_info is None:
return
@@ -30,10 +35,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
sure_id = thing[CONF_ID]
sure_type = thing[CONF_TYPE]
- if sure_type == SureThingID.FLAP.name:
- entity = Flap(sure_id, thing[CONF_NAME], spc)
- elif sure_type == SureThingID.PET.name:
- entity = Pet(sure_id, thing[CONF_NAME], spc)
+ # connectivity
+ if sure_type in [
+ SureProductID.CAT_FLAP,
+ SureProductID.PET_FLAP,
+ SureProductID.FEEDER,
+ ]:
+ entities.append(DeviceConnectivity(sure_id, sure_type, spc))
+
+ if sure_type == SureProductID.PET:
+ entity = Pet(sure_id, spc)
+ elif sure_type == SureProductID.HUB:
+ entity = Hub(sure_id, spc)
+ else:
+ continue
entities.append(entity)
@@ -44,57 +59,67 @@ class SurePetcareBinarySensor(BinarySensorDevice):
"""A binary sensor implementation for Sure Petcare Entities."""
def __init__(
- self, _id: int, name: str, spc, device_class: str, sure_type: SureThingID
+ self,
+ _id: int,
+ spc: SurePetcareAPI,
+ device_class: str,
+ sure_type: SureProductID,
):
"""Initialize a Sure Petcare binary sensor."""
self._id = _id
- self._name = name
- self._spc = spc
- self._device_class = device_class
self._sure_type = sure_type
- self._state = {}
+ self._device_class = device_class
+
+ self._spc: SurePetcareAPI = spc
+ self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id)
+ self._state: Dict[str, Any] = {}
+
+ # cover special case where a device has no name set
+ if "name" in self._spc_data:
+ name = self._spc_data["name"]
+ else:
+ name = f"Unnamed {self._sure_type.name.capitalize()}"
+
+ self._name = f"{self._sure_type.name.capitalize()} {name.capitalize()}"
self._async_unsub_dispatcher_connect = None
@property
- def is_on(self):
+ def is_on(self) -> Optional[bool]:
"""Return true if entity is on/unlocked."""
return bool(self._state)
@property
- def should_poll(self):
+ def should_poll(self) -> bool:
"""Return true."""
return False
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the device if any."""
return self._name
@property
- def device_state_attributes(self):
- """Return the state attributes of the device."""
- return self._state
-
- @property
- def device_class(self):
+ def device_class(self) -> str:
"""Return the device class."""
- return DEFAULT_DEVICE_CLASS if not self._device_class else self._device_class
+ return None if not self._device_class else self._device_class
@property
- def unique_id(self):
+ def unique_id(self: BinarySensorDevice) -> str:
"""Return an unique ID."""
- return f"{self._spc.household_id}-{self._id}"
+ return f"{self._spc_data['household_id']}-{self._id}"
- async def async_update(self):
+ async def async_update(self) -> None:
"""Get the latest data and update the state."""
- self._state = self._spc.states[self._sure_type][self._id].get("data")
+ self._spc_data = self._spc.states[self._sure_type].get(self._id)
+ self._state = self._spc_data.get("status")
+ _LOGGER.debug("%s -> self._state: %s", self._name, self._state)
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@callback
- def update():
+ def update() -> None:
"""Update the state."""
self.async_schedule_update_ha_state(True)
@@ -102,54 +127,38 @@ class SurePetcareBinarySensor(BinarySensorDevice):
self.hass, TOPIC_UPDATE, update
)
- async def async_will_remove_from_hass(self):
+ async def async_will_remove_from_hass(self) -> None:
"""Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
self._async_unsub_dispatcher_connect()
-class Flap(SurePetcareBinarySensor):
- """Sure Petcare Flap."""
+class Hub(SurePetcareBinarySensor):
+ """Sure Petcare Pet."""
- def __init__(self, _id: int, name: str, spc):
- """Initialize a Sure Petcare Flap."""
- super().__init__(
- _id,
- f"Flap {name.capitalize()}",
- spc,
- DEVICE_CLASS_LOCK,
- SureThingID.FLAP.name,
- )
+ def __init__(self, _id: int, spc: SurePetcareAPI) -> None:
+ """Initialize a Sure Petcare Hub."""
+ super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, SureProductID.HUB)
@property
- def is_on(self):
- """Return true if entity is on/unlocked."""
- try:
- return bool(self._state["locking"]["mode"] == SureLockStateID.UNLOCKED)
- except (KeyError, TypeError):
- return None
+ def available(self) -> bool:
+ """Return true if entity is available."""
+ return bool(self._state["online"])
@property
- def device_state_attributes(self):
+ def is_on(self) -> bool:
+ """Return true if entity is online."""
+ return self.available
+
+ @property
+ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return the state attributes of the device."""
attributes = None
if self._state:
- try:
- attributes = {
- "battery_voltage": self._state["battery"] / 4,
- "locking_mode": self._state["locking"]["mode"],
- "device_rssi": self._state["signal"]["device_rssi"],
- "hub_rssi": self._state["signal"]["hub_rssi"],
- }
-
- except (KeyError, TypeError) as error:
- _LOGGER.error(
- "Error getting device state attributes from %s: %s\n\n%s",
- self._name,
- error,
- self._state,
- )
- attributes = self._state
+ attributes = {
+ "led_mode": int(self._state["led_mode"]),
+ "pairing_mode": bool(self._state["pairing_mode"]),
+ }
return attributes
@@ -157,20 +166,76 @@ class Flap(SurePetcareBinarySensor):
class Pet(SurePetcareBinarySensor):
"""Sure Petcare Pet."""
- def __init__(self, _id: int, name: str, spc):
+ def __init__(self, _id: int, spc: SurePetcareAPI) -> None:
"""Initialize a Sure Petcare Pet."""
- super().__init__(
- _id,
- f"Pet {name.capitalize()}",
- spc,
- DEVICE_CLASS_PRESENCE,
- SureThingID.PET.name,
- )
+ super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, SureProductID.PET)
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return true if entity is at home."""
try:
- return bool(self._state["where"] == SureLocationID.INSIDE)
+ return bool(SureLocationID(self._state["where"]) == SureLocationID.INSIDE)
except (KeyError, TypeError):
return False
+
+ @property
+ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ """Return the state attributes of the device."""
+ attributes = None
+ if self._state:
+ attributes = {
+ "since": str(
+ datetime.fromisoformat(self._state["since"]).replace(tzinfo=None)
+ ),
+ "where": SureLocationID(self._state["where"]).name.capitalize(),
+ }
+
+ return attributes
+
+ async def async_update(self) -> None:
+ """Get the latest data and update the state."""
+ self._spc_data = self._spc.states[self._sure_type].get(self._id)
+ self._state = self._spc_data.get("position")
+ _LOGGER.debug("%s -> self._state: %s", self._name, self._state)
+
+
+class DeviceConnectivity(SurePetcareBinarySensor):
+ """Sure Petcare Pet."""
+
+ def __init__(
+ self, _id: int, sure_type: SureProductID, spc: SurePetcareAPI,
+ ) -> None:
+ """Initialize a Sure Petcare Device."""
+ super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, sure_type)
+
+ @property
+ def name(self) -> str:
+ """Return the name of the device if any."""
+ return f"{self._name}_connectivity"
+
+ @property
+ def unique_id(self: BinarySensorDevice) -> str:
+ """Return an unique ID."""
+ return f"{self._spc_data['household_id']}-{self._id}-connectivity"
+
+ @property
+ def available(self) -> bool:
+ """Return true if entity is available."""
+ return bool(self._state)
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if entity is online."""
+ return self.available
+
+ @property
+ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ """Return the state attributes of the device."""
+ attributes = None
+ if self._state:
+ attributes = {
+ "device_rssi": f'{self._state["signal"]["device_rssi"]:.2f}',
+ "hub_rssi": f'{self._state["signal"]["hub_rssi"]:.2f}',
+ }
+
+ return attributes
diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py
index 731bfba07e6..d534398784f 100644
--- a/homeassistant/components/surepetcare/const.py
+++ b/homeassistant/components/surepetcare/const.py
@@ -11,8 +11,11 @@ SPC = "spc"
SUREPY = "surepy"
CONF_HOUSEHOLD_ID = "household_id"
+CONF_FEEDERS = "feeders"
CONF_FLAPS = "flaps"
+CONF_PARENT = "parent"
CONF_PETS = "pets"
+CONF_PRODUCT_ID = "product_id"
CONF_DATA = "data"
SURE_IDS = "sure_ids"
diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json
index b4879932714..b1efa4ce639 100644
--- a/homeassistant/components/surepetcare/manifest.json
+++ b/homeassistant/components/surepetcare/manifest.json
@@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/integrations/surepetcare",
"dependencies": [],
"codeowners": ["@benleb"],
- "requirements": ["surepy==0.1.10"]
+ "requirements": ["surepy==0.2.3"]
}
diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py
index dd7fdcb0316..8dc9cf30e3c 100644
--- a/homeassistant/components/surepetcare/sensor.py
+++ b/homeassistant/components/surepetcare/sensor.py
@@ -1,19 +1,15 @@
"""Support for Sure PetCare Flaps/Pets sensors."""
import logging
+from typing import Any, Dict, Optional
-from surepy import SureThingID
+from surepy import SureLockStateID, SureProductID
-from homeassistant.const import (
- ATTR_VOLTAGE,
- CONF_ID,
- CONF_NAME,
- CONF_TYPE,
- DEVICE_CLASS_BATTERY,
-)
+from homeassistant.const import ATTR_VOLTAGE, CONF_ID, CONF_TYPE, DEVICE_CLASS_BATTERY
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
+from . import SurePetcareAPI
from .const import (
DATA_SURE_PETCARE,
SPC,
@@ -30,97 +26,82 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
if discovery_info is None:
return
+ entities = []
+
spc = hass.data[DATA_SURE_PETCARE][SPC]
- async_add_entities(
- [
- FlapBattery(entity[CONF_ID], entity[CONF_NAME], spc)
- for entity in spc.ids
- if entity[CONF_TYPE] == SureThingID.FLAP.name
- ],
- True,
- )
+
+ for entity in spc.ids:
+ sure_type = entity[CONF_TYPE]
+
+ if sure_type in [
+ SureProductID.CAT_FLAP,
+ SureProductID.PET_FLAP,
+ SureProductID.FEEDER,
+ ]:
+ entities.append(SureBattery(entity[CONF_ID], sure_type, spc))
+
+ if sure_type in [
+ SureProductID.CAT_FLAP,
+ SureProductID.PET_FLAP,
+ ]:
+ entities.append(Flap(entity[CONF_ID], sure_type, spc))
+
+ async_add_entities(entities, True)
-class FlapBattery(Entity):
- """Sure Petcare Flap."""
+class SurePetcareSensor(Entity):
+ """A binary sensor implementation for Sure Petcare Entities."""
+
+ def __init__(
+ self, _id: int, sure_type: SureProductID, spc: SurePetcareAPI,
+ ):
+ """Initialize a Sure Petcare sensor."""
- def __init__(self, _id: int, name: str, spc):
- """Initialize a Sure Petcare Flap battery sensor."""
self._id = _id
- self._name = f"Flap {name.capitalize()} Battery Level"
+ self._sure_type = sure_type
+
self._spc = spc
- self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data")
+ self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id)
+ self._state: Dict[str, Any] = {}
+
+ self._name = (
+ f"{self._sure_type.name.capitalize()} "
+ f"{self._spc_data['name'].capitalize()}"
+ )
self._async_unsub_dispatcher_connect = None
@property
- def should_poll(self):
- """Return true."""
- return False
-
- @property
- def name(self):
+ def name(self) -> str:
"""Return the name of the device if any."""
return self._name
@property
- def state(self):
- """Return battery level in percent."""
- try:
- per_battery_voltage = self._state["battery"] / 4
- voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW
- battery_percent = int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100)
- except (KeyError, TypeError):
- battery_percent = None
-
- return battery_percent
-
- @property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return an unique ID."""
- return f"{self._spc.household_id}-{self._id}"
+ return f"{self._spc_data['household_id']}-{self._id}"
@property
- def device_class(self):
- """Return the device class."""
- return DEVICE_CLASS_BATTERY
+ def available(self) -> bool:
+ """Return true if entity is available."""
+ return bool(self._state)
@property
- def device_state_attributes(self):
- """Return state attributes."""
- attributes = None
- if self._state:
- try:
- voltage_per_battery = float(self._state["battery"]) / 4
- attributes = {
- ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}",
- f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}",
- }
- except (KeyError, TypeError) as error:
- attributes = self._state
- _LOGGER.error(
- "Error getting device state attributes from %s: %s\n\n%s",
- self._name,
- error,
- self._state,
- )
+ def should_poll(self) -> bool:
+ """Return true."""
+ return False
- return attributes
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return "%"
-
- async def async_update(self):
+ async def async_update(self) -> None:
"""Get the latest data and update the state."""
- self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data")
+ self._spc_data = self._spc.states[self._sure_type].get(self._id)
+ self._state = self._spc_data.get("status")
+ _LOGGER.debug("%s -> self._state: %s", self._name, self._state)
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@callback
- def update():
+ def update() -> None:
"""Update the state."""
self.async_schedule_update_ha_state(True)
@@ -128,7 +109,77 @@ class FlapBattery(Entity):
self.hass, TOPIC_UPDATE, update
)
- async def async_will_remove_from_hass(self):
+ async def async_will_remove_from_hass(self) -> None:
"""Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
self._async_unsub_dispatcher_connect()
+
+
+class Flap(SurePetcareSensor):
+ """Sure Petcare Flap."""
+
+ @property
+ def state(self) -> Optional[int]:
+ """Return battery level in percent."""
+ return SureLockStateID(self._state["locking"]["mode"]).name.capitalize()
+
+ @property
+ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ """Return the state attributes of the device."""
+ attributes = None
+ if self._state:
+ attributes = {
+ "learn_mode": bool(self._state["learn_mode"]),
+ }
+
+ return attributes
+
+
+class SureBattery(SurePetcareSensor):
+ """Sure Petcare Flap."""
+
+ @property
+ def name(self) -> str:
+ """Return the name of the device if any."""
+ return f"{self._name} Battery Level"
+
+ @property
+ def state(self) -> Optional[int]:
+ """Return battery level in percent."""
+ battery_percent: Optional[int]
+ try:
+ per_battery_voltage = self._state["battery"] / 4
+ voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW
+ battery_percent = min(int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100)
+ except (KeyError, TypeError):
+ battery_percent = None
+
+ return battery_percent
+
+ @property
+ def unique_id(self) -> str:
+ """Return an unique ID."""
+ return f"{self._spc_data['household_id']}-{self._id}-battery"
+
+ @property
+ def device_class(self) -> str:
+ """Return the device class."""
+ return DEVICE_CLASS_BATTERY
+
+ @property
+ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ """Return state attributes."""
+ attributes = None
+ if self._state:
+ voltage_per_battery = float(self._state["battery"]) / 4
+ attributes = {
+ ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}",
+ f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}",
+ }
+
+ return attributes
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return the unit of measurement."""
+ return "%"
diff --git a/homeassistant/components/switch/.translations/sv.json b/homeassistant/components/switch/.translations/sv.json
new file mode 100644
index 00000000000..3ec36265e52
--- /dev/null
+++ b/homeassistant/components/switch/.translations/sv.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "V\u00e4xla {entity_name}",
+ "turn_off": "St\u00e4ng av {entity_name}",
+ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u00e4r avst\u00e4ngd",
+ "is_on": "{entity_name} \u00e4r p\u00e5",
+ "turn_off": "{entity_name} st\u00e4ngdes av",
+ "turn_on": "{entity_name} slogs p\u00e5"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} st\u00e4ngdes av",
+ "turned_on": "{entity_name} slogs p\u00e5"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py
index 3f823331433..d10ecaa15ed 100644
--- a/homeassistant/components/synologydsm/sensor.py
+++ b/homeassistant/components/synologydsm/sensor.py
@@ -17,6 +17,8 @@ from homeassistant.const import (
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
+ DATA_MEGABYTES,
+ DATA_RATE_KILOBYTES_PER_SECOND,
EVENT_HOMEASSISTANT_START,
TEMP_CELSIUS,
)
@@ -43,14 +45,14 @@ _UTILISATION_MON_COND = {
"cpu_5min_load": ["CPU Load (5 min)", "%", "mdi:chip"],
"cpu_15min_load": ["CPU Load (15 min)", "%", "mdi:chip"],
"memory_real_usage": ["Memory Usage (Real)", "%", "mdi:memory"],
- "memory_size": ["Memory Size", "Mb", "mdi:memory"],
- "memory_cached": ["Memory Cached", "Mb", "mdi:memory"],
- "memory_available_swap": ["Memory Available (Swap)", "Mb", "mdi:memory"],
- "memory_available_real": ["Memory Available (Real)", "Mb", "mdi:memory"],
- "memory_total_swap": ["Memory Total (Swap)", "Mb", "mdi:memory"],
- "memory_total_real": ["Memory Total (Real)", "Mb", "mdi:memory"],
- "network_up": ["Network Up", "Kbps", "mdi:upload"],
- "network_down": ["Network Down", "Kbps", "mdi:download"],
+ "memory_size": ["Memory Size", DATA_MEGABYTES, "mdi:memory"],
+ "memory_cached": ["Memory Cached", DATA_MEGABYTES, "mdi:memory"],
+ "memory_available_swap": ["Memory Available (Swap)", DATA_MEGABYTES, "mdi:memory"],
+ "memory_available_real": ["Memory Available (Real)", DATA_MEGABYTES, "mdi:memory"],
+ "memory_total_swap": ["Memory Total (Swap)", DATA_MEGABYTES, "mdi:memory"],
+ "memory_total_real": ["Memory Total (Real)", DATA_MEGABYTES, "mdi:memory"],
+ "network_up": ["Network Up", DATA_RATE_KILOBYTES_PER_SECOND, "mdi:upload"],
+ "network_down": ["Network Down", DATA_RATE_KILOBYTES_PER_SECOND, "mdi:download"],
}
_STORAGE_VOL_MON_COND = {
"volume_status": ["Status", None, "mdi:checkbox-marked-circle-outline"],
diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py
index 44ff9c49a01..0c4270eaeef 100644
--- a/homeassistant/components/system_log/__init__.py
+++ b/homeassistant/components/system_log/__init__.py
@@ -99,6 +99,7 @@ class LogEntry:
def __init__(self, record, stack, source):
"""Initialize a log entry."""
self.first_occured = self.timestamp = record.created
+ self.name = record.name
self.level = record.levelname
self.message = record.getMessage()
self.exception = ""
@@ -114,7 +115,7 @@ class LogEntry:
def hash(self):
"""Calculate a key for DedupStore."""
- return frozenset([self.message, self.root_cause])
+ return frozenset([self.name, self.message, self.root_cause])
def to_dict(self):
"""Convert object into dict to maintain backward compatibility."""
diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py
index b1a33736083..1ea8a409052 100644
--- a/homeassistant/components/systemmonitor/sensor.py
+++ b/homeassistant/components/systemmonitor/sensor.py
@@ -7,7 +7,15 @@ import psutil
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_RESOURCES, CONF_TYPE, STATE_OFF, STATE_ON
+from homeassistant.const import (
+ CONF_RESOURCES,
+ CONF_TYPE,
+ DATA_GIBIBYTES,
+ DATA_MEBIBYTES,
+ DATA_RATE_MEGABYTES_PER_SECOND,
+ STATE_OFF,
+ STATE_ON,
+)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
@@ -19,8 +27,8 @@ _LOGGER = logging.getLogger(__name__)
CONF_ARG = "arg"
SENSOR_TYPES = {
- "disk_free": ["Disk free", "GiB", "mdi:harddisk", None],
- "disk_use": ["Disk use", "GiB", "mdi:harddisk", None],
+ "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None],
+ "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None],
"disk_use_percent": ["Disk use (percent)", "%", "mdi:harddisk", None],
"ipv4_address": ["IPv4 address", "", "mdi:server-network", None],
"ipv6_address": ["IPv6 address", "", "mdi:server-network", None],
@@ -28,29 +36,29 @@ SENSOR_TYPES = {
"load_15m": ["Load (15m)", " ", "mdi:memory", None],
"load_1m": ["Load (1m)", " ", "mdi:memory", None],
"load_5m": ["Load (5m)", " ", "mdi:memory", None],
- "memory_free": ["Memory free", "MiB", "mdi:memory", None],
- "memory_use": ["Memory use", "MiB", "mdi:memory", None],
+ "memory_free": ["Memory free", DATA_MEBIBYTES, "mdi:memory", None],
+ "memory_use": ["Memory use", DATA_MEBIBYTES, "mdi:memory", None],
"memory_use_percent": ["Memory use (percent)", "%", "mdi:memory", None],
- "network_in": ["Network in", "MiB", "mdi:server-network", None],
- "network_out": ["Network out", "MiB", "mdi:server-network", None],
+ "network_in": ["Network in", DATA_MEBIBYTES, "mdi:server-network", None],
+ "network_out": ["Network out", DATA_MEBIBYTES, "mdi:server-network", None],
"packets_in": ["Packets in", " ", "mdi:server-network", None],
"packets_out": ["Packets out", " ", "mdi:server-network", None],
"throughput_network_in": [
"Network throughput in",
- "MB/s",
+ DATA_RATE_MEGABYTES_PER_SECOND,
"mdi:server-network",
None,
],
"throughput_network_out": [
"Network throughput out",
- "MB/s",
+ DATA_RATE_MEGABYTES_PER_SECOND,
"mdi:server-network",
None,
],
"process": ["Process", " ", "mdi:memory", None],
"processor_use": ["Processor use", "%", "mdi:memory", None],
- "swap_free": ["Swap free", "MiB", "mdi:harddisk", None],
- "swap_use": ["Swap use", "MiB", "mdi:harddisk", None],
+ "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None],
+ "swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None],
"swap_use_percent": ["Swap use (percent)", "%", "mdi:harddisk", None],
}
diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py
index ebf605bdc75..727fb868a33 100644
--- a/homeassistant/components/tado/__init__.py
+++ b/homeassistant/components/tado/__init__.py
@@ -12,7 +12,7 @@ from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.util import Throttle
-from .const import CONF_FALLBACK
+from .const import CONF_FALLBACK, DATA
_LOGGER = logging.getLogger(__name__)
@@ -20,19 +20,22 @@ DOMAIN = "tado"
SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}"
-TADO_COMPONENTS = ["sensor", "climate"]
+TADO_COMPONENTS = ["sensor", "climate", "water_heater"]
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
SCAN_INTERVAL = timedelta(seconds=15)
CONFIG_SCHEMA = vol.Schema(
{
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_FALLBACK, default=True): cv.boolean,
- }
+ DOMAIN: vol.All(
+ cv.ensure_list,
+ [
+ {
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_FALLBACK, default=True): cv.boolean,
+ }
+ ],
)
},
extra=vol.ALLOW_EXTRA,
@@ -41,45 +44,54 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up of the Tado component."""
- username = config[DOMAIN][CONF_USERNAME]
- password = config[DOMAIN][CONF_PASSWORD]
+ acc_list = config[DOMAIN]
- tadoconnector = TadoConnector(hass, username, password)
- if not tadoconnector.setup():
- return False
+ api_data_list = []
- hass.data[DOMAIN] = tadoconnector
+ for acc in acc_list:
+ username = acc[CONF_USERNAME]
+ password = acc[CONF_PASSWORD]
+ fallback = acc[CONF_FALLBACK]
- # Do first update
- tadoconnector.update()
+ tadoconnector = TadoConnector(hass, username, password, fallback)
+ if not tadoconnector.setup():
+ continue
+
+ # Do first update
+ tadoconnector.update()
+
+ api_data_list.append(tadoconnector)
+ # Poll for updates in the background
+ hass.helpers.event.track_time_interval(
+ # we're using here tadoconnector as a parameter of lambda
+ # to capture actual value instead of closuring of latest value
+ lambda now, tc=tadoconnector: tc.update(),
+ SCAN_INTERVAL,
+ )
+
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN][DATA] = api_data_list
# Load components
for component in TADO_COMPONENTS:
load_platform(
- hass,
- component,
- DOMAIN,
- {CONF_FALLBACK: config[DOMAIN][CONF_FALLBACK]},
- config,
+ hass, component, DOMAIN, {}, config,
)
- # Poll for updates in the background
- hass.helpers.event.track_time_interval(
- lambda now: tadoconnector.update(), SCAN_INTERVAL
- )
-
return True
class TadoConnector:
"""An object to store the Tado data."""
- def __init__(self, hass, username, password):
+ def __init__(self, hass, username, password, fallback):
"""Initialize Tado Connector."""
self.hass = hass
self._username = username
self._password = password
+ self._fallback = fallback
+ self.device_id = None
self.tado = None
self.zones = None
self.devices = None
@@ -88,6 +100,11 @@ class TadoConnector:
"device": {},
}
+ @property
+ def fallback(self):
+ """Return fallback flag to Smart Schedule."""
+ return self._fallback
+
def setup(self):
"""Connect to Tado and fetch the zones."""
try:
@@ -101,7 +118,7 @@ class TadoConnector:
# Load zones and devices
self.zones = self.tado.getZones()
self.devices = self.tado.getMe()["homes"]
-
+ self.device_id = self.devices[0]["id"]
return True
@Throttle(MIN_TIME_BETWEEN_UPDATES)
diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py
index 88433db0991..b92a54edd5e 100644
--- a/homeassistant/components/tado/climate.py
+++ b/homeassistant/components/tado/climate.py
@@ -25,13 +25,16 @@ from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from . import CONF_FALLBACK, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
+from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
from .const import (
CONST_MODE_OFF,
CONST_MODE_SMART_SCHEDULE,
CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_MODE,
+ CONST_OVERLAY_TIMER,
+ DATA,
TYPE_AIR_CONDITIONING,
+ TYPE_HEATING,
)
_LOGGER = logging.getLogger(__name__)
@@ -39,25 +42,25 @@ _LOGGER = logging.getLogger(__name__)
FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW}
HVAC_MAP_TADO_HEAT = {
- "MANUAL": HVAC_MODE_HEAT,
- "TIMER": HVAC_MODE_HEAT,
- "TADO_MODE": HVAC_MODE_HEAT,
- "SMART_SCHEDULE": HVAC_MODE_AUTO,
- "OFF": HVAC_MODE_OFF,
+ CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT,
+ CONST_OVERLAY_TIMER: HVAC_MODE_HEAT,
+ CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT,
+ CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO,
+ CONST_MODE_OFF: HVAC_MODE_OFF,
}
HVAC_MAP_TADO_COOL = {
- "MANUAL": HVAC_MODE_COOL,
- "TIMER": HVAC_MODE_COOL,
- "TADO_MODE": HVAC_MODE_COOL,
- "SMART_SCHEDULE": HVAC_MODE_AUTO,
- "OFF": HVAC_MODE_OFF,
+ CONST_OVERLAY_MANUAL: HVAC_MODE_COOL,
+ CONST_OVERLAY_TIMER: HVAC_MODE_COOL,
+ CONST_OVERLAY_TADO_MODE: HVAC_MODE_COOL,
+ CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO,
+ CONST_MODE_OFF: HVAC_MODE_OFF,
}
HVAC_MAP_TADO_HEAT_COOL = {
- "MANUAL": HVAC_MODE_HEAT_COOL,
- "TIMER": HVAC_MODE_HEAT_COOL,
- "TADO_MODE": HVAC_MODE_HEAT_COOL,
- "SMART_SCHEDULE": HVAC_MODE_AUTO,
- "OFF": HVAC_MODE_OFF,
+ CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT_COOL,
+ CONST_OVERLAY_TIMER: HVAC_MODE_HEAT_COOL,
+ CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT_COOL,
+ CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO,
+ CONST_MODE_OFF: HVAC_MODE_OFF,
}
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
@@ -70,21 +73,24 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Tado climate platform."""
- tado = hass.data[DOMAIN]
+ if discovery_info is None:
+ return
+ api_list = hass.data[DOMAIN][DATA]
entities = []
- for zone in tado.zones:
- entity = create_climate_entity(
- tado, zone["name"], zone["id"], discovery_info[CONF_FALLBACK]
- )
- if entity:
- entities.append(entity)
+
+ for tado in api_list:
+ for zone in tado.zones:
+ if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]:
+ entity = create_climate_entity(tado, zone["name"], zone["id"])
+ if entity:
+ entities.append(entity)
if entities:
add_entities(entities, True)
-def create_climate_entity(tado, name: str, zone_id: int, fallback: bool):
+def create_climate_entity(tado, name: str, zone_id: int):
"""Create a Tado climate entity."""
capabilities = tado.get_capabilities(zone_id)
_LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities)
@@ -112,15 +118,7 @@ def create_climate_entity(tado, name: str, zone_id: int, fallback: bool):
step = temperatures["celsius"].get("step", PRECISION_TENTHS)
entity = TadoClimate(
- tado,
- name,
- zone_id,
- zone_type,
- min_temp,
- max_temp,
- step,
- ac_support_heat,
- fallback,
+ tado, name, zone_id, zone_type, min_temp, max_temp, step, ac_support_heat,
)
return entity
@@ -138,7 +136,6 @@ class TadoClimate(ClimateDevice):
max_temp,
step,
ac_support_heat,
- fallback,
):
"""Initialize of Tado climate entity."""
self._tado = tado
@@ -146,6 +143,7 @@ class TadoClimate(ClimateDevice):
self.zone_name = zone_name
self.zone_id = zone_id
self.zone_type = zone_type
+ self._unique_id = f"{zone_type} {zone_id} {tado.device_id}"
self._ac_device = zone_type == TYPE_AIR_CONDITIONING
self._ac_support_heat = ac_support_heat
@@ -162,12 +160,10 @@ class TadoClimate(ClimateDevice):
self._step = step
self._target_temp = None
- if fallback:
- _LOGGER.debug("Default overlay is set to TADO MODE")
+ if tado.fallback:
# Fallback to Smart Schedule at next Schedule switch
self._default_overlay = CONST_OVERLAY_TADO_MODE
else:
- _LOGGER.debug("Default overlay is set to MANUAL MODE")
# Don't fallback to Smart Schedule, but keep in manual mode
self._default_overlay = CONST_OVERLAY_MANUAL
@@ -199,6 +195,11 @@ class TadoClimate(ClimateDevice):
"""Return the name of the entity."""
return self.zone_name
+ @property
+ def unique_id(self):
+ """Return the unique id."""
+ return self._unique_id
+
@property
def should_poll(self) -> bool:
"""Do not poll."""
@@ -358,11 +359,7 @@ class TadoClimate(ClimateDevice):
def update(self):
"""Handle update callbacks."""
_LOGGER.debug("Updating climate platform for zone %d", self.zone_id)
- try:
- data = self._tado.data["zone"][self.zone_id]
- except KeyError:
- _LOGGER.debug("No data")
- return
+ data = self._tado.data["zone"][self.zone_id]
if "sensorDataPoints" in data:
sensor_data = data["sensorDataPoints"]
@@ -375,13 +372,13 @@ class TadoClimate(ClimateDevice):
humidity = float(sensor_data["humidity"]["percentage"])
self._cur_humidity = humidity
- # temperature setting will not exist when device is off
- if (
- "temperature" in data["setting"]
- and data["setting"]["temperature"] is not None
- ):
- setting = float(data["setting"]["temperature"]["celsius"])
- self._target_temp = setting
+ # temperature setting will not exist when device is off
+ if (
+ "temperature" in data["setting"]
+ and data["setting"]["temperature"] is not None
+ ):
+ setting = float(data["setting"]["temperature"]["celsius"])
+ self._target_temp = setting
if "tadoMode" in data:
mode = data["tadoMode"]
diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py
index 3c0232c8ba2..8d67e3bf9f8 100644
--- a/homeassistant/components/tado/const.py
+++ b/homeassistant/components/tado/const.py
@@ -2,6 +2,7 @@
# Configuration
CONF_FALLBACK = "fallback"
+DATA = "data"
# Types
TYPE_AIR_CONDITIONING = "AIR_CONDITIONING"
diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json
index 7539988d42e..e51cc53caa5 100644
--- a/homeassistant/components/tado/manifest.json
+++ b/homeassistant/components/tado/manifest.json
@@ -2,7 +2,11 @@
"domain": "tado",
"name": "Tado",
"documentation": "https://www.home-assistant.io/integrations/tado",
- "requirements": ["python-tado==0.2.9"],
+ "requirements": [
+ "python-tado==0.3.0"
+ ],
"dependencies": [],
- "codeowners": ["@michaelarnauts"]
+ "codeowners": [
+ "@michaelarnauts"
+ ]
}
diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py
index a928b61a508..f5f32a6ed1a 100644
--- a/homeassistant/components/tado/sensor.py
+++ b/homeassistant/components/tado/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 . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
+from . import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
from .const import TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER
_LOGGER = logging.getLogger(__name__)
@@ -40,26 +40,29 @@ DEVICE_SENSORS = ["tado bridge status"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the sensor platform."""
- tado = hass.data[DOMAIN]
+ api_list = hass.data[DOMAIN][DATA]
- # Create zone sensors
entities = []
- for zone in tado.zones:
- entities.extend(
- [
- create_zone_sensor(tado, zone["name"], zone["id"], variable)
- for variable in ZONE_SENSORS.get(zone["type"])
- ]
- )
- # Create device sensors
- for home in tado.devices:
- entities.extend(
- [
- create_device_sensor(tado, home["name"], home["id"], variable)
- for variable in DEVICE_SENSORS
- ]
- )
+ for tado in api_list:
+ # Create zone sensors
+
+ for zone in tado.zones:
+ entities.extend(
+ [
+ create_zone_sensor(tado, zone["name"], zone["id"], variable)
+ for variable in ZONE_SENSORS.get(zone["type"])
+ ]
+ )
+
+ # Create device sensors
+ for home in tado.devices:
+ entities.extend(
+ [
+ create_device_sensor(tado, home["name"], home["id"], variable)
+ for variable in DEVICE_SENSORS
+ ]
+ )
add_entities(entities, True)
@@ -86,7 +89,7 @@ class TadoSensor(Entity):
self.zone_variable = zone_variable
self.sensor_type = sensor_type
- self._unique_id = f"{zone_variable} {zone_id}"
+ self._unique_id = f"{zone_variable} {zone_id} {tado.device_id}"
self._state = None
self._state_attributes = None
@@ -227,23 +230,16 @@ class TadoSensor(Entity):
self._state = data["tadoMode"]
elif self.zone_variable == "overlay":
- if "overlay" in data and data["overlay"] is not None:
- self._state = True
- self._state_attributes = {
- "termination": data["overlay"]["termination"]["type"]
- }
- else:
- self._state = False
- self._state_attributes = {}
+ self._state = "overlay" in data and data["overlay"] is not None
+ self._state_attributes = (
+ {"termination": data["overlay"]["termination"]["type"]}
+ if self._state
+ else {}
+ )
elif self.zone_variable == "early start":
- if "preparation" in data and data["preparation"] is not None:
- self._state = True
- else:
- self._state = False
+ self._state = "preparation" in data and data["preparation"] is not None
elif self.zone_variable == "open window":
- if "openWindowDetected" in data:
- self._state = data["openWindowDetected"]
- else:
- self._state = False
+ self._state = "openWindow" in data and data["openWindow"] is not None
+ self._state_attributes = data["openWindow"] if self._state else {}
diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py
new file mode 100644
index 00000000000..fc3a9ce9cf4
--- /dev/null
+++ b/homeassistant/components/tado/water_heater.py
@@ -0,0 +1,302 @@
+"""Support for Tado hot water zones."""
+import logging
+
+from homeassistant.components.water_heater import (
+ SUPPORT_OPERATION_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+ WaterHeaterDevice,
+)
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
+from .const import (
+ CONST_MODE_OFF,
+ CONST_MODE_SMART_SCHEDULE,
+ CONST_OVERLAY_MANUAL,
+ CONST_OVERLAY_TADO_MODE,
+ CONST_OVERLAY_TIMER,
+ DATA,
+ TYPE_HOT_WATER,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+MODE_AUTO = "auto"
+MODE_HEAT = "heat"
+MODE_OFF = "off"
+
+OPERATION_MODES = [MODE_AUTO, MODE_HEAT, MODE_OFF]
+
+WATER_HEATER_MAP_TADO = {
+ CONST_OVERLAY_MANUAL: MODE_HEAT,
+ CONST_OVERLAY_TIMER: MODE_HEAT,
+ CONST_OVERLAY_TADO_MODE: MODE_HEAT,
+ CONST_MODE_SMART_SCHEDULE: MODE_AUTO,
+ CONST_MODE_OFF: MODE_OFF,
+}
+
+SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Tado water heater platform."""
+ if discovery_info is None:
+ return
+
+ api_list = hass.data[DOMAIN][DATA]
+ entities = []
+
+ for tado in api_list:
+ for zone in tado.zones:
+ if zone["type"] in [TYPE_HOT_WATER]:
+ entity = create_water_heater_entity(tado, zone["name"], zone["id"])
+ entities.append(entity)
+
+ if entities:
+ add_entities(entities, True)
+
+
+def create_water_heater_entity(tado, name: str, zone_id: int):
+ """Create a Tado water heater device."""
+ capabilities = tado.get_capabilities(zone_id)
+ supports_temperature_control = capabilities["canSetTemperature"]
+
+ if supports_temperature_control and "temperatures" in capabilities:
+ temperatures = capabilities["temperatures"]
+ min_temp = float(temperatures["celsius"]["min"])
+ max_temp = float(temperatures["celsius"]["max"])
+ else:
+ min_temp = None
+ max_temp = None
+
+ entity = TadoWaterHeater(
+ tado, name, zone_id, supports_temperature_control, min_temp, max_temp
+ )
+
+ return entity
+
+
+class TadoWaterHeater(WaterHeaterDevice):
+ """Representation of a Tado water heater."""
+
+ def __init__(
+ self,
+ tado,
+ zone_name,
+ zone_id,
+ supports_temperature_control,
+ min_temp,
+ max_temp,
+ ):
+ """Initialize of Tado water heater entity."""
+ self._tado = tado
+
+ self.zone_name = zone_name
+ self.zone_id = zone_id
+ self._unique_id = f"{zone_id} {tado.device_id}"
+
+ self._device_is_active = False
+ self._is_away = False
+
+ self._supports_temperature_control = supports_temperature_control
+ self._min_temperature = min_temp
+ self._max_temperature = max_temp
+
+ self._target_temp = None
+
+ self._supported_features = SUPPORT_FLAGS_HEATER
+ if self._supports_temperature_control:
+ self._supported_features |= SUPPORT_TARGET_TEMPERATURE
+
+ if tado.fallback:
+ # Fallback to Smart Schedule at next Schedule switch
+ self._default_overlay = CONST_OVERLAY_TADO_MODE
+ else:
+ # Don't fallback to Smart Schedule, but keep in manual mode
+ self._default_overlay = CONST_OVERLAY_MANUAL
+
+ self._current_operation = CONST_MODE_SMART_SCHEDULE
+ self._overlay_mode = CONST_MODE_SMART_SCHEDULE
+
+ async def async_added_to_hass(self):
+ """Register for sensor updates."""
+
+ @callback
+ def async_update_callback():
+ """Schedule an entity update."""
+ self.async_schedule_update_ha_state(True)
+
+ async_dispatcher_connect(
+ self.hass,
+ SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id),
+ async_update_callback,
+ )
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return self._supported_features
+
+ @property
+ def name(self):
+ """Return the name of the entity."""
+ return self.zone_name
+
+ @property
+ def unique_id(self):
+ """Return the unique id."""
+ return self._unique_id
+
+ @property
+ def should_poll(self) -> bool:
+ """Do not poll."""
+ return False
+
+ @property
+ def current_operation(self):
+ """Return current readable operation mode."""
+ return WATER_HEATER_MAP_TADO.get(self._current_operation)
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ return self._target_temp
+
+ @property
+ def is_away_mode_on(self):
+ """Return true if away mode is on."""
+ return self._is_away
+
+ @property
+ def operation_list(self):
+ """Return the list of available operation modes (readable)."""
+ return OPERATION_MODES
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement used by the platform."""
+ return TEMP_CELSIUS
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ return self._min_temperature
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ return self._max_temperature
+
+ def set_operation_mode(self, operation_mode):
+ """Set new operation mode."""
+ mode = None
+
+ if operation_mode == MODE_OFF:
+ mode = CONST_MODE_OFF
+ elif operation_mode == MODE_AUTO:
+ mode = CONST_MODE_SMART_SCHEDULE
+ elif operation_mode == MODE_HEAT:
+ mode = self._default_overlay
+
+ self._current_operation = mode
+ self._overlay_mode = None
+
+ # Set a target temperature if we don't have any
+ if mode == CONST_OVERLAY_TADO_MODE and self._target_temp is None:
+ self._target_temp = self.min_temp
+
+ self._control_heater()
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ temperature = kwargs.get(ATTR_TEMPERATURE)
+ if not self._supports_temperature_control or temperature is None:
+ return
+
+ self._current_operation = self._default_overlay
+ self._overlay_mode = None
+ self._target_temp = temperature
+ self._control_heater()
+
+ def update(self):
+ """Handle update callbacks."""
+ _LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id)
+ data = self._tado.data["zone"][self.zone_id]
+
+ if "tadoMode" in data:
+ mode = data["tadoMode"]
+ self._is_away = mode == "AWAY"
+
+ if "setting" in data:
+ power = data["setting"]["power"]
+ if power == "OFF":
+ self._current_operation = CONST_MODE_OFF
+ # There is no overlay, the mode will always be
+ # "SMART_SCHEDULE"
+ self._overlay_mode = CONST_MODE_SMART_SCHEDULE
+ self._device_is_active = False
+ else:
+ self._device_is_active = True
+
+ # temperature setting will not exist when device is off
+ if (
+ "temperature" in data["setting"]
+ and data["setting"]["temperature"] is not None
+ ):
+ setting = float(data["setting"]["temperature"]["celsius"])
+ self._target_temp = setting
+
+ overlay = False
+ overlay_data = None
+ termination = CONST_MODE_SMART_SCHEDULE
+
+ if "overlay" in data:
+ overlay_data = data["overlay"]
+ overlay = overlay_data is not None
+
+ if overlay:
+ termination = overlay_data["termination"]["type"]
+
+ if self._device_is_active:
+ # If you set mode manually to off, there will be an overlay
+ # and a termination, but we want to see the mode "OFF"
+ self._overlay_mode = termination
+ self._current_operation = termination
+
+ def _control_heater(self):
+ """Send new target temperature."""
+ if self._current_operation == CONST_MODE_SMART_SCHEDULE:
+ _LOGGER.debug(
+ "Switching to SMART_SCHEDULE for zone %s (%d)",
+ self.zone_name,
+ self.zone_id,
+ )
+ self._tado.reset_zone_overlay(self.zone_id)
+ self._overlay_mode = self._current_operation
+ return
+
+ if self._current_operation == CONST_MODE_OFF:
+ _LOGGER.debug(
+ "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id
+ )
+ self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER)
+ self._overlay_mode = self._current_operation
+ return
+
+ _LOGGER.debug(
+ "Switching to %s for zone %s (%d) with temperature %s",
+ self._current_operation,
+ self.zone_name,
+ self.zone_id,
+ self._target_temp,
+ )
+ self._tado.set_zone_overlay(
+ self.zone_id,
+ self._current_operation,
+ self._target_temp,
+ None,
+ TYPE_HOT_WATER,
+ )
+ self._overlay_mode = self._current_operation
diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py
index 0d74d6018a5..f14e3019ac0 100644
--- a/homeassistant/components/tahoma/__init__.py
+++ b/homeassistant/components/tahoma/__init__.py
@@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
-TAHOMA_COMPONENTS = ["scene", "sensor", "cover", "switch", "binary_sensor"]
+TAHOMA_COMPONENTS = ["binary_sensor", "cover", "lock", "scene", "sensor", "switch"]
TAHOMA_TYPES = {
"io:AwningValanceIOComponent": "cover",
@@ -52,6 +52,7 @@ TAHOMA_TYPES = {
"io:VerticalExteriorAwningIOComponent": "cover",
"io:VerticalInteriorBlindVeluxIOComponent": "cover",
"io:WindowOpenerVeluxIOComponent": "cover",
+ "opendoors:OpenDoorsSmartLockComponent": "lock",
"rtds:RTDSContactSensor": "sensor",
"rtds:RTDSMotionSensor": "sensor",
"rtds:RTDSSmokeSensor": "smoke",
diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py
index 81078ab480b..7621a542838 100644
--- a/homeassistant/components/tahoma/binary_sensor.py
+++ b/homeassistant/components/tahoma/binary_sensor.py
@@ -14,6 +14,8 @@ SCAN_INTERVAL = timedelta(seconds=120)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Tahoma controller devices."""
+ if discovery_info is None:
+ return
_LOGGER.debug("Setup Tahoma Binary sensor platform")
controller = hass.data[TAHOMA_DOMAIN]["controller"]
devices = []
diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py
index fb2bedc746c..7692e9bedf7 100644
--- a/homeassistant/components/tahoma/cover.py
+++ b/homeassistant/components/tahoma/cover.py
@@ -51,6 +51,8 @@ TAHOMA_DEVICE_CLASSES = {
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Tahoma covers."""
+ if discovery_info is None:
+ return
controller = hass.data[TAHOMA_DOMAIN]["controller"]
devices = []
for device in hass.data[TAHOMA_DOMAIN]["devices"]["cover"]:
diff --git a/homeassistant/components/tahoma/lock.py b/homeassistant/components/tahoma/lock.py
new file mode 100644
index 00000000000..0b02975fc7e
--- /dev/null
+++ b/homeassistant/components/tahoma/lock.py
@@ -0,0 +1,89 @@
+"""Support for Tahoma lock."""
+from datetime import timedelta
+import logging
+
+from homeassistant.components.lock import LockDevice
+from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED
+
+from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice
+
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = timedelta(seconds=120)
+TAHOMA_STATE_LOCKED = "locked"
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Tahoma lock."""
+ if discovery_info is None:
+ return
+ controller = hass.data[TAHOMA_DOMAIN]["controller"]
+ devices = []
+ for device in hass.data[TAHOMA_DOMAIN]["devices"]["lock"]:
+ devices.append(TahomaLock(device, controller))
+ add_entities(devices, True)
+
+
+class TahomaLock(TahomaDevice, LockDevice):
+ """Representation a Tahoma lock."""
+
+ def __init__(self, tahoma_device, controller):
+ """Initialize the device."""
+ super().__init__(tahoma_device, controller)
+ self._lock_status = None
+ self._available = False
+ self._battery_level = None
+ self._name = None
+
+ def update(self):
+ """Update method."""
+ self.controller.get_states([self.tahoma_device])
+ self._battery_level = self.tahoma_device.active_states["core:BatteryState"]
+ self._name = self.tahoma_device.active_states["core:NameState"]
+ if (
+ self.tahoma_device.active_states.get("core:LockedUnlockedState")
+ == TAHOMA_STATE_LOCKED
+ ):
+ self._lock_status = STATE_LOCKED
+ else:
+ self._lock_status = STATE_UNLOCKED
+ self._available = (
+ self.tahoma_device.active_states.get("core:AvailabilityState")
+ == "available"
+ )
+
+ def unlock(self, **kwargs):
+ """Unlock method."""
+ _LOGGER.debug("Unlocking %s", self._name)
+ self.apply_action("unlock")
+
+ def lock(self, **kwargs):
+ """Lock method."""
+ _LOGGER.debug("Locking %s", self._name)
+ self.apply_action("lock")
+
+ @property
+ def name(self):
+ """Return the name of the lock."""
+ return self._name
+
+ @property
+ def available(self):
+ """Return True if the lock is available."""
+ return self._available
+
+ @property
+ def is_locked(self):
+ """Return True if the lock is locked."""
+ return self._lock_status == STATE_LOCKED
+
+ @property
+ def device_state_attributes(self):
+ """Return the lock state attributes."""
+ attr = {
+ ATTR_BATTERY_LEVEL: self._battery_level,
+ }
+ super_attr = super().device_state_attributes
+ if super_attr is not None:
+ attr.update(super_attr)
+ return attr
diff --git a/homeassistant/components/tahoma/scene.py b/homeassistant/components/tahoma/scene.py
index e54ff91a0f6..c60f245fc50 100644
--- a/homeassistant/components/tahoma/scene.py
+++ b/homeassistant/components/tahoma/scene.py
@@ -10,6 +10,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Tahoma scenes."""
+ if discovery_info is None:
+ return
controller = hass.data[TAHOMA_DOMAIN]["controller"]
scenes = []
for scene in hass.data[TAHOMA_DOMAIN]["scenes"]:
diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py
index 85ccb55761d..fb8c61607c7 100644
--- a/homeassistant/components/tahoma/sensor.py
+++ b/homeassistant/components/tahoma/sensor.py
@@ -16,6 +16,8 @@ ATTR_RSSI_LEVEL = "rssi_level"
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Tahoma controller devices."""
+ if discovery_info is None:
+ return
controller = hass.data[TAHOMA_DOMAIN]["controller"]
devices = []
for device in hass.data[TAHOMA_DOMAIN]["devices"]["sensor"]:
diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py
index 1612120f313..9f98e711ac9 100644
--- a/homeassistant/components/tahoma/switch.py
+++ b/homeassistant/components/tahoma/switch.py
@@ -13,6 +13,8 @@ ATTR_RSSI_LEVEL = "rssi_level"
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Tahoma switches."""
+ if discovery_info is None:
+ return
controller = hass.data[TAHOMA_DOMAIN]["controller"]
devices = []
for switch in hass.data[TAHOMA_DOMAIN]["devices"]["switch"]:
diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py
index fe183129eaa..f340f4a3971 100644
--- a/homeassistant/components/teksavvy/sensor.py
+++ b/homeassistant/components/teksavvy/sensor.py
@@ -6,7 +6,12 @@ import async_timeout
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_MONITORED_VARIABLES,
+ CONF_NAME,
+ DATA_GIGABYTES,
+)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -17,7 +22,6 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "TekSavvy"
CONF_TOTAL_BANDWIDTH = "total_bandwidth"
-GIGABYTES = "GB"
PERCENT = "%"
MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
@@ -25,15 +29,15 @@ REQUEST_TIMEOUT = 5 # seconds
SENSOR_TYPES = {
"usage": ["Usage Ratio", PERCENT, "mdi:percent"],
- "usage_gb": ["Usage", GIGABYTES, "mdi:download"],
- "limit": ["Data limit", GIGABYTES, "mdi:download"],
- "onpeak_download": ["On Peak Download", GIGABYTES, "mdi:download"],
- "onpeak_upload": ["On Peak Upload", GIGABYTES, "mdi:upload"],
- "onpeak_total": ["On Peak Total", GIGABYTES, "mdi:download"],
- "offpeak_download": ["Off Peak download", GIGABYTES, "mdi:download"],
- "offpeak_upload": ["Off Peak Upload", GIGABYTES, "mdi:upload"],
- "offpeak_total": ["Off Peak Total", GIGABYTES, "mdi:download"],
- "onpeak_remaining": ["Remaining", GIGABYTES, "mdi:download"],
+ "usage_gb": ["Usage", DATA_GIGABYTES, "mdi:download"],
+ "limit": ["Data limit", DATA_GIGABYTES, "mdi:download"],
+ "onpeak_download": ["On Peak Download", DATA_GIGABYTES, "mdi:download"],
+ "onpeak_upload": ["On Peak Upload", DATA_GIGABYTES, "mdi:upload"],
+ "onpeak_total": ["On Peak Total", DATA_GIGABYTES, "mdi:download"],
+ "offpeak_download": ["Off Peak download", DATA_GIGABYTES, "mdi:download"],
+ "offpeak_upload": ["Off Peak Upload", DATA_GIGABYTES, "mdi:upload"],
+ "offpeak_total": ["Off Peak Total", DATA_GIGABYTES, "mdi:download"],
+ "onpeak_remaining": ["Remaining", DATA_GIGABYTES, "mdi:download"],
}
API_HA_MAP = (
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index 9b56201f8c7..277f9108663 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -25,7 +25,6 @@ from homeassistant.const import (
ATTR_LONGITUDE,
CONF_API_KEY,
CONF_PLATFORM,
- CONF_TIMEOUT,
CONF_URL,
HTTP_DIGEST_AUTHENTICATION,
)
@@ -67,6 +66,7 @@ ATTR_URL = "url"
ATTR_USER_ID = "user_id"
ATTR_USERNAME = "username"
ATTR_VERIFY_SSL = "verify_ssl"
+ATTR_TIMEOUT = "timeout"
CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids"
CONF_PROXY_URL = "proxy_url"
@@ -135,7 +135,7 @@ BASE_SERVICE_SCHEMA = vol.Schema(
vol.Optional(ATTR_DISABLE_WEB_PREV): cv.boolean,
vol.Optional(ATTR_KEYBOARD): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list,
- vol.Optional(CONF_TIMEOUT): vol.Coerce(float),
+ vol.Optional(ATTR_TIMEOUT): cv.positive_int,
},
extra=vol.ALLOW_EXTRA,
)
@@ -499,15 +499,15 @@ class TelegramNotificationService:
ATTR_DISABLE_WEB_PREV: None,
ATTR_REPLY_TO_MSGID: None,
ATTR_REPLYMARKUP: None,
- CONF_TIMEOUT: None,
+ ATTR_TIMEOUT: None,
}
if data is not None:
if ATTR_PARSER in data:
params[ATTR_PARSER] = self._parsers.get(
data[ATTR_PARSER], self._parse_mode
)
- if CONF_TIMEOUT in data:
- params[CONF_TIMEOUT] = data[CONF_TIMEOUT]
+ if ATTR_TIMEOUT in data:
+ params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT]
if ATTR_DISABLE_NOTIF in data:
params[ATTR_DISABLE_NOTIF] = data[ATTR_DISABLE_NOTIF]
if ATTR_DISABLE_WEB_PREV in data:
diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml
index ed8720c5877..e3d303a2c52 100644
--- a/homeassistant/components/telegram_bot/services.yaml
+++ b/homeassistant/components/telegram_bot/services.yaml
@@ -21,6 +21,9 @@ send_message:
disable_web_page_preview:
description: Disables link previews for links in the message.
example: true
+ timeout:
+ description: Timeout for send message. Will help with timeout errors (poor internet connection, etc)
+ example: '1000'
keyboard:
description: List of rows of commands, comma-separated, to make a custom keyboard. Empty list clears a previously set keyboard.
example: '["/command1, /command2", "/command3"]'
@@ -55,6 +58,9 @@ send_photo:
verify_ssl:
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
example: false
+ timeout:
+ description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc)
+ example: '1000'
keyboard:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
@@ -86,6 +92,9 @@ send_sticker:
verify_ssl:
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
example: false
+ timeout:
+ description: Timeout for send sticker. Will help with timeout errors (poor internet connection, etc)
+ example: '1000'
keyboard:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
@@ -120,6 +129,9 @@ send_video:
verify_ssl:
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
example: false
+ timeout:
+ description: Timeout for send video. Will help with timeout errors (poor internet connection, etc)
+ example: '1000'
keyboard:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
@@ -154,6 +166,9 @@ send_document:
verify_ssl:
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
example: false
+ timeout:
+ description: Timeout for send document. Will help with timeout errors (poor internet connection, etc)
+ example: '1000'
keyboard:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
@@ -176,6 +191,9 @@ send_location:
disable_notification:
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
example: true
+ timeout:
+ description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc)
+ example: '1000'
keyboard:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
diff --git a/homeassistant/components/tellduslive/.translations/pl.json b/homeassistant/components/tellduslive/.translations/pl.json
index 01d3c7125c3..68e53df57f1 100644
--- a/homeassistant/components/tellduslive/.translations/pl.json
+++ b/homeassistant/components/tellduslive/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_setup": "TelldusLive jest ju\u017c skonfigurowany",
+ "already_setup": "TelldusLive jest ju\u017c skonfigurowany.",
"authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.",
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.",
"unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d"
diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py
index c5512461f34..a6855a1654b 100644
--- a/homeassistant/components/template/light.py
+++ b/homeassistant/components/template/light.py
@@ -6,8 +6,10 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
+ ATTR_HS_COLOR,
ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS,
+ SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
Light,
)
@@ -42,6 +44,8 @@ CONF_LEVEL_ACTION = "set_level"
CONF_LEVEL_TEMPLATE = "level_template"
CONF_TEMPERATURE_TEMPLATE = "temperature_template"
CONF_TEMPERATURE_ACTION = "set_temperature"
+CONF_COLOR_TEMPLATE = "color_template"
+CONF_COLOR_ACTION = "set_color"
LIGHT_SCHEMA = vol.Schema(
{
@@ -57,6 +61,8 @@ LIGHT_SCHEMA = vol.Schema(
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
+ vol.Optional(CONF_COLOR_TEMPLATE): cv.template,
+ vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA,
}
)
@@ -76,14 +82,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
icon_template = device_config.get(CONF_ICON_TEMPLATE)
entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE)
availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE)
- level_template = device_config.get(CONF_LEVEL_TEMPLATE)
on_action = device_config[CONF_ON_ACTION]
off_action = device_config[CONF_OFF_ACTION]
+
level_action = device_config.get(CONF_LEVEL_ACTION)
+ level_template = device_config.get(CONF_LEVEL_TEMPLATE)
+
temperature_action = device_config.get(CONF_TEMPERATURE_ACTION)
temperature_template = device_config.get(CONF_TEMPERATURE_TEMPLATE)
+ color_action = device_config.get(CONF_COLOR_ACTION)
+ color_template = device_config.get(CONF_COLOR_TEMPLATE)
+
templates = {
CONF_VALUE_TEMPLATE: state_template,
CONF_ICON_TEMPLATE: icon_template,
@@ -91,6 +102,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
CONF_AVAILABILITY_TEMPLATE: availability_template,
CONF_LEVEL_TEMPLATE: level_template,
CONF_TEMPERATURE_TEMPLATE: temperature_template,
+ CONF_COLOR_TEMPLATE: color_template,
}
initialise_templates(hass, templates)
@@ -114,6 +126,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
entity_ids,
temperature_action,
temperature_template,
+ color_action,
+ color_template,
)
)
@@ -144,6 +158,8 @@ class LightTemplate(Light):
entity_ids,
temperature_action,
temperature_template,
+ color_action,
+ color_template,
):
"""Initialize the light."""
self.hass = hass
@@ -165,12 +181,17 @@ class LightTemplate(Light):
if temperature_action is not None:
self._temperature_script = Script(hass, temperature_action)
self._temperature_template = temperature_template
+ self._color_script = None
+ if color_action is not None:
+ self._color_script = Script(hass, color_action)
+ self._color_template = color_template
self._state = False
self._icon = None
self._entity_picture = None
self._brightness = None
self._temperature = None
+ self._color = None
self._entities = entity_ids
self._available = True
@@ -184,6 +205,11 @@ class LightTemplate(Light):
"""Return the CT color value in mireds."""
return self._temperature
+ @property
+ def hs_color(self):
+ """Return the hue and saturation color value [float, float]."""
+ return self._color
+
@property
def name(self):
"""Return the display name of this light."""
@@ -197,6 +223,8 @@ class LightTemplate(Light):
supported_features |= SUPPORT_BRIGHTNESS
if self._temperature_script is not None:
supported_features |= SUPPORT_COLOR_TEMP
+ if self._color_script is not None:
+ supported_features |= SUPPORT_COLOR
return supported_features
@property
@@ -239,6 +267,7 @@ class LightTemplate(Light):
self._template is not None
or self._level_template is not None
or self._temperature_template is not None
+ or self._color_template is not None
or self._availability_template is not None
):
async_track_state_change(
@@ -282,6 +311,12 @@ class LightTemplate(Light):
await self._temperature_script.async_run(
{"color_temp": kwargs[ATTR_COLOR_TEMP]}, context=self._context
)
+ elif ATTR_HS_COLOR in kwargs and self._color_script:
+ hs_value = kwargs[ATTR_HS_COLOR]
+ await self._color_script.async_run(
+ {"hs": hs_value, "h": int(hs_value[0]), "s": int(hs_value[1])},
+ context=self._context,
+ )
else:
await self._on_script.async_run()
@@ -303,6 +338,8 @@ class LightTemplate(Light):
self.update_temperature()
+ self.update_color()
+
for property_name, template in (
("_icon", self._icon_template),
("_entity_picture", self._entity_picture_template),
@@ -396,3 +433,34 @@ class LightTemplate(Light):
except TemplateError:
_LOGGER.error("Cannot evaluate temperature template", exc_info=True)
self._temperature = None
+
+ @callback
+ def update_color(self):
+ """Update the hs_color from the template."""
+ if self._color_template is None:
+ return
+
+ self._color = None
+
+ try:
+ render = self._color_template.async_render()
+ h_str, s_str = map(
+ float, render.replace("(", "").replace(")", "").split(",", 1)
+ )
+ if (
+ h_str is not None
+ and s_str is not None
+ and 0 <= h_str <= 360
+ and 0 <= s_str <= 100
+ ):
+ self._color = (h_str, s_str)
+ elif h_str is not None and s_str is not None:
+ _LOGGER.error(
+ "Received invalid hs_color : (%s, %s). Expected: (0-360, 0-100)",
+ h_str,
+ s_str,
+ )
+ else:
+ _LOGGER.error("Received invalid hs_color : (%s)", render)
+ except TemplateError as ex:
+ _LOGGER.error(ex)
diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json
index e34e9644381..024dc2b7bdd 100644
--- a/homeassistant/components/tensorflow/manifest.json
+++ b/homeassistant/components/tensorflow/manifest.json
@@ -4,10 +4,10 @@
"documentation": "https://www.home-assistant.io/integrations/tensorflow",
"requirements": [
"tensorflow==1.13.2",
- "numpy==1.17.4",
+ "numpy==1.18.1",
"protobuf==3.6.1",
- "pillow==6.2.1"
+ "pillow==7.0.0"
],
"dependencies": [],
"codeowners": []
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/tesla/.translations/pl.json b/homeassistant/components/tesla/.translations/pl.json
index 5a8a3d2ebd3..89233646ef0 100644
--- a/homeassistant/components/tesla/.translations/pl.json
+++ b/homeassistant/components/tesla/.translations/pl.json
@@ -2,7 +2,7 @@
"config": {
"error": {
"connection_error": "B\u0142\u0105d po\u0142\u0105czenia; sprawd\u017a sie\u0107 i spr\u00f3buj ponownie",
- "identifier_exists": "Adres e-mail ju\u017c zarejestrowany",
+ "identifier_exists": "Adres e-mail jest ju\u017c zarejestrowany.",
"invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia",
"unknown_error": "Nieznany b\u0142\u0105d, prosz\u0119 zg\u0142osi\u0107 dane z loga"
},
diff --git a/homeassistant/components/tesla/.translations/sv.json b/homeassistant/components/tesla/.translations/sv.json
new file mode 100644
index 00000000000..46263ff64ae
--- /dev/null
+++ b/homeassistant/components/tesla/.translations/sv.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "error": {
+ "connection_error": "Fel vid anslutning; kontrollera n\u00e4tverket och f\u00f6rs\u00f6k igen",
+ "identifier_exists": "E-post redan registrerad",
+ "invalid_credentials": "Ogiltiga autentiseringsuppgifter",
+ "unknown_error": "Ok\u00e4nt fel, var god att rapportera logginformation"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "L\u00f6senord",
+ "username": "E-postadress"
+ },
+ "description": "V\u00e4nligen ange din information.",
+ "title": "Tesla - Konfiguration"
+ }
+ },
+ "title": "Tesla"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Sekunder mellan skanningar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py
index 3c2a22793db..df0664b8f4c 100644
--- a/homeassistant/components/tesla/__init__.py
+++ b/homeassistant/components/tesla/__init__.py
@@ -27,7 +27,14 @@ from .config_flow import (
configured_instances,
validate_input,
)
-from .const import DATA_LISTENER, DOMAIN, ICONS, TESLA_COMPONENTS
+from .const import (
+ DATA_LISTENER,
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+ ICONS,
+ MIN_SCAN_INTERVAL,
+ TESLA_COMPONENTS,
+)
_LOGGER = logging.getLogger(__name__)
@@ -37,9 +44,9 @@ CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_SCAN_INTERVAL, default=300): vol.All(
- cv.positive_int, vol.Clamp(min=300)
- ),
+ vol.Optional(
+ CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
+ ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)),
}
)
},
@@ -64,7 +71,7 @@ async def async_setup(hass, base_config):
def _update_entry(email, data=None, options=None):
data = data or {}
- options = options or {CONF_SCAN_INTERVAL: 300}
+ options = options or {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}
for entry in hass.config_entries.async_entries(DOMAIN):
if email != entry.title:
continue
@@ -120,7 +127,9 @@ async def async_setup_entry(hass, config_entry):
websession,
refresh_token=config[CONF_TOKEN],
access_token=config[CONF_ACCESS_TOKEN],
- update_interval=config_entry.options.get(CONF_SCAN_INTERVAL, 300),
+ update_interval=config_entry.options.get(
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
+ ),
)
(refresh_token, access_token) = await controller.connect()
except TeslaException as ex:
diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py
index 2d2bc0158d2..c719807da9f 100644
--- a/homeassistant/components/tesla/config_flow.py
+++ b/homeassistant/components/tesla/config_flow.py
@@ -15,7 +15,7 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
-from .const import DOMAIN
+from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
@@ -100,8 +100,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
{
vol.Optional(
CONF_SCAN_INTERVAL,
- default=self.config_entry.options.get(CONF_SCAN_INTERVAL, 300),
- ): vol.All(cv.positive_int, vol.Clamp(min=300))
+ default=self.config_entry.options.get(
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
+ ),
+ ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL))
}
)
return self.async_show_form(step_id="init", data_schema=data_schema)
@@ -120,7 +122,7 @@ async def validate_input(hass: core.HomeAssistant, data):
websession,
email=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
- update_interval=300,
+ update_interval=DEFAULT_SCAN_INTERVAL,
)
(config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect(
test_login=True
diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py
index be460a430ac..54cb7a2e071 100644
--- a/homeassistant/components/tesla/const.py
+++ b/homeassistant/components/tesla/const.py
@@ -1,6 +1,8 @@
"""Const file for Tesla cars."""
DOMAIN = "tesla"
DATA_LISTENER = "listener"
+DEFAULT_SCAN_INTERVAL = 660
+MIN_SCAN_INTERVAL = 60
TESLA_COMPONENTS = [
"sensor",
"lock",
diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json
index 23bc76ee6b5..9f8579e3e18 100644
--- a/homeassistant/components/tibber/manifest.json
+++ b/homeassistant/components/tibber/manifest.json
@@ -2,7 +2,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
- "requirements": ["pyTibber==0.12.0"],
+ "requirements": ["pyTibber==0.12.2"],
"dependencies": [],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver"
diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py
index cab57c59ac8..72507b3d148 100644
--- a/homeassistant/components/tod/binary_sensor.py
+++ b/homeassistant/components/tod/binary_sensor.py
@@ -126,14 +126,14 @@ class TodSensor(BinarySensorDevice):
current_local_date = self.current_datetime.astimezone(
self.hass.config.time_zone
).date()
- # calcuate utc datetime corecponding to local time
+ # calculate utc datetime corecponding to local time
utc_datetime = self.hass.config.time_zone.localize(
datetime.combine(current_local_date, naive_time)
).astimezone(tz=pytz.UTC)
return utc_datetime
def _calculate_initial_boudary_time(self):
- """Calculate internal absolute time boudaries."""
+ """Calculate internal absolute time boundaries."""
nowutc = self.current_datetime
# If after value is a sun event instead of absolute time
if is_sun_event(self._after):
diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py
index 348826a1264..612561707b1 100644
--- a/homeassistant/components/toon/__init__.py
+++ b/homeassistant/components/toon/__init__.py
@@ -137,7 +137,7 @@ class ToonData:
def update(self, now=None):
"""Update all Toon data and notify entities."""
- # Ignore the TTL meganism from client library
+ # Ignore the TTL mechanism from client library
# It causes a lots of issues, hence we take control over caching
self._toon._clear_cache() # pylint: disable=protected-access
diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py
index 020f2d9c07f..e6cfbbc629a 100644
--- a/homeassistant/components/totalconnect/__init__.py
+++ b/homeassistant/components/totalconnect/__init__.py
@@ -24,7 +24,7 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
-TOTALCONNECT_PLATFORMS = ["alarm_control_panel"]
+TOTALCONNECT_PLATFORMS = ["alarm_control_panel", "binary_sensor"]
def setup(hass, config):
diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py
index ed77fc4eea0..b255132a365 100644
--- a/homeassistant/components/totalconnect/alarm_control_panel.py
+++ b/homeassistant/components/totalconnect/alarm_control_panel.py
@@ -32,10 +32,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
client = hass.data[TOTALCONNECT_DOMAIN].client
- for location in client.locations:
- location_id = location.get("LocationID")
- name = location.get("LocationName")
- alarms.append(TotalConnectAlarm(name, location_id, client))
+ for location_id, location in client.locations.items():
+ location_name = location.location_name
+ alarms.append(TotalConnectAlarm(location_name, location_id, client))
add_entities(alarms)
@@ -72,35 +71,35 @@ class TotalConnectAlarm(alarm.AlarmControlPanel):
def update(self):
"""Return the state of the device."""
- status = self._client.get_armed_status(self._name)
+ status = self._client.get_armed_status(self._location_id)
attr = {
"location_name": self._name,
"location_id": self._location_id,
- "ac_loss": self._client.ac_loss,
- "low_battery": self._client.low_battery,
+ "ac_loss": self._client.locations[self._location_id].ac_loss,
+ "low_battery": self._client.locations[self._location_id].low_battery,
+ "cover_tampered": self._client.locations[
+ self._location_id
+ ].is_cover_tampered,
"triggered_source": None,
"triggered_zone": None,
}
- if status == self._client.DISARMED:
+ if status in (self._client.DISARMED, self._client.DISARMED_BYPASS):
state = STATE_ALARM_DISARMED
- elif status == self._client.DISARMED_BYPASS:
- state = STATE_ALARM_DISARMED
- elif status == self._client.ARMED_STAY:
- state = STATE_ALARM_ARMED_HOME
- elif status == self._client.ARMED_STAY_INSTANT:
- state = STATE_ALARM_ARMED_HOME
- elif status == self._client.ARMED_STAY_INSTANT_BYPASS:
+ elif status in (
+ self._client.ARMED_STAY,
+ self._client.ARMED_STAY_INSTANT,
+ self._client.ARMED_STAY_INSTANT_BYPASS,
+ ):
state = STATE_ALARM_ARMED_HOME
elif status == self._client.ARMED_STAY_NIGHT:
state = STATE_ALARM_ARMED_NIGHT
- elif status == self._client.ARMED_AWAY:
- state = STATE_ALARM_ARMED_AWAY
- elif status == self._client.ARMED_AWAY_BYPASS:
- state = STATE_ALARM_ARMED_AWAY
- elif status == self._client.ARMED_AWAY_INSTANT:
- state = STATE_ALARM_ARMED_AWAY
- elif status == self._client.ARMED_AWAY_INSTANT_BYPASS:
+ elif status in (
+ self._client.ARMED_AWAY,
+ self._client.ARMED_AWAY_BYPASS,
+ self._client.ARMED_AWAY_INSTANT,
+ self._client.ARMED_AWAY_INSTANT_BYPASS,
+ ):
state = STATE_ALARM_ARMED_AWAY
elif status == self._client.ARMED_CUSTOM_BYPASS:
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
@@ -128,16 +127,16 @@ class TotalConnectAlarm(alarm.AlarmControlPanel):
def alarm_disarm(self, code=None):
"""Send disarm command."""
- self._client.disarm(self._name)
+ self._client.disarm(self._location_id)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
- self._client.arm_stay(self._name)
+ self._client.arm_stay(self._location_id)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
- self._client.arm_away(self._name)
+ self._client.arm_away(self._location_id)
def alarm_arm_night(self, code=None):
"""Send arm night command."""
- self._client.arm_stay_night(self._name)
+ self._client.arm_stay_night(self._location_id)
diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py
new file mode 100644
index 00000000000..28bd58cfff8
--- /dev/null
+++ b/homeassistant/components/totalconnect/binary_sensor.py
@@ -0,0 +1,90 @@
+"""Interfaces with TotalConnect sensors."""
+import logging
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_DOOR,
+ DEVICE_CLASS_GAS,
+ DEVICE_CLASS_SMOKE,
+ BinarySensorDevice,
+)
+
+from . import DOMAIN as TOTALCONNECT_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up a sensor for a TotalConnect device."""
+ if discovery_info is None:
+ return
+
+ sensors = []
+
+ client_locations = hass.data[TOTALCONNECT_DOMAIN].client.locations
+
+ for location_id, location in client_locations.items():
+ for zone_id, zone in location.zones.items():
+ sensors.append(TotalConnectBinarySensor(zone_id, location_id, zone))
+ add_entities(sensors, True)
+
+
+class TotalConnectBinarySensor(BinarySensorDevice):
+ """Represent an TotalConnect zone."""
+
+ def __init__(self, zone_id, location_id, zone):
+ """Initialize the TotalConnect status."""
+ self._zone_id = zone_id
+ self._location_id = location_id
+ self._zone = zone
+ self._name = self._zone.description
+ self._unique_id = f"{location_id} {zone_id}"
+ self._is_on = None
+ self._is_tampered = None
+ self._is_low_battery = None
+
+ @property
+ def unique_id(self):
+ """Return the unique id."""
+ return self._unique_id
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ def update(self):
+ """Return the state of the device."""
+ self._is_tampered = self._zone.is_tampered()
+ self._is_low_battery = self._zone.is_low_battery()
+
+ if self._zone.is_faulted() or self._zone.is_triggered():
+ self._is_on = True
+ else:
+ self._is_on = False
+
+ @property
+ def is_on(self):
+ """Return true if the binary sensor is on."""
+ return self._is_on
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ if self._zone.is_type_security():
+ return DEVICE_CLASS_DOOR
+ if self._zone.is_type_fire():
+ return DEVICE_CLASS_SMOKE
+ if self._zone.is_type_carbon_monoxide():
+ return DEVICE_CLASS_GAS
+ return None
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ attributes = {
+ "zone_id": self._zone_id,
+ "location_id": self._location_id,
+ "low_battery": self._is_low_battery,
+ "tampered": self._is_tampered,
+ }
+ return attributes
diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json
index 6b2119f1cf5..967115e721a 100644
--- a/homeassistant/components/totalconnect/manifest.json
+++ b/homeassistant/components/totalconnect/manifest.json
@@ -2,7 +2,7 @@
"domain": "totalconnect",
"name": "Honeywell Total Connect Alarm",
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
- "requirements": ["total_connect_client==0.28"],
+ "requirements": ["total_connect_client==0.50"],
"dependencies": [],
- "codeowners": []
+ "codeowners": ["@austinmroczek"]
}
diff --git a/homeassistant/components/traccar/.translations/pl.json b/homeassistant/components/traccar/.translations/pl.json
index 95b7eb1af00..b7eaf7fe16e 100644
--- a/homeassistant/components/traccar/.translations/pl.json
+++ b/homeassistant/components/traccar/.translations/pl.json
@@ -10,7 +10,7 @@
"step": {
"user": {
"description": "Na pewno chcesz skonfigurowa\u0107 Traccar?",
- "title": "Skonfiguruj Traccar"
+ "title": "Konfiguracja Traccar"
}
},
"title": "Traccar"
diff --git a/homeassistant/components/traccar/.translations/sv.json b/homeassistant/components/traccar/.translations/sv.json
new file mode 100644
index 00000000000..ddd33235e01
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/sv.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot meddelanden fr\u00e5n Traccar.",
+ "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig."
+ },
+ "create_entry": {
+ "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du st\u00e4lla in webhook-funktionen i Traccar.\n\nAnv\u00e4nd f\u00f6ljande url: `{webhook_url}`\n\nMer information finns i [dokumentationen]({docs_url})."
+ },
+ "step": {
+ "user": {
+ "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill st\u00e4lla in Traccar?",
+ "title": "St\u00e4ll in Traccar"
+ }
+ },
+ "title": "Traccar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/de.json b/homeassistant/components/tradfri/.translations/de.json
index 5dc2630556e..68165dbb291 100644
--- a/homeassistant/components/tradfri/.translations/de.json
+++ b/homeassistant/components/tradfri/.translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Bridge ist bereits konfiguriert",
+ "already_configured": "Bridge ist bereits konfiguriert.",
"already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt."
},
"error": {
diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json
index fc115294031..208687839dd 100644
--- a/homeassistant/components/tradfri/.translations/pl.json
+++ b/homeassistant/components/tradfri/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Mostek jest ju\u017c skonfigurowany",
+ "already_configured": "Mostek jest ju\u017c skonfigurowany.",
"already_in_progress": "Konfiguracja mostka jest ju\u017c w toku."
},
"error": {
diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py
index 0fe826be9af..40fe7b01cb0 100644
--- a/homeassistant/components/tradfri/light.py
+++ b/homeassistant/components/tradfri/light.py
@@ -246,7 +246,7 @@ class TradfriLight(TradfriBaseDevice, Light):
color_command = self._device_control.set_hsb(**color_data)
transition_time = None
- # HSB can always be set, but color temp + brightness is bulb dependant
+ # HSB can always be set, but color temp + brightness is bulb dependent
command = dimmer_command
if command is not None:
command += color_command
diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json
index 33f634e279f..1458b717fc6 100644
--- a/homeassistant/components/trafikverket_train/manifest.json
+++ b/homeassistant/components/trafikverket_train/manifest.json
@@ -2,7 +2,7 @@
"domain": "trafikverket_train",
"name": "Trafikverket Train",
"documentation": "https://www.home-assistant.io/integrations/trafikverket_train",
- "requirements": ["pytrafikverket==0.1.5.9"],
+ "requirements": ["pytrafikverket==0.1.6.1"],
"dependencies": [],
"codeowners": ["@endor-force"]
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json
index 652cebf6730..3224df25c3f 100644
--- a/homeassistant/components/trafikverket_weatherstation/manifest.json
+++ b/homeassistant/components/trafikverket_weatherstation/manifest.json
@@ -2,7 +2,7 @@
"domain": "trafikverket_weatherstation",
"name": "Trafikverket Weather Station",
"documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation",
- "requirements": ["pytrafikverket==0.1.5.9"],
+ "requirements": ["pytrafikverket==0.1.6.1"],
"dependencies": [],
"codeowners": []
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/transmission/.translations/hu.json b/homeassistant/components/transmission/.translations/hu.json
new file mode 100644
index 00000000000..14bf5c28bdf
--- /dev/null
+++ b/homeassistant/components/transmission/.translations/hu.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_allowed": "Csak egyetlen p\u00e9ld\u00e1nyra van sz\u00fcks\u00e9g."
+ },
+ "error": {
+ "cannot_connect": "Nem lehet csatlakozni az \u00e1llom\u00e1shoz",
+ "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik",
+ "wrong_credentials": "Rossz felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3"
+ },
+ "step": {
+ "options": {
+ "data": {
+ "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g"
+ },
+ "title": "Be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa"
+ },
+ "user": {
+ "data": {
+ "host": "Kiszolg\u00e1l\u00f3",
+ "name": "N\u00e9v",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "\u00c1tviteli \u00fcgyf\u00e9l be\u00e1ll\u00edt\u00e1sa"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/transmission/.translations/pl.json b/homeassistant/components/transmission/.translations/pl.json
index a85a3f9b006..5aac538766b 100644
--- a/homeassistant/components/transmission/.translations/pl.json
+++ b/homeassistant/components/transmission/.translations/pl.json
@@ -6,7 +6,7 @@
},
"error": {
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem",
- "name_exists": "Nazwa ju\u017c istnieje",
+ "name_exists": "Nazwa ju\u017c istnieje.",
"wrong_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o"
},
"step": {
diff --git a/homeassistant/components/transmission/.translations/sv.json b/homeassistant/components/transmission/.translations/sv.json
index 30004af17db..b2a00771e85 100644
--- a/homeassistant/components/transmission/.translations/sv.json
+++ b/homeassistant/components/transmission/.translations/sv.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "V\u00e4rden \u00e4r redan konfigurerad.",
"one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig."
},
"error": {
"cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden",
+ "name_exists": "Namnet finns redan",
"wrong_credentials": "Fel anv\u00e4ndarnamn eller l\u00f6senord"
},
"step": {
@@ -21,16 +23,20 @@
"password": "L\u00f6senord",
"port": "Port",
"username": "Anv\u00e4ndarnamn"
- }
+ },
+ "title": "St\u00e4ll in Transmission-klienten"
}
- }
+ },
+ "title": "Transmission"
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Uppdateringsfrekvens"
- }
+ },
+ "description": "Konfigurera alternativ f\u00f6r Transmission",
+ "title": "Konfigurera alternativ f\u00f6r Transmission"
}
}
}
diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py
index 9a9250dbed6..659ef97d9de 100644
--- a/homeassistant/components/transmission/const.py
+++ b/homeassistant/components/transmission/const.py
@@ -1,13 +1,16 @@
"""Constants for the Transmission Bittorent Client component."""
+
+from homeassistant.const import DATA_RATE_MEGABYTES_PER_SECOND
+
DOMAIN = "transmission"
SENSOR_TYPES = {
"active_torrents": ["Active Torrents", "Torrents"],
"current_status": ["Status", None],
- "download_speed": ["Down Speed", "MB/s"],
+ "download_speed": ["Down Speed", DATA_RATE_MEGABYTES_PER_SECOND],
"paused_torrents": ["Paused Torrents", "Torrents"],
"total_torrents": ["Total Torrents", "Torrents"],
- "upload_speed": ["Up Speed", "MB/s"],
+ "upload_speed": ["Up Speed", DATA_RATE_MEGABYTES_PER_SECOND],
"completed_torrents": ["Completed Torrents", "Torrents"],
"started_torrents": ["Started Torrents", "Torrents"],
}
diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py
index 1756df7baee..3d85a76f2bd 100644
--- a/homeassistant/components/transmission/switch.py
+++ b/homeassistant/components/transmission/switch.py
@@ -80,7 +80,7 @@ class TransmissionSwitch(ToggleEntity):
def turn_off(self, **kwargs):
"""Turn the device off."""
if self.type == "on_off":
- _LOGGING.debug("Stoping all torrents")
+ _LOGGING.debug("Stopping all torrents")
self._tm_client.api.stop_torrents()
if self.type == "turtle_mode":
_LOGGING.debug("Turning Turtle Mode of Transmission off")
diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json
index 2b9e7a4eccf..2026816c090 100644
--- a/homeassistant/components/trend/manifest.json
+++ b/homeassistant/components/trend/manifest.json
@@ -2,7 +2,7 @@
"domain": "trend",
"name": "Trend",
"documentation": "https://www.home-assistant.io/integrations/trend",
- "requirements": ["numpy==1.17.4"],
+ "requirements": ["numpy==1.18.1"],
"dependencies": [],
"codeowners": [],
"quality_scale": "internal"
diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py
index 318101605e8..3a456dec531 100644
--- a/homeassistant/components/tts/__init__.py
+++ b/homeassistant/components/tts/__init__.py
@@ -22,7 +22,7 @@ from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
SERVICE_PLAY_MEDIA,
)
-from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL
+from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
@@ -90,7 +90,7 @@ SCHEMA_SERVICE_SAY = vol.Schema(
{
vol.Required(ATTR_MESSAGE): cv.string,
vol.Optional(ATTR_CACHE): cv.boolean,
- vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
+ vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Optional(ATTR_LANGUAGE): cv.string,
vol.Optional(ATTR_OPTIONS): dict,
}
@@ -148,7 +148,7 @@ async def async_setup(hass, config):
async def async_say_handle(service):
"""Service handle for say."""
- entity_ids = service.data.get(ATTR_ENTITY_ID, ENTITY_MATCH_ALL)
+ entity_ids = service.data[ATTR_ENTITY_ID]
message = service.data.get(ATTR_MESSAGE)
cache = service.data.get(ATTR_CACHE)
language = service.data.get(ATTR_LANGUAGE)
diff --git a/homeassistant/components/twentemilieu/.translations/pl.json b/homeassistant/components/twentemilieu/.translations/pl.json
index 042fcf0dda6..130672906ef 100644
--- a/homeassistant/components/twentemilieu/.translations/pl.json
+++ b/homeassistant/components/twentemilieu/.translations/pl.json
@@ -1,16 +1,16 @@
{
"config": {
"abort": {
- "address_exists": "Adres ju\u017c skonfigurowany."
+ "address_exists": "Adres jest ju\u017c skonfigurowany."
},
"error": {
- "connection_error": "Po\u0142\u0105czenie nieudane.",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
"invalid_address": "Nie znaleziono adresu w obszarze us\u0142ugi Twente Milieu."
},
"step": {
"user": {
"data": {
- "house_letter": "List domowy / dodatkowy",
+ "house_letter": "List domowy/dodatkowy",
"house_number": "Numer domu",
"post_code": "Kod pocztowy"
},
diff --git a/homeassistant/components/twentemilieu/.translations/sv.json b/homeassistant/components/twentemilieu/.translations/sv.json
new file mode 100644
index 00000000000..ba2d8743681
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/sv.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "Adressen har redan st\u00e4llts in."
+ },
+ "error": {
+ "connection_error": "Det gick inte att ansluta.",
+ "invalid_address": "Adress hittades inte i serviceomr\u00e5det Twente Milieu."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "Husbrev/till\u00e4gg",
+ "house_number": "Husnummer",
+ "post_code": "Postnummer"
+ },
+ "description": "St\u00e4ll in Twente Milieu som ger information om avfallshantering p\u00e5 din adress.",
+ "title": "Twente Milieu"
+ }
+ },
+ "title": "Twente Milieu"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py
index f4276160d6c..1bf66810e5b 100644
--- a/homeassistant/components/twitch/sensor.py
+++ b/homeassistant/components/twitch/sensor.py
@@ -6,6 +6,7 @@ from twitch import TwitchClient
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_TOKEN
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -13,6 +14,13 @@ _LOGGER = logging.getLogger(__name__)
ATTR_GAME = "game"
ATTR_TITLE = "title"
+ATTR_SUBSCRIPTION = "subscribed"
+ATTR_SUBSCRIPTION_SINCE = "subscribed_since"
+ATTR_SUBSCRIPTION_GIFTED = "subscription_is_gifted"
+ATTR_FOLLOW = "following"
+ATTR_FOLLOW_SINCE = "following_since"
+ATTR_FOLLOWING = "followers"
+ATTR_VIEWS = "views"
CONF_CHANNELS = "channels"
CONF_CLIENT_ID = "client_id"
@@ -26,6 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_TOKEN): cv.string,
}
)
@@ -34,29 +43,35 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Twitch platform."""
channels = config[CONF_CHANNELS]
client_id = config[CONF_CLIENT_ID]
- client = TwitchClient(client_id=client_id)
+ oauth_token = config.get(CONF_TOKEN)
+ client = TwitchClient(client_id, oauth_token)
try:
client.ingests.get_server_list()
except HTTPError:
- _LOGGER.error("Client ID is not valid")
+ _LOGGER.error("Client ID or OAuth token is not valid")
return
- users = client.users.translate_usernames_to_ids(channels)
+ channel_ids = client.users.translate_usernames_to_ids(channels)
- add_entities([TwitchSensor(user, client) for user in users], True)
+ add_entities([TwitchSensor(channel_id, client) for channel_id in channel_ids], True)
class TwitchSensor(Entity):
"""Representation of an Twitch channel."""
- def __init__(self, user, client):
+ def __init__(self, channel, client):
"""Initialize the sensor."""
self._client = client
- self._user = user
- self._channel = self._user.name
- self._id = self._user.id
- self._state = self._preview = self._game = self._title = None
+ self._channel = channel
+ self._oauth_enabled = client._oauth_token is not None
+ self._state = None
+ self._preview = None
+ self._game = None
+ self._title = None
+ self._subscription = None
+ self._follow = None
+ self._statistics = None
@property
def should_poll(self):
@@ -66,7 +81,7 @@ class TwitchSensor(Entity):
@property
def name(self):
"""Return the name of the sensor."""
- return self._channel
+ return self._channel.display_name
@property
def state(self):
@@ -81,28 +96,67 @@ class TwitchSensor(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
+ attr = {
+ ATTR_FRIENDLY_NAME: self._channel.display_name,
+ }
+ attr.update(self._statistics)
+
+ if self._oauth_enabled:
+ attr.update(self._subscription)
+ attr.update(self._follow)
+
if self._state == STATE_STREAMING:
- return {ATTR_GAME: self._game, ATTR_TITLE: self._title}
+ attr.update({ATTR_GAME: self._game, ATTR_TITLE: self._title})
+ return attr
@property
def unique_id(self):
"""Return unique ID for this sensor."""
- return self._id
+ return self._channel.id
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
- # pylint: disable=no-member
def update(self):
"""Update device state."""
- stream = self._client.streams.get_stream_by_user(self._id)
+
+ channel = self._client.channels.get_by_id(self._channel.id)
+
+ self._statistics = {
+ ATTR_FOLLOWING: channel.followers,
+ ATTR_VIEWS: channel.views,
+ }
+ if self._oauth_enabled:
+ user = self._client.users.get()
+
+ try:
+ sub = self._client.users.check_subscribed_to_channel(
+ user.id, self._channel.id
+ )
+ self._subscription = {
+ ATTR_SUBSCRIPTION: True,
+ ATTR_SUBSCRIPTION_SINCE: sub.created_at,
+ ATTR_SUBSCRIPTION_GIFTED: sub.is_gift,
+ }
+ except HTTPError:
+ self._subscription = {ATTR_SUBSCRIPTION: False}
+
+ try:
+ follow = self._client.users.check_follows_channel(
+ user.id, self._channel.id
+ )
+ self._follow = {ATTR_FOLLOW: True, ATTR_FOLLOW_SINCE: follow.created_at}
+ except HTTPError:
+ self._follow = {ATTR_FOLLOW: False}
+
+ stream = self._client.streams.get_stream_by_user(self._channel.id)
if stream:
- self._game = stream.get("channel").get("game")
- self._title = stream.get("channel").get("status")
- self._preview = stream.get("preview").get("medium")
+ self._game = stream.channel.get("game")
+ self._title = stream.channel.get("status")
+ self._preview = stream.preview.get("medium")
self._state = STATE_STREAMING
else:
- self._preview = self._client.users.get_by_id(self._id).get("logo")
+ self._preview = self._channel.logo
self._state = STATE_OFFLINE
diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json
index 899b532290e..89d299a2857 100644
--- a/homeassistant/components/unifi/.translations/ca.json
+++ b/homeassistant/components/unifi/.translations/ca.json
@@ -28,10 +28,13 @@
"device_tracker": {
"data": {
"detection_time": "Temps (en segons) des de s'ha vist per \u00faltima vegada fins que es considera a fora",
+ "ssid_filter": "Selecciona els SSID's on fer-hi el seguiment de clients",
"track_clients": "Segueix clients de la xarxa",
"track_devices": "Segueix dispositius de la xarxa (dispositius Ubiquiti)",
"track_wired_clients": "Inclou clients de xarxa per cable"
- }
+ },
+ "description": "Configuraci\u00f3 de seguiment de dispositius",
+ "title": "Opcions d'UniFi"
},
"init": {
"data": {
@@ -42,7 +45,9 @@
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa"
- }
+ },
+ "description": "Configuraci\u00f3 dels sensors d\u2019estad\u00edstiques",
+ "title": "Opcions d'UniFi"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json
index 46a94cc4047..1afd1ca96ce 100644
--- a/homeassistant/components/unifi/.translations/da.json
+++ b/homeassistant/components/unifi/.translations/da.json
@@ -28,10 +28,13 @@
"device_tracker": {
"data": {
"detection_time": "Tid i sekunder fra sidst set indtil betragtet som v\u00e6k",
+ "ssid_filter": "V\u00e6lg SSIDer, der skal spores tr\u00e5dl\u00f8se klienter p\u00e5",
"track_clients": "Spor netv\u00e6rksklienter",
"track_devices": "Spor netv\u00e6rksenheder (Ubiquiti-enheder)",
"track_wired_clients": "Inkluder kablede netv\u00e6rksklienter"
- }
+ },
+ "description": "Konfigurer enhedssporing",
+ "title": "UniFi-indstillinger"
},
"init": {
"data": {
@@ -41,8 +44,10 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Opret b\u00e5ndbredde-forbrugssensorer for netv\u00e6rksklienter"
- }
+ "allow_bandwidth_sensors": "B\u00e5ndbreddeforbrugssensorer for netv\u00e6rksklienter"
+ },
+ "description": "Konfigurer statistiksensorer",
+ "title": "UniFi-indstillinger"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json
index 32a378b7c00..2f3db9d9b89 100644
--- a/homeassistant/components/unifi/.translations/de.json
+++ b/homeassistant/components/unifi/.translations/de.json
@@ -28,10 +28,13 @@
"device_tracker": {
"data": {
"detection_time": "Zeit in Sekunden vom letzten Gesehenen bis zur Entfernung",
+ "ssid_filter": "W\u00e4hlen Sie SSIDs zur Verfolgung von drahtlosen Clients aus",
"track_clients": "Nachverfolgen von Netzwerkclients",
"track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)",
"track_wired_clients": "Einbinden von kabelgebundenen Netzwerk-Clients"
- }
+ },
+ "description": "Konfigurieren Sie die Ger\u00e4teverfolgung",
+ "title": "UniFi-Optionen"
},
"init": {
"data": {
@@ -42,7 +45,9 @@
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "Erstellen von Bandbreiten-Nutzungssensoren f\u00fcr Netzwerk-Clients"
- }
+ },
+ "description": "Konfigurieren Sie Statistiksensoren",
+ "title": "UniFi-Optionen"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json
index d9b65b6d1da..f1f96b3c363 100644
--- a/homeassistant/components/unifi/.translations/en.json
+++ b/homeassistant/components/unifi/.translations/en.json
@@ -28,15 +28,20 @@
"device_tracker": {
"data": {
"detection_time": "Time in seconds from last seen until considered away",
+ "ssid_filter": "Select SSIDs to track wireless clients on",
"track_clients": "Track network clients",
"track_devices": "Track network devices (Ubiquiti devices)",
"track_wired_clients": "Include wired network clients"
- }
+ },
+ "description": "Configure device tracking",
+ "title": "UniFi options"
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients"
- }
+ "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients"
+ },
+ "description": "Configure statistics sensors",
+ "title": "UniFi options"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json
index 677899c0958..6c5e9d677c2 100644
--- a/homeassistant/components/unifi/.translations/es.json
+++ b/homeassistant/components/unifi/.translations/es.json
@@ -28,10 +28,13 @@
"device_tracker": {
"data": {
"detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado",
+ "ssid_filter": "Seleccione los SSIDs para realizar seguimiento de clientes inal\u00e1mbricos",
"track_clients": "Seguimiento de los clientes de red",
"track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)",
"track_wired_clients": "Incluir clientes de red cableada"
- }
+ },
+ "description": "Configurar dispositivo de seguimiento",
+ "title": "Opciones UniFi"
},
"init": {
"data": {
@@ -42,7 +45,9 @@
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "Crear sensores para monitorizar uso de ancho de banda de clientes de red"
- }
+ },
+ "description": "Configurar estad\u00edsticas de los sensores",
+ "title": "Opciones UniFi"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/hu.json b/homeassistant/components/unifi/.translations/hu.json
index b927e652ba7..f6919f985dc 100644
--- a/homeassistant/components/unifi/.translations/hu.json
+++ b/homeassistant/components/unifi/.translations/hu.json
@@ -21,5 +21,14 @@
}
},
"title": "UniFi Vez\u00e9rl\u0151"
+ },
+ "options": {
+ "step": {
+ "statistics_sensors": {
+ "data": {
+ "allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json
index 80b546ebcf8..c1aa9afe54f 100644
--- a/homeassistant/components/unifi/.translations/it.json
+++ b/homeassistant/components/unifi/.translations/it.json
@@ -28,10 +28,13 @@
"device_tracker": {
"data": {
"detection_time": "Tempo in secondi dall'ultima volta che viene visto fino a quando non \u00e8 considerato lontano",
+ "ssid_filter": "Selezionare gli SSID su cui tracciare i client wireless",
"track_clients": "Traccia i client di rete",
"track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)",
"track_wired_clients": "Includi i client di rete cablata"
- }
+ },
+ "description": "Configurare il tracciamento del dispositivo",
+ "title": "Opzioni UniFi"
},
"init": {
"data": {
@@ -41,8 +44,10 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Creare sensori di utilizzo della larghezza di banda per i client di rete"
- }
+ "allow_bandwidth_sensors": "Sensori di utilizzo della larghezza di banda per i client di rete"
+ },
+ "description": "Configurare i sensori delle statistiche",
+ "title": "Opzioni UniFi"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json
index 295430b7284..dbcd4d7feee 100644
--- a/homeassistant/components/unifi/.translations/ko.json
+++ b/homeassistant/components/unifi/.translations/ko.json
@@ -28,15 +28,20 @@
"device_tracker": {
"data": {
"detection_time": "\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ud655\uc778\ub41c \uc2dc\uac04\ubd80\ud130 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\ub294 \uc2dc\uac04 (\ucd08)",
+ "ssid_filter": "\ubb34\uc120 \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \ucd94\uc801\ud558\ub824\uba74 SSID\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
"track_clients": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801 \ub300\uc0c1",
"track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)",
"track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568"
- }
+ },
+ "description": "\uc7a5\uce58 \ucd94\uc801 \uad6c\uc131",
+ "title": "UniFi \uc635\uc158"
},
"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"
- }
+ "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c"
+ },
+ "description": "\ud1b5\uacc4 \uc13c\uc11c \uad6c\uc131",
+ "title": "UniFi \uc635\uc158"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json
index 4fa1f62c602..9707432540d 100644
--- a/homeassistant/components/unifi/.translations/lb.json
+++ b/homeassistant/components/unifi/.translations/lb.json
@@ -31,7 +31,8 @@
"track_clients": "Netzwierk Cliente verfollegen",
"track_devices": "Netzwierk Apparater (Ubiquiti Apparater) verfollegen",
"track_wired_clients": "Kabel Netzwierk Cliente abez\u00e9ien"
- }
+ },
+ "title": "UniFi Optiounen"
},
"init": {
"data": {
@@ -42,7 +43,9 @@
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente erstellen"
- }
+ },
+ "description": "Statistik Sensoren konfigur\u00e9ieren",
+ "title": "UniFi Optiounen"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json
index 9041f018423..65730c7ab8b 100644
--- a/homeassistant/components/unifi/.translations/no.json
+++ b/homeassistant/components/unifi/.translations/no.json
@@ -28,21 +28,20 @@
"device_tracker": {
"data": {
"detection_time": "Tid i sekunder fra sist sett til den ble ansett borte",
+ "ssid_filter": "Velg SSID-er for \u00e5 spore tr\u00e5dl\u00f8se klienter p\u00e5",
"track_clients": "Spor nettverksklienter",
"track_devices": "Spore nettverksenheter (Ubiquiti-enheter)",
"track_wired_clients": "Inkluder kablede nettverksklienter"
- }
- },
- "init": {
- "data": {
- "one": "en",
- "other": "andre"
- }
+ },
+ "description": "Konfigurere enhetssporing",
+ "title": "UniFi-alternativer"
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter"
- }
+ "allow_bandwidth_sensors": "B\u00e5ndbreddebrukssensorer for nettverksklienter"
+ },
+ "description": "Konfigurer statistikk sensorer",
+ "title": "UniFi-alternativer"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json
index 5887460a8a5..e016fbc7cce 100644
--- a/homeassistant/components/unifi/.translations/pl.json
+++ b/homeassistant/components/unifi/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana",
+ "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana.",
"user_privilege": "U\u017cytkownik musi by\u0107 administratorem"
},
"error": {
@@ -28,10 +28,13 @@
"device_tracker": {
"data": {
"detection_time": "Czas w sekundach od momentu, kiedy ostatnio widziano, a\u017c do momentu, kiedy uznano go za nieobecny.",
+ "ssid_filter": "Wybierz SSIDy do \u015bledzenia klient\u00f3w bezprzewodowych",
"track_clients": "\u015aled\u017a klient\u00f3w sieciowych",
"track_devices": "\u015aled\u017a urz\u0105dzenia sieciowe (urz\u0105dzenia Ubiquiti)",
"track_wired_clients": "Uwzgl\u0119dnij klient\u00f3w sieci przewodowej"
- }
+ },
+ "description": "Konfiguracja \u015bledzenia urz\u0105dze\u0144",
+ "title": "Opcje UniFi"
},
"init": {
"data": {
@@ -44,7 +47,9 @@
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych"
- }
+ },
+ "description": "Konfiguracja sensora statystyk",
+ "title": "Opcje UniFi"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json
index 3a67d483c0c..0080474cf64 100644
--- a/homeassistant/components/unifi/.translations/ru.json
+++ b/homeassistant/components/unifi/.translations/ru.json
@@ -28,10 +28,13 @@
"device_tracker": {
"data": {
"detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\".",
+ "ssid_filter": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 SSID \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432",
"track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438",
"track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)",
"track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438"
- }
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi"
},
"init": {
"data": {
@@ -43,8 +46,10 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432"
- }
+ "allow_bandwidth_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/sv.json b/homeassistant/components/unifi/.translations/sv.json
index bc1d9f8cb72..dbf5373aa9a 100644
--- a/homeassistant/components/unifi/.translations/sv.json
+++ b/homeassistant/components/unifi/.translations/sv.json
@@ -25,11 +25,24 @@
},
"options": {
"step": {
+ "device_tracker": {
+ "data": {
+ "detection_time": "Tid i sekunder fr\u00e5n senast sett tills den anses borta",
+ "track_clients": "Sp\u00e5ra n\u00e4tverksklienter",
+ "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)",
+ "track_wired_clients": "Inkludera tr\u00e5dbundna n\u00e4tverksklienter"
+ }
+ },
"init": {
"data": {
"one": "Tom",
"other": "Tomma"
}
+ },
+ "statistics_sensors": {
+ "data": {
+ "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/.translations/zh-Hans.json b/homeassistant/components/unifi/.translations/zh-Hans.json
index 2bc6bda37e4..ebed653732f 100644
--- a/homeassistant/components/unifi/.translations/zh-Hans.json
+++ b/homeassistant/components/unifi/.translations/zh-Hans.json
@@ -28,10 +28,17 @@
"device_tracker": {
"data": {
"detection_time": "\u8ddd\u79bb\u4e0a\u6b21\u53d1\u73b0\u591a\u5c11\u79d2\u540e\u8ba4\u4e3a\u79bb\u5f00",
+ "ssid_filter": "\u9009\u62e9\u6240\u8981\u8ffd\u8e2a\u7684\u65e0\u7ebf\u7f51\u7edcSSID",
"track_clients": "\u8ddf\u8e2a\u7f51\u7edc\u5ba2\u6237\u7aef",
"track_devices": "\u8ddf\u8e2a\u7f51\u7edc\u8bbe\u5907\uff08Ubiquiti \u8bbe\u5907\uff09",
"track_wired_clients": "\u5305\u62ec\u6709\u7ebf\u7f51\u7edc\u5ba2\u6237\u7aef"
- }
+ },
+ "description": "\u914d\u7f6e\u8bbe\u5907\u8ddf\u8e2a",
+ "title": "UniFi \u9009\u9879"
+ },
+ "statistics_sensors": {
+ "description": "\u914d\u7f6e\u7edf\u8ba1\u4f20\u611f\u5668",
+ "title": "UniFi \u9009\u9879"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json
index 5e0b881af15..cce150a6765 100644
--- a/homeassistant/components/unifi/.translations/zh-Hant.json
+++ b/homeassistant/components/unifi/.translations/zh-Hant.json
@@ -28,15 +28,20 @@
"device_tracker": {
"data": {
"detection_time": "\u6700\u7d42\u51fa\u73fe\u5f8c\u8996\u70ba\u96e2\u958b\u7684\u6642\u9593\uff08\u4ee5\u79d2\u70ba\u55ae\u4f4d\uff09",
+ "ssid_filter": "\u9078\u64c7\u6240\u8981\u8ffd\u8e64\u7684\u7121\u7dda\u7db2\u8def",
"track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef",
"track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09",
"track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef"
- }
+ },
+ "description": "\u8a2d\u5b9a\u8a2d\u5099\u8ffd\u8e64",
+ "title": "UniFi \u9078\u9805"
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "\u65b0\u589e\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668"
- }
+ "allow_bandwidth_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668"
+ },
+ "description": "\u8a2d\u5b9a\u7d71\u8a08\u6578\u64da\u611f\u61c9\u5668",
+ "title": "UniFi \u9078\u9805"
}
}
}
diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py
index 65015b357a7..a21ae4ed508 100644
--- a/homeassistant/components/unifi/__init__.py
+++ b/homeassistant/components/unifi/__init__.py
@@ -1,7 +1,7 @@
"""Support for devices connected to UniFi POE."""
import voluptuous as vol
-from homeassistant.const import CONF_HOST
+from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
@@ -96,6 +96,8 @@ async def async_setup_entry(hass, config_entry):
# sw_version=config.raw['swversion'],
)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown)
+
return True
diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py
index 52ecab08856..36fa7489e81 100644
--- a/homeassistant/components/unifi/config_flow.py
+++ b/homeassistant/components/unifi/config_flow.py
@@ -1,4 +1,6 @@
"""Config flow for UniFi."""
+import socket
+
import voluptuous as vol
from homeassistant import config_entries
@@ -10,21 +12,18 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
from .const import (
CONF_ALLOW_BANDWIDTH_SENSORS,
CONF_CONTROLLER,
CONF_DETECTION_TIME,
CONF_SITE_ID,
+ CONF_SSID_FILTER,
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
CONF_TRACK_WIRED_CLIENTS,
CONTROLLER_ID,
- DEFAULT_ALLOW_BANDWIDTH_SENSORS,
- DEFAULT_DETECTION_TIME,
- DEFAULT_TRACK_CLIENTS,
- DEFAULT_TRACK_DEVICES,
- DEFAULT_TRACK_WIRED_CLIENTS,
DOMAIN,
LOGGER,
)
@@ -104,11 +103,15 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
return self.async_abort(reason="unknown")
+ host = ""
+ if await async_discover_unifi(self.hass):
+ host = "unifi"
+
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
- vol.Required(CONF_HOST): str,
+ vol.Required(CONF_HOST, default=host): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
@@ -179,33 +182,30 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
self.options.update(user_input)
return await self.async_step_statistics_sensors()
+ controller = get_controller_from_config_entry(self.hass, self.config_entry)
+
+ ssid_filter = {wlan: wlan for wlan in controller.api.wlans}
+
return self.async_show_form(
step_id="device_tracker",
data_schema=vol.Schema(
{
vol.Optional(
- CONF_TRACK_CLIENTS,
- default=self.config_entry.options.get(
- CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS
- ),
+ CONF_TRACK_CLIENTS, default=controller.option_track_clients,
): bool,
vol.Optional(
CONF_TRACK_WIRED_CLIENTS,
- default=self.config_entry.options.get(
- CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS
- ),
+ default=controller.option_track_wired_clients,
): bool,
vol.Optional(
- CONF_TRACK_DEVICES,
- default=self.config_entry.options.get(
- CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES
- ),
+ CONF_TRACK_DEVICES, default=controller.option_track_devices,
): bool,
+ vol.Optional(
+ CONF_SSID_FILTER, default=controller.option_ssid_filter
+ ): cv.multi_select(ssid_filter),
vol.Optional(
CONF_DETECTION_TIME,
- default=self.config_entry.options.get(
- CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
- ),
+ default=int(controller.option_detection_time.total_seconds()),
): int,
}
),
@@ -217,16 +217,15 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
self.options.update(user_input)
return await self._update_options()
+ controller = get_controller_from_config_entry(self.hass, self.config_entry)
+
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,
- ),
+ default=controller.option_allow_bandwidth_sensors,
): bool
}
),
@@ -235,3 +234,11 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
async def _update_options(self):
"""Update config entry options."""
return self.async_create_entry(title="", data=self.options)
+
+
+async def async_discover_unifi(hass):
+ """Discover UniFi address."""
+ try:
+ return await hass.async_add_executor_job(socket.gethostbyname, "unifi")
+ except socket.gaierror:
+ return None
diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
index 826491f6ba6..b7cd8e8b6a1 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -5,9 +5,13 @@ import ssl
from aiohttp import CookieJar
import aiounifi
+from aiounifi.controller import SIGNAL_CONNECTION_STATE
+from aiounifi.events import WIRELESS_CLIENT_CONNECTED, WIRELESS_GUEST_CONNECTED
+from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING
import async_timeout
from homeassistant.const import CONF_HOST
+from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -40,6 +44,7 @@ from .const import (
)
from .errors import AuthenticationRequired, CannotConnect
+RETRY_TIMER = 15
SUPPORTED_PLATFORMS = ["device_tracker", "sensor", "switch"]
@@ -59,6 +64,11 @@ class UniFiController:
self._site_name = None
self._site_role = None
+ @property
+ def controller_id(self):
+ """Return the controller ID."""
+ return CONTROLLER_ID.format(host=self.host, site=self.site)
+
@property
def host(self):
"""Return the host of this controller."""
@@ -130,15 +140,47 @@ class UniFiController:
return client.mac
return None
+ @callback
+ def async_unifi_signalling_callback(self, signal, data):
+ """Handle messages back from UniFi library."""
+ if signal == SIGNAL_CONNECTION_STATE:
+
+ if data == STATE_DISCONNECTED and self.available:
+ LOGGER.error("Lost connection to UniFi")
+
+ if (data == STATE_RUNNING and not self.available) or (
+ data == STATE_DISCONNECTED and self.available
+ ):
+ self.available = data == STATE_RUNNING
+ async_dispatcher_send(self.hass, self.signal_reachable)
+
+ if not self.available:
+ self.hass.loop.call_later(RETRY_TIMER, self.reconnect)
+
+ elif signal == "new_data" and data:
+ if "event" in data:
+ if data["event"].event in (
+ WIRELESS_CLIENT_CONNECTED,
+ WIRELESS_GUEST_CONNECTED,
+ ):
+ self.update_wireless_clients()
+ elif data.get("clients") or data.get("devices"):
+ async_dispatcher_send(self.hass, self.signal_update)
+
+ @property
+ def signal_reachable(self) -> str:
+ """Integration specific event to signal a change in connection status."""
+ return f"unifi-reachable-{self.controller_id}"
+
@property
def signal_update(self):
"""Event specific per UniFi entry to signal new data."""
- return f"unifi-update-{CONTROLLER_ID.format(host=self.host, site=self.site)}"
+ return f"unifi-update-{self.controller_id}"
@property
def signal_options_update(self):
"""Event specific per UniFi entry to signal new options."""
- return f"unifi-options-{CONTROLLER_ID.format(host=self.host, site=self.site)}"
+ return f"unifi-options-{self.controller_id}"
def update_wireless_clients(self):
"""Update set of known to be wireless clients."""
@@ -156,59 +198,13 @@ class UniFiController:
unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS]
unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry)
- async def request_update(self):
- """Request an update."""
- if self.progress is not None:
- return await self.progress
-
- self.progress = self.hass.async_create_task(self.async_update())
- await self.progress
-
- self.progress = None
-
- async def async_update(self):
- """Update UniFi controller information."""
- failed = False
-
- try:
- with async_timeout.timeout(10):
- await self.api.clients.update()
- await self.api.devices.update()
- if self.option_block_clients:
- await self.api.clients_all.update()
-
- except aiounifi.LoginRequired:
- try:
- with async_timeout.timeout(5):
- await self.api.login()
-
- except (asyncio.TimeoutError, aiounifi.AiounifiException):
- failed = True
- if self.available:
- LOGGER.error("Unable to reach controller %s", self.host)
- self.available = False
-
- except (asyncio.TimeoutError, aiounifi.AiounifiException):
- failed = True
- if self.available:
- LOGGER.error("Unable to reach controller %s", self.host)
- self.available = False
-
- if not failed and not self.available:
- LOGGER.info("Reconnected to controller %s", self.host)
- self.available = True
-
- self.update_wireless_clients()
-
- async_dispatcher_send(self.hass, self.signal_update)
-
async def async_setup(self):
"""Set up a UniFi controller."""
- hass = self.hass
-
try:
self.api = await get_controller(
- self.hass, **self.config_entry.data[CONF_CONTROLLER]
+ self.hass,
+ **self.config_entry.data[CONF_CONTROLLER],
+ async_callback=self.async_unifi_signalling_callback,
)
await self.api.initialize()
@@ -227,26 +223,28 @@ class UniFiController:
LOGGER.error("Unknown error connecting with UniFi controller: %s", err)
return False
- wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS]
+ wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS]
self.wireless_clients = wireless_clients.get_data(self.config_entry)
self.update_wireless_clients()
self.import_configuration()
- self.config_entry.add_update_listener(self.async_options_updated)
-
for platform in SUPPORTED_PLATFORMS:
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(
+ self.hass.async_create_task(
+ self.hass.config_entries.async_forward_entry_setup(
self.config_entry, platform
)
)
+ self.api.start_websocket()
+
+ self.config_entry.add_update_listener(self.async_config_entry_updated)
+
return True
@staticmethod
- async def async_options_updated(hass, entry):
- """Triggered by config entry options updates."""
+ async def async_config_entry_updated(hass, entry) -> None:
+ """Handle signals of config entry being updated."""
controller_id = CONTROLLER_ID.format(
host=entry.data[CONF_CONTROLLER][CONF_HOST],
site=entry.data[CONF_CONTROLLER][CONF_SITE_ID],
@@ -279,7 +277,6 @@ class UniFiController:
(CONF_SSID_FILTER, CONF_SSID_FILTER),
):
if config in import_config:
- print(config)
if config == option and import_config[
config
] != self.config_entry.options.get(option):
@@ -296,12 +293,38 @@ class UniFiController:
self.config_entry, options=options
)
+ @callback
+ def reconnect(self) -> None:
+ """Prepare to reconnect UniFi session."""
+ LOGGER.debug("Reconnecting to UniFi in %i", RETRY_TIMER)
+ self.hass.loop.create_task(self.async_reconnect())
+
+ async def async_reconnect(self) -> None:
+ """Try to reconnect UniFi session."""
+ try:
+ with async_timeout.timeout(5):
+ await self.api.login()
+ self.api.start_websocket()
+
+ except (asyncio.TimeoutError, aiounifi.AiounifiException):
+ self.hass.loop.call_later(RETRY_TIMER, self.reconnect)
+
+ @callback
+ def shutdown(self, event) -> None:
+ """Wrap the call to unifi.close.
+
+ Used as an argument to EventBus.async_listen_once.
+ """
+ self.api.stop_websocket()
+
async def async_reset(self):
"""Reset this controller to default state.
Will cancel any scheduled setup retry and will unload
the config entry.
"""
+ self.api.stop_websocket()
+
for platform in SUPPORTED_PLATFORMS:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, platform
@@ -314,7 +337,9 @@ class UniFiController:
return True
-async def get_controller(hass, host, username, password, port, site, verify_ssl):
+async def get_controller(
+ hass, host, username, password, port, site, verify_ssl, async_callback=None
+):
"""Create a controller object and verify authentication."""
sslcontext = None
@@ -335,6 +360,7 @@ async def get_controller(hass, host, username, password, port, site, verify_ssl)
site=site,
websession=session,
sslcontext=sslcontext,
+ callback=async_callback,
)
try:
diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py
index 8b45a0f227b..5dd5f0c83ae 100644
--- a/homeassistant/components/unifi/device_tracker.py
+++ b/homeassistant/components/unifi/device_tracker.py
@@ -1,19 +1,17 @@
"""Track devices using UniFi controllers."""
import logging
-from pprint import pformat
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.device_tracker.config_entry import ScannerEntity
from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER
from homeassistant.components.unifi.config_flow import get_controller_from_config_entry
from homeassistant.core import callback
-from homeassistant.helpers import entity_registry
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY
import homeassistant.util.dt as dt_util
from .const import ATTR_MANUFACTURER
+from .unifi_client import UniFiClient
LOGGER = logging.getLogger(__name__)
@@ -43,7 +41,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
controller = get_controller_from_config_entry(hass, config_entry)
tracked = {}
- registry = await entity_registry.async_get_registry(hass)
+ option_track_clients = controller.option_track_clients
+ option_track_devices = controller.option_track_devices
+ option_track_wired_clients = controller.option_track_wired_clients
+
+ registry = await hass.helpers.entity_registry.async_get_registry()
# Restore clients that is not a part of active clients list.
for entity in registry.entities.values():
@@ -65,31 +67,72 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def update_controller():
"""Update the values of the controller."""
- update_items(controller, async_add_entities, tracked)
+ nonlocal option_track_clients
+ nonlocal option_track_devices
+
+ if not option_track_clients and not option_track_devices:
+ return
+
+ add_entities(controller, async_add_entities, tracked)
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 tracked.values():
+ def options_updated():
+ """Manage entities affected by config entry options."""
+ nonlocal option_track_clients
+ nonlocal option_track_devices
+ nonlocal option_track_wired_clients
- if entity.entity_registry_enabled_default == entity.enabled:
+ update = False
+ remove = set()
+
+ for current_option, config_entry_option, tracker_class in (
+ (option_track_clients, controller.option_track_clients, UniFiClientTracker),
+ (option_track_devices, controller.option_track_devices, UniFiDeviceTracker),
+ ):
+ if current_option == config_entry_option:
continue
- disabled_by = None
- if not entity.entity_registry_enabled_default and entity.enabled:
- disabled_by = DISABLED_CONFIG_ENTRY
+ if config_entry_option:
+ update = True
+ else:
+ for mac, entity in tracked.items():
+ if isinstance(entity, tracker_class):
+ remove.add(mac)
- registry.async_update_entity(
- entity.registry_entry.entity_id, disabled_by=disabled_by
- )
+ if (
+ controller.option_track_clients
+ and option_track_wired_clients != controller.option_track_wired_clients
+ ):
+
+ if controller.option_track_wired_clients:
+ update = True
+ else:
+ for mac, entity in tracked.items():
+ if isinstance(entity, UniFiClientTracker) and entity.is_wired:
+ remove.add(mac)
+
+ option_track_clients = controller.option_track_clients
+ option_track_devices = controller.option_track_devices
+ option_track_wired_clients = controller.option_track_wired_clients
+
+ for mac in remove:
+ entity = tracked.pop(mac)
+
+ if registry.async_is_registered(entity.entity_id):
+ registry.async_remove(entity.entity_id)
+
+ hass.async_create_task(entity.async_remove())
+
+ if update:
+ update_controller()
controller.listeners.append(
async_dispatcher_connect(
- hass, controller.signal_options_update, update_disable_on_entities
+ hass, controller.signal_options_update, options_updated
)
)
@@ -97,20 +140,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
-def update_items(controller, async_add_entities, tracked):
- """Update tracked device state from the controller."""
+def add_entities(controller, async_add_entities, tracked):
+ """Add new tracker entities from the controller."""
new_tracked = []
- for items, tracker_class in (
- (controller.api.clients, UniFiClientTracker),
- (controller.api.devices, UniFiDeviceTracker),
+ for items, tracker_class, track in (
+ (controller.api.clients, UniFiClientTracker, controller.option_track_clients),
+ (controller.api.devices, UniFiDeviceTracker, controller.option_track_devices),
):
+ if not track:
+ continue
for item_id in items:
if item_id in tracked:
- if tracked[item_id].enabled:
- tracked[item_id].async_schedule_update_ha_state()
+ continue
+
+ if tracker_class is UniFiClientTracker and (
+ not controller.option_track_wired_clients and items[item_id].is_wired
+ ):
continue
tracked[item_id] = tracker_class(items[item_id], controller)
@@ -120,25 +168,24 @@ def update_items(controller, async_add_entities, tracked):
async_add_entities(new_tracked)
-class UniFiClientTracker(ScannerEntity):
+class UniFiClientTracker(UniFiClient, ScannerEntity):
"""Representation of a network client."""
def __init__(self, client, controller):
"""Set up tracked client."""
- self.client = client
- self.controller = controller
- self.is_wired = self.client.mac not in controller.wireless_clients
- self.wired_bug = None
+ super().__init__(client, controller)
+ self.wired_bug = None
if self.is_wired != self.client.is_wired:
self.wired_bug = dt_util.utcnow() - self.controller.option_detection_time
@property
- def entity_registry_enabled_default(self):
- """Return if the entity should be enabled when first added to the entity registry."""
- if not self.controller.option_track_clients:
- return False
+ def is_connected(self):
+ """Return true if the client is connected to the network.
+ If connected to unwanted ssid return False.
+ If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired.
+ """
if (
not self.is_wired
and self.controller.option_ssid_filter
@@ -146,37 +193,6 @@ class UniFiClientTracker(ScannerEntity):
):
return False
- if not self.controller.option_track_wired_clients and self.is_wired:
- return False
-
- return True
-
- async def async_added_to_hass(self):
- """Client entity created."""
- LOGGER.debug("New UniFi client tracker %s (%s)", self.name, self.client.mac)
-
- async def async_update(self):
- """Synchronize state with controller.
-
- 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.
- """
- await self.controller.request_update()
-
- if self.is_wired and self.client.mac in self.controller.wireless_clients:
- self.is_wired = False
-
- LOGGER.debug(
- "Updating UniFi tracked client %s\n%s",
- self.entity_id,
- pformat(self.client.raw),
- )
-
- @property
- def is_connected(self):
- """Return true if the client is connected to the network.
-
- If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired.
- """
if self.is_wired != self.client.is_wired:
if not self.wired_bug:
self.wired_bug = dt_util.utcnow()
@@ -198,26 +214,11 @@ class UniFiClientTracker(ScannerEntity):
"""Return the source type of the client."""
return SOURCE_TYPE_ROUTER
- @property
- def name(self) -> str:
- """Return the name of the client."""
- return self.client.name or self.client.hostname
-
@property
def unique_id(self) -> str:
"""Return a unique identifier for this client."""
return f"{self.client.mac}-{self.controller.site}"
- @property
- def available(self) -> bool:
- """Return if controller is available."""
- return self.controller.available
-
- @property
- def device_info(self):
- """Return a client description for device registry."""
- return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}}
-
@property
def device_state_attributes(self):
"""Return the client state attributes."""
@@ -239,28 +240,31 @@ class UniFiDeviceTracker(ScannerEntity):
"""Set up tracked device."""
self.device = device
self.controller = controller
-
- @property
- def entity_registry_enabled_default(self):
- """Return if the entity should be enabled when first added to the entity registry."""
- if self.controller.option_track_devices:
- return True
- return False
+ self.listeners = []
async def async_added_to_hass(self):
"""Subscribe to device events."""
LOGGER.debug("New UniFi device tracker %s (%s)", self.name, self.device.mac)
-
- async def async_update(self):
- """Synchronize state with controller."""
- await self.controller.request_update()
-
- LOGGER.debug(
- "Updating UniFi tracked device %s\n%s",
- self.entity_id,
- pformat(self.device.raw),
+ self.device.register_callback(self.async_update_callback)
+ self.listeners.append(
+ async_dispatcher_connect(
+ self.hass, self.controller.signal_reachable, self.async_update_callback
+ )
)
+ async def async_will_remove_from_hass(self) -> None:
+ """Disconnect device object when removed."""
+ self.device.remove_callback(self.async_update_callback)
+ for unsub_dispatcher in self.listeners:
+ unsub_dispatcher()
+
+ @callback
+ def async_update_callback(self):
+ """Update the sensor's state."""
+ LOGGER.debug("Updating UniFi tracked device %s", self.entity_id)
+
+ self.async_schedule_update_ha_state()
+
@property
def is_connected(self):
"""Return true if the device is connected to the network."""
@@ -325,3 +329,8 @@ class UniFiDeviceTracker(ScannerEntity):
attributes["upgradable"] = self.device.upgradable
return attributes
+
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json
index e2bcd5b68a5..a42b136e665 100644
--- a/homeassistant/components/unifi/manifest.json
+++ b/homeassistant/components/unifi/manifest.json
@@ -3,8 +3,12 @@
"name": "Ubiquiti UniFi",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifi",
- "requirements": ["aiounifi==11"],
+ "requirements": [
+ "aiounifi==13"
+ ],
"dependencies": [],
- "codeowners": ["@kane610"],
+ "codeowners": [
+ "@kane610"
+ ],
"quality_scale": "platinum"
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py
index 9145fd8e00f..942b0ef6779 100644
--- a/homeassistant/components/unifi/sensor.py
+++ b/homeassistant/components/unifi/sensor.py
@@ -3,11 +3,9 @@ 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
+
+from .unifi_client import UniFiClient
LOGGER = logging.getLogger(__name__)
@@ -24,36 +22,48 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
controller = get_controller_from_config_entry(hass, config_entry)
sensors = {}
- registry = await entity_registry.async_get_registry(hass)
+ option_allow_bandwidth_sensors = controller.option_allow_bandwidth_sensors
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
@callback
def update_controller():
"""Update the values of the controller."""
- update_items(controller, async_add_entities, sensors)
+ nonlocal option_allow_bandwidth_sensors
+
+ if not option_allow_bandwidth_sensors:
+ return
+
+ add_entities(controller, async_add_entities, sensors)
controller.listeners.append(
async_dispatcher_connect(hass, controller.signal_update, update_controller)
)
@callback
- def update_disable_on_entities():
+ def options_updated():
"""Update the values of the controller."""
- for entity in sensors.values():
+ nonlocal option_allow_bandwidth_sensors
- if entity.entity_registry_enabled_default == entity.enabled:
- continue
+ if option_allow_bandwidth_sensors != controller.option_allow_bandwidth_sensors:
+ option_allow_bandwidth_sensors = controller.option_allow_bandwidth_sensors
- disabled_by = None
- if not entity.entity_registry_enabled_default and entity.enabled:
- disabled_by = DISABLED_CONFIG_ENTRY
+ if option_allow_bandwidth_sensors:
+ update_controller()
- registry.async_update_entity(
- entity.registry_entry.entity_id, disabled_by=disabled_by
- )
+ else:
+ for sensor in sensors.values():
+
+ if entity_registry.async_is_registered(sensor.entity_id):
+ entity_registry.async_remove(sensor.entity_id)
+
+ hass.async_create_task(sensor.async_remove())
+
+ sensors.clear()
controller.listeners.append(
async_dispatcher_connect(
- hass, controller.signal_options_update, update_disable_on_entities
+ hass, controller.signal_options_update, options_updated
)
)
@@ -61,8 +71,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
-def update_items(controller, async_add_entities, sensors):
- """Update sensors from the controller."""
+def add_entities(controller, async_add_entities, sensors):
+ """Add new sensor entities from the controller."""
new_sensors = []
for client_id in controller.api.clients:
@@ -73,9 +83,6 @@ def update_items(controller, async_add_entities, sensors):
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(
@@ -87,51 +94,7 @@ def update_items(controller, async_add_entities, 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):
+class UniFiRxBandwidthSensor(UniFiClient):
"""Receiving bandwidth sensor."""
@property
@@ -153,7 +116,7 @@ class UniFiRxBandwidthSensor(UniFiBandwidthSensor):
return f"rx-{self.client.mac}"
-class UniFiTxBandwidthSensor(UniFiBandwidthSensor):
+class UniFiTxBandwidthSensor(UniFiRxBandwidthSensor):
"""Transmitting bandwidth sensor."""
@property
diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json
index ce2f2345917..e652b60ee32 100644
--- a/homeassistant/components/unifi/strings.json
+++ b/homeassistant/components/unifi/strings.json
@@ -31,15 +31,20 @@
"device_tracker": {
"data": {
"detection_time": "Time in seconds from last seen until considered away",
+ "ssid_filter": "Select SSIDs to track wireless clients on",
"track_clients": "Track network clients",
"track_devices": "Track network devices (Ubiquiti devices)",
"track_wired_clients": "Include wired network clients"
- }
+ },
+ "description": "Configure device tracking",
+ "title": "UniFi options"
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients"
- }
+ "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients"
+ },
+ "description": "Configure statistics sensors",
+ "title": "UniFi options"
}
}
}
diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index b1f62131eb4..941f4f8ab84 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -1,15 +1,15 @@
"""Support for devices connected to UniFi POE."""
import logging
-from pprint import pformat
from homeassistant.components.switch import SwitchDevice
from homeassistant.components.unifi.config_flow import get_controller_from_config_entry
from homeassistant.core import callback
from homeassistant.helpers import entity_registry
-from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity
+from .unifi_client import UniFiClient
+
LOGGER = logging.getLogger(__name__)
@@ -20,7 +20,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 switches for UniFi component.
- Switches are controlling network switch ports with Poe.
+ Switches are controlling network access and switch ports with POE.
"""
controller = get_controller_from_config_entry(hass, config_entry)
@@ -55,7 +55,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def update_controller():
"""Update the values of the controller."""
- update_items(controller, async_add_entities, switches, switches_off)
+ add_entities(controller, async_add_entities, switches, switches_off)
controller.listeners.append(
async_dispatcher_connect(hass, controller.signal_update, update_controller)
@@ -66,8 +66,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
-def update_items(controller, async_add_entities, switches, switches_off):
- """Update POE port state from the controller."""
+def add_entities(controller, async_add_entities, switches, switches_off):
+ """Add new switch entities from the controller."""
new_switches = []
devices = controller.api.devices
@@ -77,13 +77,6 @@ def update_items(controller, async_add_entities, switches, switches_off):
block_client_id = f"block-{client_id}"
if block_client_id in switches:
- if switches[block_client_id].enabled:
- LOGGER.debug(
- "Updating UniFi block switch %s (%s)",
- switches[block_client_id].entity_id,
- switches[block_client_id].client.mac,
- )
- switches[block_client_id].async_schedule_update_ha_state()
continue
if client_id not in controller.api.clients_all:
@@ -99,13 +92,6 @@ def update_items(controller, async_add_entities, switches, switches_off):
poe_client_id = f"poe-{client_id}"
if poe_client_id in switches:
- if switches[poe_client_id].enabled:
- LOGGER.debug(
- "Updating UniFi POE switch %s (%s)",
- switches[poe_client_id].entity_id,
- switches[poe_client_id].client.mac,
- )
- switches[poe_client_id].async_schedule_update_ha_state()
continue
client = controller.api.clients[client_id]
@@ -148,42 +134,21 @@ def update_items(controller, async_add_entities, switches, switches_off):
async_add_entities(new_switches)
-class UniFiClient:
- """Base class for UniFi switches."""
-
- def __init__(self, client, controller):
- """Set up switch."""
- self.client = client
- self.controller = controller
-
- async def async_update(self):
- """Synchronize state with controller."""
- await self.controller.request_update()
-
- @property
- def name(self):
- """Return the name of the client."""
- return self.client.name or self.client.hostname
-
- @property
- def device_info(self):
- """Return a device description for device registry."""
- return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}}
-
-
class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity):
"""Representation of a client that uses POE."""
def __init__(self, client, controller):
"""Set up POE switch."""
super().__init__(client, controller)
+
self.poe_mode = None
if self.client.sw_port and self.port.poe_mode != "off":
self.poe_mode = self.port.poe_mode
async def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant."""
- LOGGER.debug("New UniFi POE switch %s (%s)", self.name, self.client.mac)
+ await super().async_added_to_hass()
+
state = await self.async_get_last_state()
if state is None:
@@ -198,16 +163,6 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity):
if not self.client.sw_port:
self.client.raw["sw_port"] = state.attributes["port"]
- async def async_update(self):
- """Log client information after update."""
- await super().async_update()
-
- LOGGER.debug(
- "Updating UniFi POE controlled client %s\n%s",
- self.entity_id,
- pformat(self.client.raw),
- )
-
@property
def unique_id(self):
"""Return a unique identifier for this switch."""
@@ -261,16 +216,20 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity):
@property
def port(self):
"""Shortcut to the switch port that client is connected to."""
- return self.device.ports[self.client.sw_port]
+ try:
+ return self.device.ports[self.client.sw_port]
+ except TypeError:
+ LOGGER.warning(
+ "Entity %s reports faulty device %s or port %s",
+ self.entity_id,
+ self.client.sw_mac,
+ self.client.sw_port,
+ )
class UniFiBlockClientSwitch(UniFiClient, SwitchDevice):
"""Representation of a blockable client."""
- async def async_added_to_hass(self):
- """Call when entity about to be added to Home Assistant."""
- LOGGER.debug("New UniFi Block switch %s (%s)", self.name, self.client.mac)
-
@property
def unique_id(self):
"""Return a unique identifier for this switch."""
@@ -281,11 +240,6 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchDevice):
"""Return true if client is allowed to connect."""
return not self.client.blocked
- @property
- def available(self):
- """Return if controller is available."""
- return self.controller.available
-
async def async_turn_on(self, **kwargs):
"""Turn on connectivity for client."""
await self.controller.api.clients.async_unblock(self.client.mac)
diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py
new file mode 100644
index 00000000000..2e18f55a57b
--- /dev/null
+++ b/homeassistant/components/unifi/unifi_client.py
@@ -0,0 +1,65 @@
+"""Base class for UniFi clients."""
+
+import logging
+
+from homeassistant.core import callback
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity
+
+LOGGER = logging.getLogger(__name__)
+
+
+class UniFiClient(Entity):
+ """Base class for UniFi clients."""
+
+ def __init__(self, client, controller) -> None:
+ """Set up client."""
+ self.client = client
+ self.controller = controller
+ self.listeners = []
+ self.is_wired = self.client.mac not in controller.wireless_clients
+
+ async def async_added_to_hass(self) -> None:
+ """Client entity created."""
+ LOGGER.debug("New UniFi client %s (%s)", self.name, self.client.mac)
+ self.client.register_callback(self.async_update_callback)
+ self.listeners.append(
+ async_dispatcher_connect(
+ self.hass, self.controller.signal_reachable, self.async_update_callback
+ )
+ )
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Disconnect client object when removed."""
+ self.client.remove_callback(self.async_update_callback)
+ for unsub_dispatcher in self.listeners:
+ unsub_dispatcher()
+
+ @callback
+ def async_update_callback(self) -> None:
+ """Update the clients state."""
+ if self.is_wired and self.client.mac in self.controller.wireless_clients:
+ self.is_wired = False
+ LOGGER.debug("Updating client %s %s", self.entity_id, self.client.mac)
+ self.async_schedule_update_ha_state()
+
+ @property
+ def name(self) -> str:
+ """Return the name of the client."""
+ return self.client.name or self.client.hostname
+
+ @property
+ def available(self) -> bool:
+ """Return if controller is available."""
+ return self.controller.available
+
+ @property
+ def device_info(self) -> dict:
+ """Return a client description for device registry."""
+ return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}}
+
+ @property
+ def should_poll(self) -> bool:
+ """No polling needed."""
+ return False
diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py
index 42eb988ed56..0a2c6697f69 100644
--- a/homeassistant/components/updater/__init__.py
+++ b/homeassistant/components/updater/__init__.py
@@ -12,11 +12,9 @@ from distro import linux_distribution # pylint: disable=import-error
import voluptuous as vol
from homeassistant.const import __version__ as current_version
-from homeassistant.helpers import discovery, event
+from homeassistant.helpers import discovery, update_coordinator
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.dispatcher import async_dispatcher_send
-import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -28,8 +26,6 @@ CONF_COMPONENT_REPORTING = "include_used_components"
DOMAIN = "updater"
-DISPATCHER_REMOTE_UPDATE = "updater_remote_update"
-
UPDATER_URL = "https://updater.home-assistant.io/"
UPDATER_UUID_FILE = ".uuid"
@@ -84,30 +80,25 @@ async def async_setup(hass, config):
# This component only makes sense in release versions
_LOGGER.info("Running on 'dev', only analytics will be submitted")
- hass.async_create_task(
- discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config)
- )
-
- config = config.get(DOMAIN, {})
- if config.get(CONF_REPORTING):
+ conf = config.get(DOMAIN, {})
+ if conf.get(CONF_REPORTING):
huuid = await hass.async_add_job(_load_uuid, hass)
else:
huuid = None
- include_components = config.get(CONF_COMPONENT_REPORTING)
+ include_components = conf.get(CONF_COMPONENT_REPORTING)
- async def check_new_version(now):
+ async def check_new_version():
"""Check if a new version is available and report if one is."""
- result = await get_newest_version(hass, huuid, include_components)
+ newest, release_notes = await get_newest_version(
+ hass, huuid, include_components
+ )
- if result is None:
- return
-
- newest, release_notes = result
+ _LOGGER.debug("Fetched version %s: %s", newest, release_notes)
# Skip on dev
- if newest is None or "dev" in current_version:
- return
+ if "dev" in current_version:
+ return Updater(False, "", "")
# Load data from supervisor on Hass.io
if hass.components.hassio.is_hassio():
@@ -116,20 +107,33 @@ async def async_setup(hass, config):
# Validate version
update_available = False
if StrictVersion(newest) > StrictVersion(current_version):
- _LOGGER.info("The latest available version of Home Assistant is %s", newest)
+ _LOGGER.debug(
+ "The latest available version of Home Assistant is %s", newest
+ )
update_available = True
elif StrictVersion(newest) == StrictVersion(current_version):
- _LOGGER.info("You are on the latest version (%s) of Home Assistant", newest)
+ _LOGGER.debug(
+ "You are on the latest version (%s) of Home Assistant", newest
+ )
elif StrictVersion(newest) < StrictVersion(current_version):
_LOGGER.debug("Local version is newer than the latest version (%s)", newest)
- updater = Updater(update_available, newest, release_notes)
- async_dispatcher_send(hass, DISPATCHER_REMOTE_UPDATE, updater)
+ _LOGGER.debug("Update available: %s", update_available)
- # Update daily, start 1 hour after startup
- _dt = dt_util.utcnow() + timedelta(hours=1)
- event.async_track_utc_time_change(
- hass, check_new_version, hour=_dt.hour, minute=_dt.minute, second=_dt.second
+ return Updater(update_available, newest, release_notes)
+
+ coordinator = hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name="Home Assistant update",
+ update_method=check_new_version,
+ update_interval=timedelta(days=1),
+ )
+
+ await coordinator.async_refresh()
+
+ hass.async_create_task(
+ discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config)
)
return True
@@ -164,17 +168,17 @@ async def get_newest_version(hass, huuid, include_components):
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Could not contact Home Assistant Update to check for updates")
- return None
+ raise update_coordinator.UpdateFailed
try:
res = await req.json()
except ValueError:
_LOGGER.error("Received invalid JSON from Home Assistant Update")
- return None
+ raise update_coordinator.UpdateFailed
try:
res = RESPONSE_SCHEMA(res)
return res["version"], res["release-notes"]
except vol.Invalid:
_LOGGER.error("Got unexpected response: %s", res)
- return None
+ raise update_coordinator.UpdateFailed
diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py
index 3e026a87d4d..7abab616d5c 100644
--- a/homeassistant/components/updater/binary_sensor.py
+++ b/homeassistant/components/updater/binary_sensor.py
@@ -1,26 +1,24 @@
"""Support for Home Assistant Updater binary sensors."""
from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.core import callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DISPATCHER_REMOTE_UPDATE, Updater
+from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DOMAIN as UPDATER_DOMAIN
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the updater binary sensors."""
- async_add_entities([UpdaterBinary()])
+ if discovery_info is None:
+ return
+
+ async_add_entities([UpdaterBinary(hass.data[UPDATER_DOMAIN])])
class UpdaterBinary(BinarySensorDevice):
"""Representation of an updater binary sensor."""
- def __init__(self):
+ def __init__(self, coordinator):
"""Initialize the binary sensor."""
- self._update_available = None
- self._release_notes = None
- self._newest_version = None
- self._unsub_dispatcher = None
+ self.coordinator = coordinator
@property
def name(self) -> str:
@@ -35,12 +33,12 @@ class UpdaterBinary(BinarySensorDevice):
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
- return self._update_available
+ return self.coordinator.data.update_available
@property
def available(self) -> bool:
"""Return True if entity is available."""
- return self._update_available is not None
+ return self.coordinator.last_update_success
@property
def should_poll(self) -> bool:
@@ -50,32 +48,24 @@ class UpdaterBinary(BinarySensorDevice):
@property
def device_state_attributes(self) -> dict:
"""Return the optional state attributes."""
- data = super().device_state_attributes
- if data is None:
- data = {}
- if self._release_notes:
- data[ATTR_RELEASE_NOTES] = self._release_notes
- if self._newest_version:
- data[ATTR_NEWEST_VERSION] = self._newest_version
+ data = {}
+ if self.coordinator.data.release_notes:
+ data[ATTR_RELEASE_NOTES] = self.coordinator.data.release_notes
+ if self.coordinator.data.newest_version:
+ data[ATTR_NEWEST_VERSION] = self.coordinator.data.newest_version
return data
async def async_added_to_hass(self):
"""Register update dispatcher."""
-
- @callback
- def async_state_update(updater: Updater):
- """Update callback."""
- self._newest_version = updater.newest_version
- self._release_notes = updater.release_notes
- self._update_available = updater.update_available
- self.async_schedule_update_ha_state()
-
- self._unsub_dispatcher = async_dispatcher_connect(
- self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update
- )
+ self.coordinator.async_add_listener(self.async_write_ha_state)
async def async_will_remove_from_hass(self):
- """Register update dispatcher."""
- if self._unsub_dispatcher is not None:
- self._unsub_dispatcher()
- self._unsub_dispatcher = None
+ """When removed from hass."""
+ self.coordinator.async_remove_listener(self.async_write_ha_state)
+
+ async def async_update(self):
+ """Update the entity.
+
+ Only used by the generic entity update service.
+ """
+ await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/upnp/.translations/pl.json b/homeassistant/components/upnp/.translations/pl.json
index d7ede44d22d..964e5a6818d 100644
--- a/homeassistant/components/upnp/.translations/pl.json
+++ b/homeassistant/components/upnp/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "UPnP/IGD jest ju\u017c skonfigurowane",
+ "already_configured": "UPnP/IGD jest ju\u017c skonfigurowane.",
"incomplete_device": "Ignorowanie niekompletnego urz\u0105dzenia UPnP",
"no_devices_discovered": "Nie wykryto urz\u0105dze\u0144 UPnP/IGD",
"no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 UPnP/IGD.",
diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json
index 6dce1b3d76c..b0a7b7e7b65 100644
--- a/homeassistant/components/upnp/.translations/ru.json
+++ b/homeassistant/components/upnp/.translations/ru.json
@@ -5,7 +5,7 @@
"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 \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432.",
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"error": {
@@ -25,7 +25,7 @@
"user": {
"data": {
"enable_port_mapping": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432 \u0434\u043b\u044f Home Assistant",
- "enable_sensors": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0430",
+ "enable_sensors": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0430",
"igd": "UPnP / IGD"
},
"title": "UPnP / IGD"
diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py
index 81fd5c025b9..db121678d93 100644
--- a/homeassistant/components/upnp/sensor.py
+++ b/homeassistant/components/upnp/sensor.py
@@ -170,7 +170,7 @@ class PerSecondUPnPIGDSensor(UpnpSensor):
"""Get unit we are measuring in."""
raise NotImplementedError()
- def _async_fetch_value(self):
+ async def _async_fetch_value(self):
"""Fetch a value from the IGD."""
raise NotImplementedError()
diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py
index 3dab92b89f8..8c47e716b80 100644
--- a/homeassistant/components/utility_meter/sensor.py
+++ b/homeassistant/components/utility_meter/sensor.py
@@ -140,7 +140,7 @@ class UtilityMeterSensor(RestoreEntity):
diff = Decimal(new_state.state) - Decimal(old_state.state)
if (not self._sensor_net_consumption) and diff < 0:
- # Source sensor just rolled over for unknow reasons,
+ # Source sensor just rolled over for unknown reasons,
return
self._state += diff
diff --git a/homeassistant/components/vacuum/.translations/sv.json b/homeassistant/components/vacuum/.translations/sv.json
new file mode 100644
index 00000000000..38b7f72ab9b
--- /dev/null
+++ b/homeassistant/components/vacuum/.translations/sv.json
@@ -0,0 +1,16 @@
+{
+ "device_automation": {
+ "action_type": {
+ "clean": "L\u00e5t {entity_name} st\u00e4da",
+ "dock": "L\u00e5t {entity_name} \u00e5terg\u00e5 till dockan"
+ },
+ "condition_type": {
+ "is_cleaning": "{entity_name} st\u00e4dar",
+ "is_docked": "{entity_name} \u00e4r dockad"
+ },
+ "trigger_type": {
+ "cleaning": "{entity_name} b\u00f6rjade st\u00e4da",
+ "docked": "{entity_name} dockad"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py
index 225a6ed72bc..3cd2de600e3 100644
--- a/homeassistant/components/vacuum/__init__.py
+++ b/homeassistant/components/vacuum/__init__.py
@@ -248,7 +248,7 @@ class VacuumDevice(_BaseVacuum, ToggleEntity):
@property
def capability_attributes(self):
- """Return capabilitiy attributes."""
+ """Return capability attributes."""
if self.fan_speed is not None:
return {ATTR_FAN_SPEED_LIST: self.fan_speed_list}
@@ -330,7 +330,7 @@ class StateVacuumDevice(_BaseVacuum):
@property
def capability_attributes(self):
- """Return capabilitiy attributes."""
+ """Return capability attributes."""
if self.fan_speed is not None:
return {ATTR_FAN_SPEED_LIST: self.fan_speed_list}
diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json
index 875cc6f8787..7a082200740 100644
--- a/homeassistant/components/vallox/manifest.json
+++ b/homeassistant/components/vallox/manifest.json
@@ -2,7 +2,7 @@
"domain": "vallox",
"name": "Valloxs",
"documentation": "https://www.home-assistant.io/integrations/vallox",
- "requirements": ["vallox-websocket-api==2.2.0"],
+ "requirements": ["vallox-websocket-api==2.4.0"],
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/velbus/.translations/pl.json b/homeassistant/components/velbus/.translations/pl.json
index 72e18b0e2c8..0856d142bef 100644
--- a/homeassistant/components/velbus/.translations/pl.json
+++ b/homeassistant/components/velbus/.translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "port_exists": "Ten port jest ju\u017c skonfigurowany"
+ "port_exists": "Ten port jest ju\u017c skonfigurowany."
},
"error": {
"connection_failed": "Po\u0142\u0105czenie Velbus nie powiod\u0142o si\u0119",
- "port_exists": "Ten port jest ju\u017c skonfigurowany"
+ "port_exists": "Ten port jest ju\u017c skonfigurowany."
},
"step": {
"user": {
diff --git a/homeassistant/components/velbus/.translations/sv.json b/homeassistant/components/velbus/.translations/sv.json
new file mode 100644
index 00000000000..5a864439423
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/sv.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "port_exists": "Den h\u00e4r porten \u00e4r redan konfigurerad"
+ },
+ "error": {
+ "connection_failed": "Velbus-anslutningen misslyckades",
+ "port_exists": "Den h\u00e4r porten \u00e4r redan konfigurerad"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Namnet p\u00e5 den h\u00e4r velbus-anslutningen",
+ "port": "Anslutningsstr\u00e4ng"
+ },
+ "title": "Definiera velbus-anslutningstypen"
+ }
+ },
+ "title": "Velbus-gr\u00e4nssnitt"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py
index d8f9dae13de..b4fe49a88e7 100644
--- a/homeassistant/components/velbus/__init__.py
+++ b/homeassistant/components/velbus/__init__.py
@@ -140,11 +140,11 @@ class VelbusEntity(Entity):
"identifiers": {
(DOMAIN, self._module.get_module_address(), self._module.serial)
},
- "name": "{} {}".format(
- self._module.get_module_address(), self._module.get_module_name()
+ "name": "{} ({})".format(
+ self._module.get_module_name(), self._module.get_module_address()
),
"manufacturer": "Velleman",
- "model": self._module.get_module_name(),
+ "model": self._module.get_module_type_name(),
"sw_version": "{}.{}-{}".format(
self._module.memory_map_version,
self._module.build_year,
diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py
index 9325acf0608..1d081b711a8 100644
--- a/homeassistant/components/velbus/config_flow.py
+++ b/homeassistant/components/velbus/config_flow.py
@@ -49,7 +49,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return False
async def async_step_user(self, user_input=None):
- """Step when user intializes a integration."""
+ """Step when user initializes a integration."""
self._errors = {}
if user_input is not None:
name = slugify(user_input[CONF_NAME])
diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json
index 258b367fa5b..3063c4445bd 100644
--- a/homeassistant/components/velbus/manifest.json
+++ b/homeassistant/components/velbus/manifest.json
@@ -2,7 +2,7 @@
"domain": "velbus",
"name": "Velbus",
"documentation": "https://www.home-assistant.io/integrations/velbus",
- "requirements": ["python-velbus==2.0.36"],
+ "requirements": ["python-velbus==2.0.41"],
"config_flow": true,
"dependencies": [],
"codeowners": ["@Cereal2nd", "@brefra"]
diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py
index 01eb5faf897..5b5d50347ac 100644
--- a/homeassistant/components/verisure/lock.py
+++ b/homeassistant/components/verisure/lock.py
@@ -1,6 +1,6 @@
"""Support for Verisure locks."""
import logging
-from time import sleep, time
+from time import monotonic, sleep
from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED
@@ -71,7 +71,7 @@ class VerisureDoorlock(LockDevice):
def update(self):
"""Update lock status."""
- if time() - self._change_timestamp < 10:
+ if monotonic() - self._change_timestamp < 10:
return
hub.update_overview()
status = hub.get_first(
@@ -131,4 +131,4 @@ class VerisureDoorlock(LockDevice):
transaction = hub.session.get_lock_state_transaction(transaction_id)
if transaction["result"] == "OK":
self._state = state
- self._change_timestamp = time()
+ self._change_timestamp = monotonic()
diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py
index 32e1c1364a3..2df250303c5 100644
--- a/homeassistant/components/verisure/switch.py
+++ b/homeassistant/components/verisure/switch.py
@@ -1,6 +1,6 @@
"""Support for Verisure Smartplugs."""
import logging
-from time import time
+from time import monotonic
from homeassistant.components.switch import SwitchDevice
@@ -44,7 +44,7 @@ class VerisureSmartplug(SwitchDevice):
@property
def is_on(self):
"""Return true if on."""
- if time() - self._change_timestamp < 10:
+ if monotonic() - self._change_timestamp < 10:
return self._state
self._state = (
hub.get_first(
@@ -67,13 +67,13 @@ class VerisureSmartplug(SwitchDevice):
"""Set smartplug status on."""
hub.session.set_smartplug_state(self._device_label, True)
self._state = True
- self._change_timestamp = time()
+ self._change_timestamp = monotonic()
def turn_off(self, **kwargs):
"""Set smartplug status off."""
hub.session.set_smartplug_state(self._device_label, False)
self._state = False
- self._change_timestamp = time()
+ self._change_timestamp = monotonic()
# pylint: disable=no-self-use
def update(self):
diff --git a/homeassistant/components/vesync/.translations/sv.json b/homeassistant/components/vesync/.translations/sv.json
new file mode 100644
index 00000000000..a477ca6e5da
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/sv.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Endast en Vesync-instans \u00e4r till\u00e5ten"
+ },
+ "error": {
+ "invalid_login": "Ogiltigt anv\u00e4ndarnamn eller l\u00f6senord"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "L\u00f6senord",
+ "username": "E-postadress"
+ },
+ "title": "Ange anv\u00e4ndarnamn och l\u00f6senord"
+ }
+ },
+ "title": "VeSync"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json
index cefc244e5b8..66fd15d3a90 100644
--- a/homeassistant/components/vicare/manifest.json
+++ b/homeassistant/components/vicare/manifest.json
@@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/integrations/vicare",
"dependencies": [],
"codeowners": ["@oischinger"],
- "requirements": ["PyViCare==0.1.2"]
+ "requirements": ["PyViCare==0.1.7"]
}
diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py
index f31e4f65170..eea3d81faf6 100644
--- a/homeassistant/components/vicare/water_heater.py
+++ b/homeassistant/components/vicare/water_heater.py
@@ -122,7 +122,8 @@ class ViCareWater(WaterHeaterDevice):
"""Set new target temperatures."""
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is not None:
- self._api.setDomesticHotWaterTemperature(self._target_temperature)
+ self._api.setDomesticHotWaterTemperature(temp)
+ self._target_temperature = temp
@property
def min_temp(self):
diff --git a/homeassistant/components/vilfo/.translations/ca.json b/homeassistant/components/vilfo/.translations/ca.json
new file mode 100644
index 00000000000..07d9ddafb51
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/ca.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'encaminador Vilfo ja est\u00e0 configurat."
+ },
+ "error": {
+ "cannot_connect": "No s'ha pogut connectar. Verifica la informaci\u00f3 proporcionada i torna-ho a provar.",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida. Comprova el testimoni d'acc\u00e9s i torna-ho a provar.",
+ "unknown": "S'ha produ\u00eft un error inesperat durant la configuraci\u00f3 de la integraci\u00f3."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Testimoni d'acc\u00e9s per l'API de l'encaminador Vilfo",
+ "host": "Nom d'amfitri\u00f3 o IP de l'encaminador"
+ },
+ "description": "Configura la integraci\u00f3 de l'encaminador Vilfo. Necessites la seva IP o nom d'amfitri\u00f3 i el testimoni d'acc\u00e9s de l'API (token). Per a m\u00e9s informaci\u00f3, visita: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Connexi\u00f3 amb l'encaminador Vilfo"
+ }
+ },
+ "title": "Encaminador Vilfo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/da.json b/homeassistant/components/vilfo/.translations/da.json
new file mode 100644
index 00000000000..f233b4cb7b9
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/da.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Denne Vilfo-router er allerede konfigureret."
+ },
+ "error": {
+ "cannot_connect": "Forbindelsen kunne ikke oprettes. Tjek de oplysninger, du har angivet, og pr\u00f8v igen.",
+ "invalid_auth": "Ugyldig godkendelse. Kontroller adgangstoken og pr\u00f8v igen.",
+ "unknown": "Der opstod en uventet fejl under konfiguration af integrationen."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Adgangstoken til Vilfo-router-API",
+ "host": "Router-v\u00e6rtsnavn eller IP"
+ },
+ "description": "Indstil Vilfo-routerintegration. Du har brug for dit Vilfo-routerv\u00e6rtsnavn/IP og et API-adgangstoken. For yderligere information om denne integration og hvordan du f\u00e5r disse detaljer, kan du bes\u00f8ge: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Opret forbindelse til Vilfo-router"
+ }
+ },
+ "title": "Vilfo-router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/de.json b/homeassistant/components/vilfo/.translations/de.json
new file mode 100644
index 00000000000..9c0f938b679
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Dieser Vilfo Router ist bereits konfiguriert."
+ },
+ "error": {
+ "cannot_connect": "Verbindung nicht m\u00f6glich. Bitte \u00fcberpr\u00fcfen Sie die von Ihnen angegebenen Informationen und versuchen Sie es erneut.",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung. Bitte \u00fcberpr\u00fcfen Sie den Zugriffstoken und versuchen Sie es erneut.",
+ "unknown": "Beim Einrichten der Integration ist ein unerwarteter Fehler aufgetreten."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Zugriffstoken f\u00fcr die Vilfo Router-API",
+ "host": "Router-Hostname oder IP"
+ },
+ "title": "Stellen Sie eine Verbindung zum Vilfo Router her"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/en.json b/homeassistant/components/vilfo/.translations/en.json
new file mode 100644
index 00000000000..e6b9817f5a8
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/en.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "This Vilfo Router is already configured."
+ },
+ "error": {
+ "cannot_connect": "Failed to connect. Please check the information you provided and try again.",
+ "invalid_auth": "Invalid authentication. Please check the access token and try again.",
+ "unknown": "An unexpected error occurred while setting up the integration."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Access token for the Vilfo Router API",
+ "host": "Router hostname or IP"
+ },
+ "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Connect to the Vilfo Router"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/es.json b/homeassistant/components/vilfo/.translations/es.json
new file mode 100644
index 00000000000..170faa197da
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/es.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Este router Vilfo ya est\u00e1 configurado."
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar. Compruebe la informaci\u00f3n que proporcion\u00f3 e int\u00e9ntelo de nuevo.",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida. Compruebe el token de acceso e int\u00e9ntelo de nuevo.",
+ "unknown": "Se ha producido un error inesperado al configurar la integraci\u00f3n."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Token de acceso para la API del Router Vilfo",
+ "host": "Nombre de host o IP del router"
+ },
+ "description": "Configure la integraci\u00f3n del Router Vilfo. Necesita su nombre de host/IP del Router Vilfo y un token de acceso a la API. Para obtener informaci\u00f3n adicional sobre esta integraci\u00f3n y c\u00f3mo obtener esos detalles, visite: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Conectar con el Router Vilfo"
+ }
+ },
+ "title": "Router Vilfo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/fr.json b/homeassistant/components/vilfo/.translations/fr.json
new file mode 100644
index 00000000000..6abeb789f23
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/fr.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Routeur Vilfo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/hu.json b/homeassistant/components/vilfo/.translations/hu.json
new file mode 100644
index 00000000000..5ae11707c19
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/hu.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Router hostname vagy IP"
+ },
+ "title": "Csatlakoz\u00e1s a Vilfo routerhez"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/it.json b/homeassistant/components/vilfo/.translations/it.json
new file mode 100644
index 00000000000..5523dcc0c09
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/it.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Questo Vilfo Router \u00e8 gi\u00e0 configurato."
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi. Controllare le informazioni fornite e riprovare.",
+ "invalid_auth": "Autenticazione non valida. Controllare il token di accesso e riprovare.",
+ "unknown": "Si \u00e8 verificato un errore imprevisto durante l'impostazione dell'integrazione."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Token di accesso per il Vilfo Router API",
+ "host": "Nome host o IP del router"
+ },
+ "description": "Configurare l'integrazione del Vilfo Router. \u00c8 necessario il vostro hostname/IP del Vilfo Router e un token di accesso API. Per ulteriori informazioni su questa integrazione e su come ottenere tali dettagli, visitare il sito: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Collegamento al Vilfo Router"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/ko.json b/homeassistant/components/vilfo/.translations/ko.json
new file mode 100644
index 00000000000..85cb147ff6c
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/ko.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc774 Vilfo \ub77c\uc6b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc785\ub825\ud558\uc2e0 \ub0b4\uc6a9\uc744 \ud655\uc778\ud558\uc2e0 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ud655\uc778\ud558\uc2e0 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "unknown": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\ub294 \uc911 \uc608\uae30\uce58 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Vilfo \ub77c\uc6b0\ud130 API \uc6a9 \uc561\uc138\uc2a4 \ud1a0\ud070",
+ "host": "\ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c"
+ },
+ "description": "Vilfo \ub77c\uc6b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. Vilfo \ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 / IP \uc640 API \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ucd94\uac00 \uc815\ubcf4\uc640 \uc138\ubd80 \uc0ac\ud56d\uc740 https://www.home-assistant.io/integrations/vilfo \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694",
+ "title": "Vilfo \ub77c\uc6b0\ud130\uc5d0 \uc5f0\uacb0\ud558\uae30"
+ }
+ },
+ "title": "Vilfo \ub77c\uc6b0\ud130"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/lb.json b/homeassistant/components/vilfo/.translations/lb.json
new file mode 100644
index 00000000000..7b88bd31d17
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/lb.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "D\u00ebse Vilfo Router ass scho konfigur\u00e9iert."
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen. Iwwerpr\u00e9ift \u00e4r Informatiounen an prob\u00e9iert nach emol.",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun. Iwwerpr\u00e9ift den Acc\u00e8s jeton an prob\u00e9iert nach emol.",
+ "unknown": "Onerwaarte Feeler beim ariichten vun der Integratioun."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Acc\u00e8s Jeton fir Vilfo Router API",
+ "host": "Router Numm oder IP"
+ },
+ "description": "Vilfo Router Integratioun ariichten. Dir braucht \u00e4re Vilfo Router Numm/IP an een API Acc\u00e8s Jeton. Fir weider Informatiounen zu d\u00ebser Integratioun a w\u00e9i een zu d\u00ebsen n\u00e9idegen Informatioune k\u00ebnnt, gitt op: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Mam Vilfo Router verbannen"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/nl.json b/homeassistant/components/vilfo/.translations/nl.json
new file mode 100644
index 00000000000..db2691d3eeb
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/nl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Deze Vilfo Router is al geconfigureerd."
+ },
+ "error": {
+ "cannot_connect": "Kon niet verbinden. Controleer de door u verstrekte informatie en probeer het opnieuw.",
+ "unknown": "Er is een onverwachte fout opgetreden tijdens het instellen van de integratie."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Toegangstoken voor de Vilfo Router API",
+ "host": "Router hostnaam of IP-adres"
+ },
+ "title": "Maak verbinding met de Vilfo Router"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/no.json b/homeassistant/components/vilfo/.translations/no.json
new file mode 100644
index 00000000000..af72a4bd7b0
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/no.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Denne Vilfo Ruteren er allerede konfigurert."
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes. Vennligst sjekk informasjonen du oppga, og pr\u00f8v igjen.",
+ "invalid_auth": "Ugyldig godkjenning. Vennligst sjekk access token, og pr\u00f8v p\u00e5 nytt.",
+ "unknown": "Det oppstod en uventet feil under installasjonen av integrasjonen."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Tilgangstoken for Vilfo Router API",
+ "host": "Ruter vertsnavn eller IP"
+ },
+ "description": "Konfigurer Vilfo Router-integreringen. Du trenger ditt Vilfo Router vertsnavn/IP og et API-tilgangstoken. Hvis du vil ha mer informasjon om denne integreringen og hvordan du f\u00e5r disse detaljene, kan du g\u00e5 til: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Koble til Vilfo Ruteren"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/pl.json b/homeassistant/components/vilfo/.translations/pl.json
new file mode 100644
index 00000000000..aef0c14703f
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/pl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ten router Vilfo jest ju\u017c skonfigurowany."
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a wprowadzone dane i spr\u00f3buj ponownie.",
+ "invalid_auth": "Nieudane uwierzytelnienie. Sprawd\u017a token dost\u0119pu i spr\u00f3buj ponownie.",
+ "unknown": "Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas konfiguracji integracji."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Token dost\u0119pu do interfejsu API routera Vilfo",
+ "host": "Nazwa hosta lub adres IP routera"
+ },
+ "description": "Skonfiguruj integracj\u0119 routera Vilfo. Potrzebujesz nazwy hosta/adresu IP routera Vilfo i tokena dost\u0119pu do interfejsu API. Aby uzyska\u0107 dodatkowe informacje na temat tej integracji i sposobu uzyskania niezb\u0119dnych danych do konfiguracji, odwied\u017a: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Po\u0142\u0105cz si\u0119 z routerem Vilfo"
+ }
+ },
+ "title": "Router Vilfo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/ru.json b/homeassistant/components/vilfo/.translations/ru.json
new file mode 100644
index 00000000000..ce8f325e0ea
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/ru.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "error": {
+ "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 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a API \u0440\u043e\u0443\u0442\u0435\u0440\u0430",
+ "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Vilfo. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u0438 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 API. \u0414\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 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u0435 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442: https://www.home-assistant.io/integrations/vilfo.",
+ "title": "Vilfo Router"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/sl.json b/homeassistant/components/vilfo/.translations/sl.json
new file mode 100644
index 00000000000..a7d683e793c
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/sl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ta usmerjevalnik Vilfo je \u017ee konfiguriran."
+ },
+ "error": {
+ "cannot_connect": "Povezava ni uspela. Prosimo, preverite informacije, ki ste jih vnesli in poskusite znova.",
+ "invalid_auth": "Neveljavna avtentikacija. Preverite dostopni \u017eeton in poskusite znova.",
+ "unknown": "Med nastavitvijo integracije je pri\u0161lo do nepri\u010dakovane napake."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Dostopni \u017eeton za API Vilfo Router",
+ "host": "Ime gostitelja usmerjevalnika ali IP"
+ },
+ "description": "Nastavite integracijo Vilfo Router. Potrebujete ime gostitelja ali IP Vilfo usmerjevalnika in dostopni \u017eeton API. Za dodatne informacije o tej integraciji in kako do teh podrobnosti obi\u0161\u010dite: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Pove\u017eite se z usmerjevalnikom Vilfo"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/sv.json b/homeassistant/components/vilfo/.translations/sv.json
new file mode 100644
index 00000000000..69edce6b9d8
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/sv.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Den h\u00e4r Vilfo-routern \u00e4r redan konfigurerad."
+ },
+ "error": {
+ "cannot_connect": "Kunde inte ansluta. V\u00e4nligen kontrollera informationen du angav och f\u00f6rs\u00f6k igen.",
+ "invalid_auth": "Ogiltig autentisering. V\u00e4nligen kontrollera \u00e5tkomstnyckeln och f\u00f6rs\u00f6k igen.",
+ "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade n\u00e4r integrationen skulle konfigureras."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "\u00c5tkomstnyckel f\u00f6r Vilfo-routerns API",
+ "host": "Routerns v\u00e4rdnamn eller IP-adress"
+ },
+ "description": "St\u00e4ll in Vilfo Router-integrationen. Du beh\u00f6ver din Vilfo-routers v\u00e4rdnamn eller IP-adress och en \u00e5tkomstnyckel till dess API. F\u00f6r ytterligare information om den h\u00e4r integrationen och hur du f\u00e5r fram den n\u00f6dv\u00e4ndiga informationen, bes\u00f6k: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Anslut till Vilfo-routern"
+ }
+ },
+ "title": "Vilfo Router"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/zh-Hans.json b/homeassistant/components/vilfo/.translations/zh-Hans.json
new file mode 100644
index 00000000000..788f85b9382
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/zh-Hans.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u8fde\u63a5\u5931\u8d25\u3002\u8bf7\u68c0\u67e5\u8f93\u5165\u4fe1\u606f\u540e\uff0c\u518d\u8bd5\u4e00\u6b21\u3002",
+ "unknown": "\u8bbe\u7f6e\u6574\u5408\u65f6\u53d1\u751f\u610f\u5916\u9519\u8bef\u3002"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Vilfo \u8def\u7531\u5668 API \u5b58\u53d6\u5bc6\u94a5",
+ "host": "\u8def\u7531\u5668\u4e3b\u673a\u540d\u6216 IP \u5730\u5740"
+ },
+ "description": "\u8bbe\u7f6e Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u60a8\u9700\u8981\u8f93\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u673a\u540d/IP \u5730\u5740\u3001API\u5b58\u53d6\u5bc6\u94a5\u3002\u5176\u4ed6\u6574\u5408\u7684\u76f8\u5173\u4fe1\u606f\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/vilfo",
+ "title": "\u8fde\u63a5\u5230 Vilfo \u8def\u7531\u5668"
+ }
+ },
+ "title": "Vilfo \u8def\u7531\u5668"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/.translations/zh-Hant.json b/homeassistant/components/vilfo/.translations/zh-Hant.json
new file mode 100644
index 00000000000..7553cc683cd
--- /dev/null
+++ b/homeassistant/components/vilfo/.translations/zh-Hant.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Vilfo \u8def\u7531\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u8f38\u5165\u8cc7\u6599\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002",
+ "invalid_auth": "\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u5b58\u53d6\u5bc6\u9470\u5f8c\u518d\u8a66\u4e00\u6b21\u3002",
+ "unknown": "\u8a2d\u5b9a\u6574\u5408\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Vilfo \u8def\u7531\u5668 API \u5b58\u53d6\u5bc6\u9470",
+ "host": "\u8def\u7531\u5668\u4e3b\u6a5f\u7aef\u6216 IP \u4f4d\u5740"
+ },
+ "description": "\u8a2d\u5b9a Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u9700\u8981\u8f38\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u6a5f\u540d\u7a31/IP \u4f4d\u5740\u3001API \u5b58\u53d6\u5bc6\u9470\u3002\u5176\u4ed6\u6574\u5408\u76f8\u95dc\u8cc7\u8a0a\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/vilfo",
+ "title": "\u9023\u7dda\u81f3 Vilfo \u8def\u7531\u5668"
+ }
+ },
+ "title": "Vilfo \u8def\u7531\u5668"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py
new file mode 100644
index 00000000000..ffa628d6db2
--- /dev/null
+++ b/homeassistant/components/vilfo/__init__.py
@@ -0,0 +1,125 @@
+"""The Vilfo Router integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+from vilfo import Client as VilfoClient
+from vilfo.exceptions import VilfoException
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.util import Throttle
+
+from .const import ATTR_BOOT_TIME, ATTR_LOAD, DOMAIN, ROUTER_DEFAULT_HOST
+
+PLATFORMS = ["sensor"]
+
+DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass: HomeAssistantType, config: ConfigType):
+ """Set up the Vilfo Router component."""
+ hass.data.setdefault(DOMAIN, {})
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Vilfo Router from a config entry."""
+ host = entry.data[CONF_HOST]
+ access_token = entry.data[CONF_ACCESS_TOKEN]
+
+ vilfo_router = VilfoRouterData(hass, host, access_token)
+
+ await vilfo_router.async_update()
+
+ if not vilfo_router.available:
+ raise ConfigEntryNotReady
+
+ hass.data[DOMAIN][entry.entry_id] = vilfo_router
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+class VilfoRouterData:
+ """Define an object to hold sensor data."""
+
+ def __init__(self, hass, host, access_token):
+ """Initialize."""
+ self._vilfo = VilfoClient(host, access_token)
+ self.hass = hass
+ self.host = host
+ self.available = False
+ self.firmware_version = None
+ self.mac_address = self._vilfo.mac
+ self.data = {}
+ self._unavailable_logged = False
+
+ @property
+ def unique_id(self):
+ """Get the unique_id for the Vilfo Router."""
+ if self.mac_address:
+ return self.mac_address
+
+ if self.host == ROUTER_DEFAULT_HOST:
+ return self.host
+
+ return self.host
+
+ def _fetch_data(self):
+ board_information = self._vilfo.get_board_information()
+ load = self._vilfo.get_load()
+
+ return {
+ "board_information": board_information,
+ "load": load,
+ }
+
+ @Throttle(DEFAULT_SCAN_INTERVAL)
+ async def async_update(self):
+ """Update data using calls to VilfoClient library."""
+ try:
+ data = await self.hass.async_add_executor_job(self._fetch_data)
+
+ self.firmware_version = data["board_information"]["version"]
+ self.data[ATTR_BOOT_TIME] = data["board_information"]["bootTime"]
+ self.data[ATTR_LOAD] = data["load"]
+
+ self.available = True
+ except VilfoException as error:
+ if not self._unavailable_logged:
+ _LOGGER.error(
+ "Could not fetch data from %s, error: %s", self.host, error
+ )
+ self._unavailable_logged = True
+ self.available = False
+ return
+
+ if self.available and self._unavailable_logged:
+ _LOGGER.info("Vilfo Router %s is available again", self.host)
+ self._unavailable_logged = False
diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py
new file mode 100644
index 00000000000..2b9df3d9195
--- /dev/null
+++ b/homeassistant/components/vilfo/config_flow.py
@@ -0,0 +1,147 @@
+"""Config flow for Vilfo Router integration."""
+import ipaddress
+import logging
+import re
+
+from vilfo import Client as VilfoClient
+from vilfo.exceptions import (
+ AuthenticationException as VilfoAuthenticationException,
+ VilfoException,
+)
+import voluptuous as vol
+
+from homeassistant import config_entries, core, exceptions
+from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC
+
+from .const import DOMAIN # pylint:disable=unused-import
+from .const import ROUTER_DEFAULT_HOST
+
+_LOGGER = logging.getLogger(__name__)
+
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_HOST, default=ROUTER_DEFAULT_HOST): str,
+ vol.Required(CONF_ACCESS_TOKEN, default=""): str,
+ }
+)
+
+RESULT_SUCCESS = "success"
+RESULT_CANNOT_CONNECT = "cannot_connect"
+RESULT_INVALID_AUTH = "invalid_auth"
+
+
+def host_valid(host):
+ """Return True if hostname or IP address is valid."""
+ try:
+ if ipaddress.ip_address(host).version == (4 or 6):
+ return True
+ except ValueError:
+ disallowed = re.compile(r"[^a-zA-Z\d\-]")
+ return all(x and not disallowed.search(x) for x in host.split("."))
+
+
+def _try_connect_and_fetch_basic_info(host, token):
+ """Attempt to connect and call the ping endpoint and, if successful, fetch basic information."""
+
+ # Perform the ping. This doesn't validate authentication.
+ controller = VilfoClient(host=host, token=token)
+ result = {"type": None, "data": {}}
+
+ try:
+ controller.ping()
+ except VilfoException:
+ result["type"] = RESULT_CANNOT_CONNECT
+ result["data"] = CannotConnect
+ return result
+
+ # Perform a call that requires authentication.
+ try:
+ controller.get_board_information()
+ except VilfoAuthenticationException:
+ result["type"] = RESULT_INVALID_AUTH
+ result["data"] = InvalidAuth
+ return result
+
+ if controller.mac:
+ result["data"][CONF_ID] = controller.mac
+ result["data"][CONF_MAC] = controller.mac
+ else:
+ result["data"][CONF_ID] = host
+ result["data"][CONF_MAC] = None
+
+ result["type"] = RESULT_SUCCESS
+
+ return result
+
+
+async def validate_input(hass: core.HomeAssistant, data):
+ """Validate the user input allows us to connect.
+
+ Data has the keys from DATA_SCHEMA with values provided by the user.
+ """
+
+ # Validate the host before doing anything else.
+ if not host_valid(data[CONF_HOST]):
+ raise InvalidHost
+
+ config = {}
+
+ result = await hass.async_add_executor_job(
+ _try_connect_and_fetch_basic_info, data[CONF_HOST], data[CONF_ACCESS_TOKEN]
+ )
+
+ if result["type"] != RESULT_SUCCESS:
+ raise result["data"]
+
+ # Return some info we want to store in the config entry.
+ result_data = result["data"]
+ config["title"] = f"{data[CONF_HOST]}"
+ config[CONF_MAC] = result_data[CONF_MAC]
+ config[CONF_HOST] = data[CONF_HOST]
+ config[CONF_ID] = result_data[CONF_ID]
+
+ return config
+
+
+class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Vilfo Router."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ if user_input is not None:
+ try:
+ info = await validate_input(self.hass, user_input)
+ except InvalidHost:
+ errors[CONF_HOST] = "wrong_host"
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+ except Exception as err: # pylint: disable=broad-except
+ _LOGGER.error("Unexpected exception: %s", err)
+ errors["base"] = "unknown"
+ else:
+ await self.async_set_unique_id(info[CONF_ID])
+ self._abort_if_unique_id_configured()
+
+ return self.async_create_entry(title=info["title"], data=user_input)
+
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+ """Error to indicate we cannot connect."""
+
+
+class InvalidAuth(exceptions.HomeAssistantError):
+ """Error to indicate there is invalid auth."""
+
+
+class InvalidHost(exceptions.HomeAssistantError):
+ """Error to indicate that hostname/IP address is invalid."""
diff --git a/homeassistant/components/vilfo/const.py b/homeassistant/components/vilfo/const.py
new file mode 100644
index 00000000000..1a40b8430d7
--- /dev/null
+++ b/homeassistant/components/vilfo/const.py
@@ -0,0 +1,36 @@
+"""Constants for the Vilfo Router integration."""
+from homeassistant.const import DEVICE_CLASS_TIMESTAMP
+
+DOMAIN = "vilfo"
+
+ATTR_API_DATA_FIELD = "api_data_field"
+ATTR_API_DATA_FIELD_LOAD = "load"
+ATTR_API_DATA_FIELD_BOOT_TIME = "boot_time"
+ATTR_DEVICE_CLASS = "device_class"
+ATTR_ICON = "icon"
+ATTR_LABEL = "label"
+ATTR_LOAD = "load"
+ATTR_UNIT = "unit"
+ATTR_BOOT_TIME = "boot_time"
+
+ROUTER_DEFAULT_HOST = "admin.vilfo.com"
+ROUTER_DEFAULT_MODEL = "Vilfo Router"
+ROUTER_DEFAULT_NAME = "Vilfo Router"
+ROUTER_MANUFACTURER = "Vilfo AB"
+
+UNIT_PERCENT = "%"
+
+SENSOR_TYPES = {
+ ATTR_LOAD: {
+ ATTR_LABEL: "Load",
+ ATTR_UNIT: UNIT_PERCENT,
+ ATTR_ICON: "mdi:memory",
+ ATTR_API_DATA_FIELD: ATTR_API_DATA_FIELD_LOAD,
+ },
+ ATTR_BOOT_TIME: {
+ ATTR_LABEL: "Boot time",
+ ATTR_ICON: "mdi:timer",
+ ATTR_API_DATA_FIELD: ATTR_API_DATA_FIELD_BOOT_TIME,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
+ },
+}
diff --git a/homeassistant/components/vilfo/manifest.json b/homeassistant/components/vilfo/manifest.json
new file mode 100644
index 00000000000..cedb485fab3
--- /dev/null
+++ b/homeassistant/components/vilfo/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "vilfo",
+ "name": "Vilfo Router",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/vilfo",
+ "requirements": ["vilfo-api-client==0.3.2"],
+ "dependencies": [],
+ "codeowners": ["@ManneW"]
+}
diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py
new file mode 100644
index 00000000000..e2909647c2d
--- /dev/null
+++ b/homeassistant/components/vilfo/sensor.py
@@ -0,0 +1,94 @@
+"""Support for Vilfo Router sensors."""
+from homeassistant.helpers.entity import Entity
+
+from .const import (
+ ATTR_API_DATA_FIELD,
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ ATTR_LABEL,
+ ATTR_UNIT,
+ DOMAIN,
+ ROUTER_DEFAULT_MODEL,
+ ROUTER_DEFAULT_NAME,
+ ROUTER_MANUFACTURER,
+ SENSOR_TYPES,
+)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Add Vilfo Router entities from a config_entry."""
+ vilfo = hass.data[DOMAIN][config_entry.entry_id]
+
+ sensors = []
+
+ for sensor_type in SENSOR_TYPES:
+ sensors.append(VilfoRouterSensor(sensor_type, vilfo))
+
+ async_add_entities(sensors, True)
+
+
+class VilfoRouterSensor(Entity):
+ """Define a Vilfo Router Sensor."""
+
+ def __init__(self, sensor_type, api):
+ """Initialize."""
+ self.api = api
+ self.sensor_type = sensor_type
+ self._device_info = {
+ "identifiers": {(DOMAIN, api.host, api.mac_address)},
+ "name": ROUTER_DEFAULT_NAME,
+ "manufacturer": ROUTER_MANUFACTURER,
+ "model": ROUTER_DEFAULT_MODEL,
+ "sw_version": api.firmware_version,
+ }
+ self._unique_id = f"{self.api.unique_id}_{self.sensor_type}"
+ self._state = None
+
+ @property
+ def available(self):
+ """Return whether the sensor is available or not."""
+ return self.api.available
+
+ @property
+ def device_info(self):
+ """Return the device info."""
+ return self._device_info
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return SENSOR_TYPES[self.sensor_type].get(ATTR_DEVICE_CLASS)
+
+ @property
+ def icon(self):
+ """Return the icon for the sensor."""
+ return SENSOR_TYPES[self.sensor_type][ATTR_ICON]
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ parent_device_name = self._device_info["name"]
+ sensor_name = SENSOR_TYPES[self.sensor_type][ATTR_LABEL]
+ return f"{parent_device_name} {sensor_name}"
+
+ @property
+ def state(self):
+ """Return the state."""
+ return self._state
+
+ @property
+ def unique_id(self):
+ """Return a unique_id for this entity."""
+ return self._unique_id
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity."""
+ return SENSOR_TYPES[self.sensor_type].get(ATTR_UNIT)
+
+ async def async_update(self):
+ """Update the router data."""
+ await self.api.async_update()
+ self._state = self.api.data.get(
+ SENSOR_TYPES[self.sensor_type][ATTR_API_DATA_FIELD]
+ )
diff --git a/homeassistant/components/vilfo/strings.json b/homeassistant/components/vilfo/strings.json
new file mode 100644
index 00000000000..e7a55c55f1f
--- /dev/null
+++ b/homeassistant/components/vilfo/strings.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "title": "Vilfo Router",
+ "step": {
+ "user": {
+ "title": "Connect to the Vilfo Router",
+ "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo",
+ "data": {
+ "host": "Router hostname or IP",
+ "access_token": "Access token for the Vilfo Router API"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "Failed to connect. Please check the information you provided and try again.",
+ "invalid_auth": "Invalid authentication. Please check the access token and try again.",
+ "unknown": "An unexpected error occurred while setting up the integration."
+ },
+ "abort": {
+ "already_configured": "This Vilfo Router is already configured."
+ }
+ }
+}
diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py
index f4a195f5b0c..6bf9fdace5a 100644
--- a/homeassistant/components/vivotek/camera.py
+++ b/homeassistant/components/vivotek/camera.py
@@ -25,8 +25,8 @@ CONF_FRAMERATE = "framerate"
CONF_SECURITY_LEVEL = "security_level"
CONF_STREAM_PATH = "stream_path"
-DEFAULT_CAMERA_BRAND = "Vivotek"
-DEFAULT_NAME = "Vivotek Camera"
+DEFAULT_CAMERA_BRAND = "VIVOTEK"
+DEFAULT_NAME = "VIVOTEK Camera"
DEFAULT_EVENT_0_KEY = "event_i0_enable"
DEFAULT_SECURITY_LEVEL = "admin"
DEFAULT_STREAM_SOURCE = "live.sdp"
diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json
index 9246bc4c89b..3b4a4211f34 100644
--- a/homeassistant/components/vivotek/manifest.json
+++ b/homeassistant/components/vivotek/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "vivotek",
- "name": "Vivotek",
+ "name": "VIVOTEK",
"documentation": "https://www.home-assistant.io/integrations/vivotek",
"requirements": ["libpyvivotek==0.4.0"],
"dependencies": [],
diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json
index 6901ffc1736..834138e9221 100644
--- a/homeassistant/components/vizio/.translations/ca.json
+++ b/homeassistant/components/vizio/.translations/ca.json
@@ -1,18 +1,19 @@
{
"config": {
"abort": {
- "already_in_progress": "El flux de configuraci\u00f3 pel component Vizio ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de dades de configuraci\u00f3 pel component Vizio ja est\u00e0 en curs.",
"already_setup": "Aquesta entrada ja ha estat configurada.",
"already_setup_with_diff_host_and_name": "Sembla que aquesta entrada ja s'ha configurat amb un amfitri\u00f3 i nom diferents a partir del n\u00famero de s\u00e8rie. Elimina les entrades antigues de configuraction.yaml i del men\u00fa d'integracions abans de provar d'afegir el dispositiu novament.",
"host_exists": "Ja existeix un component Vizio configurat amb el host.",
"name_exists": "Ja existeix un component Vizio configurat amb el nom.",
+ "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom i les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat.",
"updated_options": "Aquesta entrada ja s'ha configurat per\u00f2 les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat.",
"updated_volume_step": "Aquesta entrada ja s'ha configurat per\u00f2 la mida de l'increment de volum definit a la configuraci\u00f3 no coincideix, en conseq\u00fc\u00e8ncia, s'ha actualitzat."
},
"error": {
"cant_connect": "No s'ha pogut connectar amb el dispositiu. [Comprova la documentaci\u00f3](https://www.home-assistant.io/integrations/vizio/) i torna a verificar que: \n - El dispositiu est\u00e0 engegat \n - El dispositiu est\u00e0 connectat a la xarxa \n - Els valors que has intridu\u00eft s\u00f3n correctes\n abans d\u2019intentar tornar a presentar.",
- "host_exists": "L'amfitri\u00f3 ja est\u00e0 configurat.",
- "name_exists": "El nom ja est\u00e0 configurat.",
+ "host_exists": "Dispositiu Vizio amb aquest nom d'amfitri\u00f3 ja configurat.",
+ "name_exists": "Dispositiu Vizio amb aquest nom ja configurat.",
"tv_needs_token": "Si el tipus de dispositiu \u00e9s 'tv', cal un testimoni d'acc\u00e9s v\u00e0lid (token)."
},
"step": {
diff --git a/homeassistant/components/vizio/.translations/da.json b/homeassistant/components/vizio/.translations/da.json
index 9ec9c4122ee..9bfd5864025 100644
--- a/homeassistant/components/vizio/.translations/da.json
+++ b/homeassistant/components/vizio/.translations/da.json
@@ -24,7 +24,7 @@
"host": ":",
"name": "Navn"
},
- "title": "Ops\u00e6tning af Vizio SmartCast-klient"
+ "title": "Ops\u00e6t Vizio SmartCast-enhed"
}
},
"title": "Vizio SmartCast"
diff --git a/homeassistant/components/vizio/.translations/en.json b/homeassistant/components/vizio/.translations/en.json
index 80d8f500615..cee436c9647 100644
--- a/homeassistant/components/vizio/.translations/en.json
+++ b/homeassistant/components/vizio/.translations/en.json
@@ -6,7 +6,7 @@
"already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.",
"host_exists": "Vizio component with host already configured.",
"name_exists": "Vizio component with name already configured.",
- "updated_entry": "This entry has already been setup but the name and/or options defined in the config do not match the previously imported config so the config entry has been updated accordingly.",
+ "updated_entry": "This entry has already been setup but the name and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly.",
"updated_options": "This entry has already been setup but the options defined in the config do not match the previously imported options values so the config entry has been updated accordingly.",
"updated_volume_step": "This entry has already been setup but the volume step size in the config does not match the config entry so the config entry has been updated accordingly."
},
@@ -24,7 +24,7 @@
"host": ":",
"name": "Name"
},
- "title": "Setup Vizio SmartCast Client"
+ "title": "Setup Vizio SmartCast Device"
}
},
"title": "Vizio SmartCast"
diff --git a/homeassistant/components/vizio/.translations/es.json b/homeassistant/components/vizio/.translations/es.json
index 009f93a50c6..408d94825f1 100644
--- a/homeassistant/components/vizio/.translations/es.json
+++ b/homeassistant/components/vizio/.translations/es.json
@@ -6,6 +6,7 @@
"already_setup_with_diff_host_and_name": "Esta entrada parece haber sido ya configurada con un host y un nombre diferentes basados en su n\u00famero de serie. Elimine las entradas antiguas de su archivo configuration.yaml y del men\u00fa Integraciones antes de volver a intentar agregar este dispositivo.",
"host_exists": "Host ya configurado del componente de Vizio",
"name_exists": "Nombre ya configurado del componente de Vizio",
+ "updated_entry": "Esta entrada ya ha sido configurada pero el nombre y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n previamente importada, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia.",
"updated_options": "Esta entrada ya ha sido configurada pero las opciones definidas en la configuraci\u00f3n no coinciden con los valores de las opciones importadas previamente, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia.",
"updated_volume_step": "Esta entrada ya ha sido configurada pero el tama\u00f1o del paso de volumen en la configuraci\u00f3n no coincide con la entrada de la configuraci\u00f3n, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia."
},
diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json
index 78d2347bfac..cf0cdea787f 100644
--- a/homeassistant/components/vizio/.translations/fr.json
+++ b/homeassistant/components/vizio/.translations/fr.json
@@ -6,6 +6,7 @@
"already_setup_with_diff_host_and_name": "Cette entr\u00e9e semble avoir d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e avec un h\u00f4te et un nom diff\u00e9rents en fonction de son num\u00e9ro de s\u00e9rie. Veuillez supprimer toutes les anciennes entr\u00e9es de votre configuration.yaml et du menu Int\u00e9grations avant de r\u00e9essayer d'ajouter ce p\u00e9riph\u00e9rique.",
"host_exists": "Composant Vizio avec h\u00f4te d\u00e9j\u00e0 configur\u00e9.",
"name_exists": "Composant Vizio dont le nom est d\u00e9j\u00e0 configur\u00e9.",
+ "updated_entry": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais le nom et/ou les options d\u00e9finis dans la configuration ne correspondent pas \u00e0 la configuration pr\u00e9c\u00e9demment import\u00e9e, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence.",
"updated_options": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais les options d\u00e9finies dans la configuration ne correspondent pas aux valeurs des options pr\u00e9c\u00e9demment import\u00e9es, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence.",
"updated_volume_step": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e, mais la taille du pas du volume dans la configuration ne correspond pas \u00e0 l'entr\u00e9e de configuration, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence."
},
@@ -32,7 +33,8 @@
"step": {
"init": {
"data": {
- "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)"
+ "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)",
+ "volume_step": "Taille du pas de volume"
},
"title": "Mettre \u00e0 jour les options de Vizo SmartCast"
}
diff --git a/homeassistant/components/vizio/.translations/hu.json b/homeassistant/components/vizio/.translations/hu.json
new file mode 100644
index 00000000000..650d5133dbd
--- /dev/null
+++ b/homeassistant/components/vizio/.translations/hu.json
@@ -0,0 +1,43 @@
+{
+ "config": {
+ "abort": {
+ "already_in_progress": "A vizio komponens konfigur\u00e1ci\u00f3s folyamata m\u00e1r folyamatban van.",
+ "already_setup": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva.",
+ "already_setup_with_diff_host_and_name": "\u00dagy t\u0171nik, hogy ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva egy m\u00e1sik \u00e1llom\u00e1ssal \u00e9s n\u00e9vvel a sorozatsz\u00e1ma alapj\u00e1n. T\u00e1vol\u00edtsa el a r\u00e9gi bejegyz\u00e9seket a configuration.yaml \u00e9s az Integr\u00e1ci\u00f3k men\u00fcb\u0151l, miel\u0151tt \u00fajra megpr\u00f3b\u00e1ln\u00e1 hozz\u00e1adni ezt az eszk\u00f6zt.",
+ "host_exists": "Vizio-\u00f6sszetev\u0151, amelynek az kiszolg\u00e1l\u00f3neve m\u00e1r konfigur\u00e1lva van.",
+ "name_exists": "Vizio-\u00f6sszetev\u0151, amelynek neve m\u00e1r konfigur\u00e1lva van.",
+ "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt.",
+ "updated_options": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban megadott be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt be\u00e1ll\u00edt\u00e1si \u00e9rt\u00e9kekkel, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt.",
+ "updated_volume_step": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151 henger\u0151l\u00e9p\u00e9s m\u00e9rete nem egyezik meg a konfigur\u00e1ci\u00f3s bejegyz\u00e9ssel, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt."
+ },
+ "error": {
+ "cant_connect": "Nem lehetett csatlakozni az eszk\u00f6zh\u00f6z. [Tekintsd \u00e1t a dokumentumokat] (https://www.home-assistant.io/integrations/vizio/) \u00e9s \u00fajra ellen\u0151rizd, hogy:\n- A k\u00e9sz\u00fcl\u00e9k be van kapcsolva\n- A k\u00e9sz\u00fcl\u00e9k csatlakozik a h\u00e1l\u00f3zathoz\n- A kit\u00f6lt\u00f6tt \u00e9rt\u00e9kek pontosak\nmiel\u0151tt \u00fajra elk\u00fclden\u00e9d.",
+ "host_exists": "A megadott kiszolg\u00e1l\u00f3n\u00e9vvel rendelkez\u0151 Vizio-eszk\u00f6z m\u00e1r konfigur\u00e1lva van.",
+ "name_exists": "A megadott n\u00e9vvel rendelkez\u0151 Vizio-eszk\u00f6z m\u00e1r konfigur\u00e1lva van.",
+ "tv_needs_token": "Ha az eszk\u00f6z t\u00edpusa \"tv\", akkor \u00e9rv\u00e9nyes hozz\u00e1f\u00e9r\u00e9si tokenre van sz\u00fcks\u00e9g."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token",
+ "device_class": "Eszk\u00f6zt\u00edpus",
+ "name": "N\u00e9v"
+ },
+ "title": "A Vizio SmartCast Client be\u00e1ll\u00edt\u00e1sa"
+ }
+ },
+ "title": "Vizio SmartCast"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "API-k\u00e9r\u00e9s id\u0151t\u00fall\u00e9p\u00e9se (m\u00e1sodpercben)",
+ "volume_step": "Hanger\u0151 l\u00e9p\u00e9s nagys\u00e1ga"
+ },
+ "title": "Friss\u00edtse a Vizo SmartCast be\u00e1ll\u00edt\u00e1sokat"
+ }
+ },
+ "title": "Friss\u00edtse a Vizo SmartCast be\u00e1ll\u00edt\u00e1sokat"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json
index f3cb7b83026..dd27133453e 100644
--- a/homeassistant/components/vizio/.translations/it.json
+++ b/homeassistant/components/vizio/.translations/it.json
@@ -6,6 +6,7 @@
"already_setup_with_diff_host_and_name": "Sembra che questa voce sia gi\u00e0 stata configurata con un host e un nome diversi in base al suo numero seriale. Rimuovere eventuali voci precedenti da configuration.yaml e dal menu Integrazioni prima di tentare nuovamente di aggiungere questo dispositivo.",
"host_exists": "Componente Vizio con host gi\u00e0 configurato.",
"name_exists": "Componente Vizio con nome gi\u00e0 configurato.",
+ "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza.",
"updated_options": "Questa voce \u00e8 gi\u00e0 stata impostata, ma le opzioni definite nella configurazione non corrispondono ai valori delle opzioni importate in precedenza, quindi la voce di configurazione \u00e8 stata aggiornata di conseguenza.",
"updated_volume_step": "Questa voce \u00e8 gi\u00e0 stata impostata, ma la dimensione del passo del volume nella configurazione non corrisponde alla voce di configurazione, quindi \u00e8 stata aggiornata di conseguenza."
},
diff --git a/homeassistant/components/vizio/.translations/ko.json b/homeassistant/components/vizio/.translations/ko.json
index 4c0460ec0e1..64c0887b3f8 100644
--- a/homeassistant/components/vizio/.translations/ko.json
+++ b/homeassistant/components/vizio/.translations/ko.json
@@ -24,7 +24,7 @@
"host": "<\ud638\uc2a4\ud2b8/ip>:",
"name": "\uc774\ub984"
},
- "title": "Vizio SmartCast \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815"
+ "title": "Vizio SmartCast \uae30\uae30 \uc124\uc815"
}
},
"title": "Vizio SmartCast"
diff --git a/homeassistant/components/vizio/.translations/nl.json b/homeassistant/components/vizio/.translations/nl.json
new file mode 100644
index 00000000000..bbc95d73bbc
--- /dev/null
+++ b/homeassistant/components/vizio/.translations/nl.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_in_progress": "Configuratie stroom voor vizio component al in uitvoering.",
+ "already_setup": "Dit item is al ingesteld.",
+ "already_setup_with_diff_host_and_name": "Dit item lijkt al te zijn ingesteld met een andere host en naam op basis van het serienummer. Verwijder alle oude vermeldingen uit uw configuratie.yaml en uit het menu Integraties voordat u opnieuw probeert dit apparaat toe te voegen.",
+ "host_exists": "Vizio apparaat met opgegeven host al geconfigureerd.",
+ "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd.",
+ "updated_entry": "Dit item is al ingesteld, maar de naam en/of opties die zijn gedefinieerd in de configuratie komen niet overeen met de eerder ge\u00efmporteerde configuratie, dus het configuratie-item is dienovereenkomstig bijgewerkt.",
+ "updated_options": "Dit item is al ingesteld, maar de opties die in de configuratie zijn gedefinieerd komen niet overeen met de eerder ge\u00efmporteerde optiewaarden, dus de configuratie-invoer is dienovereenkomstig bijgewerkt.",
+ "updated_volume_step": "Dit item is al ingesteld, maar de volumestapgrootte in de configuratie komt niet overeen met het configuratie-item, dus het configuratie-item is dienovereenkomstig bijgewerkt."
+ },
+ "error": {
+ "cant_connect": "Kan geen verbinding maken met het apparaat. [Bekijk de documenten] (https://www.home-assistant.io/integrations/vizio/) en controleer of:\n- Het apparaat is ingeschakeld\n- Het apparaat is aangesloten op het netwerk\n- De waarden die u ingevuld correct zijn\nvoordat u weer probeert om opnieuw in te dienen.",
+ "host_exists": "Vizio apparaat met opgegeven host al geconfigureerd.",
+ "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd.",
+ "tv_needs_token": "Wanneer het apparaattype `tv` is, dan is er een geldig toegangstoken nodig."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Toegangstoken",
+ "device_class": "Apparaattype",
+ "host": ":",
+ "name": "Naam"
+ },
+ "title": "Vizio SmartCast Client instellen"
+ }
+ },
+ "title": "Vizio SmartCast"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Time-out van API-aanvragen (seconden)",
+ "volume_step": "Volume Stapgrootte"
+ },
+ "title": "Update Vizo SmartCast Opties"
+ }
+ },
+ "title": "Update Vizo SmartCast Opties"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vizio/.translations/no.json b/homeassistant/components/vizio/.translations/no.json
index be1cae7aaf1..0b92497a5e7 100644
--- a/homeassistant/components/vizio/.translations/no.json
+++ b/homeassistant/components/vizio/.translations/no.json
@@ -6,7 +6,7 @@
"already_setup_with_diff_host_and_name": "Denne oppf\u00f8ringen ser ut til \u00e5 allerede v\u00e6re konfigurert med en annen vert og navn basert p\u00e5 serienummeret. Fjern den gamle oppf\u00f8ringer fra konfigurasjonen.yaml og fra integrasjonsmenyen f\u00f8r du pr\u00f8ver ut \u00e5 legge til denne enheten p\u00e5 nytt.",
"host_exists": "Vizio komponent med vert allerede konfigurert.",
"name_exists": "Vizio-komponent med navn som allerede er konfigurert.",
- "updated_entry": "Denne oppf\u00f8ringen er allerede konfigurert, men navnet og / eller alternativene som er definert i konfigurasjonen, stemmer ikke overens med den tidligere importerte konfigurasjonen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.",
+ "updated_entry": "Denne oppf\u00f8ringen er allerede konfigurert, men navnet og / eller alternativene som er definert i konfigurasjonen samsvarer ikke med den tidligere importerte konfigurasjonen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.",
"updated_options": "Denne oppf\u00f8ringen er allerede konfigurert, men alternativene som er definert i konfigurasjonen samsvarer ikke med de tidligere importerte alternativverdiene, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.",
"updated_volume_step": "Denne oppf\u00f8ringen er allerede konfigurert, men volumstrinnst\u00f8rrelsen i konfigurasjonen samsvarer ikke med konfigurasjonsoppf\u00f8ringen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter."
},
@@ -24,7 +24,7 @@
"host": ":",
"name": "Navn"
},
- "title": "Oppsett Vizio SmartCast Client"
+ "title": "Sett opp Vizio SmartCast-enhet"
}
},
"title": "Vizio SmartCast"
diff --git a/homeassistant/components/vizio/.translations/sl.json b/homeassistant/components/vizio/.translations/sl.json
new file mode 100644
index 00000000000..55faaaf26a8
--- /dev/null
+++ b/homeassistant/components/vizio/.translations/sl.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_in_progress": "Konfiguracijski tok za komponento vizio je \u017ee v teku.",
+ "already_setup": "Ta vnos je \u017ee nastavljen.",
+ "already_setup_with_diff_host_and_name": "Zdi se, da je bil ta vnos \u017ee nastavljen z drugim gostiteljem in imenom glede na njegovo serijsko \u0161tevilko. Pred ponovnim poskusom dodajanja te naprave, odstranite vse stare vnose iz config.yaml in iz menija Integrations.",
+ "host_exists": "VIZIO komponenta z gostiteljem \u017ee nastavljen.",
+ "name_exists": "Vizio komponenta z imenom je \u017ee konfigurirana.",
+ "updated_entry": "Ta vnos je \u017ee nastavljen, vendar se ime in / ali mo\u017enosti, opredeljene v config, ne ujemajo s predhodno uvo\u017eenim configom, zato je bil vnos konfiguracije ustrezno posodobljen.",
+ "updated_options": "Ta vnos je \u017ee nastavljen, vendar se mo\u017enosti, definirane v config-u, ne ujemajo s predhodno uvo\u017eenimi vrednostmi, zato je bil vnos konfiguracije ustrezno posodobljen.",
+ "updated_volume_step": "Ta vnos je \u017ee nastavljen, vendar velikost koraka glasnosti v config-u ne ustreza vnosu konfiguracije, zato je bil vnos konfiguracije ustrezno posodobljen."
+ },
+ "error": {
+ "cant_connect": "Ni bilo mogo\u010de povezati z napravo. [Preglejte dokumente] (https://www.home-assistant.io/integrations/vizio/) in ponovno preverite, ali: \n \u2013 Naprava je vklopljena \n \u2013 Naprava je povezana z omre\u017ejem \n \u2013 Vrednosti, ki ste jih izpolnili, so to\u010dne \nnato poskusite ponovno.",
+ "host_exists": "Naprava Vizio z dolo\u010denim gostiteljem je \u017ee konfigurirana.",
+ "name_exists": "Naprava Vizio z navedenim imenom je \u017ee konfigurirana.",
+ "tv_needs_token": "Ko je vrsta naprave\u00bb TV \u00ab, je potreben veljaven \u017eeton za dostop."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "\u017deton za dostop",
+ "device_class": "Vrsta naprave",
+ "host": ":",
+ "name": "Ime"
+ },
+ "title": "Nastavite odjemalec Vizio SmartCast"
+ }
+ },
+ "title": "Vizio SmartCast"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "\u010casovna omejitev zahteve za API (sekunde)",
+ "volume_step": "Velikost koraka glasnosti"
+ },
+ "title": "Posodobite mo\u017enosti Vizo SmartCast"
+ }
+ },
+ "title": "Posodobite mo\u017enosti Vizo SmartCast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vizio/.translations/sv.json b/homeassistant/components/vizio/.translations/sv.json
index 2c127f602ce..072b441a071 100644
--- a/homeassistant/components/vizio/.translations/sv.json
+++ b/homeassistant/components/vizio/.translations/sv.json
@@ -1,31 +1,44 @@
{
"config": {
"abort": {
- "host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad."
+ "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r vizio-komponenten p\u00e5g\u00e5r\nredan.",
+ "already_setup": "Den h\u00e4r posten har redan st\u00e4llts in.",
+ "already_setup_with_diff_host_and_name": "Den h\u00e4r posten verkar redan ha st\u00e4llts in med en annan v\u00e4rd och ett annat namn baserat p\u00e5 dess serienummer. Ta bort alla gamla poster fr\u00e5n configuration.yaml och fr\u00e5n menyn Integrationer innan du f\u00f6rs\u00f6ker l\u00e4gga till den h\u00e4r enheten igen.",
+ "host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad.",
+ "name_exists": "Vizio-komponent med namn redan konfigurerad.",
+ "updated_entry": "Den h\u00e4r posten har redan konfigurerats, men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen och d\u00e4rf\u00f6r har konfigureringsposten uppdaterats i enlighet med detta.",
+ "updated_options": "Den h\u00e4r posten har redan st\u00e4llts in men de alternativ som definierats i konfigurationen matchar inte de tidigare importerade alternativv\u00e4rdena s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta.",
+ "updated_volume_step": "Den h\u00e4r posten har redan st\u00e4llts in men volymstegstorleken i konfigurationen matchar inte konfigurationsposten s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta."
},
"error": {
+ "cant_connect": "Det gick inte att ansluta till enheten. [Granska dokumentationen] (https://www.home-assistant.io/integrations/vizio/) och p\u00e5 nytt kontrollera att\n- Enheten \u00e4r p\u00e5slagen\n- Enheten \u00e4r ansluten till n\u00e4tverket\n- De v\u00e4rden du fyllt i \u00e4r korrekta\ninnan du f\u00f6rs\u00f6ker skicka in igen.",
"host_exists": "Vizio-enheten med angivet v\u00e4rdnamn \u00e4r redan konfigurerad.",
- "name_exists": "Vizio-enheten med angivet namn \u00e4r redan konfigurerad."
+ "name_exists": "Vizio-enheten med angivet namn \u00e4r redan konfigurerad.",
+ "tv_needs_token": "N\u00e4r Enhetstyp \u00e4r 'tv' beh\u00f6vs en giltig \u00e5tkomsttoken."
},
"step": {
"user": {
"data": {
"access_token": "\u00c5tkomstnyckel",
"device_class": "Enhetstyp",
+ "host": ":",
"name": "Namn"
},
"title": "St\u00e4ll in Vizio SmartCast-klient"
}
},
- "title": ""
+ "title": "Vizio SmartCast"
},
"options": {
"step": {
"init": {
"data": {
- "timeout": "Timeout f\u00f6r API-anrop (sekunder)"
- }
+ "timeout": "Timeout f\u00f6r API-anrop (sekunder)",
+ "volume_step": "Storlek p\u00e5 volymsteg"
+ },
+ "title": "Uppdatera Vizo SmartCast-alternativ"
}
- }
+ },
+ "title": "Uppdatera Vizo SmartCast-alternativ"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/vizio/.translations/zh-Hant.json b/homeassistant/components/vizio/.translations/zh-Hant.json
index cd859977551..24128bb1b9e 100644
--- a/homeassistant/components/vizio/.translations/zh-Hant.json
+++ b/homeassistant/components/vizio/.translations/zh-Hant.json
@@ -24,7 +24,7 @@
"host": "<\u4e3b\u6a5f\u7aef/IP>:",
"name": "\u540d\u7a31"
},
- "title": "\u8a2d\u5b9a Vizio SmartCast \u5ba2\u6236\u7aef"
+ "title": "\u8a2d\u5b9a Vizio SmartCast \u8a2d\u5099"
}
},
"title": "Vizio SmartCast"
diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py
index 04f70da4a8c..ea0e9ede237 100644
--- a/homeassistant/components/vizio/config_flow.py
+++ b/homeassistant/components/vizio/config_flow.py
@@ -177,7 +177,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if entry.data[CONF_NAME] != import_config[CONF_NAME]:
updated_name[CONF_NAME] = import_config[CONF_NAME]
- if entry.data[CONF_VOLUME_STEP] != import_config[CONF_VOLUME_STEP]:
+ if entry.data.get(CONF_VOLUME_STEP) != import_config[CONF_VOLUME_STEP]:
updated_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP]
if updated_options or updated_name:
@@ -205,6 +205,11 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> Dict[str, Any]:
"""Handle zeroconf discovery."""
+ # Set unique ID early to prevent device from getting rediscovered multiple times
+ await self.async_set_unique_id(
+ unique_id=discovery_info[CONF_HOST].split(":")[0], raise_on_progress=True
+ )
+
discovery_info[
CONF_HOST
] = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}"
diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py
index 92fb37c153e..e3ac66e05c3 100644
--- a/homeassistant/components/vizio/const.py
+++ b/homeassistant/components/vizio/const.py
@@ -1,6 +1,4 @@
"""Constants used by vizio component."""
-from datetime import timedelta
-
from pyvizio.const import (
DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER,
DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV,
@@ -72,6 +70,3 @@ VIZIO_SCHEMA = {
vol.Coerce(int), vol.Range(min=1, max=10)
),
}
-
-MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
-MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json
index ea1162540cf..bf88ed9f437 100644
--- a/homeassistant/components/vizio/manifest.json
+++ b/homeassistant/components/vizio/manifest.json
@@ -2,9 +2,10 @@
"domain": "vizio",
"name": "Vizio SmartCast TV",
"documentation": "https://www.home-assistant.io/integrations/vizio",
- "requirements": ["pyvizio==0.1.4"],
+ "requirements": ["pyvizio==0.1.21"],
"dependencies": [],
"codeowners": ["@raman325"],
"config_flow": true,
- "zeroconf": ["_viziocast._tcp.local."]
+ "zeroconf": ["_viziocast._tcp.local."],
+ "quality_scale": "platinum"
}
diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py
index b2f529bce10..7d76505a457 100644
--- a/homeassistant/components/vizio/media_player.py
+++ b/homeassistant/components/vizio/media_player.py
@@ -1,10 +1,10 @@
"""Vizio SmartCast Device support."""
+from datetime import timedelta
import logging
from typing import Callable, List
from pyvizio import VizioAsync
-from homeassistant import util
from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -31,15 +31,13 @@ from .const import (
DEVICE_ID,
DOMAIN,
ICON,
- MIN_TIME_BETWEEN_FORCED_SCANS,
- MIN_TIME_BETWEEN_SCANS,
SUPPORTED_COMMANDS,
VIZIO_DEVICE_CLASSES,
)
_LOGGER = logging.getLogger(__name__)
-
+SCAN_INTERVAL = timedelta(seconds=10)
PARALLEL_UPDATES = 0
@@ -55,13 +53,21 @@ async def async_setup_entry(
device_class = config_entry.data[CONF_DEVICE_CLASS]
# If config entry options not set up, set them up, otherwise assign values managed in options
+ volume_step = config_entry.options.get(
+ CONF_VOLUME_STEP, config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP),
+ )
+
+ params = {}
if not config_entry.options:
- volume_step = config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP)
- hass.config_entries.async_update_entry(
- config_entry, options={CONF_VOLUME_STEP: volume_step}
- )
- else:
- volume_step = config_entry.options[CONF_VOLUME_STEP]
+ params["options"] = {CONF_VOLUME_STEP: volume_step}
+
+ if not config_entry.data.get(CONF_VOLUME_STEP):
+ new_data = config_entry.data.copy()
+ new_data.update({CONF_VOLUME_STEP: volume_step})
+ params["data"] = new_data
+
+ if params:
+ hass.config_entries.async_update_entry(config_entry, **params)
device = VizioAsync(
DEVICE_ID,
@@ -74,18 +80,7 @@ async def async_setup_entry(
)
if not await device.can_connect():
- fail_auth_msg = ""
- if token:
- fail_auth_msg = f"and auth token '{token}' are correct."
- else:
- fail_auth_msg = "is correct."
- _LOGGER.warning(
- "Failed to connect to Vizio device, please check if host '%s' "
- "is valid and available. Also check if device class '%s' %s",
- host,
- device_class,
- fail_auth_msg,
- )
+ _LOGGER.warning("Failed to connect to %s", host)
raise PlatformNotReady
entity = VizioDevice(config_entry, device, name, volume_step, device_class)
@@ -120,17 +115,32 @@ class VizioDevice(MediaPlayerDevice):
self._max_volume = float(self._device.get_max_volume())
self._icon = ICON[device_class]
self._available = True
+ self._model = None
+ self._sw_version = None
- @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
async def async_update(self) -> None:
"""Retrieve latest state of the device."""
+ if not self._model:
+ self._model = await self._device.get_model()
+
+ if not self._sw_version:
+ self._sw_version = await self._device.get_version()
+
is_on = await self._device.get_power_state(log_api_exception=False)
if is_on is None:
- self._available = False
+ if self._available:
+ _LOGGER.warning(
+ "Lost connection to %s", self._config_entry.data[CONF_HOST]
+ )
+ self._available = False
return
- self._available = True
+ if not self._available:
+ _LOGGER.info(
+ "Restored connection to %s", self._config_entry.data[CONF_HOST]
+ )
+ self._available = True
if not is_on:
self._state = STATE_OFF
@@ -147,9 +157,9 @@ class VizioDevice(MediaPlayerDevice):
input_ = await self._device.get_current_input(log_api_exception=False)
if input_ is not None:
- self._current_input = input_.meta_name
+ self._current_input = input_
- inputs = await self._device.get_inputs(log_api_exception=False)
+ inputs = await self._device.get_inputs_list(log_api_exception=False)
if inputs is not None:
self._available_inputs = [input_.name for input_ in inputs]
@@ -157,7 +167,7 @@ class VizioDevice(MediaPlayerDevice):
async def _async_send_update_options_signal(
hass: HomeAssistantType, config_entry: ConfigEntry
) -> None:
- """Send update event when when Vizio config entry is updated."""
+ """Send update event when Vizio config entry is updated."""
# Move this method to component level if another entity ever gets added for a single config entry.
# See here: https://github.com/home-assistant/home-assistant/pull/30653#discussion_r366426121
async_dispatcher_send(hass, config_entry.entry_id, config_entry)
@@ -241,6 +251,8 @@ class VizioDevice(MediaPlayerDevice):
"identifiers": {(DOMAIN, self._config_entry.unique_id)},
"name": self.name,
"manufacturer": "VIZIO",
+ "model": self._model,
+ "sw_version": self._sw_version,
}
@property
@@ -273,10 +285,10 @@ class VizioDevice(MediaPlayerDevice):
async def async_select_source(self, source: str) -> None:
"""Select input source."""
- await self._device.input_switch(source)
+ await self._device.set_input(source)
async def async_volume_up(self) -> None:
- """Increasing volume of the device."""
+ """Increase volume of the device."""
await self._device.vol_up(num=self._volume_step)
if self._volume_level is not None:
@@ -285,7 +297,7 @@ class VizioDevice(MediaPlayerDevice):
)
async def async_volume_down(self) -> None:
- """Decreasing volume of the device."""
+ """Decrease volume of the device."""
await self._device.vol_down(num=self._volume_step)
if self._volume_level is not None:
diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json
index 64b2fb5f936..5a554b7e3db 100644
--- a/homeassistant/components/vizio/strings.json
+++ b/homeassistant/components/vizio/strings.json
@@ -21,7 +21,7 @@
"abort": {
"already_setup": "This entry has already been setup.",
"already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.",
- "updated_entry": "This entry has already been setup but the name and/or options defined in the config do not match the previously imported config so the config entry has been updated accordingly."
+ "updated_entry": "This entry has already been setup but the name and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly."
}
},
"options": {
diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py
index fec912f00d8..0bcdcf9d4c1 100644
--- a/homeassistant/components/vultr/sensor.py
+++ b/homeassistant/components/vultr/sensor.py
@@ -4,7 +4,7 @@ import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME
+from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, DATA_GIGABYTES
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -21,7 +21,7 @@ DEFAULT_NAME = "Vultr {} {}"
MONITORED_CONDITIONS = {
ATTR_CURRENT_BANDWIDTH_USED: [
"Current Bandwidth Used",
- "GB",
+ DATA_GIGABYTES,
"mdi:chart-histogram",
],
ATTR_PENDING_CHARGES: ["Pending Charges", "US$", "mdi:currency-usd"],
diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py
index ecff3105ae0..4de0a58a881 100644
--- a/homeassistant/components/water_heater/__init__.py
+++ b/homeassistant/components/water_heater/__init__.py
@@ -145,7 +145,7 @@ class WaterHeaterDevice(Entity):
@property
def capability_attributes(self):
- """Return capabilitiy attributes."""
+ """Return capability attributes."""
supported_features = self.supported_features or 0
data = {
diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py
index 99df9fd17ce..f4d9f97fe42 100644
--- a/homeassistant/components/webostv/media_player.py
+++ b/homeassistant/components/webostv/media_player.py
@@ -29,6 +29,7 @@ from homeassistant.const import (
CONF_HOST,
CONF_NAME,
ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
STATE_OFF,
STATE_ON,
)
@@ -137,6 +138,9 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice):
async def async_signal_handler(self, data):
"""Handle domain-specific signal by calling appropriate method."""
entity_ids = data[ATTR_ENTITY_ID]
+ if entity_ids == ENTITY_MATCH_NONE:
+ return
+
if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids:
params = {
key: value
diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py
index c270c0f0ccc..bd6013aac0a 100644
--- a/homeassistant/components/websocket_api/permissions.py
+++ b/homeassistant/components/websocket_api/permissions.py
@@ -7,6 +7,7 @@ from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED
from homeassistant.components.persistent_notification import (
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED,
)
+from homeassistant.components.shopping_list import EVENT as EVENT_SHOPPING_LIST_UPDATED
from homeassistant.const import (
EVENT_COMPONENT_LOADED,
EVENT_CORE_CONFIG_UPDATE,
@@ -22,16 +23,17 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
# These are events that do not contain any sensitive data
# Except for state_changed, which is handled accordingly.
SUBSCRIBE_WHITELIST = {
+ EVENT_AREA_REGISTRY_UPDATED,
EVENT_COMPONENT_LOADED,
EVENT_CORE_CONFIG_UPDATE,
+ EVENT_DEVICE_REGISTRY_UPDATED,
+ EVENT_ENTITY_REGISTRY_UPDATED,
+ EVENT_LOVELACE_UPDATED,
EVENT_PANELS_UPDATED,
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED,
EVENT_SERVICE_REGISTERED,
EVENT_SERVICE_REMOVED,
+ EVENT_SHOPPING_LIST_UPDATED,
EVENT_STATE_CHANGED,
EVENT_THEMES_UPDATED,
- EVENT_AREA_REGISTRY_UPDATED,
- EVENT_DEVICE_REGISTRY_UPDATED,
- EVENT_ENTITY_REGISTRY_UPDATED,
- EVENT_LOVELACE_UPDATED,
}
diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml
index 93d53159702..500f1fd3f2a 100644
--- a/homeassistant/components/wink/services.yaml
+++ b/homeassistant/components/wink/services.yaml
@@ -131,7 +131,7 @@ set_nimbus_dial_configuration:
description: The minimum value allowed to be set
example: 0
max_value:
- description: The maximum value allowd to be set
+ description: The maximum value allowed to be set
example: 500
min_position:
description: The minimum position the dial hand can rotate to generally [0-360]
@@ -141,10 +141,10 @@ set_nimbus_dial_configuration:
example: 360
set_nimbus_dial_state:
- description: Set the value and lables of an individual nimbus dial
+ description: Set the value and labels of an individual nimbus dial
fields:
entity_id:
- description: Name fo the entity to set.
+ description: Name of the entity to set.
example: 'wink.nimbus_dial_3'
value:
description: The value that should be set (Should be between min_value and max_value)
diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json
index 067fa97ebdc..ae8ab679593 100644
--- a/homeassistant/components/withings/.translations/de.json
+++ b/homeassistant/components/withings/.translations/de.json
@@ -6,7 +6,7 @@
"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."
+ "default": "Erfolgreiche Authentifizierung mit Withings."
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json
index bd0ec740421..0ad55e7eaa7 100644
--- a/homeassistant/components/withings/.translations/fr.json
+++ b/homeassistant/components/withings/.translations/fr.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "missing_configuration": "L'int\u00e9gration Withings n'est pas configur\u00e9e. Veuillez suivre la documentation.",
"no_flows": "Vous devez configurer Withings avant de pouvoir vous authentifier avec celui-ci. Veuillez lire la documentation."
},
"create_entry": {
diff --git a/homeassistant/components/withings/.translations/hu.json b/homeassistant/components/withings/.translations/hu.json
index 000e19c2067..503013e402f 100644
--- a/homeassistant/components/withings/.translations/hu.json
+++ b/homeassistant/components/withings/.translations/hu.json
@@ -2,12 +2,31 @@
"config": {
"abort": {
"authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.",
- "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t."
+ "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.",
+ "no_flows": "Konfigur\u00e1lnia kell a Withings-et, miel\u0151tt hiteles\u00edtheti mag\u00e1t vele. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t."
+ },
+ "create_entry": {
+ "default": "A Withings sikeresen hiteles\u00edtett."
},
"step": {
"pick_implementation": {
"title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
+ },
+ "profile": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "Melyik profilt v\u00e1lasztottad ki a Withings weboldalon? Fontos, hogy a profilok egyeznek, k\u00fcl\u00f6nben az adatok helytelen c\u00edmk\u00e9vel lesznek ell\u00e1tva.",
+ "title": "Felhaszn\u00e1l\u00f3i profil."
+ },
+ "user": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "V\u00e1lasszon egy felhaszn\u00e1l\u00f3i profilt, amelyet szeretn\u00e9, hogy a Home Assistant hozz\u00e1rendeljen a Withings profilhoz. \u00dcgyeljen arra, hogy ugyanazt a felhaszn\u00e1l\u00f3t v\u00e1lassza a Withings oldalon, k\u00fcl\u00f6nben az adatok nem lesznek megfelel\u0151en felcimk\u00e9zve.",
+ "title": "Felhaszn\u00e1l\u00f3i profil."
}
- }
+ },
+ "title": "Withings"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json
index c831561a439..0b01fc8c16a 100644
--- a/homeassistant/components/withings/.translations/nl.json
+++ b/homeassistant/components/withings/.translations/nl.json
@@ -1,12 +1,17 @@
{
"config": {
"abort": {
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen.",
"no_flows": "U moet Withings configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de documentatie te lezen]"
},
"create_entry": {
"default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel."
},
"step": {
+ "pick_implementation": {
+ "title": "Kies Authenticatiemethode"
+ },
"profile": {
"data": {
"profile": "Profiel"
diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json
index 2ee52b29b2d..600b2dbf450 100644
--- a/homeassistant/components/withings/.translations/sl.json
+++ b/homeassistant/components/withings/.translations/sl.json
@@ -1,12 +1,17 @@
{
"config": {
"abort": {
+ "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.",
+ "missing_configuration": "Integracija Withings ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo.",
"no_flows": "Withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo."
},
"create_entry": {
"default": "Uspe\u0161no overjen z Withings za izbrani profil."
},
"step": {
+ "pick_implementation": {
+ "title": "Izberite na\u010din preverjanja pristnosti"
+ },
"profile": {
"data": {
"profile": "Profil"
diff --git a/homeassistant/components/withings/.translations/sv.json b/homeassistant/components/withings/.translations/sv.json
index e2493e9afa7..dc8954af2c7 100644
--- a/homeassistant/components/withings/.translations/sv.json
+++ b/homeassistant/components/withings/.translations/sv.json
@@ -2,12 +2,31 @@
"config": {
"abort": {
"authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.",
- "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen."
+ "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.",
+ "no_flows": "Du m\u00e5ste konfigurera Withings innan du kan autentisera med den. L\u00e4s dokumentationen."
+ },
+ "create_entry": {
+ "default": "Lyckad autentisering med Withings."
},
"step": {
"pick_implementation": {
"title": "V\u00e4lj autentiseringsmetod"
+ },
+ "profile": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "Vilken profil valde du p\u00e5 Withings webbplats? Det \u00e4r viktigt att profilerna matchar, annars kommer data att vara felm\u00e4rkta.",
+ "title": "Anv\u00e4ndarprofil."
+ },
+ "user": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "V\u00e4lj en anv\u00e4ndarprofil som du vill att Home Assistant ska kartl\u00e4gga med en Withings-profil. Var noga med att v\u00e4lja samma anv\u00e4ndare p\u00e5 visningssidan eller s\u00e5 kommer inte data att betecknas korrekt.",
+ "title": "Anv\u00e4ndarprofil."
}
- }
+ },
+ "title": "Withings"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wled/.translations/hu.json b/homeassistant/components/wled/.translations/hu.json
new file mode 100644
index 00000000000..644b61ceb73
--- /dev/null
+++ b/homeassistant/components/wled/.translations/hu.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ez a WLED eszk\u00f6z m\u00e1r konfigur\u00e1lva van.",
+ "connection_error": "Nem siker\u00fclt csatlakozni a WLED eszk\u00f6zh\u00f6z."
+ },
+ "error": {
+ "connection_error": "Nem siker\u00fclt csatlakozni a WLED eszk\u00f6zh\u00f6z."
+ },
+ "flow_title": "WLED: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hosztn\u00e9v vagy IP c\u00edm"
+ },
+ "description": "\u00c1ll\u00edtsa be a WLED-et, hogy integr\u00e1l\u00f3djon a Home Assistant alkalmaz\u00e1sba.",
+ "title": "Csatlakoztassa a WLED-t"
+ },
+ "zeroconf_confirm": {
+ "description": "Hozz\u00e1 akarja adni a {name} `nev\u0171 WLED-et a Home Assistant-hez?",
+ "title": "Felfedezett WLED eszk\u00f6z"
+ }
+ },
+ "title": "WLED"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/.translations/pl.json b/homeassistant/components/wled/.translations/pl.json
index c10c8ab34d6..6080336c44f 100644
--- a/homeassistant/components/wled/.translations/pl.json
+++ b/homeassistant/components/wled/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "To urz\u0105dzenie WLED jest ju\u017c skonfigurowane",
+ "already_configured": "To urz\u0105dzenie WLED jest ju\u017c skonfigurowane.",
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem WLED."
},
"error": {
diff --git a/homeassistant/components/wled/.translations/sv.json b/homeassistant/components/wled/.translations/sv.json
new file mode 100644
index 00000000000..980c023118e
--- /dev/null
+++ b/homeassistant/components/wled/.translations/sv.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Den h\u00e4r WLED-enheten \u00e4r redan konfigurerad.",
+ "connection_error": "Det gick inte att ansluta till WLED-enheten."
+ },
+ "error": {
+ "connection_error": "Det gick inte att ansluta till WLED-enheten."
+ },
+ "flow_title": "WLED: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "V\u00e4rd eller IP-adress"
+ },
+ "description": "St\u00e4ll in din WLED f\u00f6r att integrera med Home Assistant.",
+ "title": "L\u00e4nka din WLED"
+ },
+ "zeroconf_confirm": {
+ "description": "Vill du l\u00e4gga till WLED med namnet `{name}` till Home Assistant?",
+ "title": "Uppt\u00e4ckt WLED-enhet"
+ }
+ },
+ "title": "WLED"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py
index 155cd022fd7..dbcd55a7b17 100644
--- a/homeassistant/components/wled/config_flow.py
+++ b/homeassistant/components/wled/config_flow.py
@@ -11,8 +11,8 @@ from homeassistant.config_entries import (
ConfigFlow,
)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
-from homeassistant.helpers import ConfigType
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN # pylint: disable=unused-import
diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py
index dcfdad963a7..94ee513f134 100644
--- a/homeassistant/components/wled/const.py
+++ b/homeassistant/components/wled/const.py
@@ -30,4 +30,3 @@ ATTR_UDP_PORT = "udp_port"
# Units of measurement
CURRENT_MA = "mA"
-DATA_BYTES = "bytes"
diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py
index c3fc2d4e6c2..41e03d8c728 100644
--- a/homeassistant/components/wled/sensor.py
+++ b/homeassistant/components/wled/sensor.py
@@ -4,20 +4,13 @@ import logging
from typing import Callable, List, Optional, Union
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import DEVICE_CLASS_TIMESTAMP
+from homeassistant.const import DATA_BYTES, DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.dt import utcnow
from . import WLED, WLEDDeviceEntity
-from .const import (
- ATTR_LED_COUNT,
- ATTR_MAX_POWER,
- CURRENT_MA,
- DATA_BYTES,
- DATA_WLED_CLIENT,
- DOMAIN,
-)
+from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DATA_WLED_CLIENT, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json
index e888b0c5614..21b84d87cbb 100644
--- a/homeassistant/components/workday/manifest.json
+++ b/homeassistant/components/workday/manifest.json
@@ -2,7 +2,7 @@
"domain": "workday",
"name": "Workday",
"documentation": "https://www.home-assistant.io/integrations/workday",
- "requirements": ["holidays==0.9.12"],
+ "requirements": ["holidays==0.10.1"],
"dependencies": [],
"codeowners": ["@fabaff"],
"quality_scale": "internal"
diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json
index 652d580644f..658dbebbe45 100644
--- a/homeassistant/components/wwlln/.translations/pl.json
+++ b/homeassistant/components/wwlln/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "identifier_exists": "Lokalizacja ju\u017c zarejestrowana"
+ "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana."
},
"step": {
"user": {
diff --git a/homeassistant/components/wwlln/geo_location.py b/homeassistant/components/wwlln/geo_location.py
index e8dd7ec08c7..e1ca47664d5 100644
--- a/homeassistant/components/wwlln/geo_location.py
+++ b/homeassistant/components/wwlln/geo_location.py
@@ -35,7 +35,7 @@ DEFAULT_EVENT_NAME = "Lightning Strike: {0}"
DEFAULT_ICON = "mdi:flash"
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10)
-SIGNAL_DELETE_ENTITY = "delete_entity_{0}"
+SIGNAL_DELETE_ENTITY = "wwlln_delete_entity_{0}"
async def async_setup_entry(hass, entry, async_add_entities):
diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json
index 4568f67dbf5..fade5e1a51b 100644
--- a/homeassistant/components/xiaomi_aqara/manifest.json
+++ b/homeassistant/components/xiaomi_aqara/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
"requirements": ["PyXiaomiGateway==0.12.4"],
"dependencies": [],
+ "after_dependencies": ["discovery"],
"codeowners": ["@danielhiversen", "@syssi"]
}
diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py
index 50de263fb15..110ca7cff49 100644
--- a/homeassistant/components/xiaomi_miio/air_quality.py
+++ b/homeassistant/components/xiaomi_miio/air_quality.py
@@ -21,6 +21,8 @@ DEFAULT_NAME = "Xiaomi Miio Air Quality Monitor"
ATTR_CO2E = "carbon_dioxide_equivalent"
ATTR_TVOC = "total_volatile_organic_compounds"
+ATTR_TEMP = "temperature"
+ATTR_HUM = "humidity"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -33,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
PROP_TO_ATTR = {
"carbon_dioxide_equivalent": ATTR_CO2E,
"total_volatile_organic_compounds": ATTR_TVOC,
+ "temperature": ATTR_TEMP,
+ "humidity": ATTR_HUM,
}
@@ -91,6 +95,8 @@ class AirMonitorB1(AirQualityEntity):
self._carbon_dioxide_equivalent = None
self._particulate_matter_2_5 = None
self._total_volatile_organic_compounds = None
+ self._temperature = None
+ self._humidity = None
async def async_update(self):
"""Fetch state from the miio device."""
@@ -100,6 +106,8 @@ class AirMonitorB1(AirQualityEntity):
self._carbon_dioxide_equivalent = state.co2e
self._particulate_matter_2_5 = round(state.pm25, 1)
self._total_volatile_organic_compounds = round(state.tvoc, 3)
+ self._temperature = round(state.temperature, 2)
+ self._humidity = round(state.humidity, 2)
self._available = True
except DeviceException as ex:
self._available = False
@@ -150,6 +158,16 @@ class AirMonitorB1(AirQualityEntity):
"""Return the total volatile organic compounds."""
return self._total_volatile_organic_compounds
+ @property
+ def temperature(self):
+ """Return the current temperature."""
+ return self._temperature
+
+ @property
+ def humidity(self):
+ """Return the current humidity."""
+ return self._humidity
+
@property
def device_state_attributes(self):
"""Return the state attributes."""
@@ -179,6 +197,8 @@ class AirMonitorS1(AirMonitorB1):
self._carbon_dioxide = state.co2
self._particulate_matter_2_5 = state.pm25
self._total_volatile_organic_compounds = state.tvoc
+ self._temperature = state.temperature
+ self._humidity = state.humidity
self._available = True
except DeviceException as ex:
self._available = False
diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py
index bcc83bae454..61462bcdbc0 100644
--- a/homeassistant/components/xiaomi_miio/light.py
+++ b/homeassistant/components/xiaomi_miio/light.py
@@ -19,7 +19,6 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
- ATTR_ENTITY_ID,
ATTR_HS_COLOR,
PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
@@ -27,7 +26,7 @@ from homeassistant.components.light import (
SUPPORT_COLOR_TEMP,
Light,
)
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
+from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.util import color, dt
diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml
index 592a1d1342e..c92522008be 100644
--- a/homeassistant/components/yamaha/services.yaml
+++ b/homeassistant/components/yamaha/services.yaml
@@ -2,7 +2,7 @@ enable_output:
description: Enable or disable an output port
fields:
entity_id:
- description: Name(s) of entites to enable/disable port on.
+ description: Name(s) of entities to enable/disable port on.
example: 'media_player.yamaha'
port:
description: Name of port to enable/disable.
diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json
index 35c2a8ddfac..1a181536d0b 100644
--- a/homeassistant/components/yeelight/manifest.json
+++ b/homeassistant/components/yeelight/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.5.0"],
"dependencies": [],
+ "after_dependencies": ["discovery"],
"codeowners": ["@rytilahti", "@zewelor"]
}
diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py
index b4dbbda51f1..206f529344f 100644
--- a/homeassistant/components/zeroconf/__init__.py
+++ b/homeassistant/components/zeroconf/__init__.py
@@ -108,7 +108,6 @@ def setup(hass, config):
def stop_zeroconf(_):
"""Stop Zeroconf."""
- zeroconf.unregister_service(info)
zeroconf.close()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf)
diff --git a/homeassistant/components/zha/.translations/ca.json b/homeassistant/components/zha/.translations/ca.json
index 2b8230ad689..e5181fb5106 100644
--- a/homeassistant/components/zha/.translations/ca.json
+++ b/homeassistant/components/zha/.translations/ca.json
@@ -54,14 +54,14 @@
"device_shaken": "Dispositiu sacsejat",
"device_slid": "Dispositiu lliscat a \"{subtype}\"",
"device_tilted": "Dispositiu inclinat",
- "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives",
- "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament",
+ "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades",
+ "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament",
"remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut",
- "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives",
- "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives",
+ "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades",
+ "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades",
"remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut",
"remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat",
- "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives"
+ "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json
index 4189ea6d9be..4698a0a37ef 100644
--- a/homeassistant/components/zha/.translations/pl.json
+++ b/homeassistant/components/zha/.translations/pl.json
@@ -54,14 +54,14 @@
"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}\" 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"
+ "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty",
+ "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
+ "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
+ "remote_button_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty",
+ "remote_button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty",
+ "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty",
+ "remote_button_short_release": "\"{subtype}\" zostanie zwolniony",
+ "remote_button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json
index 8850fdfc07a..38b0aa8359c 100644
--- a/homeassistant/components/zha/.translations/ru.json
+++ b/homeassistant/components/zha/.translations/ru.json
@@ -12,10 +12,10 @@
"radio_type": "\u0422\u0438\u043f \u0420\u0430\u0434\u0438\u043e",
"usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
},
- "title": "Zigbee Home Automation (ZHA)"
+ "title": "Zigbee Home Automation"
}
},
- "title": "Zigbee Home Automation"
+ "title": "Zigbee Home Automation (ZHA)"
},
"device_automation": {
"action_type": {
diff --git a/homeassistant/components/zha/.translations/sv.json b/homeassistant/components/zha/.translations/sv.json
index 2762adc0fba..473cf1cd2a9 100644
--- a/homeassistant/components/zha/.translations/sv.json
+++ b/homeassistant/components/zha/.translations/sv.json
@@ -18,15 +18,50 @@
"title": "ZHA"
},
"device_automation": {
+ "action_type": {
+ "squawk": "Kraxa",
+ "warn": "Varna"
+ },
"trigger_subtype": {
+ "both_buttons": "B\u00e5da knapparna",
+ "button_1": "F\u00f6rsta knappen",
+ "button_2": "Andra knappen",
+ "button_3": "Tredje knappen",
+ "button_4": "Fj\u00e4rde knappen",
+ "button_5": "Femte knappen",
+ "button_6": "Sj\u00e4tte knappen",
"close": "St\u00e4ng",
"dim_down": "Dimma ned",
"dim_up": "Dimma upp",
+ "face_1": "med bildsida 1 aktiverat",
+ "face_2": "med bildsida 2 aktiverat",
+ "face_3": "med bildsida 3 aktiverat",
+ "face_4": "med bildsida 4 aktiverat",
+ "face_5": "med bildsida 5 aktiverat",
+ "face_6": "med bildsida 6 aktiverat",
+ "face_any": "Med valfri/specificerad bildsida(or) aktiverat",
"left": "V\u00e4nster",
"open": "\u00d6ppen",
"right": "H\u00f6ger",
"turn_off": "St\u00e4ng av",
"turn_on": "Starta"
+ },
+ "trigger_type": {
+ "device_dropped": "Enheten tappades",
+ "device_flipped": "Enheten v\u00e4nd \"{subtype}\"",
+ "device_knocked": "Enheten knackad \"{subtype}\"",
+ "device_rotated": "Enheten roterade \"{subtype}\"",
+ "device_shaken": "Enheten skakad",
+ "device_slid": "Enheten gled \"{subtype}\"",
+ "device_tilted": "Enheten lutad",
+ "remote_button_double_press": "\"{subtype}\"-knappen dubbelklickades",
+ "remote_button_long_press": "\"{subtype}\"-knappen kontinuerligt nedtryckt",
+ "remote_button_long_release": "\"{subtype}\"-knappen sl\u00e4pptes efter ett l\u00e5ngttryck",
+ "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt",
+ "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt",
+ "remote_button_short_press": "\"{subtype}\"-knappen trycktes in",
+ "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt",
+ "remote_button_triple_press": "\"{subtype}\"-knappen trippelklickades"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py
index a5ecf21e0c3..d899f51b487 100644
--- a/homeassistant/components/zha/core/channels/__init__.py
+++ b/homeassistant/components/zha/core/channels/__init__.py
@@ -194,8 +194,7 @@ class ZigbeeChannel(LogMixin):
async def async_configure(self):
"""Set cluster binding and attribute reporting."""
- # Xiaomi devices don't need this and it disrupts pairing
- if self._zha_device.manufacturer != "LUMI":
+ if not self._zha_device.skip_configuration:
await self.bind()
if self.cluster.is_server:
for report_config in self._report_config:
@@ -203,8 +202,9 @@ class ZigbeeChannel(LogMixin):
report_config["attr"], report_config["config"]
)
await asyncio.sleep(uniform(0.1, 0.5))
-
- self.debug("finished channel configuration")
+ self.debug("finished channel configuration")
+ else:
+ self.debug("skipping channel configuration")
self._status = ChannelStatus.CONFIGURED
async def async_initialize(self, from_cache):
@@ -264,7 +264,7 @@ class ZigbeeChannel(LogMixin):
def log(self, level, msg, *args):
"""Log a message."""
msg = f"[%s:%s]: {msg}"
- args = (self.device.nwk, self._id,) + args
+ args = (self.device.nwk, self._id) + args
_LOGGER.log(level, msg, *args)
def __getattr__(self, name):
diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py
index 69e4ea1a27a..781738fc048 100644
--- a/homeassistant/components/zha/core/channels/security.py
+++ b/homeassistant/components/zha/core/channels/security.py
@@ -146,9 +146,8 @@ class IASZoneChannel(ZigbeeChannel):
async def async_configure(self):
"""Configure IAS device."""
- # Xiaomi devices don't need this and it disrupts pairing
- if self._zha_device.manufacturer == "LUMI":
- self.debug("finished IASZoneChannel configuration")
+ if self._zha_device.skip_configuration:
+ self.debug("skipping IASZoneChannel configuration")
return
self.debug("started IASZoneChannel configuration")
diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py
index b8782101cd4..f4cccfa4e52 100644
--- a/homeassistant/components/zha/core/const.py
+++ b/homeassistant/components/zha/core/const.py
@@ -96,6 +96,7 @@ DATA_ZHA_GATEWAY = "zha_gateway"
DEBUG_COMP_BELLOWS = "bellows"
DEBUG_COMP_ZHA = "homeassistant.components.zha"
DEBUG_COMP_ZIGPY = "zigpy"
+DEBUG_COMP_ZIGPY_CC = "zigpy_cc"
DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz"
DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee"
DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate"
@@ -105,8 +106,9 @@ DEBUG_LEVELS = {
DEBUG_COMP_BELLOWS: logging.DEBUG,
DEBUG_COMP_ZHA: logging.DEBUG,
DEBUG_COMP_ZIGPY: logging.DEBUG,
- DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG,
+ DEBUG_COMP_ZIGPY_CC: logging.DEBUG,
DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG,
+ DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG,
DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG,
}
DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY]
@@ -131,9 +133,10 @@ POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown"
class RadioType(enum.Enum):
"""Possible options for radio type."""
- ezsp = "ezsp"
- xbee = "xbee"
deconz = "deconz"
+ ezsp = "ezsp"
+ ti_cc = "ti_cc"
+ xbee = "xbee"
zigate = "zigate"
@classmethod
diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py
index 8810fd77fe7..2e7c48c639f 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -213,6 +213,11 @@ class ZHADevice(LogMixin):
if Groups.cluster_id in clusters:
return True
+ @property
+ def skip_configuration(self):
+ """Return true if the device should not issue configuration related commands."""
+ return self._zigpy_device.skip_configuration
+
@property
def gateway(self):
"""Return the gateway for this device."""
diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py
index e5a199c5bbd..33faaa334cb 100644
--- a/homeassistant/components/zha/core/gateway.py
+++ b/homeassistant/components/zha/core/gateway.py
@@ -41,6 +41,7 @@ from .const import (
DEBUG_COMP_BELLOWS,
DEBUG_COMP_ZHA,
DEBUG_COMP_ZIGPY,
+ DEBUG_COMP_ZIGPY_CC,
DEBUG_COMP_ZIGPY_DECONZ,
DEBUG_COMP_ZIGPY_XBEE,
DEBUG_COMP_ZIGPY_ZIGATE,
@@ -424,7 +425,7 @@ class ZHAGateway:
# ZHA already has an initialized device so either the device was assigned a
# new nwk or device was physically reset and added again without being removed
_LOGGER.debug(
- "device - %s has been reset and readded or its nwk address changed",
+ "device - %s has been reset and re-added or its nwk address changed",
f"0x{device.nwk:04x}:{device.ieee}",
)
await self._async_device_rejoined(zha_device)
@@ -555,12 +556,13 @@ def async_capture_log_levels():
DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(),
DEBUG_COMP_ZHA: logging.getLogger(DEBUG_COMP_ZHA).getEffectiveLevel(),
DEBUG_COMP_ZIGPY: logging.getLogger(DEBUG_COMP_ZIGPY).getEffectiveLevel(),
- DEBUG_COMP_ZIGPY_XBEE: logging.getLogger(
- DEBUG_COMP_ZIGPY_XBEE
- ).getEffectiveLevel(),
+ DEBUG_COMP_ZIGPY_CC: logging.getLogger(DEBUG_COMP_ZIGPY_CC).getEffectiveLevel(),
DEBUG_COMP_ZIGPY_DECONZ: logging.getLogger(
DEBUG_COMP_ZIGPY_DECONZ
).getEffectiveLevel(),
+ DEBUG_COMP_ZIGPY_XBEE: logging.getLogger(
+ DEBUG_COMP_ZIGPY_XBEE
+ ).getEffectiveLevel(),
DEBUG_COMP_ZIGPY_ZIGATE: logging.getLogger(
DEBUG_COMP_ZIGPY_ZIGATE
).getEffectiveLevel(),
@@ -573,8 +575,9 @@ def async_set_logger_levels(levels):
logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS])
logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA])
logging.getLogger(DEBUG_COMP_ZIGPY).setLevel(levels[DEBUG_COMP_ZIGPY])
- logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE])
+ logging.getLogger(DEBUG_COMP_ZIGPY_CC).setLevel(levels[DEBUG_COMP_ZIGPY_CC])
logging.getLogger(DEBUG_COMP_ZIGPY_DECONZ).setLevel(levels[DEBUG_COMP_ZIGPY_DECONZ])
+ logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE])
logging.getLogger(DEBUG_COMP_ZIGPY_ZIGATE).setLevel(levels[DEBUG_COMP_ZIGPY_ZIGATE])
diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py
index 311f8fa275f..4f5c6fc5c6b 100644
--- a/homeassistant/components/zha/core/registries.py
+++ b/homeassistant/components/zha/core/registries.py
@@ -13,6 +13,8 @@ import bellows.zigbee.application
import zigpy.profiles.zha
import zigpy.profiles.zll
import zigpy.zcl as zcl
+import zigpy_cc.api
+import zigpy_cc.zigbee.application
import zigpy_deconz.api
import zigpy_deconz.zigbee.application
import zigpy_xbee.api
@@ -127,15 +129,20 @@ LIGHT_CLUSTERS = SetRegistry()
OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry()
RADIO_TYPES = {
+ RadioType.deconz.name: {
+ ZHA_GW_RADIO: zigpy_deconz.api.Deconz,
+ CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication,
+ ZHA_GW_RADIO_DESCRIPTION: "Deconz",
+ },
RadioType.ezsp.name: {
ZHA_GW_RADIO: bellows.ezsp.EZSP,
CONTROLLER: bellows.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "EZSP",
},
- RadioType.deconz.name: {
- ZHA_GW_RADIO: zigpy_deconz.api.Deconz,
- CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication,
- ZHA_GW_RADIO_DESCRIPTION: "Deconz",
+ RadioType.ti_cc.name: {
+ ZHA_GW_RADIO: zigpy_cc.api.API,
+ CONTROLLER: zigpy_cc.zigbee.application.ControllerApplication,
+ ZHA_GW_RADIO_DESCRIPTION: "TI CC",
},
RadioType.xbee.name: {
ZHA_GW_RADIO: zigpy_xbee.api.XBee,
diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json
index f5ec96690bc..16c5604587d 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -5,7 +5,8 @@
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
"bellows-homeassistant==0.13.2",
- "zha-quirks==0.0.32",
+ "zha-quirks==0.0.33",
+ "zigpy-cc==0.1.0",
"zigpy-deconz==0.7.0",
"zigpy-homeassistant==0.13.2",
"zigpy-xbee-homeassistant==0.9.0",
diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py
index 203131afdb1..62f5b9acbaf 100644
--- a/homeassistant/components/zhong_hong/climate.py
+++ b/homeassistant/components/zhong_hong/climate.py
@@ -88,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
hub_is_initialized = False
async def startup():
- """Start hub socket after all climate entity is setted up."""
+ """Start hub socket after all climate entity is set up."""
nonlocal hub_is_initialized
if not all([device.is_initialized for device in devices]):
return
diff --git a/homeassistant/components/zone/.translations/pl.json b/homeassistant/components/zone/.translations/pl.json
index e649de4c75e..5c013d5da8f 100644
--- a/homeassistant/components/zone/.translations/pl.json
+++ b/homeassistant/components/zone/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "Nazwa ju\u017c istnieje"
+ "name_exists": "Nazwa ju\u017c istnieje."
},
"step": {
"init": {
diff --git a/homeassistant/components/zone/.translations/zh-Hans.json b/homeassistant/components/zone/.translations/zh-Hans.json
index 6d06b68dad8..6972b2946e4 100644
--- a/homeassistant/components/zone/.translations/zh-Hans.json
+++ b/homeassistant/components/zone/.translations/zh-Hans.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728"
+ "name_exists": "\u8be5\u540d\u79f0\u5df2\u5b58\u5728"
},
"step": {
"init": {
@@ -13,7 +13,7 @@
"passive": "\u88ab\u52a8",
"radius": "\u534a\u5f84"
},
- "title": "\u5b9a\u4e49\u533a\u57df\u76f8\u5173\u53d8\u91cf"
+ "title": "\u5b9a\u4e49\u533a\u57df\u53c2\u6570"
}
},
"title": "\u533a\u57df"
diff --git a/homeassistant/components/zwave/.translations/pl.json b/homeassistant/components/zwave/.translations/pl.json
index 254008ddb4c..a985405c009 100644
--- a/homeassistant/components/zwave/.translations/pl.json
+++ b/homeassistant/components/zwave/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Z-Wave jest ju\u017c skonfigurowany",
+ "already_configured": "Z-Wave jest ju\u017c skonfigurowany.",
"one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 Z-Wave"
},
"error": {
diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py
index 9b9236de1c2..ba7e26ee58c 100644
--- a/homeassistant/components/zwave/__init__.py
+++ b/homeassistant/components/zwave/__init__.py
@@ -678,7 +678,7 @@ async def async_setup_entry(hass, config_entry):
if value.type == const.TYPE_BOOL:
value.data = int(selection == "True")
_LOGGER.info(
- "Setting config parameter %s on Node %s with bool selection %s",
+ "Setting configuration parameter %s on Node %s with bool selection %s",
param,
node_id,
str(selection),
@@ -687,7 +687,7 @@ async def async_setup_entry(hass, config_entry):
if value.type == const.TYPE_LIST:
value.data = str(selection)
_LOGGER.info(
- "Setting config parameter %s on Node %s with list selection %s",
+ "Setting configuration parameter %s on Node %s with list selection %s",
param,
node_id,
str(selection),
@@ -697,7 +697,7 @@ async def async_setup_entry(hass, config_entry):
network.manager.pressButton(value.value_id)
network.manager.releaseButton(value.value_id)
_LOGGER.info(
- "Setting config parameter %s on Node %s "
+ "Setting configuration parameter %s on Node %s "
"with button selection %s",
param,
node_id,
@@ -706,7 +706,7 @@ async def async_setup_entry(hass, config_entry):
return
value.data = int(selection)
_LOGGER.info(
- "Setting config parameter %s on Node %s with selection %s",
+ "Setting configuration parameter %s on Node %s with selection %s",
param,
node_id,
selection,
@@ -714,7 +714,7 @@ async def async_setup_entry(hass, config_entry):
return
node.set_config_param(param, selection, size)
_LOGGER.info(
- "Setting unknown config parameter %s on Node %s with selection %s",
+ "Setting unknown configuration parameter %s on Node %s with selection %s",
param,
node_id,
selection,
diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py
index 840418fb063..4ee9b8b9cc9 100644
--- a/homeassistant/components/zwave/climate.py
+++ b/homeassistant/components/zwave/climate.py
@@ -529,7 +529,7 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice):
self._mode().data = operation_mode
def turn_aux_heat_on(self):
- """Turn auxillary heater on."""
+ """Turn auxiliary heater on."""
if not self._aux_heat:
return
operation_mode = AUX_HEAT_ZWAVE_MODE
@@ -537,7 +537,7 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice):
self._mode().data = operation_mode
def turn_aux_heat_off(self):
- """Turn auxillary heater off."""
+ """Turn auxiliary heater off."""
if not self._aux_heat:
return
if HVAC_MODE_HEAT in self._hvac_mapping:
diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py
index 44e73da320f..382d2c4dbf2 100644
--- a/homeassistant/components/zwave/lock.py
+++ b/homeassistant/components/zwave/lock.py
@@ -180,7 +180,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if len(str(usercode)) < 4:
_LOGGER.error(
"Invalid code provided: (%s) "
- "usercode must be atleast 4 and at most"
+ "usercode must be at least 4 and at most"
" %s digits",
usercode,
len(value.data),
diff --git a/homeassistant/config.py b/homeassistant/config.py
index f5870d683a0..6ff571f0d6b 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -697,6 +697,9 @@ async def async_process_component_config(
except (vol.Invalid, HomeAssistantError) as ex:
async_log_exception(ex, domain, config, hass, integration.documentation)
return None
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unknown error calling %s config validator", domain)
+ return None
# No custom config validator, proceed with schema validation
if hasattr(component, "CONFIG_SCHEMA"):
@@ -705,6 +708,9 @@ async def async_process_component_config(
except vol.Invalid as ex:
async_log_exception(ex, domain, config, hass, integration.documentation)
return None
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unknown error calling %s CONFIG_SCHEMA", domain)
+ return None
component_platform_schema = getattr(
component, "PLATFORM_SCHEMA_BASE", getattr(component, "PLATFORM_SCHEMA", None)
@@ -721,6 +727,13 @@ async def async_process_component_config(
except vol.Invalid as ex:
async_log_exception(ex, domain, p_config, hass, integration.documentation)
continue
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception(
+ "Unknown error validating %s platform config with %s component platform schema",
+ p_name,
+ domain,
+ )
+ continue
# Not all platform components follow same pattern for platforms
# So if p_name is None we are not going to validate platform
@@ -756,6 +769,13 @@ async def async_process_component_config(
p_integration.documentation,
)
continue
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception(
+ "Unknown error validating config for %s platform for %s component with PLATFORM_SCHEMA",
+ p_name,
+ domain,
+ )
+ continue
platforms.append(p_validated)
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 793e8be0045..1cec1e75fe9 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -183,7 +183,7 @@ class ConfigEntry:
component = integration.get_component()
except ImportError as err:
_LOGGER.error(
- "Error importing integration %s to set up %s config entry: %s",
+ "Error importing integration %s to set up %s configuration entry: %s",
integration.domain,
self.domain,
err,
@@ -197,7 +197,7 @@ class ConfigEntry:
integration.get_platform("config_flow")
except ImportError as err:
_LOGGER.error(
- "Error importing platform config_flow from integration %s to set up %s config entry: %s",
+ "Error importing platform config_flow from integration %s to set up %s configuration entry: %s",
integration.domain,
self.domain,
err,
@@ -503,7 +503,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
integration.get_platform("config_flow")
except ImportError as err:
_LOGGER.error(
- "Error occurred loading config flow for integration %s: %s",
+ "Error occurred loading configuration flow for integration %s: %s",
handler_key,
err,
)
@@ -775,6 +775,7 @@ class ConfigEntries:
return await entry.async_unload(self.hass, integration=integration)
+ @callback
def _async_schedule_save(self) -> None:
"""Save the entity registry to a file."""
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
@@ -947,7 +948,7 @@ class SystemOptions:
self.disable_new_entities = disable_new_entities
def as_dict(self) -> Dict[str, Any]:
- """Return dictionary version of this config entrys system options."""
+ """Return dictionary version of this config entries system options."""
return {"disable_new_entities": self.disable_new_entities}
@@ -1024,7 +1025,7 @@ class EntityRegistryDisabledHandler:
self.changed = set()
_LOGGER.info(
- "Reloading config entries because disabled_by changed in entity registry: %s",
+ "Reloading configuration entries because disabled_by changed in entity registry: %s",
", ".join(self.changed),
)
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 9db5a72292c..ff8e6bf8a9e 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 105
-PATCH_VERSION = "5"
+MINOR_VERSION = 106
+PATCH_VERSION = "0"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 0)
@@ -16,6 +16,7 @@ PLATFORM_FORMAT = "{platform}.{domain}"
MATCH_ALL = "*"
# Entity target all constant
+ENTITY_MATCH_NONE = "none"
ENTITY_MATCH_ALL = "all"
# If no name is specified
@@ -377,6 +378,40 @@ MASS_POUNDS: str = "lb"
# UV Index units
UNIT_UV_INDEX: str = "UV index"
+# Data units
+DATA_BITS = "bit"
+DATA_KILOBITS = "kbit"
+DATA_MEGABITS = "Mbit"
+DATA_GIGABITS = "Gbit"
+DATA_BYTES = "B"
+DATA_KILOBYTES = "kB"
+DATA_MEGABYTES = "MB"
+DATA_GIGABYTES = "GB"
+DATA_TERABYTES = "TB"
+DATA_PETABYTES = "PB"
+DATA_EXABYTES = "EB"
+DATA_ZETTABYTES = "ZB"
+DATA_YOTTABYTES = "YB"
+DATA_KIBIBYTES = "KiB"
+DATA_MEBIBYTES = "MiB"
+DATA_GIBIBYTES = "GiB"
+DATA_TEBIBYTES = "TiB"
+DATA_PEBIBYTES = "PiB"
+DATA_EXBIBYTES = "EiB"
+DATA_ZEBIBYTES = "ZiB"
+DATA_YOBIBYTES = "YiB"
+DATA_RATE_BITS_PER_SECOND = f"{DATA_BITS}/s"
+DATA_RATE_KILOBITS_PER_SECOND = f"{DATA_KILOBITS}/s"
+DATA_RATE_MEGABITS_PER_SECOND = f"{DATA_MEGABITS}/s"
+DATA_RATE_GIGABITS_PER_SECOND = f"{DATA_GIGABITS}/s"
+DATA_RATE_BYTES_PER_SECOND = f"{DATA_BYTES}/s"
+DATA_RATE_KILOBYTES_PER_SECOND = f"{DATA_KILOBYTES}/s"
+DATA_RATE_MEGABYTES_PER_SECOND = f"{DATA_MEGABYTES}/s"
+DATA_RATE_GIGABYTES_PER_SECOND = f"{DATA_GIGABYTES}/s"
+DATA_RATE_KIBIBYTES_PER_SECOND = f"{DATA_KIBIBYTES}/s"
+DATA_RATE_MEBIBYTES_PER_SECOND = f"{DATA_MEBIBYTES}/s"
+DATA_RATE_GIBIBYTES_PER_SECOND = f"{DATA_GIBIBYTES}/s"
+
# #### SERVICES ####
SERVICE_HOMEASSISTANT_STOP = "stop"
SERVICE_HOMEASSISTANT_RESTART = "restart"
diff --git a/homeassistant/core.py b/homeassistant/core.py
index 3f561cdfab8..c17c1f698ce 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -298,10 +298,10 @@ class HomeAssistant:
if asyncio.iscoroutine(check_target):
task = self.loop.create_task(target) # type: ignore
- elif is_callback(check_target):
- self.loop.call_soon(target, *args)
elif asyncio.iscoroutinefunction(check_target):
task = self.loop.create_task(target(*args))
+ elif is_callback(check_target):
+ self.loop.call_soon(target, *args)
else:
task = self.loop.run_in_executor( # type: ignore
None, target, *args
@@ -360,7 +360,11 @@ class HomeAssistant:
target: target to call.
args: parameters for method to call.
"""
- if not asyncio.iscoroutine(target) and is_callback(target):
+ if (
+ not asyncio.iscoroutine(target)
+ and not asyncio.iscoroutinefunction(target)
+ and is_callback(target)
+ ):
target(*args)
else:
self.async_add_job(target, *args)
@@ -1245,10 +1249,10 @@ class ServiceRegistry:
self, handler: Service, service_call: ServiceCall
) -> None:
"""Execute a service."""
- if handler.is_callback:
- handler.func(service_call)
- elif handler.is_coroutinefunction:
+ if handler.is_coroutinefunction:
await handler.func(service_call)
+ elif handler.is_callback:
+ handler.func(service_call)
else:
await self._hass.async_add_executor_job(handler.func, service_call)
@@ -1284,6 +1288,9 @@ class Config:
# List of allowed external dirs to access
self.whitelist_external_dirs: Set[str] = set()
+ # If Home Assistant is running in safe mode
+ self.safe_mode: bool = False
+
def distance(self, lat: float, lon: float) -> Optional[float]:
"""Calculate distance from Home Assistant.
@@ -1346,6 +1353,7 @@ class Config:
"whitelist_external_dirs": self.whitelist_external_dirs,
"version": __version__,
"config_source": self.config_source,
+ "safe_mode": self.safe_mode,
}
def set_time_zone(self, time_zone_str: str) -> None:
diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py
index 4dd1c7acf50..4a115762be4 100644
--- a/homeassistant/data_entry_flow.py
+++ b/homeassistant/data_entry_flow.py
@@ -30,7 +30,7 @@ class UnknownHandler(FlowError):
class UnknownFlow(FlowError):
- """Uknown flow specified."""
+ """Unknown flow specified."""
class UnknownStep(FlowError):
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index cf77dae7fb2..39a9bccf607 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -20,11 +20,13 @@ FLOWS = [
"daikin",
"deconz",
"dialogflow",
+ "dynalite",
"ecobee",
"elgato",
"emulated_roku",
"esphome",
"garmin_connect",
+ "gdacs",
"geofency",
"geonetnz_quakes",
"geonetnz_volcano",
@@ -45,6 +47,7 @@ FLOWS = [
"ipma",
"iqvia",
"izone",
+ "konnected",
"life360",
"lifx",
"linky",
@@ -53,8 +56,11 @@ FLOWS = [
"logi_circle",
"luftdaten",
"mailgun",
+ "melcloud",
"met",
+ "meteo_france",
"mikrotik",
+ "minecraft_server",
"mobile_app",
"mqtt",
"neato",
@@ -95,6 +101,7 @@ FLOWS = [
"upnp",
"velbus",
"vesync",
+ "vilfo",
"vizio",
"wemo",
"withings",
diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py
index bea04484b11..0eb9af0231d 100644
--- a/homeassistant/generated/ssdp.py
+++ b/homeassistant/generated/ssdp.py
@@ -36,6 +36,11 @@ SSDP = {
"modelName": "Philips hue bridge 2015"
}
],
+ "konnected": [
+ {
+ "manufacturer": "konnected.io"
+ }
+ ],
"samsungtv": [
{
"st": "urn:samsung.com:device:RemoteControlReceiver:1"
diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py
index ad97456968b..7189f519724 100644
--- a/homeassistant/helpers/__init__.py
+++ b/homeassistant/helpers/__init__.py
@@ -1,10 +1,10 @@
"""Helper methods for components within Home Assistant."""
import re
-from typing import Any, Dict, Iterable, Sequence, Tuple
+from typing import Any, Iterable, Sequence, Tuple
from homeassistant.const import CONF_PLATFORM
-ConfigType = Dict[str, Any]
+from .typing import ConfigType
def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, Any]]:
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 852948220de..1ff2644fa58 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -52,6 +52,7 @@ from homeassistant.const import (
CONF_UNIT_SYSTEM_METRIC,
CONF_VALUE_TEMPLATE,
ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
SUN_EVENT_SUNRISE,
SUN_EVENT_SUNSET,
TEMP_CELSIUS,
@@ -231,7 +232,9 @@ def entity_ids(value: Union[str, List]) -> List[str]:
return [entity_id(ent_id) for ent_id in value]
-comp_entity_ids = vol.Any(vol.All(vol.Lower, ENTITY_MATCH_ALL), entity_ids)
+comp_entity_ids = vol.Any(
+ vol.All(vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_NONE)), entity_ids
+)
def entity_domain(domain: str) -> Callable[[Any], str]:
@@ -585,6 +588,25 @@ def ensure_list_csv(value: Any) -> List:
return ensure_list(value)
+class multi_select:
+ """Multi select validator returning list of selected values."""
+
+ def __init__(self, options: dict) -> None:
+ """Initialize multi select."""
+ self.options = options
+
+ def __call__(self, selected: list) -> list:
+ """Validate input."""
+ if not isinstance(selected, list):
+ raise vol.Invalid("Not a list")
+
+ for value in selected:
+ if value not in self.options:
+ raise vol.Invalid(f"{value} is not a valid option")
+
+ return selected
+
+
def deprecated(
key: str,
replacement_key: Optional[str] = None,
@@ -710,6 +732,9 @@ def custom_serializer(schema: Any) -> Any:
if schema is positive_time_period_dict:
return {"type": "positive_time_period_dict"}
+ if isinstance(schema, multi_select):
+ return {"type": "multi_select", "options": schema.options}
+
return voluptuous_serialize.UNSUPPORTED
@@ -736,7 +761,9 @@ def make_entity_service_schema(
{
**schema,
vol.Optional(ATTR_ENTITY_ID): comp_entity_ids,
- vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]),
+ vol.Optional(ATTR_AREA_ID): vol.Any(
+ ENTITY_MATCH_NONE, vol.All(ensure_list, [str])
+ ),
},
extra=extra,
),
diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py
index ac5fb608675..05f49cd9f53 100644
--- a/homeassistant/helpers/data_entry_flow.py
+++ b/homeassistant/helpers/data_entry_flow.py
@@ -5,6 +5,7 @@ import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
+import homeassistant.helpers.config_validation as cv
# mypy: allow-untyped-calls, allow-untyped-defs
@@ -36,7 +37,9 @@ class _BaseFlowManagerView(HomeAssistantView):
if schema is None:
data["data_schema"] = []
else:
- data["data_schema"] = voluptuous_serialize.convert(schema)
+ data["data_schema"] = voluptuous_serialize.convert(
+ schema, custom_serializer=cv.custom_serializer
+ )
return data
diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py
index 5bacbdb7d11..bbaf6dacfeb 100644
--- a/homeassistant/helpers/debounce.py
+++ b/homeassistant/helpers/debounce.py
@@ -13,6 +13,7 @@ class Debouncer:
self,
hass: HomeAssistant,
logger: Logger,
+ *,
cooldown: float,
immediate: bool,
function: Optional[Callable[..., Awaitable[Any]]] = None,
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index a2a0ae840e0..49ed0f4a567 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -337,7 +337,7 @@ class Entity(ABC):
if name is not None:
attr[ATTR_FRIENDLY_NAME] = name
- icon = self.icon
+ icon = (entry and entry.icon) or self.icon
if icon is not None:
attr[ATTR_ICON] = icon
@@ -365,13 +365,25 @@ class Entity(ABC):
if end - start > 0.4 and not self._slow_reported:
self._slow_reported = True
+ extra = ""
+ if "custom_components" in type(self).__module__:
+ extra = "Please report it to the custom component author."
+ else:
+ extra = (
+ "Please create a bug report at "
+ "https://github.com/home-assistant/home-assistant/issues?q=is%3Aopen+is%3Aissue"
+ )
+ if self.platform:
+ extra += (
+ f"+label%3A%22integration%3A+{self.platform.platform_name}%22"
+ )
+
_LOGGER.warning(
- "Updating state for %s (%s) took %.3f seconds. "
- "Please report platform to the developers at "
- "https://goo.gl/Nvioub",
+ "Updating state for %s (%s) took %.3f seconds. %s",
self.entity_id,
type(self),
end - start,
+ extra,
)
# Overwrite properties that have been set in the config file.
@@ -429,7 +441,10 @@ class Entity(ABC):
If state is changed more than once before the ha state change task has
been executed, the intermediate state transitions will be missed.
"""
- self.hass.async_create_task(self.async_update_ha_state(force_refresh))
+ if force_refresh:
+ self.hass.async_create_task(self.async_update_ha_state(force_refresh))
+ else:
+ self.async_write_ha_state()
async def async_device_update(self, warning=True):
"""Process 'update' or 'async_update' from entity.
@@ -568,7 +583,6 @@ class Entity(ABC):
# call an requests
async def async_request_call(self, coro):
"""Process request batched."""
-
if self.parallel_updates:
await self.parallel_updates.acquire()
diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index e26dc5dfbea..f6c473dd418 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -273,6 +273,7 @@ class EntityComponent:
return processed_conf
+ @callback
def _async_init_entity_platform(
self,
platform_type: str,
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index 8fedc198fe2..e1e046eaa6d 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -62,22 +62,42 @@ class EntityPlatform:
# Platform is None for the EntityComponent "catch-all" EntityPlatform
# which powers entity_component.add_entities
if platform is None:
- self.parallel_updates = None
- self.parallel_updates_semaphore: Optional[asyncio.Semaphore] = None
+ self.parallel_updates_created = True
+ self.parallel_updates: Optional[asyncio.Semaphore] = None
return
- self.parallel_updates = getattr(platform, "PARALLEL_UPDATES", None)
- # semaphore will be created on demand
- self.parallel_updates_semaphore = None
+ self.parallel_updates_created = False
+ self.parallel_updates = None
- def _get_parallel_updates_semaphore(self) -> asyncio.Semaphore:
- """Get or create a semaphore for parallel updates."""
- if self.parallel_updates_semaphore is None:
- self.parallel_updates_semaphore = asyncio.Semaphore(
- self.parallel_updates if self.parallel_updates else 1,
- loop=self.hass.loop,
- )
- return self.parallel_updates_semaphore
+ @callback
+ def _get_parallel_updates_semaphore(
+ self, entity_has_async_update: bool
+ ) -> Optional[asyncio.Semaphore]:
+ """Get or create a semaphore for parallel updates.
+
+ Semaphore will be created on demand because we base it off if update method is async or not.
+
+ If parallel updates is set to 0, we skip the semaphore.
+ If parallel updates is set to a number, we initialize the semaphore to that number.
+ Default for entities with `async_update` method is 1. Otherwise it's 0.
+ """
+ if self.parallel_updates_created:
+ return self.parallel_updates
+
+ self.parallel_updates_created = True
+
+ parallel_updates = getattr(self.platform, "PARALLEL_UPDATES", None)
+
+ if parallel_updates is None and not entity_has_async_update:
+ parallel_updates = 1
+
+ if parallel_updates == 0:
+ parallel_updates = None
+
+ if parallel_updates is not None:
+ self.parallel_updates = asyncio.Semaphore(parallel_updates)
+
+ return self.parallel_updates
async def async_setup(self, platform_config, discovery_info=None):
"""Set up the platform from a config file."""
@@ -282,21 +302,9 @@ class EntityPlatform:
entity.hass = self.hass
entity.platform = self
-
- # Async entity
- # PARALLEL_UPDATES == None: entity.parallel_updates = None
- # PARALLEL_UPDATES == 0: entity.parallel_updates = None
- # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p)
- # Sync entity
- # PARALLEL_UPDATES == None: entity.parallel_updates = Semaphore(1)
- # PARALLEL_UPDATES == 0: entity.parallel_updates = None
- # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p)
- if hasattr(entity, "async_update") and not self.parallel_updates:
- entity.parallel_updates = None
- elif not hasattr(entity, "async_update") and self.parallel_updates == 0:
- entity.parallel_updates = None
- else:
- entity.parallel_updates = self._get_parallel_updates_semaphore()
+ entity.parallel_updates = self._get_parallel_updates_semaphore(
+ hasattr(entity, "async_update")
+ )
# Update properties before we generate the entity_id
if update_before_add:
@@ -361,6 +369,8 @@ class EntityPlatform:
supported_features=entity.supported_features,
device_class=entity.device_class,
unit_of_measurement=entity.unit_of_measurement,
+ original_name=entity.name,
+ original_icon=entity.icon,
)
entity.registry_entry = entry
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index 635f7feba13..5996fb6eaf7 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -17,6 +17,8 @@ import attr
from homeassistant.const import (
ATTR_DEVICE_CLASS,
+ ATTR_FRIENDLY_NAME,
+ ATTR_ICON,
ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT,
EVENT_HOMEASSISTANT_START,
@@ -60,6 +62,7 @@ class RegistryEntry:
unique_id = attr.ib(type=str)
platform = attr.ib(type=str)
name = attr.ib(type=str, default=None)
+ icon = attr.ib(type=str, default=None)
device_id: Optional[str] = attr.ib(default=None)
config_entry_id: Optional[str] = attr.ib(default=None)
disabled_by = attr.ib(
@@ -79,6 +82,9 @@ class RegistryEntry:
supported_features: int = attr.ib(default=0)
device_class: Optional[str] = attr.ib(default=None)
unit_of_measurement: Optional[str] = attr.ib(default=None)
+ # As set by integration
+ original_name: Optional[str] = attr.ib(default=None)
+ original_icon: Optional[str] = attr.ib(default=None)
domain = attr.ib(type=str, init=False, repr=False)
@domain.default
@@ -167,6 +173,8 @@ class EntityRegistry:
supported_features: Optional[int] = None,
device_class: Optional[str] = None,
unit_of_measurement: Optional[str] = None,
+ original_name: Optional[str] = None,
+ original_icon: Optional[str] = None,
) -> RegistryEntry:
"""Get entity. Create if it doesn't exist."""
config_entry_id = None
@@ -184,6 +192,8 @@ class EntityRegistry:
supported_features=supported_features or _UNDEF,
device_class=device_class or _UNDEF,
unit_of_measurement=unit_of_measurement or _UNDEF,
+ original_name=original_name or _UNDEF,
+ original_icon=original_icon or _UNDEF,
# When we changed our slugify algorithm, we invalidated some
# stored entity IDs with either a __ or ending in _.
# Fix introduced in 0.86 (Jan 23, 2019). Next line can be
@@ -215,6 +225,8 @@ class EntityRegistry:
supported_features=supported_features or 0,
device_class=device_class,
unit_of_measurement=unit_of_measurement,
+ original_name=original_name,
+ original_icon=original_icon,
)
self.entities[entity_id] = entity
_LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id)
@@ -254,6 +266,7 @@ class EntityRegistry:
entity_id,
*,
name=_UNDEF,
+ icon=_UNDEF,
new_entity_id=_UNDEF,
new_unique_id=_UNDEF,
disabled_by=_UNDEF,
@@ -264,6 +277,7 @@ class EntityRegistry:
self._async_update_entity(
entity_id,
name=name,
+ icon=icon,
new_entity_id=new_entity_id,
new_unique_id=new_unique_id,
disabled_by=disabled_by,
@@ -276,6 +290,7 @@ class EntityRegistry:
entity_id,
*,
name=_UNDEF,
+ icon=_UNDEF,
config_entry_id=_UNDEF,
new_entity_id=_UNDEF,
device_id=_UNDEF,
@@ -285,6 +300,8 @@ class EntityRegistry:
supported_features=_UNDEF,
device_class=_UNDEF,
unit_of_measurement=_UNDEF,
+ original_name=_UNDEF,
+ original_icon=_UNDEF,
):
"""Private facing update properties method."""
old = self.entities[entity_id]
@@ -293,6 +310,7 @@ class EntityRegistry:
for attr_name, value in (
("name", name),
+ ("icon", icon),
("config_entry_id", config_entry_id),
("device_id", device_id),
("disabled_by", disabled_by),
@@ -300,6 +318,8 @@ class EntityRegistry:
("supported_features", supported_features),
("device_class", device_class),
("unit_of_measurement", unit_of_measurement),
+ ("original_name", original_name),
+ ("original_icon", original_icon),
):
if value is not _UNDEF and value != getattr(old, attr_name):
changes[attr_name] = value
@@ -372,11 +392,14 @@ class EntityRegistry:
unique_id=entity["unique_id"],
platform=entity["platform"],
name=entity.get("name"),
+ icon=entity.get("icon"),
disabled_by=entity.get("disabled_by"),
capabilities=entity.get("capabilities") or {},
supported_features=entity.get("supported_features", 0),
device_class=entity.get("device_class"),
unit_of_measurement=entity.get("unit_of_measurement"),
+ original_name=entity.get("original_name"),
+ original_icon=entity.get("original_icon"),
)
self.entities = entities
@@ -399,11 +422,14 @@ class EntityRegistry:
"unique_id": entry.unique_id,
"platform": entry.platform,
"name": entry.name,
+ "icon": entry.icon,
"disabled_by": entry.disabled_by,
"capabilities": entry.capabilities,
"supported_features": entry.supported_features,
"device_class": entry.device_class,
"unit_of_measurement": entry.unit_of_measurement,
+ "original_name": entry.original_name,
+ "original_icon": entry.original_icon,
}
for entry in self.entities.values()
]
@@ -523,6 +549,14 @@ def async_setup_entity_restore(
if entry.unit_of_measurement is not None:
attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement
+ name = entry.name or entry.original_name
+ if name is not None:
+ attrs[ATTR_FRIENDLY_NAME] = name
+
+ icon = entry.icon or entry.original_icon
+ if icon is not None:
+ attrs[ATTR_ICON] = icon
+
states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs)
hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states)
diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py
index 0b274458045..2e3270879f0 100644
--- a/homeassistant/helpers/logging.py
+++ b/homeassistant/helpers/logging.py
@@ -42,7 +42,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter):
def process(
self, msg: Any, kwargs: MutableMapping[str, Any]
) -> Tuple[Any, MutableMapping[str, Any]]:
- """Process the keyward args in preparation for logging."""
+ """Process the keyword args in preparation for logging."""
return (
msg,
{
diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py
index e8f0f9a6bac..d57d3ad9920 100644
--- a/homeassistant/helpers/restore_state.py
+++ b/homeassistant/helpers/restore_state.py
@@ -2,7 +2,7 @@
import asyncio
from datetime import datetime, timedelta
import logging
-from typing import Any, Dict, List, Optional, Set
+from typing import Any, Awaitable, Dict, List, Optional, Set, cast
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
@@ -20,9 +20,6 @@ from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.storage import Store
import homeassistant.util.dt as dt_util
-# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
-# mypy: no-warn-return-any
-
DATA_RESTORE_STATE_TASK = "restore_state_task"
_LOGGER = logging.getLogger(__name__)
@@ -45,7 +42,7 @@ class StoredState:
self.state = state
self.last_seen = last_seen
- def as_dict(self) -> Dict:
+ def as_dict(self) -> Dict[str, Any]:
"""Return a dict representation of the stored state."""
return {"state": self.state.as_dict(), "last_seen": self.last_seen}
@@ -104,7 +101,7 @@ class RestoreStateData:
load_instance(hass)
)
- return await task
+ return await cast(Awaitable["RestoreStateData"], task)
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the restore state data class."""
@@ -174,6 +171,7 @@ class RestoreStateData:
def async_setup_dump(self, *args: Any) -> None:
"""Set up the restore state listeners."""
+ @callback
def _async_dump_states(*_: Any) -> None:
self.hass.async_create_task(self.async_dump_states())
@@ -210,15 +208,18 @@ class RestoreStateData:
self.entity_ids.remove(entity_id)
-def _encode(value):
+def _encode(value: Any) -> Any:
"""Little helper to JSON encode a value."""
try:
- return JSONEncoder.default(None, value)
+ return JSONEncoder.default(
+ None, # type: ignore
+ value,
+ )
except TypeError:
return value
-def _encode_complex(value):
+def _encode_complex(value: Any) -> Any:
"""Recursively encode all values with the JSONEncoder."""
if isinstance(value, dict):
return {_encode(key): _encode_complex(value) for key, value in value.items()}
diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py
index b30cab3fbd4..9085c929651 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -7,7 +7,12 @@ from typing import Callable
import voluptuous as vol
from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL
-from homeassistant.const import ATTR_AREA_ID, ATTR_ENTITY_ID, ENTITY_MATCH_ALL
+from homeassistant.const import (
+ ATTR_AREA_ID,
+ ATTR_ENTITY_ID,
+ ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
+)
import homeassistant.core as ha
from homeassistant.exceptions import (
HomeAssistantError,
@@ -121,11 +126,25 @@ async def async_extract_entities(hass, entities, service_call, expand_group=True
entity_ids = await async_extract_entity_ids(hass, service_call, expand_group)
- return [
- entity
- for entity in entities
- if entity.available and entity.entity_id in entity_ids
- ]
+ found = []
+
+ for entity in entities:
+ if entity.entity_id not in entity_ids:
+ continue
+
+ entity_ids.remove(entity.entity_id)
+
+ if not entity.available:
+ continue
+
+ found.append(entity)
+
+ if entity_ids:
+ _LOGGER.warning(
+ "Unable to find referenced entities %s", ", ".join(sorted(entity_ids))
+ )
+
+ return found
@bind_hass
@@ -137,12 +156,15 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True):
entity_ids = service_call.data.get(ATTR_ENTITY_ID)
area_ids = service_call.data.get(ATTR_AREA_ID)
- if not entity_ids and not area_ids:
- return []
-
extracted = set()
- if entity_ids:
+ if entity_ids in (None, ENTITY_MATCH_NONE) and area_ids in (
+ None,
+ ENTITY_MATCH_NONE,
+ ):
+ return extracted
+
+ if entity_ids and entity_ids != ENTITY_MATCH_NONE:
# Entity ID attr can be a list or a string
if isinstance(entity_ids, str):
entity_ids = [entity_ids]
@@ -152,7 +174,7 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True):
extracted.update(entity_ids)
- if area_ids:
+ if area_ids and area_ids != ENTITY_MATCH_NONE:
if isinstance(area_ids, str):
area_ids = [area_ids]
@@ -294,16 +316,15 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non
# Check the permissions
- # A list with for each platform in platforms a list of entities to call
- # the service on.
- platforms_entities = []
+ # A list with entities to call the service on.
+ entity_candidates = []
if entity_perms is None:
for platform in platforms:
if target_all_entities:
- platforms_entities.append(list(platform.entities.values()))
+ entity_candidates.extend(platform.entities.values())
else:
- platforms_entities.append(
+ entity_candidates.extend(
[
entity
for entity in platform.entities.values()
@@ -315,7 +336,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non
# If we target all entities, we will select all entities the user
# is allowed to control.
for platform in platforms:
- platforms_entities.append(
+ entity_candidates.extend(
[
entity
for entity in platform.entities.values()
@@ -340,29 +361,20 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non
platform_entities.append(entity)
- platforms_entities.append(platform_entities)
+ entity_candidates.extend(platform_entities)
- tasks = [
- _handle_service_platform_call(
- hass, func, data, entities, call.context, required_features
- )
- for platform, entities in zip(platforms, platforms_entities)
- ]
+ if not target_all_entities:
+ for entity in entity_candidates:
+ entity_ids.remove(entity.entity_id)
- if tasks:
- done, pending = await asyncio.wait(tasks)
- assert not pending
- for future in done:
- future.result() # pop exception if have
+ if entity_ids:
+ _LOGGER.warning(
+ "Unable to find referenced entities %s", ", ".join(sorted(entity_ids))
+ )
+ entities = []
-async def _handle_service_platform_call(
- hass, func, data, entities, context, required_features
-):
- """Handle a function call."""
- tasks = []
-
- for entity in entities:
+ for entity in entity_candidates:
if not entity.available:
continue
@@ -372,27 +384,33 @@ async def _handle_service_platform_call(
):
continue
- entity.async_set_context(context)
+ entities.append(entity)
- if isinstance(func, str):
- result = hass.async_add_job(partial(getattr(entity, func), **data))
- else:
- result = hass.async_add_job(func, entity, data)
+ if not entities:
+ return
- # Guard because callback functions do not return a task when passed to async_add_job.
- if result is not None:
- result = await result
-
- if asyncio.iscoroutine(result):
- _LOGGER.error(
- "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to integration author.",
- func,
- entity.entity_id,
+ done, pending = await asyncio.wait(
+ [
+ entity.async_request_call(
+ _handle_entity_call(hass, entity, func, data, call.context)
)
- await result
+ for entity in entities
+ ]
+ )
+ assert not pending
+ for future in done:
+ future.result() # pop exception if have
- if entity.should_poll:
- tasks.append(entity.async_update_ha_state(True))
+ tasks = []
+
+ for entity in entities:
+ if not entity.should_poll:
+ continue
+
+ # Context expires if the turn on commands took a long time.
+ # Set context again so it's there when we update
+ entity.async_set_context(call.context)
+ tasks.append(entity.async_update_ha_state(True))
if tasks:
done, pending = await asyncio.wait(tasks)
@@ -401,6 +419,28 @@ async def _handle_service_platform_call(
future.result() # pop exception if have
+async def _handle_entity_call(hass, entity, func, data, context):
+ """Handle calling service method."""
+ entity.async_set_context(context)
+
+ if isinstance(func, str):
+ result = hass.async_add_job(partial(getattr(entity, func), **data))
+ else:
+ result = hass.async_add_job(func, entity, data)
+
+ # Guard because callback functions do not return a task when passed to async_add_job.
+ if result is not None:
+ await result
+
+ if asyncio.iscoroutine(result):
+ _LOGGER.error(
+ "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to integration author.",
+ func,
+ entity.entity_id,
+ )
+ await result
+
+
@bind_hass
@ha.callback
def async_register_admin_service(
@@ -421,7 +461,9 @@ def async_register_admin_service(
if not user.is_admin:
raise Unauthorized(context=call.context)
- await hass.async_add_job(service_func, call)
+ result = hass.async_add_job(service_func, call)
+ if result is not None:
+ await result
hass.services.async_register(domain, service, admin_handler, schema)
@@ -442,6 +484,7 @@ def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable:
return await service_handler(call)
user = await hass.auth.async_get_user(call.context.user_id)
+
if user is None:
raise UnknownUser(
context=call.context,
@@ -450,14 +493,12 @@ def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable:
)
reg = await hass.helpers.entity_registry.async_get_registry()
- entities = [
- entity.entity_id
- for entity in reg.entities.values()
- if entity.platform == domain
- ]
- for entity_id in entities:
- if user.permissions.check_entity(entity_id, POLICY_CONTROL):
+ for entity in reg.entities.values():
+ if entity.platform != domain:
+ continue
+
+ if user.permissions.check_entity(entity.entity_id, POLICY_CONTROL):
return await service_handler(call)
raise Unauthorized(
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 8565315f87f..e7f89b482e2 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -378,7 +378,7 @@ class DomainStates:
raise TemplateError(f"Invalid entity ID '{entity_id}'")
return _get_state(self._hass, entity_id)
- def _collect_domain(self):
+ def _collect_domain(self) -> None:
entity_collect = self._hass.data.get(_RENDER_INFO)
if entity_collect is not None:
# pylint: disable=protected-access
@@ -398,12 +398,12 @@ class DomainStates:
)
)
- def __len__(self):
+ def __len__(self) -> int:
"""Return number of states."""
self._collect_domain()
return len(self._hass.states.async_entity_ids(self._domain))
- def __repr__(self):
+ def __repr__(self) -> str:
"""Representation of Domain States."""
return f""
@@ -426,7 +426,7 @@ class TemplateState(State):
return state
@property
- def state_with_unit(self):
+ def state_with_unit(self) -> str:
"""Return the state concatenated with the unit if available."""
state = object.__getattribute__(self, "_access_state")()
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@@ -447,7 +447,7 @@ class TemplateState(State):
state = object.__getattribute__(self, "_access_state")()
return getattr(state, name)
- def __repr__(self):
+ def __repr__(self) -> str:
"""Representation of Template State."""
state = object.__getattribute__(self, "_access_state")()
rep = state.__repr__()
@@ -469,7 +469,7 @@ def _wrap_state(hass, state):
def _get_state(hass, entity_id):
state = hass.states.get(entity_id)
if state is None:
- # Only need to collect if none, if not none collect first actuall
+ # Only need to collect if none, if not none collect first actual
# access to the state properties in the state wrapper.
_collect_state(hass, entity_id)
return None
diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py
index dc990637e31..fe877fe9bb8 100644
--- a/homeassistant/helpers/update_coordinator.py
+++ b/homeassistant/helpers/update_coordinator.py
@@ -11,6 +11,9 @@ from homeassistant.util.dt import utcnow
from .debounce import Debouncer
+REQUEST_REFRESH_DEFAULT_COOLDOWN = 10
+REQUEST_REFRESH_DEFAULT_IMMEDIATE = True
+
class UpdateFailed(Exception):
"""Raised when an update has failed."""
@@ -23,10 +26,11 @@ class DataUpdateCoordinator:
self,
hass: HomeAssistant,
logger: logging.Logger,
+ *,
name: str,
update_method: Callable[[], Awaitable],
update_interval: timedelta,
- request_refresh_debouncer: Debouncer,
+ request_refresh_debouncer: Optional[Debouncer] = None,
):
"""Initialize global data updater."""
self.hass = hass
@@ -40,9 +44,20 @@ class DataUpdateCoordinator:
self._listeners: List[CALLBACK_TYPE] = []
self._unsub_refresh: Optional[CALLBACK_TYPE] = None
self._request_refresh_task: Optional[asyncio.TimerHandle] = None
- self.failed_last_update = False
+ self.last_update_success = True
+
+ if request_refresh_debouncer is None:
+ request_refresh_debouncer = Debouncer(
+ hass,
+ logger,
+ cooldown=REQUEST_REFRESH_DEFAULT_COOLDOWN,
+ immediate=REQUEST_REFRESH_DEFAULT_IMMEDIATE,
+ function=self.async_refresh,
+ )
+ else:
+ request_refresh_debouncer.function = self.async_refresh
+
self._debounced_refresh = request_refresh_debouncer
- request_refresh_debouncer.function = self._async_do_refresh
@callback
def async_add_listener(self, update_callback: CALLBACK_TYPE) -> None:
@@ -64,14 +79,6 @@ class DataUpdateCoordinator:
self._unsub_refresh()
self._unsub_refresh = None
- async def async_refresh(self) -> None:
- """Refresh the data."""
- if self._unsub_refresh:
- self._unsub_refresh()
- self._unsub_refresh = None
-
- await self._async_do_refresh()
-
@callback
def _schedule_refresh(self) -> None:
"""Schedule a refresh."""
@@ -86,7 +93,7 @@ class DataUpdateCoordinator:
async def _handle_refresh_interval(self, _now: datetime) -> None:
"""Handle a refresh interval occurrence."""
self._unsub_refresh = None
- await self._async_do_refresh()
+ await self.async_refresh()
async def async_request_refresh(self) -> None:
"""Request a refresh.
@@ -95,8 +102,8 @@ class DataUpdateCoordinator:
"""
await self._debounced_refresh.async_call()
- async def _async_do_refresh(self) -> None:
- """Time to update."""
+ async def async_refresh(self) -> None:
+ """Update data."""
if self._unsub_refresh:
self._unsub_refresh()
self._unsub_refresh = None
@@ -108,20 +115,20 @@ class DataUpdateCoordinator:
self.data = await self.update_method()
except UpdateFailed as err:
- if not self.failed_last_update:
+ if self.last_update_success:
self.logger.error("Error fetching %s data: %s", self.name, err)
- self.failed_last_update = True
+ self.last_update_success = False
except Exception as err: # pylint: disable=broad-except
- self.failed_last_update = True
+ self.last_update_success = False
self.logger.exception(
"Unexpected error fetching %s data: %s", self.name, err
)
else:
- if self.failed_last_update:
- self.failed_last_update = False
- self.logger.info("Fetching %s data recovered")
+ if not self.last_update_success:
+ self.last_update_success = True
+ self.logger.info("Fetching %s data recovered", self.name)
finally:
self.logger.debug(
diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index 0f69f4600b2..4c46d437760 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -41,7 +41,6 @@ DATA_INTEGRATIONS = "integrations"
DATA_CUSTOM_COMPONENTS = "custom_components"
PACKAGE_CUSTOM_COMPONENTS = "custom_components"
PACKAGE_BUILTIN = "homeassistant.components"
-LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
CUSTOM_WARNING = (
"You are using a custom integration for %s which has not "
"been tested by Home Assistant. This component might "
@@ -67,6 +66,9 @@ async def _async_get_custom_components(
hass: "HomeAssistant",
) -> Dict[str, "Integration"]:
"""Return list of custom integrations."""
+ if hass.config.safe_mode:
+ return {}
+
try:
import custom_components
except ImportError:
@@ -178,7 +180,7 @@ class Integration:
Will create a stub manifest.
"""
- comp = _load_file(hass, domain, LOOKUP_PATHS)
+ comp = _load_file(hass, domain, _lookup_path(hass))
if comp is None:
return None
@@ -464,7 +466,7 @@ class Components:
component: Optional[ModuleType] = integration.get_component()
else:
# Fallback to importing old-school
- component = _load_file(self._hass, comp_name, LOOKUP_PATHS)
+ component = _load_file(self._hass, comp_name, _lookup_path(self._hass))
if component is None:
raise ImportError(f"Unable to load {comp_name}")
@@ -541,8 +543,15 @@ def _async_mount_config_dir(hass: "HomeAssistant") -> bool:
Async friendly but not a coroutine.
"""
if hass.config.config_dir is None:
- _LOGGER.error("Can't load integrations - config dir is not set")
+ _LOGGER.error("Can't load integrations - configuration directory is not set")
return False
if hass.config.config_dir not in sys.path:
sys.path.insert(0, hass.config.config_dir)
return True
+
+
+def _lookup_path(hass: "HomeAssistant") -> List[str]:
+ """Return the lookup paths for legacy lookups."""
+ if hass.config.safe_mode:
+ return [PACKAGE_BUILTIN]
+ return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 50b9a4398dd..7d6f445c1e4 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -11,8 +11,8 @@ cryptography==2.8
defusedxml==0.6.0
distro==1.4.0
hass-nabucasa==0.31
-home-assistant-frontend==20200130.3
-importlib-metadata==1.4.0
+home-assistant-frontend==20200220.4
+importlib-metadata==1.5.0
jinja2>=2.10.3
netdisco==2.6.0
pip>=8.0.3
@@ -28,6 +28,9 @@ zeroconf==0.24.4
pycryptodome>=3.6.6
+# Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324
+urllib3>=1.24.3
+
# Not needed for our supported Python versions
enum34==1000000000.0.0
diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py
index 2d6221dd580..94dc816e03c 100644
--- a/homeassistant/util/json.py
+++ b/homeassistant/util/json.py
@@ -4,7 +4,7 @@ import json
import logging
import os
import tempfile
-from typing import Dict, List, Optional, Type, Union
+from typing import Any, Dict, List, Optional, Type, Union
from homeassistant.exceptions import HomeAssistantError
@@ -85,7 +85,7 @@ def save_json(
_LOGGER.error("JSON replacement cleanup failed: %s", err)
-def find_paths_unserializable_data(bad_data: Union[List, Dict]) -> List[str]:
+def find_paths_unserializable_data(bad_data: Any) -> List[str]:
"""Find the paths to unserializable data.
This method is slow! Only use for error handling.
@@ -98,9 +98,9 @@ def find_paths_unserializable_data(bad_data: Union[List, Dict]) -> List[str]:
try:
json.dumps(obj)
- valid = True
+ continue
except TypeError:
- valid = False
+ pass
if isinstance(obj, dict):
for key, value in obj.items():
@@ -115,7 +115,7 @@ def find_paths_unserializable_data(bad_data: Union[List, Dict]) -> List[str]:
elif isinstance(obj, list):
for idx, value in enumerate(obj):
to_process.append((value, f"{obj_path}[{idx}]"))
- elif not valid: # type: ignore
+ else:
invalid.append(obj_path)
return invalid
diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py
index de04f23d9dd..1a46a34c1a8 100644
--- a/homeassistant/util/logging.py
+++ b/homeassistant/util/logging.py
@@ -80,16 +80,19 @@ class AsyncHandler:
def _process(self) -> None:
"""Process log in a thread."""
- while True:
- record = asyncio.run_coroutine_threadsafe(
- self._queue.get(), self.loop
- ).result()
+ try:
+ while True:
+ record = asyncio.run_coroutine_threadsafe(
+ self._queue.get(), self.loop
+ ).result()
- if record is None:
- self.handler.close()
- return
+ if record is None:
+ self.handler.close()
+ return
- self.handler.emit(record)
+ self.handler.emit(record)
+ except asyncio.CancelledError:
+ self.handler.close()
def createLock(self) -> None:
"""Ignore lock stuff."""
diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py
index 24cf8309228..9a5ae82d4a2 100644
--- a/homeassistant/util/package.py
+++ b/homeassistant/util/package.py
@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
def is_virtual_env() -> bool:
- """Return if we run in a virtual environtment."""
+ """Return if we run in a virtual environment."""
# Check supports venv && virtualenv
return getattr(sys, "base_prefix", sys.prefix) != sys.prefix or hasattr(
sys, "real_prefix"
diff --git a/requirements_all.txt b/requirements_all.txt
index 4456aae7a39..a340f99d7c6 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -5,7 +5,7 @@ async_timeout==3.0.1
attrs==19.3.0
bcrypt==3.1.7
certifi>=2019.11.28
-importlib-metadata==1.4.0
+importlib-metadata==1.5.0
jinja2>=2.10.3
PyJWT==1.7.1
cryptography==2.8
@@ -77,7 +77,7 @@ PySocks==1.7.1
PyTransportNSW==0.1.1
# homeassistant.components.vicare
-PyViCare==0.1.2
+PyViCare==0.1.7
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.12.4
@@ -108,10 +108,10 @@ YesssSMS==0.4.1
abodepy==0.17.0
# homeassistant.components.mcp23017
-adafruit-blinka==1.2.1
+adafruit-blinka==3.9.0
# homeassistant.components.mcp23017
-adafruit-circuitpython-mcp230xx==1.1.2
+adafruit-circuitpython-mcp230xx==2.2.2
# homeassistant.components.androidtv
adb-shell==0.1.1
@@ -123,13 +123,16 @@ adguardhome==0.4.1
afsapi==0.0.4
# homeassistant.components.geonetnz_quakes
-aio_geojson_geonetnz_quakes==0.11
+aio_geojson_geonetnz_quakes==0.12
# homeassistant.components.geonetnz_volcano
aio_geojson_geonetnz_volcano==0.5
# homeassistant.components.nsw_rural_fire_service_feed
-aio_geojson_nsw_rfs_incidents==0.1
+aio_geojson_nsw_rfs_incidents==0.3
+
+# homeassistant.components.gdacs
+aio_georss_gdacs==0.3
# homeassistant.components.ambient_station
aioambient==1.0.2
@@ -163,7 +166,7 @@ aioharmony==0.1.13
aiohttp_cors==0.7.0
# homeassistant.components.hue
-aiohue==1.10.1
+aiohue==2.0.0
# homeassistant.components.imap
aioimaplib==0.7.15
@@ -172,7 +175,7 @@ aioimaplib==0.7.15
aiokafka==0.5.1
# homeassistant.components.kef
-aiokef==0.2.6
+aiokef==0.2.7
# homeassistant.components.lifx
aiolifx==0.6.7
@@ -196,7 +199,7 @@ aiopylgtv==0.3.3
aioswitcher==2019.4.26
# homeassistant.components.unifi
-aiounifi==11
+aiounifi==13
# homeassistant.components.wwlln
aiowwlln==2.0.2
@@ -211,13 +214,13 @@ aladdin_connect==0.3
alarmdecoder==1.13.2
# homeassistant.components.alpha_vantage
-alpha_vantage==2.1.2
+alpha_vantage==2.1.3
# homeassistant.components.ambiclimate
ambiclimate==0.2.1
# homeassistant.components.amcrest
-amcrest==1.5.3
+amcrest==1.5.6
# homeassistant.components.androidtv
androidtv==0.0.39
@@ -235,7 +238,7 @@ apcaccess==0.0.13
apns2==0.3.0
# homeassistant.components.apprise
-apprise==0.8.3
+apprise==0.8.4
# homeassistant.components.aprs
aprslib==0.6.46
@@ -302,7 +305,7 @@ beewi_smartclim==0.0.7
bellows-homeassistant==0.13.2
# homeassistant.components.bmw_connected_drive
-bimmer_connected==0.7.0
+bimmer_connected==0.7.1
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@@ -334,13 +337,13 @@ bomradarloop==0.1.3
boto3==1.9.252
# homeassistant.components.braviatv
-braviarc-homeassistant==0.3.7.dev0
+bravia-tv==1.0
# homeassistant.components.broadlink
broadlink==0.12.0
# homeassistant.components.brother
-brother==0.1.4
+brother==0.1.6
# homeassistant.components.brottsplatskartan
brottsplatskartan==0.0.1
@@ -423,16 +426,16 @@ defusedxml==0.6.0
deluge-client==1.7.1
# homeassistant.components.denonavr
-denonavr==0.7.11
+denonavr==0.7.12
# homeassistant.components.directv
-directpy==0.5
+directpy==0.6
# homeassistant.components.discogs
discogs_client==2.2.2
# homeassistant.components.discord
-discord.py==1.2.5
+discord.py==1.3.1
# homeassistant.components.updater
distro==1.4.0
@@ -452,6 +455,9 @@ dsmr_parser==0.18
# homeassistant.components.dweet
dweepy==0.3.0
+# homeassistant.components.dynalite
+dynalite_devices==0.1.22
+
# homeassistant.components.rainforest_eagle
eagle200_reader==0.2.1
@@ -548,9 +554,6 @@ freesms==0.1.2
# homeassistant.components.fritzbox_netmonitor
fritzconnection==1.2.0
-# homeassistant.components.fritzdect
-fritzhome==1.0.4
-
# homeassistant.components.google_translate
gTTS-token==1.1.3
@@ -585,6 +588,7 @@ georss_qld_bushfire_alert_client==0.3
# homeassistant.components.braviatv
# homeassistant.components.huawei_lte
# homeassistant.components.kef
+# homeassistant.components.minecraft_server
# homeassistant.components.nmap_tracker
getmac==0.8.1
@@ -622,7 +626,7 @@ gpiozero==1.5.1
gps3==0.33.3
# homeassistant.components.greeneye_monitor
-greeneye_monitor==1.0.1
+greeneye_monitor==2.0
# homeassistant.components.greenwave
greenwavereality==0.5.1
@@ -640,7 +644,7 @@ ha-ffmpeg==2.0
ha-philipsjs==0.0.8
# homeassistant.components.plugwise
-haanna==0.14.1
+haanna==0.14.3
# homeassistant.components.habitica
habitipy==0.2.0
@@ -655,7 +659,7 @@ hass-nabucasa==0.31
hbmqtt==0.9.5
# homeassistant.components.jewish_calendar
-hdate==0.9.3
+hdate==0.9.5
# homeassistant.components.heatmiser
heatmiserV3==1.1.18
@@ -676,10 +680,10 @@ hlk-sw16==0.0.8
hole==0.5.0
# homeassistant.components.workday
-holidays==0.9.12
+holidays==0.10.1
# homeassistant.components.frontend
-home-assistant-frontend==20200130.3
+home-assistant-frontend==20200220.4
# homeassistant.components.zwave
homeassistant-pyozw==0.1.8
@@ -688,7 +692,7 @@ homeassistant-pyozw==0.1.8
homekit[IP]==0.15.0
# homeassistant.components.homematicip_cloud
-homematicip==0.10.15
+homematicip==0.10.17
# homeassistant.components.horizon
horimote==0.4.1
@@ -721,7 +725,7 @@ ibmiotf==0.3.4
iglo==1.2.7
# homeassistant.components.ihc
-ihcsdk==2.5.0
+ihcsdk==2.6.0
# homeassistant.components.incomfort
incomfort-client==0.4.0
@@ -730,7 +734,7 @@ incomfort-client==0.4.0
influxdb==5.2.3
# homeassistant.components.insteon
-insteonplm==0.16.6
+insteonplm==0.16.7
# homeassistant.components.iperf3
iperf3==0.1.11
@@ -738,6 +742,7 @@ iperf3==0.1.11
# homeassistant.components.route53
ipify==1.0.0
+# homeassistant.components.rest
# homeassistant.components.verisure
jsonpath==0.82
@@ -763,7 +768,7 @@ keyrings.alt==3.4.0
kiwiki-client==0.1.1
# homeassistant.components.konnected
-konnected==0.1.5
+konnected==1.1.0
# homeassistant.components.eufy
lakeside==0.12
@@ -801,9 +806,6 @@ limitlessled==1.1.3
# homeassistant.components.linode
linode-api==4.1.9b1
-# homeassistant.components.liveboxplaytv
-liveboxplaytv==2.0.3
-
# homeassistant.components.lametric
lmnotify==0.0.4
@@ -840,6 +842,9 @@ maxcube-api==0.1.0
# homeassistant.components.mythicbeastsdns
mbddns==0.1.2
+# homeassistant.components.minecraft_server
+mcstatus==2.3.0
+
# homeassistant.components.message_bird
messagebird==1.2.0
@@ -905,7 +910,7 @@ niko-home-control==0.2.1
niluclient==0.1.2
# homeassistant.components.nederlandse_spoorwegen
-nsapi==3.0.2
+nsapi==3.0.3
# homeassistant.components.nsw_fuel_station
nsw-fuel-api-client==1.0.10
@@ -917,7 +922,7 @@ nuheat==0.3.0
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==1.17.4
+numpy==1.18.1
# homeassistant.components.oasa_telematics
oasatelematics==0.3
@@ -1000,7 +1005,7 @@ pilight==0.1.1
# homeassistant.components.qrcode
# homeassistant.components.seven_segments
# homeassistant.components.tensorflow
-pillow==6.2.1
+pillow==7.0.0
# homeassistant.components.dominos
pizzapi==0.0.3
@@ -1025,7 +1030,7 @@ pmsensor==0.4
pocketcasts==0.1
# homeassistant.components.reddit
-praw==6.5.0
+praw==6.5.1
# homeassistant.components.islamic_prayer_times
prayer_times_calculator==0.0.3
@@ -1063,11 +1068,14 @@ pushbullet.py==0.11.0
# homeassistant.components.pushetta
pushetta==1.0.15
+# homeassistant.components.pushover
+pushover_complete==1.1.1
+
# homeassistant.components.rpi_gpio_pwm
-pwmled==1.4.1
+pwmled==1.5.0
# homeassistant.components.august
-py-august==0.8.1
+py-august==0.14.0
# homeassistant.components.canary
py-canary==0.5.0
@@ -1101,7 +1109,7 @@ pyRFXtrx==0.25
# pySwitchmate==0.4.6
# homeassistant.components.tibber
-pyTibber==0.12.0
+pyTibber==0.12.2
# homeassistant.components.dlink
pyW215==0.6.0
@@ -1137,7 +1145,7 @@ pyalmond==0.0.2
pyarlo==0.2.3
# homeassistant.components.netatmo
-pyatmo==3.2.2
+pyatmo==3.2.4
# homeassistant.components.atome
pyatome==0.1.1
@@ -1191,7 +1199,7 @@ pydaikin==1.6.2
pydanfossair==0.1.0
# homeassistant.components.deconz
-pydeconz==69
+pydeconz==70
# homeassistant.components.delijn
pydelijn==0.5.1
@@ -1205,9 +1213,6 @@ pydoods==1.0.2
# homeassistant.components.android_ip_webcam
pydroid-ipcam==0.8
-# homeassistant.components.duke_energy
-pydukeenergy==0.0.6
-
# homeassistant.components.ebox
pyebox==1.1.4
@@ -1215,10 +1220,10 @@ pyebox==1.1.4
pyeconet==0.0.11
# homeassistant.components.edimax
-pyedimax==0.1
+pyedimax==0.2.1
# homeassistant.components.eight_sleep
-pyeight==0.1.2
+pyeight==0.1.3
# homeassistant.components.emby
pyemby==1.6
@@ -1285,7 +1290,7 @@ pyhik==0.2.5
pyhiveapi==0.2.19.3
# homeassistant.components.homematic
-pyhomematic==0.1.63
+pyhomematic==0.1.64
# homeassistant.components.homeworks
pyhomeworks==0.0.6
@@ -1300,7 +1305,7 @@ pyicloud==0.9.2
pyintesishome==1.6
# homeassistant.components.ipma
-pyipma==2.0.2
+pyipma==2.0.3
# homeassistant.components.iqvia
pyiqvia==0.2.1
@@ -1353,6 +1358,9 @@ pymailgunner==1.4
# homeassistant.components.mediaroom
pymediaroom==0.6.4
+# homeassistant.components.melcloud
+pymelcloud==2.1.0
+
# homeassistant.components.somfy
pymfy==0.7.1
@@ -1452,7 +1460,7 @@ pypjlink2==1.2.0
pypoint==1.1.2
# homeassistant.components.ps4
-pyps4-2ndscreen==1.0.6
+pyps4-2ndscreen==1.0.7
# homeassistant.components.qwikswitch
pyqwikswitch==0.93
@@ -1494,7 +1502,7 @@ pysesame2==1.0.1
pysher==1.0.1
# homeassistant.components.signal_messenger
-pysignalclirestapi==0.1.4
+pysignalclirestapi==0.2.4
# homeassistant.components.sma
pysma==0.3.5
@@ -1535,9 +1543,6 @@ pysyncthru==0.5.0
# homeassistant.components.tautulli
pytautulli==0.5.0
-# homeassistant.components.liveboxplaytv
-pyteleloisirs==3.6
-
# homeassistant.components.tfiac
pytfiac==0.4
@@ -1554,7 +1559,7 @@ python-clementine-remote==1.0.1
python-digitalocean==1.13.2
# homeassistant.components.ecobee
-python-ecobee-api==0.1.4
+python-ecobee-api==0.2.1
# homeassistant.components.eq3btsmart
# python-eq3bt==0.1.11
@@ -1607,9 +1612,6 @@ python-nest==4.1.0
# homeassistant.components.nmap_tracker
python-nmap==0.6.1
-# homeassistant.components.pushover
-python-pushover==0.4
-
# homeassistant.components.qbittorrent
python-qbittorrent==0.4.1
@@ -1626,7 +1628,7 @@ python-songpal==0.11.2
python-synology==0.4.0
# homeassistant.components.tado
-python-tado==0.2.9
+python-tado==0.3.0
# homeassistant.components.telegram_bot
python-telegram-bot==11.1.0
@@ -1638,7 +1640,7 @@ python-telnet-vlc==1.0.4
python-twitch-client==0.6.0
# homeassistant.components.velbus
-python-velbus==2.0.36
+python-velbus==2.0.41
# homeassistant.components.vlc
python-vlc==1.1.2
@@ -1675,7 +1677,7 @@ pytradfri[async]==6.4.0
# homeassistant.components.trafikverket_train
# homeassistant.components.trafikverket_weatherstation
-pytrafikverket==0.1.5.9
+pytrafikverket==0.1.6.1
# homeassistant.components.ubee
pyubee==0.8
@@ -1696,7 +1698,7 @@ pyversasense==0.0.6
pyvesync==1.1.0
# homeassistant.components.vizio
-pyvizio==0.1.4
+pyvizio==0.1.21
# homeassistant.components.velux
pyvlx==0.2.12
@@ -1785,6 +1787,9 @@ russound_rio==0.1.7
# homeassistant.components.yamaha
rxv==0.6.0
+# homeassistant.components.salt
+saltbox==0.1.3
+
# homeassistant.components.samsungtv
samsungctl[websocket]==0.7.1
@@ -1798,7 +1803,7 @@ schiene==0.23
scsgate==0.1.0
# homeassistant.components.sendgrid
-sendgrid==6.1.0
+sendgrid==6.1.1
# homeassistant.components.sensehat
sense-hat==2.2.0
@@ -1822,7 +1827,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
-simplisafe-python==6.1.0
+simplisafe-python==8.1.1
# homeassistant.components.sisyphus
sisyphus-control==2.2.1
@@ -1860,7 +1865,7 @@ smhi-pkg==1.0.10
snapcast==2.0.10
# homeassistant.components.socialblade
-socialbladeclient==0.2
+socialbladeclient==0.5
# homeassistant.components.solaredge_local
solaredge-local==0.2.0
@@ -1927,7 +1932,7 @@ sucks==0.9.4
sunwatcher==0.2.1
# homeassistant.components.surepetcare
-surepy==0.1.10
+surepy==0.2.3
# homeassistant.components.swiss_hydrological_data
swisshydrodata==0.0.3
@@ -1984,7 +1989,7 @@ todoist-python==8.0.0
toonapilib==3.2.4
# homeassistant.components.totalconnect
-total_connect_client==0.28
+total_connect_client==0.50
# homeassistant.components.tplink_lte
tp-connected==0.0.4
@@ -2001,6 +2006,9 @@ twentemilieu==0.2.0
# homeassistant.components.twilio
twilio==6.32.0
+# homeassistant.components.rainforest_eagle
+uEagle==0.0.1
+
# homeassistant.components.unifiled
unifiled==0.11
@@ -2017,7 +2025,7 @@ uscisstatus==0.1.1
uvcclient==0.11.0
# homeassistant.components.vallox
-vallox-websocket-api==2.2.0
+vallox-websocket-api==2.4.0
# homeassistant.components.venstar
venstarcolortouch==0.12
@@ -2025,6 +2033,9 @@ venstarcolortouch==0.12
# homeassistant.components.meteo_france
vigilancemeteo==3.0.0
+# homeassistant.components.vilfo
+vilfo-api-client==0.3.2
+
# homeassistant.components.volkszaehler
volkszaehler==0.1.2
@@ -2084,6 +2095,7 @@ xfinity-gateway==0.0.4
xknx==0.11.2
# homeassistant.components.bluesound
+# homeassistant.components.rest
# homeassistant.components.startca
# homeassistant.components.ted5000
# homeassistant.components.yr
@@ -2109,7 +2121,7 @@ yeelight==0.5.0
yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2020.01.24
+youtube_dl==2020.02.16
# homeassistant.components.zengge
zengge==0.2
@@ -2118,7 +2130,7 @@ zengge==0.2
zeroconf==0.24.4
# homeassistant.components.zha
-zha-quirks==0.0.32
+zha-quirks==0.0.33
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -2126,6 +2138,9 @@ zhong_hong_hvac==1.0.9
# homeassistant.components.ziggo_mediabox_xl
ziggo-mediabox-xl==1.1.0
+# homeassistant.components.zha
+zigpy-cc==0.1.0
+
# homeassistant.components.zha
zigpy-deconz==0.7.0
diff --git a/requirements_test.txt b/requirements_test.txt
index b8ab2c23040..db76d1ec46b 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -7,7 +7,7 @@ asynctest==0.13.0
codecov==2.0.15
mock-open==1.3.1
mypy==0.761
-pre-commit==2.0.0
+pre-commit==2.1.0
pylint==2.4.4
astroid==2.3.3
pylint-strict-informational==0.1
@@ -15,6 +15,6 @@ pytest-aiohttp==0.3.0
pytest-cov==2.8.1
pytest-sugar==0.9.2
pytest-timeout==1.3.3
-pytest==5.3.4
+pytest==5.3.5
requests_mock==1.7.0
responses==0.10.6
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 6cb9a63a208..be073a4b929 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -35,13 +35,16 @@ adb-shell==0.1.1
adguardhome==0.4.1
# homeassistant.components.geonetnz_quakes
-aio_geojson_geonetnz_quakes==0.11
+aio_geojson_geonetnz_quakes==0.12
# homeassistant.components.geonetnz_volcano
aio_geojson_geonetnz_volcano==0.5
# homeassistant.components.nsw_rural_fire_service_feed
-aio_geojson_nsw_rfs_incidents==0.1
+aio_geojson_nsw_rfs_incidents==0.3
+
+# homeassistant.components.gdacs
+aio_georss_gdacs==0.3
# homeassistant.components.ambient_station
aioambient==1.0.2
@@ -63,7 +66,7 @@ aioesphomeapi==2.6.1
aiohttp_cors==0.7.0
# homeassistant.components.hue
-aiohue==1.10.1
+aiohue==2.0.0
# homeassistant.components.notion
aionotion==1.1.0
@@ -75,7 +78,7 @@ aiopylgtv==0.3.3
aioswitcher==2019.4.26
# homeassistant.components.unifi
-aiounifi==11
+aiounifi==13
# homeassistant.components.wwlln
aiowwlln==2.0.2
@@ -93,7 +96,7 @@ androidtv==0.0.39
apns2==0.3.0
# homeassistant.components.apprise
-apprise==0.8.3
+apprise==0.8.4
# homeassistant.components.aprs
aprslib==0.6.46
@@ -121,7 +124,7 @@ bomradarloop==0.1.3
broadlink==0.12.0
# homeassistant.components.brother
-brother==0.1.4
+brother==0.1.6
# homeassistant.components.buienradar
buienradar==1.0.1
@@ -153,10 +156,10 @@ datadog==0.15.0
defusedxml==0.6.0
# homeassistant.components.denonavr
-denonavr==0.7.11
+denonavr==0.7.12
# homeassistant.components.directv
-directpy==0.5
+directpy==0.6
# homeassistant.components.updater
distro==1.4.0
@@ -164,6 +167,9 @@ distro==1.4.0
# homeassistant.components.dsmr
dsmr_parser==0.18
+# homeassistant.components.dynalite
+dynalite_devices==0.1.22
+
# homeassistant.components.ee_brightbox
eebrightbox==0.0.4
@@ -207,6 +213,7 @@ georss_qld_bushfire_alert_client==0.3
# homeassistant.components.braviatv
# homeassistant.components.huawei_lte
# homeassistant.components.kef
+# homeassistant.components.minecraft_server
# homeassistant.components.nmap_tracker
getmac==0.8.1
@@ -235,7 +242,7 @@ hass-nabucasa==0.31
hbmqtt==0.9.5
# homeassistant.components.jewish_calendar
-hdate==0.9.3
+hdate==0.9.5
# homeassistant.components.here_travel_time
herepy==2.0.0
@@ -244,10 +251,10 @@ herepy==2.0.0
hole==0.5.0
# homeassistant.components.workday
-holidays==0.9.12
+holidays==0.10.1
# homeassistant.components.frontend
-home-assistant-frontend==20200130.3
+home-assistant-frontend==20200220.4
# homeassistant.components.zwave
homeassistant-pyozw==0.1.8
@@ -256,7 +263,7 @@ homeassistant-pyozw==0.1.8
homekit[IP]==0.15.0
# homeassistant.components.homematicip_cloud
-homematicip==0.10.15
+homematicip==0.10.17
# homeassistant.components.google
# homeassistant.components.remember_the_milk
@@ -271,6 +278,7 @@ iaqualink==0.3.1
# homeassistant.components.influxdb
influxdb==5.2.3
+# homeassistant.components.rest
# homeassistant.components.verisure
jsonpath==0.82
@@ -280,6 +288,9 @@ keyring==20.0.0
# homeassistant.scripts.keyring
keyrings.alt==3.4.0
+# homeassistant.components.konnected
+konnected==1.1.0
+
# homeassistant.components.dyson
libpurecool==0.6.1
@@ -298,6 +309,12 @@ luftdaten==0.6.3
# homeassistant.components.mythicbeastsdns
mbddns==0.1.2
+# homeassistant.components.minecraft_server
+mcstatus==2.3.0
+
+# homeassistant.components.meteo_france
+meteofrance==0.3.7
+
# homeassistant.components.mfi
mficlient==0.3.0
@@ -324,7 +341,7 @@ nuheat==0.3.0
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==1.17.4
+numpy==1.18.1
# homeassistant.components.google
oauth2client==4.0.0
@@ -356,7 +373,7 @@ plexwebsocket==0.0.6
pmsensor==0.4
# homeassistant.components.reddit
-praw==6.5.0
+praw==6.5.1
# homeassistant.components.islamic_prayer_times
prayer_times_calculator==0.0.3
@@ -373,6 +390,9 @@ pure-python-adb==0.2.2.dev0
# homeassistant.components.pushbullet
pushbullet.py==0.11.0
+# homeassistant.components.august
+py-august==0.14.0
+
# homeassistant.components.canary
py-canary==0.5.0
@@ -405,7 +425,7 @@ pyalmond==0.0.2
pyarlo==0.2.3
# homeassistant.components.netatmo
-pyatmo==3.2.2
+pyatmo==3.2.4
# homeassistant.components.blackbird
pyblackbird==0.5
@@ -423,7 +443,7 @@ pycoolmasternet==0.0.4
pydaikin==1.6.2
# homeassistant.components.deconz
-pydeconz==69
+pydeconz==70
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -451,13 +471,13 @@ pyhaversion==3.2.0
pyheos==0.6.0
# homeassistant.components.homematic
-pyhomematic==0.1.63
+pyhomematic==0.1.64
# homeassistant.components.icloud
pyicloud==0.9.2
# homeassistant.components.ipma
-pyipma==2.0.2
+pyipma==2.0.3
# homeassistant.components.iqvia
pyiqvia==0.2.1
@@ -474,6 +494,9 @@ pylitejet==0.1
# homeassistant.components.mailgun
pymailgunner==1.4
+# homeassistant.components.melcloud
+pymelcloud==2.1.0
+
# homeassistant.components.somfy
pymfy==0.7.1
@@ -510,11 +533,14 @@ pyotp==2.3.0
pypoint==1.1.2
# homeassistant.components.ps4
-pyps4-2ndscreen==1.0.6
+pyps4-2ndscreen==1.0.7
# homeassistant.components.qwikswitch
pyqwikswitch==0.93
+# homeassistant.components.signal_messenger
+pysignalclirestapi==0.2.4
+
# homeassistant.components.sma
pysma==0.3.5
@@ -534,7 +560,7 @@ pysonos==0.0.24
pyspcwebgw==0.4.0
# homeassistant.components.ecobee
-python-ecobee-api==0.1.4
+python-ecobee-api==0.2.1
# homeassistant.components.darksky
python-forecastio==1.4.0
@@ -548,8 +574,11 @@ python-miio==0.4.8
# homeassistant.components.nest
python-nest==4.1.0
+# homeassistant.components.twitch
+python-twitch-client==0.6.0
+
# homeassistant.components.velbus
-python-velbus==2.0.36
+python-velbus==2.0.41
# homeassistant.components.awair
python_awair==0.0.4
@@ -567,7 +596,7 @@ pyvera==0.3.7
pyvesync==1.1.0
# homeassistant.components.vizio
-pyvizio==0.1.4
+pyvizio==0.1.21
# homeassistant.components.html5
pywebpush==1.9.2
@@ -597,7 +626,7 @@ sentry-sdk==0.13.5
simplehound==0.3
# homeassistant.components.simplisafe
-simplisafe-python==6.1.0
+simplisafe-python==8.1.1
# homeassistant.components.sleepiq
sleepyq==0.7
@@ -660,6 +689,12 @@ url-normalize==1.4.1
# homeassistant.components.uvc
uvcclient==0.11.0
+# homeassistant.components.meteo_france
+vigilancemeteo==3.0.0
+
+# homeassistant.components.vilfo
+vilfo-api-client==0.3.2
+
# homeassistant.components.verisure
vsure==1.5.4
@@ -680,6 +715,7 @@ withings-api==2.1.3
wled==0.2.1
# homeassistant.components.bluesound
+# homeassistant.components.rest
# homeassistant.components.startca
# homeassistant.components.ted5000
# homeassistant.components.yr
@@ -696,7 +732,10 @@ yahooweather==0.10
zeroconf==0.24.4
# homeassistant.components.zha
-zha-quirks==0.0.32
+zha-quirks==0.0.33
+
+# homeassistant.components.zha
+zigpy-cc==0.1.0
# homeassistant.components.zha
zigpy-deconz==0.7.0
diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt
index 8af2cbb6123..b0deb01b3da 100644
--- a/requirements_test_pre_commit.txt
+++ b/requirements_test_pre_commit.txt
@@ -1,7 +1,8 @@
-# Automatically generated from .pre-commit-config-all.yaml by gen_requirements_all.py, do not edit
+# Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit
bandit==1.6.2
black==19.10b0
+codespell==v1.16.0
flake8-docstrings==1.5.0
flake8==3.7.9
isort==v4.3.21
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index 3b30bf04363..c4a94f99b18 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -58,6 +58,9 @@ CONSTRAINT_PATH = os.path.join(
CONSTRAINT_BASE = """
pycryptodome>=3.6.6
+# Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324
+urllib3>=1.24.3
+
# Not needed for our supported Python versions
enum34==1000000000.0.0
@@ -253,7 +256,7 @@ def requirements_test_output(reqs):
def requirements_pre_commit_output():
"""Generate output for pre-commit dependencies."""
- source = ".pre-commit-config-all.yaml"
+ source = ".pre-commit-config.yaml"
pre_commit_conf = load_yaml(source)
reqs = []
for repo in (x for x in pre_commit_conf["repos"] if x.get("rev")):
diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py
index 52c8bfecf95..c909b6216a9 100644
--- a/script/hassfest/dependencies.py
+++ b/script/hassfest/dependencies.py
@@ -103,14 +103,12 @@ ALLOWED_USED_COMPONENTS = {
"homeassistant",
"system_log",
"person",
- # Discovery
- "discovery",
# Other
"mjpeg", # base class, has no reqs or component to load.
"stream", # Stream cannot install on all systems, can be imported without reqs.
}
-IGNORE_VIOLATIONS = [
+IGNORE_VIOLATIONS = {
# Has same requirement, gets defaults.
("sql", "recorder"),
# Sharing a base class
@@ -122,6 +120,7 @@ IGNORE_VIOLATIONS = [
("demo", "openalpr_local"),
# This should become a helper method that integrations can submit data to
("websocket_api", "lovelace"),
+ ("websocket_api", "shopping_list"),
# Expose HA to external systems
"homekit",
"alexa",
@@ -134,9 +133,7 @@ IGNORE_VIOLATIONS = [
# These should be extracted to external package
"pvoutput",
"dwd_weather_warnings",
- # Should be rewritten to use own data fetcher
- "scrape",
-]
+}
def calc_allowed_references(integration: Integration) -> Set[str]:
diff --git a/script/run-in-env.sh b/script/run-in-env.sh
new file mode 100755
index 00000000000..d9fe17f4b17
--- /dev/null
+++ b/script/run-in-env.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env sh -eu
+
+# Activate pyenv and virtualenv if present, then run the specified command
+
+# pyenv, pyenv-virtualenv
+if [ -s .python-version ]; then
+ PYENV_VERSION=$(head -n 1 .python-version)
+ export PYENV_VERSION
+fi
+
+# other common virtualenvs
+for venv in venv .venv .; do
+ if [ -f $venv/bin/activate ]; then
+ . $venv/bin/activate
+ fi
+done
+
+exec "$@"
diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py
index 8fa2814e54f..d3b68914104 100644
--- a/script/scaffold/__main__.py
+++ b/script/scaffold/__main__.py
@@ -82,6 +82,12 @@ def main():
subprocess.run(["python", "-m", "script.gen_requirements_all"], **pipe_null)
print()
+ print("Running script/translations_develop to pick up new translation strings.")
+ subprocess.run(
+ ["script/translations_develop", "--integration", info.domain], **pipe_null
+ )
+ print()
+
if args.develop:
print("Running tests")
print(f"$ pytest -vvv tests/components/{info.domain}")
diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py
index 48d0a20ea73..fda5081e7c3 100644
--- a/script/scaffold/gather_info.py
+++ b/script/scaffold/gather_info.py
@@ -56,7 +56,7 @@ def gather_info(arguments) -> Info:
YES_NO = {
"validators": [["Type either 'yes' or 'no'", lambda value: value in ("yes", "no")]],
- "convertor": lambda value: value == "yes",
+ "converter": lambda value: value == "yes",
}
@@ -155,8 +155,8 @@ def _gather_info(fields) -> dict:
break
if hint is None:
- if "convertor" in info:
- value = info["convertor"](value)
+ if "converter" in info:
+ value = info["converter"](value)
answers[key] = value
return answers
diff --git a/script/translations_develop b/script/translations_develop
index eb9d685fa8e..f0976f3d676 100755
--- a/script/translations_develop
+++ b/script/translations_develop
@@ -1,21 +1,81 @@
-#!/usr/bin/env bash
+#!/usr/bin/env python
# Compile the current translation strings files for testing
-# Safe bash settings
-# -e Exit on command fail
-# -u Exit on unset variable
-# -o pipefail Exit if piped command has error code
-set -eu -o pipefail
+import argparse
+import json
+import os
+from pathlib import Path
+from shutil import rmtree
+import subprocess
+import sys
-cd "$(dirname "$0")/.."
-mkdir -p build/translations-download
+def valid_integration(integration):
+ """Test if it's a valid integration."""
+ if not Path(f"homeassistant/components/{integration}").exists():
+ raise argparse.ArgumentTypeError(
+ f"The integration {integration} does not exist."
+ )
-script/translations_upload_merge.py
+ return integration
-# Use the generated translations upload file as the mock output from the
-# Lokalise download
-mv build/translations-upload.json build/translations-download/en.json
-script/translations_download_split.py
+def get_arguments() -> argparse.Namespace:
+ """Get parsed passed in arguments."""
+ parser = argparse.ArgumentParser(description="Develop Translations")
+ parser.add_argument(
+ "--integration", type=valid_integration, help="Integration to process."
+ )
+
+ arguments = parser.parse_args()
+
+ return arguments
+
+
+def main():
+ """Run the script."""
+ if not os.path.isfile("requirements_all.txt"):
+ print("Run this from HA root dir")
+ return
+
+ args = get_arguments()
+ if args.integration:
+ integration = args.integration
+ else:
+ integration = None
+ while (
+ integration is None
+ or not Path(f"homeassistant/components/{integration}").exists()
+ ):
+ if integration is not None:
+ print(f"Integration {integration} doesn't exist!")
+ print()
+ integration = input("Integration to process: ")
+
+ download_dir = Path("build/translations-download")
+
+ if download_dir.is_dir():
+ rmtree(str(download_dir))
+
+ download_dir.mkdir(parents=True)
+
+ subprocess.run("script/translations_upload_merge.py")
+
+ raw_data = json.loads(Path("build/translations-upload.json").read_text())
+
+ if integration not in raw_data["component"]:
+ print("Integration has no strings.json")
+ sys.exit(1)
+
+ Path("build/translations-download/en.json").write_text(
+ json.dumps({"component": {integration: raw_data["component"][integration]}})
+ )
+
+ subprocess.run(
+ ["script/translations_download_split.py", "--integration", "{integration}"]
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/setup.py b/setup.py
index 521b9f2678c..7f9155d9a05 100755
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,7 @@ REQUIRES = [
"attrs==19.3.0",
"bcrypt==3.1.7",
"certifi>=2019.11.28",
- "importlib-metadata==1.4.0",
+ "importlib-metadata==1.5.0",
"jinja2>=2.10.3",
"PyJWT==1.7.1",
# PyJWT has loose dependency. We want the latest one.
diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py
index bc4ecaab712..c79d76baf4f 100644
--- a/tests/auth/mfa_modules/test_notify.py
+++ b/tests/auth/mfa_modules/test_notify.py
@@ -321,7 +321,7 @@ async def test_include_exclude_config(hass):
async def test_setup_user_no_notify_service(hass):
- """Test setup flow abort if there is no avilable notify service."""
+ """Test setup flow abort if there is no available notify service."""
async_mock_service(hass, "notify", "test1", NOTIFY_SERVICE_SCHEMA)
notify_auth_module = await auth_mfa_module_from_config(
hass, {"type": "notify", "exclude": "test1"}
diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py
index 2ff75c579e5..82c0c0dbdbd 100644
--- a/tests/auth/test_init.py
+++ b/tests/auth/test_init.py
@@ -31,7 +31,7 @@ async def test_auth_manager_from_config_validates_config(mock_hass):
[
{"name": "Test Name", "type": "insecure_example", "users": []},
{
- "name": "Invalid config because no users",
+ "name": "Invalid configuration because no users",
"type": "insecure_example",
"id": "invalid_config",
},
@@ -81,7 +81,7 @@ async def test_auth_manager_from_config_auth_modules(mock_hass):
[
{"name": "Module 1", "type": "insecure_example", "data": []},
{
- "name": "Invalid config because no data",
+ "name": "Invalid configuration because no data",
"type": "insecure_example",
"id": "another",
},
@@ -453,7 +453,7 @@ async def test_refresh_token_type_long_lived_access_token(hass):
async def test_cannot_deactive_owner(mock_hass):
- """Test that we cannot deactive the owner."""
+ """Test that we cannot deactivate the owner."""
manager = await auth.auth_manager_from_config(mock_hass, [], [])
owner = MockUser(is_owner=True).add_to_auth_manager(manager)
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index ca6b1e1ccb6..a714b69461c 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -16,6 +16,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
+ SUPPORT_VOLUME_STEP,
)
import homeassistant.components.vacuum as vacuum
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
@@ -886,6 +887,7 @@ async def test_media_player(hass):
| SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET,
"volume_level": 0.75,
+ "source_list": ["hdmi", "tv"],
},
)
appliance = await discovery_test(device, hass)
@@ -904,7 +906,6 @@ async def test_media_player(hass):
"Alexa.PlaybackStateReporter",
"Alexa.PowerController",
"Alexa.Speaker",
- "Alexa.StepSpeaker",
)
playback_capability = get_capability(capabilities, "Alexa.PlaybackController")
@@ -958,93 +959,6 @@ async def test_media_player(hass):
hass,
)
- call, _ = await assert_request_calls_service(
- "Alexa.Speaker",
- "SetVolume",
- "media_player#test",
- "media_player.volume_set",
- hass,
- payload={"volume": 50},
- )
- assert call.data["volume_level"] == 0.5
-
- call, _ = await assert_request_calls_service(
- "Alexa.Speaker",
- "SetMute",
- "media_player#test",
- "media_player.volume_mute",
- hass,
- payload={"mute": True},
- )
- assert call.data["is_volume_muted"]
-
- call, _, = await assert_request_calls_service(
- "Alexa.Speaker",
- "SetMute",
- "media_player#test",
- "media_player.volume_mute",
- hass,
- payload={"mute": False},
- )
- assert not call.data["is_volume_muted"]
-
- await assert_percentage_changes(
- hass,
- [(0.7, "-5"), (0.8, "5"), (0, "-80")],
- "Alexa.Speaker",
- "AdjustVolume",
- "media_player#test",
- "volume",
- "media_player.volume_set",
- "volume_level",
- )
-
- call, _ = await assert_request_calls_service(
- "Alexa.StepSpeaker",
- "SetMute",
- "media_player#test",
- "media_player.volume_mute",
- hass,
- payload={"mute": True},
- )
- assert call.data["is_volume_muted"]
-
- call, _, = await assert_request_calls_service(
- "Alexa.StepSpeaker",
- "SetMute",
- "media_player#test",
- "media_player.volume_mute",
- hass,
- payload={"mute": False},
- )
- assert not call.data["is_volume_muted"]
-
- call, _ = await assert_request_calls_service(
- "Alexa.StepSpeaker",
- "AdjustVolume",
- "media_player#test",
- "media_player.volume_up",
- hass,
- payload={"volumeSteps": 1, "volumeStepsDefault": False},
- )
-
- call, _ = await assert_request_calls_service(
- "Alexa.StepSpeaker",
- "AdjustVolume",
- "media_player#test",
- "media_player.volume_down",
- hass,
- 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",
@@ -1134,13 +1048,11 @@ async def test_media_player_power(hass):
"Alexa",
"Alexa.ChannelController",
"Alexa.EndpointHealth",
- "Alexa.InputController",
"Alexa.PlaybackController",
"Alexa.PlaybackStateReporter",
"Alexa.PowerController",
"Alexa.SeekController",
"Alexa.Speaker",
- "Alexa.StepSpeaker",
)
await assert_request_calls_service(
@@ -1264,23 +1176,177 @@ async def test_media_player_inputs(hass):
assert call.data["source"] == "tv"
-async def test_media_player_speaker(hass):
- """Test media player discovery with device class speaker."""
+async def test_media_player_no_supported_inputs(hass):
+ """Test media player discovery with no supported inputs."""
device = (
- "media_player.test",
+ "media_player.test_no_inputs",
"off",
{
"friendly_name": "Test media player",
- "supported_features": 51765,
+ "supported_features": SUPPORT_SELECT_SOURCE,
+ "volume_level": 0.75,
+ "source_list": [
+ "foo",
+ "foo_2",
+ "vcr",
+ "betamax",
+ "record_player",
+ "f.m.",
+ "a.m.",
+ "tape_deck",
+ "laser_disc",
+ "hd_dvd",
+ ],
+ },
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance["endpointId"] == "media_player#test_no_inputs"
+ assert appliance["displayCategories"][0] == "TV"
+ assert appliance["friendlyName"] == "Test media player"
+
+ # Assert Alexa.InputController is not in capabilities list.
+ assert_endpoint_capabilities(
+ appliance, "Alexa", "Alexa.EndpointHealth", "Alexa.PowerController"
+ )
+
+
+async def test_media_player_speaker(hass):
+ """Test media player with speaker interface."""
+ device = (
+ "media_player.test_speaker",
+ "off",
+ {
+ "friendly_name": "Test media player speaker",
+ "supported_features": SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET,
"volume_level": 0.75,
"device_class": "speaker",
},
)
appliance = await discovery_test(device, hass)
- assert appliance["endpointId"] == "media_player#test"
+ assert appliance["endpointId"] == "media_player#test_speaker"
assert appliance["displayCategories"][0] == "SPEAKER"
- assert appliance["friendlyName"] == "Test media player"
+ assert appliance["friendlyName"] == "Test media player speaker"
+
+ capabilities = assert_endpoint_capabilities(
+ appliance,
+ "Alexa",
+ "Alexa.EndpointHealth",
+ "Alexa.PowerController",
+ "Alexa.Speaker",
+ )
+
+ speaker_capability = get_capability(capabilities, "Alexa.Speaker")
+ properties = speaker_capability["properties"]
+ assert {"name": "volume"} in properties["supported"]
+ assert {"name": "muted"} in properties["supported"]
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.Speaker",
+ "SetVolume",
+ "media_player#test_speaker",
+ "media_player.volume_set",
+ hass,
+ payload={"volume": 50},
+ )
+ assert call.data["volume_level"] == 0.5
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.Speaker",
+ "SetMute",
+ "media_player#test_speaker",
+ "media_player.volume_mute",
+ hass,
+ payload={"mute": True},
+ )
+ assert call.data["is_volume_muted"]
+
+ call, _, = await assert_request_calls_service(
+ "Alexa.Speaker",
+ "SetMute",
+ "media_player#test_speaker",
+ "media_player.volume_mute",
+ hass,
+ payload={"mute": False},
+ )
+ assert not call.data["is_volume_muted"]
+
+ await assert_percentage_changes(
+ hass,
+ [(0.7, "-5"), (0.8, "5"), (0, "-80")],
+ "Alexa.Speaker",
+ "AdjustVolume",
+ "media_player#test_speaker",
+ "volume",
+ "media_player.volume_set",
+ "volume_level",
+ )
+
+
+async def test_media_player_step_speaker(hass):
+ """Test media player with step speaker interface."""
+ device = (
+ "media_player.test_step_speaker",
+ "off",
+ {
+ "friendly_name": "Test media player step speaker",
+ "supported_features": SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP,
+ "device_class": "speaker",
+ },
+ )
+ appliance = await discovery_test(device, hass)
+
+ assert appliance["endpointId"] == "media_player#test_step_speaker"
+ assert appliance["displayCategories"][0] == "SPEAKER"
+ assert appliance["friendlyName"] == "Test media player step speaker"
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.StepSpeaker",
+ "SetMute",
+ "media_player#test_step_speaker",
+ "media_player.volume_mute",
+ hass,
+ payload={"mute": True},
+ )
+ assert call.data["is_volume_muted"]
+
+ call, _, = await assert_request_calls_service(
+ "Alexa.StepSpeaker",
+ "SetMute",
+ "media_player#test_step_speaker",
+ "media_player.volume_mute",
+ hass,
+ payload={"mute": False},
+ )
+ assert not call.data["is_volume_muted"]
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.StepSpeaker",
+ "AdjustVolume",
+ "media_player#test_step_speaker",
+ "media_player.volume_up",
+ hass,
+ payload={"volumeSteps": 1, "volumeStepsDefault": False},
+ )
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.StepSpeaker",
+ "AdjustVolume",
+ "media_player#test_step_speaker",
+ "media_player.volume_down",
+ hass,
+ payload={"volumeSteps": -1, "volumeStepsDefault": False},
+ )
+
+ call, _ = await assert_request_calls_service(
+ "Alexa.StepSpeaker",
+ "AdjustVolume",
+ "media_player#test_step_speaker",
+ "media_player.volume_up",
+ hass,
+ payload={"volumeSteps": 10, "volumeStepsDefault": True},
+ )
async def test_media_player_seek(hass):
diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py
index dbc08a43bfa..01c8b27bfcc 100644
--- a/tests/components/api/test_init.py
+++ b/tests/components/api/test_init.py
@@ -541,7 +541,7 @@ async def test_rendering_template_legacy_user(
async def test_api_call_service_not_found(hass, mock_api_client):
- """Test if the API failes 400 if unknown service."""
+ """Test if the API fails 400 if unknown service."""
resp = await mock_api_client.post(
const.URL_API_SERVICES_SERVICE.format("test_domain", "test_service")
)
@@ -549,7 +549,7 @@ async def test_api_call_service_not_found(hass, mock_api_client):
async def test_api_call_service_bad_data(hass, mock_api_client):
- """Test if the API failes 400 if unknown service."""
+ """Test if the API fails 400 if unknown service."""
test_value = []
@ha.callback
diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py
index a275e57653d..8135f4e8e2c 100644
--- a/tests/components/apprise/test_notify.py
+++ b/tests/components/apprise/test_notify.py
@@ -95,7 +95,7 @@ async def test_apprise_notification(hass):
assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()
- # Test the existance of our service
+ # Test the existence of our service
assert hass.services.has_service(BASE_COMPONENT, "test")
# Test the call to our underlining notify() call
@@ -134,7 +134,7 @@ async def test_apprise_notification_with_target(hass, tmp_path):
assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()
- # Test the existance of our service
+ # Test the existence of our service
assert hass.services.has_service(BASE_COMPONENT, "test")
# Test the call to our underlining notify() call
diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py
index 8448a25a7fd..2ff31a8fd4f 100644
--- a/tests/components/arcam_fmj/test_media_player.py
+++ b/tests/components/arcam_fmj/test_media_player.py
@@ -123,7 +123,7 @@ async def test_turn_off(player, state):
@pytest.mark.parametrize("mute", [True, False])
async def test_mute_volume(player, state, mute):
- """Test mute functionallity."""
+ """Test mute functionality."""
await player.async_mute_volume(mute)
state.set_mute.assert_called_with(mute)
player.async_schedule_update_ha_state.assert_called_with()
@@ -200,14 +200,14 @@ async def test_select_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_m
async def test_volume_up(player, state):
- """Test mute functionallity."""
+ """Test mute functionality."""
await player.async_volume_up()
state.inc_volume.assert_called_with()
player.async_schedule_update_ha_state.assert_called_with()
async def test_volume_down(player, state):
- """Test mute functionallity."""
+ """Test mute functionality."""
await player.async_volume_down()
state.dec_volume.assert_called_with()
player.async_schedule_update_ha_state.assert_called_with()
diff --git a/tests/components/august/__init__.py b/tests/components/august/__init__.py
new file mode 100644
index 00000000000..156b6170511
--- /dev/null
+++ b/tests/components/august/__init__.py
@@ -0,0 +1 @@
+"""Tests for the august component."""
diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py
new file mode 100644
index 00000000000..9be8f697b8b
--- /dev/null
+++ b/tests/components/august/mocks.py
@@ -0,0 +1,195 @@
+"""Mocks for the august component."""
+import datetime
+from unittest.mock import MagicMock, PropertyMock
+
+from august.activity import Activity
+from august.api import Api
+from august.exceptions import AugustApiHTTPError
+from august.lock import Lock, LockDetail
+
+from homeassistant.components.august import AugustData
+from homeassistant.components.august.binary_sensor import AugustDoorBinarySensor
+from homeassistant.components.august.lock import AugustLock
+from homeassistant.util import dt
+
+
+class MockAugustApiFailing(Api):
+ """A mock for py-august Api class that always has an AugustApiHTTPError."""
+
+ def _call_api(self, *args, **kwargs):
+ """Mock the time activity started."""
+ raise AugustApiHTTPError("This should bubble up as its user consumable")
+
+
+class MockActivity(Activity):
+ """A mock for py-august Activity class."""
+
+ def __init__(
+ self, action=None, activity_start_timestamp=None, activity_end_timestamp=None
+ ):
+ """Init the py-august Activity class mock."""
+ self._action = action
+ self._activity_start_timestamp = activity_start_timestamp
+ self._activity_end_timestamp = activity_end_timestamp
+
+ @property
+ def activity_start_time(self):
+ """Mock the time activity started."""
+ return datetime.datetime.fromtimestamp(self._activity_start_timestamp)
+
+ @property
+ def activity_end_time(self):
+ """Mock the time activity ended."""
+ return datetime.datetime.fromtimestamp(self._activity_end_timestamp)
+
+ @property
+ def action(self):
+ """Mock the action."""
+ return self._action
+
+
+class MockAugustComponentDoorBinarySensor(AugustDoorBinarySensor):
+ """A mock for august component AugustDoorBinarySensor class."""
+
+ def _update_door_state(self, door_state, activity_start_time_utc):
+ """Mock updating the lock status."""
+ self._data.set_last_door_state_update_time_utc(
+ self._door.device_id, activity_start_time_utc
+ )
+ self.last_update_door_state = {}
+ self.last_update_door_state["door_state"] = door_state
+ self.last_update_door_state["activity_start_time_utc"] = activity_start_time_utc
+
+
+class MockAugustComponentLock(AugustLock):
+ """A mock for august component AugustLock class."""
+
+ def _update_lock_status(self, lock_status, activity_start_time_utc):
+ """Mock updating the lock status."""
+ self._data.set_last_lock_status_update_time_utc(
+ self._lock.device_id, activity_start_time_utc
+ )
+ self.last_update_lock_status = {}
+ self.last_update_lock_status["lock_status"] = lock_status
+ self.last_update_lock_status[
+ "activity_start_time_utc"
+ ] = activity_start_time_utc
+
+
+class MockAugustComponentData(AugustData):
+ """A wrapper to mock AugustData."""
+
+ # AugustData support multiple locks, however for the purposes of
+ # mocking we currently only mock one lockid
+
+ def __init__(
+ self,
+ last_lock_status_update_timestamp=1,
+ last_door_state_update_timestamp=1,
+ api=MockAugustApiFailing(),
+ access_token="mocked_access_token",
+ locks=[],
+ doorbells=[],
+ ):
+ """Mock AugustData."""
+ self._last_lock_status_update_time_utc = dt.as_utc(
+ datetime.datetime.fromtimestamp(last_lock_status_update_timestamp)
+ )
+ self._last_door_state_update_time_utc = dt.as_utc(
+ datetime.datetime.fromtimestamp(last_lock_status_update_timestamp)
+ )
+ self._api = api
+ self._access_token = access_token
+ self._locks = locks
+ self._doorbells = doorbells
+ self._lock_status_by_id = {}
+ self._lock_last_status_update_time_utc_by_id = {}
+
+ def set_mocked_locks(self, locks):
+ """Set lock mocks."""
+ self._locks = locks
+
+ def set_mocked_doorbells(self, doorbells):
+ """Set doorbell mocks."""
+ self._doorbells = doorbells
+
+ def get_last_lock_status_update_time_utc(self, device_id):
+ """Mock to get last lock status update time."""
+ return self._last_lock_status_update_time_utc
+
+ def set_last_lock_status_update_time_utc(self, device_id, update_time):
+ """Mock to set last lock status update time."""
+ self._last_lock_status_update_time_utc = update_time
+
+ def get_last_door_state_update_time_utc(self, device_id):
+ """Mock to get last door state update time."""
+ return self._last_door_state_update_time_utc
+
+ def set_last_door_state_update_time_utc(self, device_id, update_time):
+ """Mock to set last door state update time."""
+ self._last_door_state_update_time_utc = update_time
+
+
+def _mock_august_authenticator():
+ authenticator = MagicMock(name="august.authenticator")
+ authenticator.should_refresh = MagicMock(
+ name="august.authenticator.should_refresh", return_value=0
+ )
+ authenticator.refresh_access_token = MagicMock(
+ name="august.authenticator.refresh_access_token"
+ )
+ return authenticator
+
+
+def _mock_august_authentication(token_text, token_timestamp):
+ authentication = MagicMock(name="august.authentication")
+ type(authentication).access_token = PropertyMock(return_value=token_text)
+ type(authentication).access_token_expires = PropertyMock(
+ return_value=token_timestamp
+ )
+ return authentication
+
+
+def _mock_august_lock(lockid="mocklockid1", houseid="mockhouseid1"):
+ return Lock(lockid, _mock_august_lock_data(lockid=lockid, houseid=houseid))
+
+
+def _mock_august_lock_data(lockid="mocklockid1", houseid="mockhouseid1"):
+ return {
+ "_id": lockid,
+ "LockID": lockid,
+ "LockName": lockid + " Name",
+ "HouseID": houseid,
+ "UserType": "owner",
+ "SerialNumber": "mockserial",
+ "battery": 90,
+ "currentFirmwareVersion": "mockfirmware",
+ "Bridge": {
+ "_id": "bridgeid1",
+ "firmwareVersion": "mockfirm",
+ "operative": True,
+ },
+ "LockStatus": {"doorState": "open"},
+ }
+
+
+def _mock_operative_august_lock_detail(lockid):
+ operative_lock_detail_data = _mock_august_lock_data(lockid=lockid)
+ return LockDetail(operative_lock_detail_data)
+
+
+def _mock_inoperative_august_lock_detail(lockid):
+ inoperative_lock_detail_data = _mock_august_lock_data(lockid=lockid)
+ del inoperative_lock_detail_data["Bridge"]
+ return LockDetail(inoperative_lock_detail_data)
+
+
+def _mock_doorsense_enabled_august_lock_detail(lockid):
+ doorsense_lock_detail_data = _mock_august_lock_data(lockid=lockid)
+ return LockDetail(doorsense_lock_detail_data)
+
+
+def _mock_doorsense_missing_august_lock_detail(lockid):
+ doorsense_lock_detail_data = _mock_august_lock_data(lockid=lockid)
+ del doorsense_lock_detail_data["LockStatus"]["doorState"]
+ return LockDetail(doorsense_lock_detail_data)
diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py
new file mode 100644
index 00000000000..0fbd120ea8b
--- /dev/null
+++ b/tests/components/august/test_binary_sensor.py
@@ -0,0 +1,89 @@
+"""The lock tests for the august platform."""
+
+import datetime
+
+from august.activity import ACTION_DOOR_CLOSED, ACTION_DOOR_OPEN
+from august.lock import LockDoorStatus
+
+from homeassistant.util import dt
+
+from tests.components.august.mocks import (
+ MockActivity,
+ MockAugustComponentData,
+ MockAugustComponentDoorBinarySensor,
+ _mock_august_lock,
+)
+
+
+def test__sync_door_activity_doored_via_dooropen():
+ """Test _sync_door_activity dooropen."""
+ data = MockAugustComponentData(last_door_state_update_timestamp=1)
+ lock = _mock_august_lock()
+ data.set_mocked_locks([lock])
+ door = MockAugustComponentDoorBinarySensor(data, "door_open", lock)
+ door_activity_start_timestamp = 1234
+ door_activity = MockActivity(
+ action=ACTION_DOOR_OPEN,
+ activity_start_timestamp=door_activity_start_timestamp,
+ activity_end_timestamp=5678,
+ )
+ door._sync_door_activity(door_activity)
+ assert door.last_update_door_state["door_state"] == LockDoorStatus.OPEN
+ assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc(
+ datetime.datetime.fromtimestamp(door_activity_start_timestamp)
+ )
+
+
+def test__sync_door_activity_doorclosed():
+ """Test _sync_door_activity doorclosed."""
+ data = MockAugustComponentData(last_door_state_update_timestamp=1)
+ lock = _mock_august_lock()
+ data.set_mocked_locks([lock])
+ door = MockAugustComponentDoorBinarySensor(data, "door_open", lock)
+ door_activity_timestamp = 1234
+ door_activity = MockActivity(
+ action=ACTION_DOOR_CLOSED,
+ activity_start_timestamp=door_activity_timestamp,
+ activity_end_timestamp=door_activity_timestamp,
+ )
+ door._sync_door_activity(door_activity)
+ assert door.last_update_door_state["door_state"] == LockDoorStatus.CLOSED
+ assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc(
+ datetime.datetime.fromtimestamp(door_activity_timestamp)
+ )
+
+
+def test__sync_door_activity_ignores_old_data():
+ """Test _sync_door_activity dooropen then expired doorclosed."""
+ data = MockAugustComponentData(last_door_state_update_timestamp=1)
+ lock = _mock_august_lock()
+ data.set_mocked_locks([lock])
+ door = MockAugustComponentDoorBinarySensor(data, "door_open", lock)
+ first_door_activity_timestamp = 1234
+ door_activity = MockActivity(
+ action=ACTION_DOOR_OPEN,
+ activity_start_timestamp=first_door_activity_timestamp,
+ activity_end_timestamp=first_door_activity_timestamp,
+ )
+ door._sync_door_activity(door_activity)
+ assert door.last_update_door_state["door_state"] == LockDoorStatus.OPEN
+ assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc(
+ datetime.datetime.fromtimestamp(first_door_activity_timestamp)
+ )
+
+ # Now we do the update with an older start time to
+ # make sure it ignored
+ data.set_last_door_state_update_time_utc(
+ lock.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000))
+ )
+ door_activity_timestamp = 2
+ door_activity = MockActivity(
+ action=ACTION_DOOR_CLOSED,
+ activity_start_timestamp=door_activity_timestamp,
+ activity_end_timestamp=door_activity_timestamp,
+ )
+ door._sync_door_activity(door_activity)
+ assert door.last_update_door_state["door_state"] == LockDoorStatus.OPEN
+ assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc(
+ datetime.datetime.fromtimestamp(first_door_activity_timestamp)
+ )
diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py
new file mode 100644
index 00000000000..3a43a0a841a
--- /dev/null
+++ b/tests/components/august/test_init.py
@@ -0,0 +1,137 @@
+"""The tests for the august platform."""
+import asyncio
+from unittest.mock import MagicMock
+
+from august.lock import LockDetail
+from requests import RequestException
+
+from homeassistant.components import august
+from homeassistant.exceptions import HomeAssistantError
+
+from tests.components.august.mocks import (
+ MockAugustApiFailing,
+ MockAugustComponentData,
+ _mock_august_authentication,
+ _mock_august_authenticator,
+ _mock_august_lock,
+ _mock_doorsense_enabled_august_lock_detail,
+ _mock_doorsense_missing_august_lock_detail,
+ _mock_inoperative_august_lock_detail,
+ _mock_operative_august_lock_detail,
+)
+
+
+def test_get_lock_name():
+ """Get the lock name from August data."""
+ data = MockAugustComponentData(last_lock_status_update_timestamp=1)
+ lock = _mock_august_lock()
+ data.set_mocked_locks([lock])
+ assert data.get_lock_name("mocklockid1") == "mocklockid1 Name"
+
+
+def test_unlock_throws_august_api_http_error():
+ """Test unlock."""
+ data = MockAugustComponentData(api=MockAugustApiFailing())
+ lock = _mock_august_lock()
+ data.set_mocked_locks([lock])
+ last_err = None
+ try:
+ data.unlock("mocklockid1")
+ except HomeAssistantError as err:
+ last_err = err
+ assert (
+ str(last_err)
+ == "mocklockid1 Name: This should bubble up as its user consumable"
+ )
+
+
+def test_lock_throws_august_api_http_error():
+ """Test lock."""
+ data = MockAugustComponentData(api=MockAugustApiFailing())
+ lock = _mock_august_lock()
+ data.set_mocked_locks([lock])
+ last_err = None
+ try:
+ data.unlock("mocklockid1")
+ except HomeAssistantError as err:
+ last_err = err
+ assert (
+ str(last_err)
+ == "mocklockid1 Name: This should bubble up as its user consumable"
+ )
+
+
+def test_inoperative_locks_are_filtered_out():
+ """Ensure inoperative locks do not get setup."""
+ august_operative_lock = _mock_operative_august_lock_detail("oplockid1")
+ data = _create_august_data_with_lock_details(
+ [august_operative_lock, _mock_inoperative_august_lock_detail("inoplockid1")]
+ )
+
+ assert len(data.locks) == 1
+ assert data.locks[0].device_id == "oplockid1"
+
+
+def test_lock_has_doorsense():
+ """Check to see if a lock has doorsense."""
+ data = _create_august_data_with_lock_details(
+ [
+ _mock_doorsense_enabled_august_lock_detail("doorsenselock1"),
+ _mock_doorsense_missing_august_lock_detail("nodoorsenselock1"),
+ RequestException("mocked request error"),
+ RequestException("mocked request error"),
+ ]
+ )
+
+ assert data.lock_has_doorsense("doorsenselock1") is True
+ assert data.lock_has_doorsense("nodoorsenselock1") is False
+
+ # The api calls are mocked to fail on the second
+ # run of async_get_lock_detail
+ #
+ # This will be switched to await data.async_get_lock_detail("doorsenselock1")
+ # once we mock the full home assistant setup
+ data._update_locks_detail()
+ # doorsenselock1 should be false if we cannot tell due
+ # to an api error
+ assert data.lock_has_doorsense("doorsenselock1") is False
+
+
+async def test__refresh_access_token(hass):
+ """Test refresh of the access token."""
+ authentication = _mock_august_authentication("original_token", 1234)
+ authenticator = _mock_august_authenticator()
+ token_refresh_lock = asyncio.Lock()
+
+ data = august.AugustData(
+ hass, MagicMock(name="api"), authentication, authenticator, token_refresh_lock
+ )
+ await data._async_refresh_access_token_if_needed()
+ authenticator.refresh_access_token.assert_not_called()
+
+ authenticator.should_refresh.return_value = 1
+ authenticator.refresh_access_token.return_value = _mock_august_authentication(
+ "new_token", 5678
+ )
+ await data._async_refresh_access_token_if_needed()
+ authenticator.refresh_access_token.assert_called()
+ assert data._access_token == "new_token"
+ assert data._access_token_expires == 5678
+
+
+def _create_august_data_with_lock_details(lock_details):
+ locks = []
+ for lock in lock_details:
+ if isinstance(lock, LockDetail):
+ locks.append(_mock_august_lock(lock.device_id))
+ authentication = _mock_august_authentication("original_token", 1234)
+ authenticator = _mock_august_authenticator()
+ token_refresh_lock = MagicMock()
+ api = MagicMock()
+ api.get_lock_status = MagicMock(return_value=(MagicMock(), MagicMock()))
+ api.get_lock_detail = MagicMock(side_effect=lock_details)
+ api.get_operable_locks = MagicMock(return_value=locks)
+ api.get_doorbells = MagicMock(return_value=[])
+ return august.AugustData(
+ MagicMock(), api, authentication, authenticator, token_refresh_lock
+ )
diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py
new file mode 100644
index 00000000000..8b036861899
--- /dev/null
+++ b/tests/components/august/test_lock.py
@@ -0,0 +1,110 @@
+"""The lock tests for the august platform."""
+
+import datetime
+
+from august.activity import (
+ ACTION_LOCK_LOCK,
+ ACTION_LOCK_ONETOUCHLOCK,
+ ACTION_LOCK_UNLOCK,
+)
+from august.lock import LockStatus
+
+from homeassistant.util import dt
+
+from tests.components.august.mocks import (
+ MockActivity,
+ MockAugustComponentData,
+ MockAugustComponentLock,
+ _mock_august_lock,
+)
+
+
+def test__sync_lock_activity_locked_via_onetouchlock():
+ """Test _sync_lock_activity locking."""
+ lock = _mocked_august_component_lock()
+ lock_activity_start_timestamp = 1234
+ lock_activity = MockActivity(
+ action=ACTION_LOCK_ONETOUCHLOCK,
+ activity_start_timestamp=lock_activity_start_timestamp,
+ activity_end_timestamp=5678,
+ )
+ lock._sync_lock_activity(lock_activity)
+ assert lock.last_update_lock_status["lock_status"] == LockStatus.LOCKED
+ assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc(
+ datetime.datetime.fromtimestamp(lock_activity_start_timestamp)
+ )
+
+
+def test__sync_lock_activity_locked_via_lock():
+ """Test _sync_lock_activity locking."""
+ lock = _mocked_august_component_lock()
+ lock_activity_start_timestamp = 1234
+ lock_activity = MockActivity(
+ action=ACTION_LOCK_LOCK,
+ activity_start_timestamp=lock_activity_start_timestamp,
+ activity_end_timestamp=5678,
+ )
+ lock._sync_lock_activity(lock_activity)
+ assert lock.last_update_lock_status["lock_status"] == LockStatus.LOCKED
+ assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc(
+ datetime.datetime.fromtimestamp(lock_activity_start_timestamp)
+ )
+
+
+def test__sync_lock_activity_unlocked():
+ """Test _sync_lock_activity unlocking."""
+ lock = _mocked_august_component_lock()
+ lock_activity_timestamp = 1234
+ lock_activity = MockActivity(
+ action=ACTION_LOCK_UNLOCK,
+ activity_start_timestamp=lock_activity_timestamp,
+ activity_end_timestamp=lock_activity_timestamp,
+ )
+ lock._sync_lock_activity(lock_activity)
+ assert lock.last_update_lock_status["lock_status"] == LockStatus.UNLOCKED
+ assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc(
+ datetime.datetime.fromtimestamp(lock_activity_timestamp)
+ )
+
+
+def test__sync_lock_activity_ignores_old_data():
+ """Test _sync_lock_activity unlocking."""
+ data = MockAugustComponentData(last_lock_status_update_timestamp=1)
+ august_lock = _mock_august_lock()
+ data.set_mocked_locks([august_lock])
+ lock = MockAugustComponentLock(data, august_lock)
+ first_lock_activity_timestamp = 1234
+ lock_activity = MockActivity(
+ action=ACTION_LOCK_UNLOCK,
+ activity_start_timestamp=first_lock_activity_timestamp,
+ activity_end_timestamp=first_lock_activity_timestamp,
+ )
+ lock._sync_lock_activity(lock_activity)
+ assert lock.last_update_lock_status["lock_status"] == LockStatus.UNLOCKED
+ assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc(
+ datetime.datetime.fromtimestamp(first_lock_activity_timestamp)
+ )
+
+ # Now we do the update with an older start time to
+ # make sure it ignored
+ data.set_last_lock_status_update_time_utc(
+ august_lock.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000))
+ )
+ lock_activity_timestamp = 2
+ lock_activity = MockActivity(
+ action=ACTION_LOCK_LOCK,
+ activity_start_timestamp=lock_activity_timestamp,
+ activity_end_timestamp=lock_activity_timestamp,
+ )
+ lock._sync_lock_activity(lock_activity)
+ assert lock.last_update_lock_status["lock_status"] == LockStatus.UNLOCKED
+ assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc(
+ datetime.datetime.fromtimestamp(first_lock_activity_timestamp)
+ )
+
+
+def _mocked_august_component_lock():
+ data = MockAugustComponentData(last_lock_status_update_timestamp=1)
+ august_lock = _mock_august_lock()
+ data.set_mocked_locks([august_lock])
+ return MockAugustComponentLock(data, august_lock)
diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py
index 809c71c5cb1..2e4c3e9f8be 100644
--- a/tests/components/axis/test_config_flow.py
+++ b/tests/components/axis/test_config_flow.py
@@ -4,7 +4,7 @@ from unittest.mock import Mock, patch
from homeassistant.components import axis
from homeassistant.components.axis import config_flow
-from .test_device import MAC, setup_axis_integration
+from .test_device import MAC, MODEL, NAME, setup_axis_integration
from tests.common import MockConfigEntry, mock_coro
@@ -54,12 +54,10 @@ async def test_flow_manual_configuration(hass):
assert result["type"] == "create_entry"
assert result["title"] == f"prodnbr - {MAC}"
assert result["data"] == {
- axis.CONF_DEVICE: {
- config_flow.CONF_HOST: "1.2.3.4",
- config_flow.CONF_USERNAME: "user",
- config_flow.CONF_PASSWORD: "pass",
- config_flow.CONF_PORT: 80,
- },
+ config_flow.CONF_HOST: "1.2.3.4",
+ config_flow.CONF_USERNAME: "user",
+ config_flow.CONF_PASSWORD: "pass",
+ config_flow.CONF_PORT: 80,
config_flow.CONF_MAC: MAC,
config_flow.CONF_MODEL: "prodnbr",
config_flow.CONF_NAME: "prodnbr 0",
@@ -95,11 +93,8 @@ async def test_manual_configuration_update_configuration(hass):
)
assert result["type"] == "abort"
- assert result["reason"] == "updated_configuration"
- assert (
- device.config_entry.data[config_flow.CONF_DEVICE][config_flow.CONF_HOST]
- == "2.3.4.5"
- )
+ assert result["reason"] == "already_configured"
+ assert device.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5"
async def test_flow_fails_already_configured(hass):
@@ -223,12 +218,10 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass):
assert result["type"] == "create_entry"
assert result["title"] == f"prodnbr - {MAC}"
assert result["data"] == {
- axis.CONF_DEVICE: {
- config_flow.CONF_HOST: "1.2.3.4",
- config_flow.CONF_USERNAME: "user",
- config_flow.CONF_PASSWORD: "pass",
- config_flow.CONF_PORT: 80,
- },
+ config_flow.CONF_HOST: "1.2.3.4",
+ config_flow.CONF_USERNAME: "user",
+ config_flow.CONF_PASSWORD: "pass",
+ config_flow.CONF_PORT: 80,
config_flow.CONF_MAC: MAC,
config_flow.CONF_MODEL: "prodnbr",
config_flow.CONF_NAME: "prodnbr 2",
@@ -271,12 +264,10 @@ async def test_zeroconf_flow(hass):
assert result["type"] == "create_entry"
assert result["title"] == f"prodnbr - {MAC}"
assert result["data"] == {
- axis.CONF_DEVICE: {
- config_flow.CONF_HOST: "1.2.3.4",
- config_flow.CONF_USERNAME: "user",
- config_flow.CONF_PASSWORD: "pass",
- config_flow.CONF_PORT: 80,
- },
+ config_flow.CONF_HOST: "1.2.3.4",
+ config_flow.CONF_USERNAME: "user",
+ config_flow.CONF_PASSWORD: "pass",
+ config_flow.CONF_PORT: 80,
config_flow.CONF_MAC: MAC,
config_flow.CONF_MODEL: "prodnbr",
config_flow.CONF_NAME: "prodnbr 0",
@@ -310,6 +301,15 @@ async def test_zeroconf_flow_updated_configuration(hass):
"""Test that zeroconf update configuration with new parameters."""
device = await setup_axis_integration(hass)
assert device.host == "1.2.3.4"
+ assert device.config_entry.data == {
+ config_flow.CONF_HOST: "1.2.3.4",
+ config_flow.CONF_PORT: 80,
+ config_flow.CONF_USERNAME: "username",
+ config_flow.CONF_PASSWORD: "password",
+ config_flow.CONF_MAC: MAC,
+ config_flow.CONF_MODEL: MODEL,
+ config_flow.CONF_NAME: NAME,
+ }
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
@@ -323,11 +323,16 @@ async def test_zeroconf_flow_updated_configuration(hass):
)
assert result["type"] == "abort"
- assert result["reason"] == "updated_configuration"
- assert device.host == "2.3.4.5"
- assert (
- device.config_entry.data[config_flow.CONF_DEVICE][config_flow.CONF_PORT] == 8080
- )
+ assert result["reason"] == "already_configured"
+ assert device.config_entry.data == {
+ config_flow.CONF_HOST: "2.3.4.5",
+ config_flow.CONF_PORT: 8080,
+ config_flow.CONF_USERNAME: "username",
+ config_flow.CONF_PASSWORD: "password",
+ config_flow.CONF_MAC: MAC,
+ config_flow.CONF_MODEL: MODEL,
+ config_flow.CONF_NAME: NAME,
+ }
async def test_zeroconf_flow_ignore_non_axis_device(hass):
diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py
index b175d22cfb4..3d2ed432c1c 100644
--- a/tests/components/axis/test_device.py
+++ b/tests/components/axis/test_device.py
@@ -14,18 +14,14 @@ MAC = "00408C12345"
MODEL = "model"
NAME = "name"
-DEVICE_DATA = {
- axis.device.CONF_HOST: "1.2.3.4",
- axis.device.CONF_USERNAME: "username",
- axis.device.CONF_PASSWORD: "password",
- axis.device.CONF_PORT: 80,
-}
-
-ENTRY_OPTIONS = {axis.device.CONF_CAMERA: True, axis.device.CONF_EVENTS: True}
+ENTRY_OPTIONS = {axis.CONF_CAMERA: True, axis.CONF_EVENTS: True}
ENTRY_CONFIG = {
- axis.device.CONF_DEVICE: DEVICE_DATA,
- axis.device.CONF_MAC: MAC,
+ axis.CONF_HOST: "1.2.3.4",
+ axis.CONF_USERNAME: "username",
+ axis.CONF_PASSWORD: "password",
+ axis.CONF_PORT: 80,
+ axis.CONF_MAC: MAC,
axis.device.CONF_MODEL: MODEL,
axis.device.CONF_NAME: NAME,
}
@@ -76,6 +72,7 @@ async def setup_axis_integration(
connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
options=deepcopy(options),
entry_id="1",
+ version=2,
)
config_entry.add_to_hass(hass)
@@ -116,10 +113,10 @@ async def test_device_setup(hass):
assert forward_entry_setup.mock_calls[1][1] == (entry, "binary_sensor")
assert forward_entry_setup.mock_calls[2][1] == (entry, "switch")
- assert device.host == DEVICE_DATA[axis.device.CONF_HOST]
+ assert device.host == ENTRY_CONFIG[axis.CONF_HOST]
assert device.model == ENTRY_CONFIG[axis.device.CONF_MODEL]
assert device.name == ENTRY_CONFIG[axis.device.CONF_NAME]
- assert device.serial == ENTRY_CONFIG[axis.device.CONF_MAC]
+ assert device.serial == ENTRY_CONFIG[axis.CONF_MAC]
async def test_update_address(hass):
@@ -204,7 +201,7 @@ async def test_get_device_fails(hass):
with patch(
"axis.param_cgi.Params.update_brand", side_effect=axislib.Unauthorized
), pytest.raises(axis.errors.AuthenticationRequired):
- await axis.device.get_device(hass, DEVICE_DATA)
+ await axis.device.get_device(hass, host="", port="", username="", password="")
async def test_get_device_device_unavailable(hass):
@@ -212,7 +209,7 @@ async def test_get_device_device_unavailable(hass):
with patch(
"axis.param_cgi.Params.update_brand", side_effect=axislib.RequestError
), pytest.raises(axis.errors.CannotConnect):
- await axis.device.get_device(hass, DEVICE_DATA)
+ await axis.device.get_device(hass, host="", port="", username="", password="")
async def test_get_device_unknown_error(hass):
@@ -220,4 +217,4 @@ async def test_get_device_unknown_error(hass):
with patch(
"axis.param_cgi.Params.update_brand", side_effect=axislib.AxisException
), pytest.raises(axis.errors.AuthenticationRequired):
- await axis.device.get_device(hass, DEVICE_DATA)
+ await axis.device.get_device(hass, host="", port="", username="", password="")
diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py
index 748bb539369..cf5a3b2785a 100644
--- a/tests/components/axis/test_init.py
+++ b/tests/components/axis/test_init.py
@@ -16,7 +16,7 @@ async def test_setup_device_already_configured(hass):
assert await async_setup_component(
hass,
axis.DOMAIN,
- {axis.DOMAIN: {"device_name": {axis.config_flow.CONF_HOST: "1.2.3.4"}}},
+ {axis.DOMAIN: {"device_name": {axis.CONF_HOST: "1.2.3.4"}}},
)
assert not mock_config_entries.flow.mock_calls
@@ -37,9 +37,10 @@ async def test_setup_entry(hass):
async def test_setup_entry_fails(hass):
"""Test successful setup of entry."""
- entry = MockConfigEntry(
- domain=axis.DOMAIN, data={axis.device.CONF_MAC: "0123"}, options=True
+ config_entry = MockConfigEntry(
+ domain=axis.DOMAIN, data={axis.CONF_MAC: "0123"}, options=True, version=2
)
+ config_entry.add_to_hass(hass)
mock_device = Mock()
mock_device.async_setup.return_value = mock_coro(False)
@@ -47,7 +48,7 @@ async def test_setup_entry_fails(hass):
with patch.object(axis, "AxisNetworkDevice") as mock_device_class:
mock_device_class.return_value = mock_device
- assert not await axis.async_setup_entry(hass, entry)
+ assert not await hass.config_entries.async_setup(config_entry.entry_id)
assert not hass.data[axis.DOMAIN]
@@ -57,21 +58,54 @@ async def test_unload_entry(hass):
device = await setup_axis_integration(hass)
assert hass.data[axis.DOMAIN]
- assert await axis.async_unload_entry(hass, device.config_entry)
+ assert await hass.config_entries.async_unload(device.config_entry.entry_id)
assert not hass.data[axis.DOMAIN]
async def test_populate_options(hass):
"""Test successful populate options."""
- entry = MockConfigEntry(domain=axis.DOMAIN, data={"device": {}})
- entry.add_to_hass(hass)
+ device = await setup_axis_integration(hass, options=None)
- with patch.object(axis, "get_device", return_value=mock_coro(Mock())):
-
- await axis.async_populate_options(hass, entry)
-
- assert entry.options == {
+ assert device.config_entry.options == {
axis.CONF_CAMERA: True,
axis.CONF_EVENTS: True,
axis.CONF_TRIGGER_TIME: axis.DEFAULT_TRIGGER_TIME,
}
+
+
+async def test_migrate_entry(hass):
+ """Test successful migration of entry data."""
+ legacy_config = {
+ axis.CONF_DEVICE: {
+ axis.CONF_HOST: "1.2.3.4",
+ axis.CONF_USERNAME: "username",
+ axis.CONF_PASSWORD: "password",
+ axis.CONF_PORT: 80,
+ },
+ axis.CONF_MAC: "mac",
+ axis.device.CONF_MODEL: "model",
+ axis.device.CONF_NAME: "name",
+ }
+ entry = MockConfigEntry(domain=axis.DOMAIN, data=legacy_config)
+
+ assert entry.data == legacy_config
+ assert entry.version == 1
+
+ await entry.async_migrate(hass)
+
+ assert entry.data == {
+ axis.CONF_DEVICE: {
+ axis.CONF_HOST: "1.2.3.4",
+ axis.CONF_USERNAME: "username",
+ axis.CONF_PASSWORD: "password",
+ axis.CONF_PORT: 80,
+ },
+ axis.CONF_HOST: "1.2.3.4",
+ axis.CONF_USERNAME: "username",
+ axis.CONF_PASSWORD: "password",
+ axis.CONF_PORT: 80,
+ axis.CONF_MAC: "mac",
+ axis.device.CONF_MODEL: "model",
+ axis.device.CONF_NAME: "name",
+ }
+ assert entry.version == 2
diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py
index d9341bb3271..c8a23517ae1 100644
--- a/tests/components/bayesian/test_binary_sensor.py
+++ b/tests/components/bayesian/test_binary_sensor.py
@@ -149,7 +149,7 @@ class TestBayesianBinarySensor(unittest.TestCase):
assert state.state == "off"
def test_threshold(self):
- """Test sensor on probabilty threshold limits."""
+ """Test sensor on probability threshold limits."""
config = {
"binary_sensor": {
"name": "Test_Binary",
diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py
index c0be635988a..fa6f331363f 100644
--- a/tests/components/caldav/test_calendar.py
+++ b/tests/components/caldav/test_calendar.py
@@ -124,6 +124,96 @@ LOCATION:Hamburg
DESCRIPTION:What a day
END:VEVENT
END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//E-Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:9
+DTSTAMP:20171125T000000Z
+DTSTART:20171027T220000Z
+DTEND:20171027T223000Z
+SUMMARY:This is a recurring event
+LOCATION:Hamburg
+DESCRIPTION:Every day for a while
+RRULE:FREQ=DAILY;UNTIL=20171227T215959
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//E-Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:10
+DTSTAMP:20171125T000000Z
+DTSTART:20171027T230000Z
+DURATION:PT30M
+SUMMARY:This is a recurring event with a duration
+LOCATION:Hamburg
+DESCRIPTION:Every day for a while as well
+RRULE:FREQ=DAILY;UNTIL=20171227T215959
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//E-Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:11
+DTSTAMP:20171125T000000Z
+DTSTART:20171027T233000Z
+DTEND:20171027T235959Z
+SUMMARY:This is a recurring event that has ended
+LOCATION:Hamburg
+DESCRIPTION:Every day for a while
+RRULE:FREQ=DAILY;UNTIL=20171127T225959
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//E-Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:12
+DTSTAMP:20171125T000000Z
+DTSTART:20171027T234500Z
+DTEND:20171027T235959Z
+SUMMARY:This is a recurring event that never ends
+LOCATION:Hamburg
+DESCRIPTION:Every day forever
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Global Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:13
+DTSTAMP:20161125T000000Z
+DTSTART:20161127
+DTEND:20161128
+SUMMARY:This is a recurring all day event
+LOCATION:Hamburg
+DESCRIPTION:Groundhog Day
+RRULE:FREQ=DAILY;COUNT=100
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Global Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:14
+DTSTAMP:20151125T000000Z
+DTSTART:20151127T000000Z
+DTEND:20151127T003000Z
+SUMMARY:This is an hourly recurring event
+LOCATION:Hamburg
+DESCRIPTION:The bell tolls for thee
+RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12
+END:VEVENT
+END:VCALENDAR
""",
]
@@ -461,3 +551,227 @@ async def test_all_day_event_returned(mock_now, hass, calendar):
"location": "Hamburg",
"description": "What a beautiful day",
}
+
+
+@patch("homeassistant.util.dt.now", return_value=_local_datetime(21, 45))
+async def test_event_rrule(mock_now, hass, calendar):
+ """Test that the future recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_OFF
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is a recurring event",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2017-11-27 22:00:00",
+ "end_time": "2017-11-27 22:30:00",
+ "location": "Hamburg",
+ "description": "Every day for a while",
+ }
+
+
+@patch("homeassistant.util.dt.now", return_value=_local_datetime(22, 15))
+async def test_event_rrule_ongoing(mock_now, hass, calendar):
+ """Test that the current recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is a recurring event",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2017-11-27 22:00:00",
+ "end_time": "2017-11-27 22:30:00",
+ "location": "Hamburg",
+ "description": "Every day for a while",
+ }
+
+
+@patch("homeassistant.util.dt.now", return_value=_local_datetime(22, 45))
+async def test_event_rrule_duration(mock_now, hass, calendar):
+ """Test that the future recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_OFF
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is a recurring event with a duration",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2017-11-27 23:00:00",
+ "end_time": "2017-11-27 23:30:00",
+ "location": "Hamburg",
+ "description": "Every day for a while as well",
+ }
+
+
+@patch("homeassistant.util.dt.now", return_value=_local_datetime(23, 15))
+async def test_event_rrule_duration_ongoing(mock_now, hass, calendar):
+ """Test that the ongoing recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is a recurring event with a duration",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2017-11-27 23:00:00",
+ "end_time": "2017-11-27 23:30:00",
+ "location": "Hamburg",
+ "description": "Every day for a while as well",
+ }
+
+
+@patch("homeassistant.util.dt.now", return_value=_local_datetime(23, 37))
+async def test_event_rrule_endless(mock_now, hass, calendar):
+ """Test that the endless recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_OFF
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is a recurring event that never ends",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2017-11-27 23:45:00",
+ "end_time": "2017-11-27 23:59:59",
+ "location": "Hamburg",
+ "description": "Every day forever",
+ }
+
+
+@patch(
+ "homeassistant.util.dt.now",
+ return_value=dt.as_local(datetime.datetime(2016, 12, 1, 17, 30)),
+)
+async def test_event_rrule_all_day(mock_now, hass, calendar):
+ """Test that the recurring all day event is returned."""
+ config = dict(CALDAV_CONFIG)
+ config["custom_calendars"] = [
+ {"name": "Private", "calendar": "Private", "search": ".*"}
+ ]
+
+ assert await async_setup_component(hass, "calendar", {"calendar": config})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private_private")
+ assert state.name == calendar.name
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is a recurring all day event",
+ "all_day": True,
+ "offset_reached": False,
+ "start_time": "2016-12-01 00:00:00",
+ "end_time": "2016-12-02 00:00:00",
+ "location": "Hamburg",
+ "description": "Groundhog Day",
+ }
+
+
+@patch(
+ "homeassistant.util.dt.now",
+ return_value=dt.as_local(datetime.datetime(2015, 11, 27, 0, 15)),
+)
+async def test_event_rrule_hourly_on_first(mock_now, hass, calendar):
+ """Test that the endless recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is an hourly recurring event",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2015-11-27 00:00:00",
+ "end_time": "2015-11-27 00:30:00",
+ "location": "Hamburg",
+ "description": "The bell tolls for thee",
+ }
+
+
+@patch(
+ "homeassistant.util.dt.now",
+ return_value=dt.as_local(datetime.datetime(2015, 11, 27, 11, 15)),
+)
+async def test_event_rrule_hourly_on_last(mock_now, hass, calendar):
+ """Test that the endless recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_ON
+ assert dict(state.attributes) == {
+ "friendly_name": "Private",
+ "message": "This is an hourly recurring event",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2015-11-27 11:00:00",
+ "end_time": "2015-11-27 11:30:00",
+ "location": "Hamburg",
+ "description": "The bell tolls for thee",
+ }
+
+
+@patch(
+ "homeassistant.util.dt.now",
+ return_value=dt.as_local(datetime.datetime(2015, 11, 27, 0, 45)),
+)
+async def test_event_rrule_hourly_off_first(mock_now, hass, calendar):
+ """Test that the endless recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_OFF
+
+
+@patch(
+ "homeassistant.util.dt.now",
+ return_value=dt.as_local(datetime.datetime(2015, 11, 27, 11, 45)),
+)
+async def test_event_rrule_hourly_off_last(mock_now, hass, calendar):
+ """Test that the endless recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_OFF
+
+
+@patch(
+ "homeassistant.util.dt.now",
+ return_value=dt.as_local(datetime.datetime(2015, 11, 27, 12, 15)),
+)
+async def test_event_rrule_hourly_ended(mock_now, hass, calendar):
+ """Test that the endless recurring event is returned."""
+ assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("calendar.private")
+ assert state.name == calendar.name
+ assert state.state == STATE_OFF
diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py
index bcd1482195d..71005672fdb 100644
--- a/tests/components/cert_expiry/test_config_flow.py
+++ b/tests/components/cert_expiry/test_config_flow.py
@@ -19,7 +19,7 @@ HOST = "example.com"
@pytest.fixture(name="test_connect")
def mock_controller():
- """Mock a successfull _prt_in_configuration_exists."""
+ """Mock a successful _prt_in_configuration_exists."""
with patch(
"homeassistant.components.cert_expiry.config_flow.CertexpiryConfigFlow._test_connection",
side_effect=lambda *_: mock_coro(True),
diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py
index 50402af2bd1..b9e6524b62e 100644
--- a/tests/components/cloud/test_client.py
+++ b/tests/components/cloud/test_client.py
@@ -217,7 +217,7 @@ async def test_google_config_should_2fa(hass, mock_cloud_setup, mock_cloud_login
async def test_set_username(hass):
- """Test we set username during loggin."""
+ """Test we set username during login."""
prefs = MagicMock(
alexa_enabled=False,
google_enabled=False,
diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py
index 133c88d9ceb..8fe7e8fdbe4 100644
--- a/tests/components/config/test_entity_registry.py
+++ b/tests/components/config/test_entity_registry.py
@@ -41,6 +41,9 @@ async def test_list_entities(hass, client):
"disabled_by": None,
"entity_id": "test_domain.name",
"name": "Hello World",
+ "icon": None,
+ "original_name": None,
+ "original_icon": None,
"platform": "test_platform",
},
{
@@ -49,6 +52,9 @@ async def test_list_entities(hass, client):
"disabled_by": None,
"entity_id": "test_domain.no_name",
"name": None,
+ "icon": None,
+ "original_name": None,
+ "original_icon": None,
"platform": "test_platform",
},
]
@@ -85,6 +91,9 @@ async def test_get_entity(hass, client):
"platform": "test_platform",
"entity_id": "test_domain.name",
"name": "Hello World",
+ "icon": None,
+ "original_name": None,
+ "original_icon": None,
}
await client.send_json(
@@ -103,6 +112,9 @@ async def test_get_entity(hass, client):
"platform": "test_platform",
"entity_id": "test_domain.no_name",
"name": None,
+ "icon": None,
+ "original_name": None,
+ "original_icon": None,
}
@@ -117,6 +129,7 @@ async def test_update_entity(hass, client):
# Using component.async_add_entities is equal to platform "domain"
platform="test_platform",
name="before update",
+ icon="icon:before update",
)
},
)
@@ -127,14 +140,16 @@ async def test_update_entity(hass, client):
state = hass.states.get("test_domain.world")
assert state is not None
assert state.name == "before update"
+ assert state.attributes["icon"] == "icon:before update"
- # UPDATE NAME
+ # UPDATE NAME & ICON
await client.send_json(
{
"id": 6,
"type": "config/entity_registry/update",
"entity_id": "test_domain.world",
"name": "after update",
+ "icon": "icon:after update",
}
)
@@ -147,10 +162,14 @@ async def test_update_entity(hass, client):
"platform": "test_platform",
"entity_id": "test_domain.world",
"name": "after update",
+ "icon": "icon:after update",
+ "original_name": None,
+ "original_icon": None,
}
state = hass.states.get("test_domain.world")
assert state.name == "after update"
+ assert state.attributes["icon"] == "icon:after update"
# UPDATE DISABLED_BY TO USER
await client.send_json(
@@ -186,6 +205,9 @@ async def test_update_entity(hass, client):
"platform": "test_platform",
"entity_id": "test_domain.world",
"name": "after update",
+ "icon": "icon:after update",
+ "original_name": None,
+ "original_icon": None,
}
@@ -229,6 +251,9 @@ async def test_update_entity_no_changes(hass, client):
"platform": "test_platform",
"entity_id": "test_domain.world",
"name": "name of entity",
+ "icon": None,
+ "original_name": None,
+ "original_icon": None,
}
state = hass.states.get("test_domain.world")
@@ -301,6 +326,9 @@ async def test_update_entity_id(hass, client):
"platform": "test_platform",
"entity_id": "test_domain.planet",
"name": None,
+ "icon": None,
+ "original_name": None,
+ "original_icon": None,
}
assert hass.states.get("test_domain.world") is None
diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py
index 267c57717f9..059cdb1f1e8 100644
--- a/tests/components/config/test_zwave.py
+++ b/tests/components/config/test_zwave.py
@@ -480,7 +480,7 @@ async def test_set_protection_value_failed(hass, client):
resp = await client.post(
"/api/zwave/protection/18",
- data=json.dumps({"value_id": "123456", "selection": "Protecton by Seuence"}),
+ data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}),
)
assert resp.status == 202
@@ -512,7 +512,7 @@ async def test_set_protection_value_nonexisting_node(hass, client):
resp = await client.post(
"/api/zwave/protection/18",
- data=json.dumps({"value_id": "123456", "selection": "Protecton by Seuence"}),
+ data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}),
)
assert resp.status == 404
@@ -532,7 +532,7 @@ async def test_set_protection_value_missing_class(hass, client):
resp = await client.post(
"/api/zwave/protection/17",
- data=json.dumps({"value_id": "123456", "selection": "Protecton by Seuence"}),
+ data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}),
)
assert resp.status == 404
diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py
index f84d2109095..737d99cbddd 100644
--- a/tests/components/conversation/test_init.py
+++ b/tests/components/conversation/test_init.py
@@ -201,11 +201,9 @@ async def test_toggle_intent(hass, sentence):
async def test_http_api(hass, hass_client):
"""Test the HTTP conversation API."""
- result = await async_setup_component(hass, "homeassistant", {})
- assert result
-
- result = await async_setup_component(hass, "conversation", {})
- assert result
+ assert await async_setup_component(hass, "homeassistant", {})
+ assert await async_setup_component(hass, "conversation", {})
+ assert await async_setup_component(hass, "intent", {})
client = await hass_client()
hass.states.async_set("light.kitchen", "off")
diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py
index bb716ed17ec..eff06e3bf7d 100644
--- a/tests/components/darksky/test_sensor.py
+++ b/tests/components/darksky/test_sensor.py
@@ -30,7 +30,7 @@ INVALID_CONFIG_MINIMAL = {
"api_key": "foo",
"forecast": [1, 2],
"hourly_forecast": [1, 2],
- "monitored_conditions": ["sumary", "iocn", "temperature_high"],
+ "monitored_conditions": ["summary", "iocn", "temperature_high"],
"scan_interval": timedelta(seconds=120),
}
}
diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py
index 1dc8e61183b..864ba91fbc1 100644
--- a/tests/components/deconz/test_binary_sensor.py
+++ b/tests/components/deconz/test_binary_sensor.py
@@ -96,7 +96,7 @@ async def test_binary_sensors(hass):
"id": "1",
"state": {"presence": True},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
presence_sensor = hass.states.get("binary_sensor.presence_sensor")
@@ -134,6 +134,28 @@ async def test_allow_clip_sensor(hass):
vibration_sensor = hass.states.get("binary_sensor.vibration_sensor")
assert vibration_sensor.state == "on"
+ hass.config_entries.async_update_entry(
+ gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False}
+ )
+ await hass.async_block_till_done()
+
+ assert "binary_sensor.presence_sensor" in gateway.deconz_ids
+ assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids
+ assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids
+ assert "binary_sensor.vibration_sensor" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 3
+
+ hass.config_entries.async_update_entry(
+ gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}
+ )
+ await hass.async_block_till_done()
+
+ assert "binary_sensor.presence_sensor" in gateway.deconz_ids
+ assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids
+ assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids
+ assert "binary_sensor.vibration_sensor" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 4
+
async def test_add_new_binary_sensor(hass):
"""Test that adding a new binary sensor works."""
@@ -147,7 +169,7 @@ async def test_add_new_binary_sensor(hass):
"id": "1",
"sensor": deepcopy(SENSORS["1"]),
}
- gateway.api.async_event_handler(state_added_event)
+ gateway.api.event_handler(state_added_event)
await hass.async_block_till_done()
assert "binary_sensor.presence_sensor" in gateway.deconz_ids
diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py
index 00c03caaac7..c03dc72019e 100644
--- a/tests/components/deconz/test_climate.py
+++ b/tests/components/deconz/test_climate.py
@@ -95,7 +95,7 @@ async def test_climate_devices(hass):
"id": "1",
"config": {"mode": "off"},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
thermostat = hass.states.get("climate.thermostat")
@@ -109,7 +109,7 @@ async def test_climate_devices(hass):
"config": {"mode": "other"},
"state": {"on": True},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
thermostat = hass.states.get("climate.thermostat")
@@ -122,7 +122,7 @@ async def test_climate_devices(hass):
"id": "1",
"state": {"on": False},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
thermostat = hass.states.get("climate.thermostat")
@@ -214,6 +214,30 @@ async def test_clip_climate_device(hass):
clip_thermostat = hass.states.get("climate.clip_thermostat")
assert clip_thermostat.state == "heat"
+ hass.config_entries.async_update_entry(
+ gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False}
+ )
+ await hass.async_block_till_done()
+
+ assert "climate.thermostat" in gateway.deconz_ids
+ assert "sensor.thermostat" not in gateway.deconz_ids
+ assert "sensor.thermostat_battery_level" in gateway.deconz_ids
+ assert "climate.presence_sensor" not in gateway.deconz_ids
+ assert "climate.clip_thermostat" not in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 3
+
+ hass.config_entries.async_update_entry(
+ gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}
+ )
+ await hass.async_block_till_done()
+
+ assert "climate.thermostat" in gateway.deconz_ids
+ assert "sensor.thermostat" not in gateway.deconz_ids
+ assert "sensor.thermostat_battery_level" in gateway.deconz_ids
+ assert "climate.presence_sensor" not in gateway.deconz_ids
+ assert "climate.clip_thermostat" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 4
+
async def test_verify_state_update(hass):
"""Test that state update properly."""
@@ -232,7 +256,7 @@ async def test_verify_state_update(hass):
"id": "1",
"state": {"on": False},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
thermostat = hass.states.get("climate.thermostat")
@@ -252,7 +276,7 @@ async def test_add_new_climate_device(hass):
"id": "1",
"sensor": deepcopy(SENSORS["1"]),
}
- gateway.api.async_event_handler(state_added_event)
+ gateway.api.event_handler(state_added_event)
await hass.async_block_till_done()
assert "climate.thermostat" in gateway.deconz_ids
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
index d79f80b96b0..4873528d982 100644
--- a/tests/components/deconz/test_config_flow.py
+++ b/tests/components/deconz/test_config_flow.py
@@ -3,13 +3,23 @@ import asyncio
import pydeconz
+from homeassistant import data_entry_flow
from homeassistant.components import ssdp
from homeassistant.components.deconz import config_flow
+from homeassistant.components.deconz.config_flow import (
+ CONF_SERIAL,
+ DECONZ_MANUFACTURERURL,
+)
+from homeassistant.components.deconz.const import (
+ CONF_ALLOW_CLIP_SENSOR,
+ CONF_ALLOW_DECONZ_GROUPS,
+ CONF_MASTER_GATEWAY,
+ DOMAIN,
+)
+from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration
-from tests.common import MockConfigEntry
-
async def test_flow_1_discovered_bridge(hass, aioclient_mock):
"""Test that config flow for one discovered bridge works."""
@@ -20,10 +30,10 @@ async def test_flow_1_discovered_bridge(hass, aioclient_mock):
)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN, context={"source": "user"}
+ DOMAIN, context={"source": "user"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
aioclient_mock.post(
@@ -36,12 +46,12 @@ async def test_flow_1_discovered_bridge(hass, aioclient_mock):
result["flow_id"], user_input={}
)
- assert result["type"] == "create_entry"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == BRIDGEID
assert result["data"] == {
- config_flow.CONF_HOST: "1.2.3.4",
- config_flow.CONF_PORT: 80,
- config_flow.CONF_API_KEY: API_KEY,
+ CONF_HOST: "1.2.3.4",
+ CONF_PORT: 80,
+ CONF_API_KEY: API_KEY,
}
@@ -57,17 +67,17 @@ async def test_flow_2_discovered_bridges(hass, aioclient_mock):
)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN, context={"source": "user"}
+ DOMAIN, context={"source": "user"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.flow.async_configure(
- result["flow_id"], user_input={config_flow.CONF_HOST: "1.2.3.4"}
+ result["flow_id"], user_input={CONF_HOST: "1.2.3.4"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
aioclient_mock.post(
@@ -80,12 +90,12 @@ async def test_flow_2_discovered_bridges(hass, aioclient_mock):
result["flow_id"], user_input={}
)
- assert result["type"] == "create_entry"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == BRIDGEID
assert result["data"] == {
- config_flow.CONF_HOST: "1.2.3.4",
- config_flow.CONF_PORT: 80,
- config_flow.CONF_API_KEY: API_KEY,
+ CONF_HOST: "1.2.3.4",
+ CONF_PORT: 80,
+ CONF_API_KEY: API_KEY,
}
@@ -98,18 +108,17 @@ async def test_flow_manual_configuration(hass, aioclient_mock):
)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN, context={"source": "user"}
+ DOMAIN, context={"source": "user"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80},
+ result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80},
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
aioclient_mock.post(
@@ -128,12 +137,12 @@ async def test_flow_manual_configuration(hass, aioclient_mock):
result["flow_id"], user_input={}
)
- assert result["type"] == "create_entry"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == BRIDGEID
assert result["data"] == {
- config_flow.CONF_HOST: "1.2.3.4",
- config_flow.CONF_PORT: 80,
- config_flow.CONF_API_KEY: API_KEY,
+ CONF_HOST: "1.2.3.4",
+ CONF_PORT: 80,
+ CONF_API_KEY: API_KEY,
}
@@ -142,10 +151,10 @@ async def test_manual_configuration_after_discovery_timeout(hass, aioclient_mock
aioclient_mock.get(pydeconz.utils.URL_DISCOVER, exc=asyncio.TimeoutError)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN, context={"source": "user"}
+ DOMAIN, context={"source": "user"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
assert not hass.config_entries.flow._progress[result["flow_id"]].bridges
@@ -155,10 +164,10 @@ async def test_manual_configuration_after_discovery_ResponseError(hass, aioclien
aioclient_mock.get(pydeconz.utils.URL_DISCOVER, exc=config_flow.ResponseError)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN, context={"source": "user"}
+ DOMAIN, context={"source": "user"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
assert not hass.config_entries.flow._progress[result["flow_id"]].bridges
@@ -174,18 +183,17 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock):
)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN, context={"source": "user"}
+ DOMAIN, context={"source": "user"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={config_flow.CONF_HOST: "2.3.4.5", config_flow.CONF_PORT: 80},
+ result["flow_id"], user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 80},
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
aioclient_mock.post(
@@ -204,9 +212,9 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock):
result["flow_id"], user_input={}
)
- assert result["type"] == "abort"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5"
+ assert gateway.config_entry.data[CONF_HOST] == "2.3.4.5"
async def test_manual_configuration_dont_update_configuration(hass, aioclient_mock):
@@ -220,18 +228,17 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo
)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN, context={"source": "user"}
+ DOMAIN, context={"source": "user"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80},
+ result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80},
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
aioclient_mock.post(
@@ -250,7 +257,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo
result["flow_id"], user_input={}
)
- assert result["type"] == "abort"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
@@ -263,18 +270,17 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock):
)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN, context={"source": "user"}
+ DOMAIN, context={"source": "user"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80},
+ result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80},
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
aioclient_mock.post(
@@ -291,7 +297,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock):
result["flow_id"], user_input={}
)
- assert result["type"] == "abort"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "no_bridges"
@@ -304,10 +310,10 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock):
)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN, context={"source": "user"}
+ DOMAIN, context={"source": "user"}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
aioclient_mock.post("http://1.2.3.4:80/api", exc=pydeconz.errors.ResponseError)
@@ -316,7 +322,7 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock):
result["flow_id"], user_input={}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
assert result["errors"] == {"base": "no_key"}
@@ -324,16 +330,16 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock):
async def test_flow_ssdp_discovery(hass, aioclient_mock):
"""Test that config flow for one discovered bridge works."""
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
+ DOMAIN,
data={
ssdp.ATTR_SSDP_LOCATION: "http://1.2.3.4:80/",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL,
ssdp.ATTR_UPNP_SERIAL: BRIDGEID,
},
context={"source": "ssdp"},
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
aioclient_mock.post(
@@ -346,24 +352,24 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock):
result["flow_id"], user_input={}
)
- assert result["type"] == "create_entry"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == BRIDGEID
assert result["data"] == {
- config_flow.CONF_HOST: "1.2.3.4",
- config_flow.CONF_PORT: 80,
- config_flow.CONF_API_KEY: API_KEY,
+ CONF_HOST: "1.2.3.4",
+ CONF_PORT: 80,
+ CONF_API_KEY: API_KEY,
}
async def test_ssdp_discovery_not_deconz_bridge(hass):
"""Test a non deconz bridge being discovered over ssdp."""
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
+ DOMAIN,
data={ssdp.ATTR_UPNP_MANUFACTURER_URL: "not deconz bridge"},
context={"source": "ssdp"},
)
- assert result["type"] == "abort"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_deconz_bridge"
@@ -372,18 +378,18 @@ async def test_ssdp_discovery_update_configuration(hass):
gateway = await setup_deconz_integration(hass)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
+ DOMAIN,
data={
ssdp.ATTR_SSDP_LOCATION: "http://2.3.4.5:80/",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL,
ssdp.ATTR_UPNP_SERIAL: BRIDGEID,
},
context={"source": "ssdp"},
)
- assert result["type"] == "abort"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5"
+ assert gateway.config_entry.data[CONF_HOST] == "2.3.4.5"
async def test_ssdp_discovery_dont_update_configuration(hass):
@@ -391,18 +397,18 @@ async def test_ssdp_discovery_dont_update_configuration(hass):
gateway = await setup_deconz_integration(hass)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
+ DOMAIN,
data={
ssdp.ATTR_SSDP_LOCATION: "http://1.2.3.4:80/",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL,
ssdp.ATTR_UPNP_SERIAL: BRIDGEID,
},
context={"source": "ssdp"},
)
- assert result["type"] == "abort"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[config_flow.CONF_HOST] == "1.2.3.4"
+ assert gateway.config_entry.data[CONF_HOST] == "1.2.3.4"
async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass):
@@ -410,34 +416,34 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass):
gateway = await setup_deconz_integration(hass, source="hassio")
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
+ DOMAIN,
data={
ssdp.ATTR_SSDP_LOCATION: "http://1.2.3.4:80/",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL,
ssdp.ATTR_UPNP_SERIAL: BRIDGEID,
},
context={"source": "ssdp"},
)
- assert result["type"] == "abort"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[config_flow.CONF_HOST] == "1.2.3.4"
+ assert gateway.config_entry.data[CONF_HOST] == "1.2.3.4"
async def test_flow_hassio_discovery(hass):
"""Test hassio discovery flow works."""
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
+ DOMAIN,
data={
"addon": "Mock Addon",
- config_flow.CONF_HOST: "mock-deconz",
- config_flow.CONF_PORT: 80,
- config_flow.CONF_SERIAL: BRIDGEID,
- config_flow.CONF_API_KEY: API_KEY,
+ CONF_HOST: "mock-deconz",
+ CONF_PORT: 80,
+ CONF_SERIAL: BRIDGEID,
+ CONF_API_KEY: API_KEY,
},
context={"source": "hassio"},
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "hassio_confirm"
assert result["description_placeholders"] == {"addon": "Mock Addon"}
@@ -445,11 +451,11 @@ async def test_flow_hassio_discovery(hass):
result["flow_id"], user_input={}
)
- assert result["type"] == "create_entry"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["result"].data == {
- config_flow.CONF_HOST: "mock-deconz",
- config_flow.CONF_PORT: 80,
- config_flow.CONF_API_KEY: API_KEY,
+ CONF_HOST: "mock-deconz",
+ CONF_PORT: 80,
+ CONF_API_KEY: API_KEY,
}
@@ -458,21 +464,21 @@ async def test_hassio_discovery_update_configuration(hass):
gateway = await setup_deconz_integration(hass)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
+ DOMAIN,
data={
- config_flow.CONF_HOST: "2.3.4.5",
- config_flow.CONF_PORT: 8080,
- config_flow.CONF_API_KEY: "updated",
- config_flow.CONF_SERIAL: BRIDGEID,
+ CONF_HOST: "2.3.4.5",
+ CONF_PORT: 8080,
+ CONF_API_KEY: "updated",
+ CONF_SERIAL: BRIDGEID,
},
context={"source": "hassio"},
)
- assert result["type"] == "abort"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5"
- assert gateway.config_entry.data[config_flow.CONF_PORT] == 8080
- assert gateway.config_entry.data[config_flow.CONF_API_KEY] == "updated"
+ assert gateway.config_entry.data[CONF_HOST] == "2.3.4.5"
+ assert gateway.config_entry.data[CONF_PORT] == 8080
+ assert gateway.config_entry.data[CONF_API_KEY] == "updated"
async def test_hassio_discovery_dont_update_configuration(hass):
@@ -480,41 +486,37 @@ async def test_hassio_discovery_dont_update_configuration(hass):
await setup_deconz_integration(hass)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
+ DOMAIN,
data={
- config_flow.CONF_HOST: "1.2.3.4",
- config_flow.CONF_PORT: 80,
- config_flow.CONF_API_KEY: API_KEY,
- config_flow.CONF_SERIAL: BRIDGEID,
+ CONF_HOST: "1.2.3.4",
+ CONF_PORT: 80,
+ CONF_API_KEY: API_KEY,
+ CONF_SERIAL: BRIDGEID,
},
context={"source": "hassio"},
)
- assert result["type"] == "abort"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
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)
+ gateway = await setup_deconz_integration(hass)
- flow = await hass.config_entries.options.async_create_flow(
- entry.entry_id, context={"source": "test"}, data=None
- )
+ result = await hass.config_entries.options.async_init(gateway.config_entry.entry_id)
- result = await flow.async_step_init()
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "deconz_devices"
- result = await flow.async_step_deconz_devices(
- user_input={
- config_flow.CONF_ALLOW_CLIP_SENSOR: False,
- config_flow.CONF_ALLOW_DECONZ_GROUPS: False,
- }
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_ALLOW_CLIP_SENSOR: False, CONF_ALLOW_DECONZ_GROUPS: False},
)
- assert result["type"] == "create_entry"
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] == {
- config_flow.CONF_ALLOW_CLIP_SENSOR: False,
- config_flow.CONF_ALLOW_DECONZ_GROUPS: False,
+ CONF_ALLOW_CLIP_SENSOR: False,
+ CONF_ALLOW_DECONZ_GROUPS: False,
+ CONF_MASTER_GATEWAY: True,
}
diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py
index 5242fc6326a..4bf0ec86f4a 100644
--- a/tests/components/deconz/test_cover.py
+++ b/tests/components/deconz/test_cover.py
@@ -74,7 +74,7 @@ async def test_cover(hass):
"id": "1",
"state": {"on": True},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
level_controllable_cover = hass.states.get("cover.level_controllable_cover")
diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py
index 349b359d9b8..dd3289dea23 100644
--- a/tests/components/deconz/test_deconz_event.py
+++ b/tests/components/deconz/test_deconz_event.py
@@ -70,7 +70,7 @@ async def test_deconz_events(hass):
mock_listener = Mock()
unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener)
- gateway.api.sensors["1"].async_update({"state": {"buttonevent": 2000}})
+ gateway.api.sensors["1"].update({"state": {"buttonevent": 2000}})
await hass.async_block_till_done()
assert len(mock_listener.mock_calls) == 1
@@ -85,7 +85,7 @@ async def test_deconz_events(hass):
mock_listener = Mock()
unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener)
- gateway.api.sensors["3"].async_update({"state": {"buttonevent": 2000}})
+ gateway.api.sensors["3"].update({"state": {"buttonevent": 2000}})
await hass.async_block_till_done()
assert len(mock_listener.mock_calls) == 1
@@ -101,7 +101,7 @@ async def test_deconz_events(hass):
mock_listener = Mock()
unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener)
- gateway.api.sensors["4"].async_update({"state": {"gesture": 0}})
+ gateway.api.sensors["4"].update({"state": {"gesture": 0}})
await hass.async_block_till_done()
assert len(mock_listener.mock_calls) == 1
diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py
index fbe3dd0bb32..e39722fdacb 100644
--- a/tests/components/deconz/test_light.py
+++ b/tests/components/deconz/test_light.py
@@ -131,7 +131,7 @@ async def test_lights_and_groups(hass):
"id": "1",
"state": {"on": False},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
rgb_light = hass.states.get("light.rgb_light")
@@ -245,3 +245,29 @@ async def test_disable_light_groups(hass):
empty_group = hass.states.get("light.empty_group")
assert empty_group is None
+
+ hass.config_entries.async_update_entry(
+ gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: True}
+ )
+ await hass.async_block_till_done()
+
+ assert "light.rgb_light" in gateway.deconz_ids
+ assert "light.tunable_white_light" in gateway.deconz_ids
+ assert "light.light_group" in gateway.deconz_ids
+ assert "light.empty_group" not in gateway.deconz_ids
+ assert "light.on_off_switch" not in gateway.deconz_ids
+ # 3 entities
+ assert len(hass.states.async_all()) == 5
+
+ hass.config_entries.async_update_entry(
+ gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False}
+ )
+ await hass.async_block_till_done()
+
+ assert "light.rgb_light" in gateway.deconz_ids
+ assert "light.tunable_white_light" in gateway.deconz_ids
+ assert "light.light_group" not in gateway.deconz_ids
+ assert "light.empty_group" not in gateway.deconz_ids
+ assert "light.on_off_switch" not in gateway.deconz_ids
+ # 3 entities
+ assert len(hass.states.async_all()) == 4
diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py
index 2229031fa90..cda3138557d 100644
--- a/tests/components/deconz/test_sensor.py
+++ b/tests/components/deconz/test_sensor.py
@@ -144,7 +144,7 @@ async def test_sensors(hass):
"id": "1",
"state": {"lightlevel": 2000},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
state_changed_event = {
"t": "event",
@@ -153,7 +153,7 @@ async def test_sensors(hass):
"id": "4",
"config": {"battery": 75},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
light_level_sensor = hass.states.get("sensor.light_level_sensor")
@@ -218,6 +218,40 @@ async def test_allow_clip_sensors(hass):
clip_light_level_sensor = hass.states.get("sensor.clip_light_level_sensor")
assert clip_light_level_sensor.state == "999.8"
+ hass.config_entries.async_update_entry(
+ gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False}
+ )
+ await hass.async_block_till_done()
+
+ assert "sensor.light_level_sensor" in gateway.deconz_ids
+ assert "sensor.presence_sensor" not in gateway.deconz_ids
+ assert "sensor.switch_1" not in gateway.deconz_ids
+ assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
+ assert "sensor.switch_2" not in gateway.deconz_ids
+ assert "sensor.switch_2_battery_level" in gateway.deconz_ids
+ assert "sensor.daylight_sensor" not in gateway.deconz_ids
+ assert "sensor.power_sensor" in gateway.deconz_ids
+ assert "sensor.consumption_sensor" in gateway.deconz_ids
+ assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 5
+
+ hass.config_entries.async_update_entry(
+ gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}
+ )
+ await hass.async_block_till_done()
+
+ assert "sensor.light_level_sensor" in gateway.deconz_ids
+ assert "sensor.presence_sensor" not in gateway.deconz_ids
+ assert "sensor.switch_1" not in gateway.deconz_ids
+ assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
+ assert "sensor.switch_2" not in gateway.deconz_ids
+ assert "sensor.switch_2_battery_level" in gateway.deconz_ids
+ assert "sensor.daylight_sensor" not in gateway.deconz_ids
+ assert "sensor.power_sensor" in gateway.deconz_ids
+ assert "sensor.consumption_sensor" in gateway.deconz_ids
+ assert "sensor.clip_light_level_sensor" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 6
+
async def test_add_new_sensor(hass):
"""Test that adding a new sensor works."""
@@ -231,7 +265,7 @@ async def test_add_new_sensor(hass):
"id": "1",
"sensor": deepcopy(SENSORS["1"]),
}
- gateway.api.async_event_handler(state_added_event)
+ gateway.api.event_handler(state_added_event)
await hass.async_block_till_done()
assert "sensor.light_level_sensor" in gateway.deconz_ids
@@ -248,14 +282,14 @@ async def test_add_battery_later(hass):
remote = gateway.api.sensors["1"]
assert len(gateway.deconz_ids) == 0
assert len(gateway.events) == 1
- assert len(remote._async_callbacks) == 2
+ assert len(remote._callbacks) == 2
- remote.async_update({"config": {"battery": 50}})
+ remote.update({"config": {"battery": 50}})
await hass.async_block_till_done()
assert len(gateway.deconz_ids) == 1
assert len(gateway.events) == 1
- assert len(remote._async_callbacks) == 2
+ assert len(remote._callbacks) == 2
battery_sensor = hass.states.get("sensor.switch_1_battery_level")
assert battery_sensor is not None
diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py
index bb48a6243c6..6e151ebd47a 100644
--- a/tests/components/deconz/test_switch.py
+++ b/tests/components/deconz/test_switch.py
@@ -97,7 +97,7 @@ async def test_switches(hass):
"id": "1",
"state": {"on": False},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
state_changed_event = {
"t": "event",
"e": "changed",
@@ -105,7 +105,7 @@ async def test_switches(hass):
"id": "3",
"state": {"alert": None},
}
- gateway.api.async_event_handler(state_changed_event)
+ gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
on_off_switch = hass.states.get("switch.on_off_switch")
diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py
index 05ce55223d0..9a26d2de5ce 100644
--- a/tests/components/derivative/test_sensor.py
+++ b/tests/components/derivative/test_sensor.py
@@ -124,18 +124,18 @@ async def test_dataSet6(hass):
async def test_data_moving_average_for_discrete_sensor(hass):
"""Test derivative sensor state."""
# We simulate the following situation:
- # The temperature rises 1 °C per minute for 1 hour long.
- # There is a data point every second, however, the sensor returns
+ # The temperature rises 1 °C per minute for 30 minutes long.
+ # There is a data point every 30 seconds, however, the sensor returns
# the temperature rounded down to an integer value.
# We use a time window of 10 minutes and therefore we can expect
# (because the true derivative is 1 °C/min) an error of less than 10%.
temperature_values = []
- for temperature in range(60):
- temperature_values += [temperature] * 60
+ for temperature in range(30):
+ temperature_values += [temperature] * 2 # two values per minute
time_window = 600
+ times = list(range(0, 1800 + 30, 30))
- times = list(range(len(temperature_values)))
config, entity_id = await _setup_sensor(
hass, {"time_window": {"seconds": time_window}, "unit_time": "min", "round": 1}
) # two minute window
@@ -150,8 +150,8 @@ async def test_data_moving_average_for_discrete_sensor(hass):
state = hass.states.get("sensor.power")
derivative = round(float(state.state), config["sensor"]["round"])
# Test that the error is never more than
- # (time_window_in_minutes / true_derivative * 100) = 10%
- assert abs(1 - derivative) <= 0.1
+ # (time_window_in_minutes / true_derivative * 100) = 10% + ε
+ assert abs(1 - derivative) <= 0.1 + 1e-6
async def test_prefix(hass):
diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py
index 651d989d105..48426e2640e 100644
--- a/tests/components/device_automation/test_init.py
+++ b/tests/components/device_automation/test_init.py
@@ -761,3 +761,17 @@ async def test_automation_with_bad_trigger(hass, caplog):
)
assert "required key not provided" in caplog.text
+
+
+async def test_websocket_device_not_found(hass, hass_ws_client):
+ """Test caling command with unknown device."""
+ await async_setup_component(hass, "device_automation", {})
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {"id": 1, "type": "device_automation/action/list", "device_id": "non-existing"}
+ )
+ msg = await client.receive_json()
+
+ assert msg["id"] == 1
+ assert not msg["success"]
+ assert msg["error"] == {"code": "not_found", "message": "Device not found"}
diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py
index dda4a90f31b..c8d0e334412 100644
--- a/tests/components/device_sun_light_trigger/test_init.py
+++ b/tests/components/device_sun_light_trigger/test_init.py
@@ -187,6 +187,7 @@ async def test_lights_turn_on_when_coming_home_after_sun_set_person(hass, scanne
# person home switches on
hass.states.async_set(device_1, STATE_HOME)
await hass.async_block_till_done()
+ await hass.async_block_till_done()
assert all(
light.is_on(hass, ent_id)
diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py
index 81249c04046..c881f4b9168 100644
--- a/tests/components/dsmr/test_sensor.py
+++ b/tests/components/dsmr/test_sensor.py
@@ -52,8 +52,9 @@ async def test_default_setup(hass, mock_connection_factory):
from dsmr_parser.obis_references import (
CURRENT_ELECTRICITY_USAGE,
ELECTRICITY_ACTIVE_TARIFF,
+ GAS_METER_READING,
)
- from dsmr_parser.objects import CosemObject
+ from dsmr_parser.objects import CosemObject, MBusObject
config = {"platform": "dsmr"}
@@ -62,6 +63,12 @@ async def test_default_setup(hass, mock_connection_factory):
[{"value": Decimal("0.0"), "unit": "kWh"}]
),
ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
+ GAS_METER_READING: MBusObject(
+ [
+ {"value": datetime.datetime.fromtimestamp(1551642213)},
+ {"value": Decimal(745.695), "unit": "m3"},
+ ]
+ ),
}
with assert_setup_component(1):
@@ -90,6 +97,11 @@ async def test_default_setup(hass, mock_connection_factory):
assert power_tariff.state == "low"
assert power_tariff.attributes.get("unit_of_measurement") == ""
+ # check if gas consumption is parsed correctly
+ gas_consumption = hass.states.get("sensor.gas_consumption")
+ assert gas_consumption.state == "745.695"
+ assert gas_consumption.attributes.get("unit_of_measurement") == "m3"
+
async def test_derivative():
"""Test calculation of derivative value."""
@@ -131,6 +143,124 @@ async def test_derivative():
assert entity.unit_of_measurement == "m3/h"
+async def test_v4_meter(hass, mock_connection_factory):
+ """Test if v4 meter is correctly parsed."""
+ (connection_factory, transport, protocol) = mock_connection_factory
+
+ from dsmr_parser.obis_references import (
+ HOURLY_GAS_METER_READING,
+ ELECTRICITY_ACTIVE_TARIFF,
+ )
+ from dsmr_parser.objects import CosemObject, MBusObject
+
+ config = {"platform": "dsmr", "dsmr_version": "4"}
+
+ telegram = {
+ HOURLY_GAS_METER_READING: MBusObject(
+ [
+ {"value": datetime.datetime.fromtimestamp(1551642213)},
+ {"value": Decimal(745.695), "unit": "m3"},
+ ]
+ ),
+ ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
+ }
+
+ with assert_setup_component(1):
+ await async_setup_component(hass, "sensor", {"sensor": config})
+
+ telegram_callback = connection_factory.call_args_list[0][0][2]
+
+ # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
+ telegram_callback(telegram)
+
+ # after receiving telegram entities need to have the chance to update
+ await asyncio.sleep(0)
+
+ # tariff should be translated in human readable and have no unit
+ power_tariff = hass.states.get("sensor.power_tariff")
+ assert power_tariff.state == "low"
+ assert power_tariff.attributes.get("unit_of_measurement") == ""
+
+ # check if gas consumption is parsed correctly
+ gas_consumption = hass.states.get("sensor.gas_consumption")
+ assert gas_consumption.state == "745.695"
+ assert gas_consumption.attributes.get("unit_of_measurement") == "m3"
+
+
+async def test_belgian_meter(hass, mock_connection_factory):
+ """Test if Belgian meter is correctly parsed."""
+ (connection_factory, transport, protocol) = mock_connection_factory
+
+ from dsmr_parser.obis_references import (
+ BELGIUM_HOURLY_GAS_METER_READING,
+ ELECTRICITY_ACTIVE_TARIFF,
+ )
+ from dsmr_parser.objects import CosemObject, MBusObject
+
+ config = {"platform": "dsmr", "dsmr_version": "5B"}
+
+ telegram = {
+ BELGIUM_HOURLY_GAS_METER_READING: MBusObject(
+ [
+ {"value": datetime.datetime.fromtimestamp(1551642213)},
+ {"value": Decimal(745.695), "unit": "m3"},
+ ]
+ ),
+ ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
+ }
+
+ with assert_setup_component(1):
+ await async_setup_component(hass, "sensor", {"sensor": config})
+
+ telegram_callback = connection_factory.call_args_list[0][0][2]
+
+ # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
+ telegram_callback(telegram)
+
+ # after receiving telegram entities need to have the chance to update
+ await asyncio.sleep(0)
+
+ # tariff should be translated in human readable and have no unit
+ power_tariff = hass.states.get("sensor.power_tariff")
+ assert power_tariff.state == "normal"
+ assert power_tariff.attributes.get("unit_of_measurement") == ""
+
+ # check if gas consumption is parsed correctly
+ gas_consumption = hass.states.get("sensor.gas_consumption")
+ assert gas_consumption.state == "745.695"
+ assert gas_consumption.attributes.get("unit_of_measurement") == "m3"
+
+
+async def test_belgian_meter_low(hass, mock_connection_factory):
+ """Test if Belgian meter is correctly parsed."""
+ (connection_factory, transport, protocol) = mock_connection_factory
+
+ from dsmr_parser.obis_references import ELECTRICITY_ACTIVE_TARIFF
+ from dsmr_parser.objects import CosemObject
+
+ config = {"platform": "dsmr", "dsmr_version": "5B"}
+
+ telegram = {
+ ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}]),
+ }
+
+ with assert_setup_component(1):
+ await async_setup_component(hass, "sensor", {"sensor": config})
+
+ telegram_callback = connection_factory.call_args_list[0][0][2]
+
+ # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
+ telegram_callback(telegram)
+
+ # after receiving telegram entities need to have the chance to update
+ await asyncio.sleep(0)
+
+ # tariff should be translated in human readable and have no unit
+ power_tariff = hass.states.get("sensor.power_tariff")
+ assert power_tariff.state == "low"
+ assert power_tariff.attributes.get("unit_of_measurement") == ""
+
+
async def test_tcp(hass, mock_connection_factory):
"""If proper config provided TCP connection should be made."""
(connection_factory, transport, protocol) = mock_connection_factory
diff --git a/tests/components/dynalite/__init__.py b/tests/components/dynalite/__init__.py
new file mode 100755
index 00000000000..f97770cbac9
--- /dev/null
+++ b/tests/components/dynalite/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Dynalite component."""
diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py
new file mode 100755
index 00000000000..133e03d9f3d
--- /dev/null
+++ b/tests/components/dynalite/test_bridge.py
@@ -0,0 +1,81 @@
+"""Test Dynalite bridge."""
+from unittest.mock import Mock, call
+
+from asynctest import patch
+from dynalite_lib import CONF_ALL
+import pytest
+
+from homeassistant.components import dynalite
+
+
+@pytest.fixture
+def dyn_bridge():
+ """Define a basic mock bridge."""
+ hass = Mock()
+ host = "1.2.3.4"
+ bridge = dynalite.DynaliteBridge(hass, {dynalite.CONF_HOST: host})
+ return bridge
+
+
+async def test_update_device(dyn_bridge):
+ """Test a successful setup."""
+ async_dispatch = Mock()
+
+ with patch(
+ "homeassistant.components.dynalite.bridge.async_dispatcher_send", async_dispatch
+ ):
+ dyn_bridge.update_device(CONF_ALL)
+ async_dispatch.assert_called_once()
+ assert async_dispatch.mock_calls[0] == call(
+ dyn_bridge.hass, f"dynalite-update-{dyn_bridge.host}"
+ )
+ async_dispatch.reset_mock()
+ device = Mock
+ device.unique_id = "abcdef"
+ dyn_bridge.update_device(device)
+ async_dispatch.assert_called_once()
+ assert async_dispatch.mock_calls[0] == call(
+ dyn_bridge.hass, f"dynalite-update-{dyn_bridge.host}-{device.unique_id}"
+ )
+
+
+async def test_add_devices_then_register(dyn_bridge):
+ """Test that add_devices work."""
+ # First test empty
+ dyn_bridge.add_devices_when_registered([])
+ assert not dyn_bridge.waiting_devices
+ # Now with devices
+ device1 = Mock()
+ device1.category = "light"
+ device2 = Mock()
+ device2.category = "switch"
+ dyn_bridge.add_devices_when_registered([device1, device2])
+ reg_func = Mock()
+ dyn_bridge.register_add_devices(reg_func)
+ reg_func.assert_called_once()
+ assert reg_func.mock_calls[0][1][0][0] is device1
+
+
+async def test_register_then_add_devices(dyn_bridge):
+ """Test that add_devices work after register_add_entities."""
+ device1 = Mock()
+ device1.category = "light"
+ device2 = Mock()
+ device2.category = "switch"
+ reg_func = Mock()
+ dyn_bridge.register_add_devices(reg_func)
+ dyn_bridge.add_devices_when_registered([device1, device2])
+ reg_func.assert_called_once()
+ assert reg_func.mock_calls[0][1][0][0] is device1
+
+
+async def test_try_connection(dyn_bridge):
+ """Test that try connection works."""
+ # successful
+ with patch.object(dyn_bridge.dynalite_devices, "connected", True):
+ assert await dyn_bridge.try_connection()
+ # unsuccessful
+ with patch.object(dyn_bridge.dynalite_devices, "connected", False), patch(
+ "homeassistant.components.dynalite.bridge.CONNECT_INTERVAL", 0
+ ):
+ assert not await dyn_bridge.try_connection()
diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py
new file mode 100755
index 00000000000..1f8be61f646
--- /dev/null
+++ b/tests/components/dynalite/test_config_flow.py
@@ -0,0 +1,90 @@
+"""Test Dynalite config flow."""
+from asynctest import patch
+
+from homeassistant import config_entries
+from homeassistant.components import dynalite
+
+from tests.common import MockConfigEntry
+
+
+async def run_flow(hass, setup, connection):
+ """Run a flow with or without errors and return result."""
+ host = "1.2.3.4"
+ with patch(
+ "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
+ return_value=setup,
+ ), patch(
+ "homeassistant.components.dynalite.bridge.DynaliteDevices.available", connection
+ ), patch(
+ "homeassistant.components.dynalite.bridge.CONNECT_INTERVAL", 0
+ ):
+ result = await hass.config_entries.flow.async_init(
+ dynalite.DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={dynalite.CONF_HOST: host},
+ )
+ return result
+
+
+async def test_flow_works(hass):
+ """Test a successful config flow."""
+ result = await run_flow(hass, True, True)
+ assert result["type"] == "create_entry"
+
+
+async def test_flow_setup_fails(hass):
+ """Test a flow where async_setup fails."""
+ result = await run_flow(hass, False, True)
+ assert result["type"] == "abort"
+ assert result["reason"] == "bridge_setup_failed"
+
+
+async def test_flow_no_connection(hass):
+ """Test a flow where connection times out."""
+ result = await run_flow(hass, True, False)
+ assert result["type"] == "abort"
+ assert result["reason"] == "no_connection"
+
+
+async def test_existing(hass):
+ """Test when the entry exists with the same config."""
+ host = "1.2.3.4"
+ MockConfigEntry(
+ domain=dynalite.DOMAIN, unique_id=host, data={dynalite.CONF_HOST: host}
+ ).add_to_hass(hass)
+ with patch(
+ "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.dynalite.bridge.DynaliteDevices.available", True
+ ):
+ result = await hass.config_entries.flow.async_init(
+ dynalite.DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={dynalite.CONF_HOST: host},
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_existing_update(hass):
+ """Test when the entry exists with the same config."""
+ host = "1.2.3.4"
+ mock_entry = MockConfigEntry(
+ domain=dynalite.DOMAIN, unique_id=host, data={dynalite.CONF_HOST: host}
+ )
+ mock_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.dynalite.bridge.DynaliteDevices.available", True
+ ):
+ result = await hass.config_entries.flow.async_init(
+ dynalite.DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={dynalite.CONF_HOST: host, "aaa": "bbb"},
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+ assert mock_entry.data.get("aaa") == "bbb"
diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py
new file mode 100755
index 00000000000..d8ef0d7d259
--- /dev/null
+++ b/tests/components/dynalite/test_init.py
@@ -0,0 +1,62 @@
+"""Test Dynalite __init__."""
+
+from asynctest import patch
+
+from homeassistant.components import dynalite
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry
+
+
+async def test_empty_config(hass):
+ """Test with an empty config."""
+ assert await async_setup_component(hass, dynalite.DOMAIN, {}) is True
+ assert len(hass.config_entries.flow.async_progress()) == 0
+ assert hass.data[dynalite.DOMAIN] == {}
+
+
+async def test_async_setup(hass):
+ """Test a successful setup."""
+ host = "1.2.3.4"
+ with patch(
+ "dynalite_devices_lib.DynaliteDevices.async_setup", return_value=True
+ ), patch("dynalite_devices_lib.DynaliteDevices.available", True):
+ assert await async_setup_component(
+ hass,
+ dynalite.DOMAIN,
+ {dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}},
+ )
+
+ assert len(hass.data[dynalite.DOMAIN]) == 1
+
+
+async def test_async_setup_failed(hass):
+ """Test a setup when DynaliteBridge.async_setup fails."""
+ host = "1.2.3.4"
+ with patch("dynalite_devices_lib.DynaliteDevices.async_setup", return_value=False):
+ assert await async_setup_component(
+ hass,
+ dynalite.DOMAIN,
+ {dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}},
+ )
+ assert hass.data[dynalite.DOMAIN] == {}
+
+
+async def test_unload_entry(hass):
+ """Test being able to unload an entry."""
+ host = "1.2.3.4"
+ entry = MockConfigEntry(domain=dynalite.DOMAIN, data={"host": host})
+ entry.add_to_hass(hass)
+
+ with patch(
+ "dynalite_devices_lib.DynaliteDevices.async_setup", return_value=True
+ ), patch("dynalite_devices_lib.DynaliteDevices.available", True):
+ assert await async_setup_component(
+ hass,
+ dynalite.DOMAIN,
+ {dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}},
+ )
+ assert hass.data[dynalite.DOMAIN].get(entry.entry_id)
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ assert not hass.data[dynalite.DOMAIN].get(entry.entry_id)
diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py
new file mode 100755
index 00000000000..9934bac8720
--- /dev/null
+++ b/tests/components/dynalite/test_light.py
@@ -0,0 +1,78 @@
+"""Test Dynalite light."""
+from unittest.mock import Mock
+
+from asynctest import CoroutineMock, patch
+import pytest
+
+from homeassistant.components import dynalite
+from homeassistant.components.light import SUPPORT_BRIGHTNESS
+from homeassistant.setup import async_setup_component
+
+
+@pytest.fixture
+def mock_device():
+ """Mock a Dynalite device."""
+ device = Mock()
+ device.category = "light"
+ device.unique_id = "UNIQUE"
+ device.name = "NAME"
+ device.device_info = {
+ "identifiers": {(dynalite.DOMAIN, device.unique_id)},
+ "name": device.name,
+ "manufacturer": "Dynalite",
+ }
+ return device
+
+
+async def create_light_from_device(hass, device):
+ """Set up the component and platform and create a light based on the device provided."""
+ host = "1.2.3.4"
+ with patch(
+ "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.dynalite.bridge.DynaliteDevices.available", True
+ ):
+ assert await async_setup_component(
+ hass,
+ dynalite.DOMAIN,
+ {dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}},
+ )
+ await hass.async_block_till_done()
+ # Find the bridge
+ bridge = None
+ assert len(hass.data[dynalite.DOMAIN]) == 1
+ key = next(iter(hass.data[dynalite.DOMAIN]))
+ bridge = hass.data[dynalite.DOMAIN][key]
+ bridge.dynalite_devices.newDeviceFunc([device])
+ await hass.async_block_till_done()
+
+
+async def test_light_setup(hass, mock_device):
+ """Test a successful setup."""
+ await create_light_from_device(hass, mock_device)
+ entity_state = hass.states.get("light.name")
+ assert entity_state.attributes["brightness"] == mock_device.brightness
+ assert entity_state.attributes["supported_features"] == SUPPORT_BRIGHTNESS
+
+
+async def test_turn_on(hass, mock_device):
+ """Test turning a light on."""
+ mock_device.async_turn_on = CoroutineMock(return_value=True)
+ await create_light_from_device(hass, mock_device)
+ await hass.services.async_call(
+ "light", "turn_on", {"entity_id": "light.name"}, blocking=True
+ )
+ await hass.async_block_till_done()
+ mock_device.async_turn_on.assert_awaited_once()
+
+
+async def test_turn_off(hass, mock_device):
+ """Test turning a light off."""
+ mock_device.async_turn_off = CoroutineMock(return_value=True)
+ await create_light_from_device(hass, mock_device)
+ await hass.services.async_call(
+ "light", "turn_off", {"entity_id": "light.name"}, blocking=True
+ )
+ await hass.async_block_till_done()
+ mock_device.async_turn_off.assert_awaited_once()
diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py
index 51c3da7f08d..30b715c136b 100644
--- a/tests/components/emulated_hue/test_hue_api.py
+++ b/tests/components/emulated_hue/test_hue_api.py
@@ -610,7 +610,7 @@ async def test_close_cover(hass_hue, hue_client):
async def test_set_position_cover(hass_hue, hue_client):
- """Test setting postion cover ."""
+ """Test setting position cover ."""
COVER_ID = "cover.living_room_window"
# Turn the office light off first
await hass_hue.services.async_call(
diff --git a/tests/components/facebook/test_notify.py b/tests/components/facebook/test_notify.py
index e23cc4f0982..c4c85d1cee0 100644
--- a/tests/components/facebook/test_notify.py
+++ b/tests/components/facebook/test_notify.py
@@ -88,7 +88,7 @@ class TestFacebook(unittest.TestCase):
"""Test sending a message without a target."""
mock.register_uri(requests_mock.POST, facebook.BASE_URL, status_code=200)
- self.facebook.send_message(message="goin nowhere")
+ self.facebook.send_message(message="going nowhere")
assert not mock.called
@requests_mock.Mocker()
diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py
index bbd332bb487..b535b35e182 100644
--- a/tests/components/fritzbox/test_climate.py
+++ b/tests/components/fritzbox/test_climate.py
@@ -91,7 +91,7 @@ class TestFritzboxClimate(unittest.TestCase):
@patch.object(FritzboxThermostat, "set_hvac_mode")
def test_set_temperature_operation_mode_precedence(self, mock_set_op):
- """Test set_temperature for precedence of operation_mode arguement."""
+ """Test set_temperature for precedence of operation_mode argument."""
self.thermostat.set_temperature(hvac_mode="heat", temperature=23.0)
mock_set_op.assert_called_once_with("heat")
self.thermostat._device.set_target_temperature.assert_not_called()
diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py
index f9f25192211..627bf23341d 100644
--- a/tests/components/frontend/test_init.py
+++ b/tests/components/frontend/test_init.py
@@ -126,6 +126,16 @@ async def test_themes_api(hass, hass_ws_client):
assert msg["result"]["default_theme"] == "default"
assert msg["result"]["themes"] == {"happy": {"primary-color": "red"}}
+ # safe mode
+ hass.config.safe_mode = True
+ await client.send_json({"id": 6, "type": "frontend/get_themes"})
+ msg = await client.receive_json()
+
+ assert msg["result"]["default_theme"] == "safe_mode"
+ assert msg["result"]["themes"] == {
+ "safe_mode": {"primary-color": "#db4437", "accent-color": "#eeee02"}
+ }
+
async def test_themes_set_theme(hass, hass_ws_client):
"""Test frontend.set_theme service."""
diff --git a/tests/components/gdacs/__init__.py b/tests/components/gdacs/__init__.py
new file mode 100644
index 00000000000..6e61b86dbb7
--- /dev/null
+++ b/tests/components/gdacs/__init__.py
@@ -0,0 +1,41 @@
+"""Tests for the GDACS component."""
+from unittest.mock import MagicMock
+
+
+def _generate_mock_feed_entry(
+ external_id,
+ title,
+ distance_to_home,
+ coordinates,
+ attribution=None,
+ alert_level=None,
+ country=None,
+ duration_in_week=None,
+ event_name=None,
+ event_type_short=None,
+ event_type=None,
+ from_date=None,
+ to_date=None,
+ population=None,
+ severity=None,
+ vulnerability=None,
+):
+ """Construct a mock feed entry for testing purposes."""
+ feed_entry = MagicMock()
+ feed_entry.external_id = external_id
+ feed_entry.title = title
+ feed_entry.distance_to_home = distance_to_home
+ feed_entry.coordinates = coordinates
+ feed_entry.attribution = attribution
+ feed_entry.alert_level = alert_level
+ feed_entry.country = country
+ feed_entry.duration_in_week = duration_in_week
+ feed_entry.event_name = event_name
+ feed_entry.event_type_short = event_type_short
+ feed_entry.event_type = event_type
+ feed_entry.from_date = from_date
+ feed_entry.to_date = to_date
+ feed_entry.population = population
+ feed_entry.severity = severity
+ feed_entry.vulnerability = vulnerability
+ return feed_entry
diff --git a/tests/components/gdacs/conftest.py b/tests/components/gdacs/conftest.py
new file mode 100644
index 00000000000..47185cf5387
--- /dev/null
+++ b/tests/components/gdacs/conftest.py
@@ -0,0 +1,31 @@
+"""Configuration for GDACS tests."""
+import pytest
+
+from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN
+from homeassistant.const import (
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_RADIUS,
+ CONF_SCAN_INTERVAL,
+ CONF_UNIT_SYSTEM,
+)
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture
+def config_entry():
+ """Create a mock GDACS config entry."""
+ return MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_LATITUDE: -41.2,
+ CONF_LONGITUDE: 174.7,
+ CONF_RADIUS: 25,
+ CONF_UNIT_SYSTEM: "metric",
+ CONF_SCAN_INTERVAL: 300.0,
+ CONF_CATEGORIES: [],
+ },
+ title="-41.2, 174.7",
+ unique_id="-41.2, 174.7",
+ )
diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py
new file mode 100644
index 00000000000..f04f8158862
--- /dev/null
+++ b/tests/components/gdacs/test_config_flow.py
@@ -0,0 +1,76 @@
+"""Define tests for the GDACS config flow."""
+from datetime import timedelta
+
+from homeassistant import data_entry_flow
+from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN
+from homeassistant.const import (
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_RADIUS,
+ CONF_SCAN_INTERVAL,
+)
+
+
+async def test_duplicate_error(hass, config_entry):
+ """Test that errors are shown when duplicates are added."""
+ conf = {CONF_LATITUDE: -41.2, CONF_LONGITUDE: 174.7, CONF_RADIUS: 25}
+ config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}, data=conf
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_show_form(hass):
+ """Test that the form is served with no input."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+
+async def test_step_import(hass):
+ """Test that the import step works."""
+ conf = {
+ CONF_LATITUDE: -41.2,
+ CONF_LONGITUDE: 174.7,
+ CONF_RADIUS: 25,
+ CONF_SCAN_INTERVAL: timedelta(minutes=4),
+ CONF_CATEGORIES: ["Drought", "Earthquake"],
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "import"}, data=conf
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "-41.2, 174.7"
+ assert result["data"] == {
+ CONF_LATITUDE: -41.2,
+ CONF_LONGITUDE: 174.7,
+ CONF_RADIUS: 25,
+ CONF_SCAN_INTERVAL: 240.0,
+ CONF_CATEGORIES: ["Drought", "Earthquake"],
+ }
+
+
+async def test_step_user(hass):
+ """Test that the user step works."""
+ hass.config.latitude = -41.2
+ hass.config.longitude = 174.7
+ conf = {CONF_RADIUS: 25}
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}, data=conf
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "-41.2, 174.7"
+ assert result["data"] == {
+ CONF_LATITUDE: -41.2,
+ CONF_LONGITUDE: 174.7,
+ CONF_RADIUS: 25,
+ CONF_SCAN_INTERVAL: 300.0,
+ CONF_CATEGORIES: [],
+ }
diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py
new file mode 100644
index 00000000000..c426b081e21
--- /dev/null
+++ b/tests/components/gdacs/test_geo_location.py
@@ -0,0 +1,242 @@
+"""The tests for the GDACS Feed integration."""
+import datetime
+
+from asynctest import patch
+
+from homeassistant.components import gdacs
+from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED
+from homeassistant.components.gdacs.geo_location import (
+ ATTR_ALERT_LEVEL,
+ ATTR_COUNTRY,
+ ATTR_DESCRIPTION,
+ ATTR_DURATION_IN_WEEK,
+ ATTR_EVENT_TYPE,
+ ATTR_EXTERNAL_ID,
+ ATTR_FROM_DATE,
+ ATTR_POPULATION,
+ ATTR_SEVERITY,
+ ATTR_TO_DATE,
+ ATTR_VULNERABILITY,
+)
+from homeassistant.components.geo_location import ATTR_SOURCE
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ ATTR_FRIENDLY_NAME,
+ ATTR_ICON,
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+ ATTR_UNIT_OF_MEASUREMENT,
+ CONF_RADIUS,
+ EVENT_HOMEASSISTANT_START,
+)
+from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+
+from tests.common import async_fire_time_changed
+from tests.components.gdacs import _generate_mock_feed_entry
+
+CONFIG = {gdacs.DOMAIN: {CONF_RADIUS: 200}}
+
+
+async def test_setup(hass):
+ """Test the general setup of the integration."""
+ # Set up some mock feed entries for this test.
+ mock_entry_1 = _generate_mock_feed_entry(
+ "1234",
+ "Description 1",
+ 15.5,
+ (38.0, -3.0),
+ event_name="Name 1",
+ event_type_short="DR",
+ event_type="Drought",
+ alert_level="Alert Level 1",
+ country="Country 1",
+ attribution="Attribution 1",
+ from_date=datetime.datetime(2020, 1, 10, 8, 0, tzinfo=datetime.timezone.utc),
+ to_date=datetime.datetime(2020, 1, 20, 8, 0, tzinfo=datetime.timezone.utc),
+ duration_in_week=1,
+ population="Population 1",
+ severity="Severity 1",
+ vulnerability="Vulnerability 1",
+ )
+ mock_entry_2 = _generate_mock_feed_entry(
+ "2345",
+ "Description 2",
+ 20.5,
+ (38.1, -3.1),
+ event_name="Name 2",
+ event_type_short="TC",
+ event_type="Tropical Cyclone",
+ )
+ mock_entry_3 = _generate_mock_feed_entry(
+ "3456",
+ "Description 3",
+ 25.5,
+ (38.2, -3.2),
+ event_name="Name 3",
+ event_type_short="TC",
+ event_type="Tropical Cyclone",
+ country="Country 2",
+ )
+ mock_entry_4 = _generate_mock_feed_entry(
+ "4567", "Description 4", 12.5, (38.3, -3.3)
+ )
+
+ # Patching 'utcnow' to gain more control over the timed update.
+ utcnow = dt_util.utcnow()
+ with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch(
+ "aio_georss_client.feed.GeoRssFeed.update"
+ ) as mock_feed_update:
+ mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3]
+ assert await async_setup_component(hass, gdacs.DOMAIN, CONFIG)
+ # Artificially trigger update and collect events.
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ # 3 geolocation and 1 sensor entities
+ assert len(all_states) == 4
+
+ state = hass.states.get("geo_location.drought_name_1")
+ assert state is not None
+ assert state.name == "Drought: Name 1"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "1234",
+ ATTR_LATITUDE: 38.0,
+ ATTR_LONGITUDE: -3.0,
+ ATTR_FRIENDLY_NAME: "Drought: Name 1",
+ ATTR_DESCRIPTION: "Description 1",
+ ATTR_COUNTRY: "Country 1",
+ ATTR_ATTRIBUTION: "Attribution 1",
+ ATTR_FROM_DATE: datetime.datetime(
+ 2020, 1, 10, 8, 0, tzinfo=datetime.timezone.utc
+ ),
+ ATTR_TO_DATE: datetime.datetime(
+ 2020, 1, 20, 8, 0, tzinfo=datetime.timezone.utc
+ ),
+ ATTR_DURATION_IN_WEEK: 1,
+ ATTR_ALERT_LEVEL: "Alert Level 1",
+ ATTR_POPULATION: "Population 1",
+ ATTR_EVENT_TYPE: "Drought",
+ ATTR_SEVERITY: "Severity 1",
+ ATTR_VULNERABILITY: "Vulnerability 1",
+ ATTR_UNIT_OF_MEASUREMENT: "km",
+ ATTR_SOURCE: "gdacs",
+ ATTR_ICON: "mdi:water-off",
+ }
+ assert float(state.state) == 15.5
+
+ state = hass.states.get("geo_location.tropical_cyclone_name_2")
+ assert state is not None
+ assert state.name == "Tropical Cyclone: Name 2"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "2345",
+ ATTR_LATITUDE: 38.1,
+ ATTR_LONGITUDE: -3.1,
+ ATTR_FRIENDLY_NAME: "Tropical Cyclone: Name 2",
+ ATTR_DESCRIPTION: "Description 2",
+ ATTR_EVENT_TYPE: "Tropical Cyclone",
+ ATTR_UNIT_OF_MEASUREMENT: "km",
+ ATTR_SOURCE: "gdacs",
+ ATTR_ICON: "mdi:weather-hurricane",
+ }
+ assert float(state.state) == 20.5
+
+ state = hass.states.get("geo_location.tropical_cyclone_name_3")
+ assert state is not None
+ assert state.name == "Tropical Cyclone: Name 3"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "3456",
+ ATTR_LATITUDE: 38.2,
+ ATTR_LONGITUDE: -3.2,
+ ATTR_FRIENDLY_NAME: "Tropical Cyclone: Name 3",
+ ATTR_DESCRIPTION: "Description 3",
+ ATTR_EVENT_TYPE: "Tropical Cyclone",
+ ATTR_COUNTRY: "Country 2",
+ ATTR_UNIT_OF_MEASUREMENT: "km",
+ ATTR_SOURCE: "gdacs",
+ ATTR_ICON: "mdi:weather-hurricane",
+ }
+ assert float(state.state) == 25.5
+
+ # Simulate an update - two existing, one new entry, one outdated entry
+ mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_4, mock_entry_3]
+ async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 4
+
+ # Simulate an update - empty data, but successful update,
+ # so no changes to entities.
+ mock_feed_update.return_value = "OK_NO_DATA", None
+ async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 4
+
+ # Simulate an update - empty data, removes all entities
+ mock_feed_update.return_value = "ERROR", None
+ async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 1
+
+
+async def test_setup_imperial(hass):
+ """Test the setup of the integration using imperial unit system."""
+ hass.config.units = IMPERIAL_SYSTEM
+ # Set up some mock feed entries for this test.
+ mock_entry_1 = _generate_mock_feed_entry(
+ "1234",
+ "Description 1",
+ 15.5,
+ (38.0, -3.0),
+ event_name="Name 1",
+ event_type_short="DR",
+ event_type="Drought",
+ )
+
+ # Patching 'utcnow' to gain more control over the timed update.
+ utcnow = dt_util.utcnow()
+ with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch(
+ "aio_georss_client.feed.GeoRssFeed.update"
+ ) as mock_feed_update, patch(
+ "aio_georss_client.feed.GeoRssFeed.last_timestamp", create=True
+ ):
+ mock_feed_update.return_value = "OK", [mock_entry_1]
+ assert await async_setup_component(hass, gdacs.DOMAIN, CONFIG)
+ # Artificially trigger update and collect events.
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 2
+
+ # Test conversion of 200 miles to kilometers.
+ feeds = hass.data[DOMAIN][FEED]
+ assert feeds is not None
+ assert len(feeds) == 1
+ manager = list(feeds.values())[0]
+ # Ensure that the filter value in km is correctly set.
+ assert manager._feed_manager._feed._filter_radius == 321.8688
+
+ state = hass.states.get("geo_location.drought_name_1")
+ assert state is not None
+ assert state.name == "Drought: Name 1"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "1234",
+ ATTR_LATITUDE: 38.0,
+ ATTR_LONGITUDE: -3.0,
+ ATTR_FRIENDLY_NAME: "Drought: Name 1",
+ ATTR_DESCRIPTION: "Description 1",
+ ATTR_EVENT_TYPE: "Drought",
+ ATTR_UNIT_OF_MEASUREMENT: "mi",
+ ATTR_SOURCE: "gdacs",
+ ATTR_ICON: "mdi:water-off",
+ }
+ # 15.5km (as defined in mock entry) has been converted to 9.6mi.
+ assert float(state.state) == 9.6
diff --git a/tests/components/gdacs/test_init.py b/tests/components/gdacs/test_init.py
new file mode 100644
index 00000000000..40bda2a196b
--- /dev/null
+++ b/tests/components/gdacs/test_init.py
@@ -0,0 +1,19 @@
+"""Define tests for the GDACS general setup."""
+from asynctest import patch
+
+from homeassistant.components.gdacs import DOMAIN, FEED
+
+
+async def test_component_unload_config_entry(hass, config_entry):
+ """Test that loading and unloading of a config entry works."""
+ config_entry.add_to_hass(hass)
+ with patch("aio_georss_gdacs.GdacsFeedManager.update") as mock_feed_manager_update:
+ # Load config entry.
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert mock_feed_manager_update.call_count == 1
+ assert hass.data[DOMAIN][FEED][config_entry.entry_id] is not None
+ # Unload config entry.
+ assert await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None
diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py
new file mode 100644
index 00000000000..5e8fd5ad30f
--- /dev/null
+++ b/tests/components/gdacs/test_sensor.py
@@ -0,0 +1,100 @@
+"""The tests for the GDACS Feed integration."""
+from asynctest import patch
+
+from homeassistant.components import gdacs
+from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL
+from homeassistant.components.gdacs.sensor import (
+ ATTR_CREATED,
+ ATTR_LAST_UPDATE,
+ ATTR_LAST_UPDATE_SUCCESSFUL,
+ ATTR_REMOVED,
+ ATTR_STATUS,
+ ATTR_UPDATED,
+)
+from homeassistant.const import (
+ ATTR_ICON,
+ ATTR_UNIT_OF_MEASUREMENT,
+ CONF_RADIUS,
+ EVENT_HOMEASSISTANT_START,
+)
+from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
+
+from tests.common import async_fire_time_changed
+from tests.components.gdacs import _generate_mock_feed_entry
+
+CONFIG = {gdacs.DOMAIN: {CONF_RADIUS: 200}}
+
+
+async def test_setup(hass):
+ """Test the general setup of the integration."""
+ # Set up some mock feed entries for this test.
+ mock_entry_1 = _generate_mock_feed_entry(
+ "1234", "Title 1", 15.5, (38.0, -3.0), attribution="Attribution 1",
+ )
+ mock_entry_2 = _generate_mock_feed_entry("2345", "Title 2", 20.5, (38.1, -3.1),)
+ mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 25.5, (38.2, -3.2),)
+ mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (38.3, -3.3))
+
+ # Patching 'utcnow' to gain more control over the timed update.
+ utcnow = dt_util.utcnow()
+ with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch(
+ "aio_georss_client.feed.GeoRssFeed.update"
+ ) as mock_feed_update:
+ mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3]
+ assert await async_setup_component(hass, gdacs.DOMAIN, CONFIG)
+ # Artificially trigger update and collect events.
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ # 3 geolocation and 1 sensor entities
+ assert len(all_states) == 4
+
+ state = hass.states.get("sensor.gdacs_32_87336_117_22743")
+ assert state is not None
+ assert int(state.state) == 3
+ assert state.name == "GDACS (32.87336, -117.22743)"
+ attributes = state.attributes
+ assert attributes[ATTR_STATUS] == "OK"
+ assert attributes[ATTR_CREATED] == 3
+ assert attributes[ATTR_LAST_UPDATE].tzinfo == dt_util.UTC
+ assert attributes[ATTR_LAST_UPDATE_SUCCESSFUL].tzinfo == dt_util.UTC
+ assert attributes[ATTR_LAST_UPDATE] == attributes[ATTR_LAST_UPDATE_SUCCESSFUL]
+ assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "alerts"
+ assert attributes[ATTR_ICON] == "mdi:alert"
+
+ # Simulate an update - two existing, one new entry, one outdated entry
+ mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_4, mock_entry_3]
+ async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 4
+
+ state = hass.states.get("sensor.gdacs_32_87336_117_22743")
+ attributes = state.attributes
+ assert attributes[ATTR_CREATED] == 1
+ assert attributes[ATTR_UPDATED] == 2
+ assert attributes[ATTR_REMOVED] == 1
+
+ # Simulate an update - empty data, but successful update,
+ # so no changes to entities.
+ mock_feed_update.return_value = "OK_NO_DATA", None
+ async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 4
+
+ # Simulate an update - empty data, removes all entities
+ mock_feed_update.return_value = "ERROR", None
+ async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 1
+
+ state = hass.states.get("sensor.gdacs_32_87336_117_22743")
+ attributes = state.attributes
+ assert attributes[ATTR_REMOVED] == 3
diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py
index e5be52e6b33..8734ca0e60d 100644
--- a/tests/components/glances/test_config_flow.py
+++ b/tests/components/glances/test_config_flow.py
@@ -3,8 +3,8 @@ 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 import data_entry_flow
+from homeassistant.components import glances
from homeassistant.const import CONF_SCAN_INTERVAL
from tests.common import MockConfigEntry, mock_coro
@@ -29,22 +29,22 @@ DEMO_USER_INPUT = {
}
-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)
+
+ result = await hass.config_entries.flow.async_init(
+ glances.DOMAIN, context={"source": "user"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
with patch("glances_api.Glances"), patch.object(
Glances, "get_data", return_value=mock_coro()
):
- result = await flow.async_step_user(DEMO_USER_INPUT)
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=DEMO_USER_INPUT
+ )
assert result["type"] == "create_entry"
assert result["title"] == NAME
@@ -53,10 +53,14 @@ async def test_form(hass):
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)
+ result = await hass.config_entries.flow.async_init(
+ glances.DOMAIN, context={"source": "user"}
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=DEMO_USER_INPUT
+ )
assert result["type"] == "form"
assert result["errors"] == {"base": "cannot_connect"}
@@ -64,11 +68,15 @@ async def test_form_cannot_connect(hass):
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)
+ result = await hass.config_entries.flow.async_init(
+ glances.DOMAIN, context={"source": "user"}
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=user_input
+ )
assert result["type"] == "form"
assert result["errors"] == {"version": "wrong_version"}
@@ -77,13 +85,16 @@ async def test_form_wrong_version(hass):
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}
+ domain=glances.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)
-
+ result = await hass.config_entries.flow.async_init(
+ glances.DOMAIN, context={"source": "user"}
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=DEMO_USER_INPUT
+ )
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
@@ -91,12 +102,20 @@ async def test_form_already_configured(hass):
async def test_options(hass):
"""Test options for Glances."""
entry = MockConfigEntry(
- domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60}
+ domain=glances.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
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={glances.CONF_SCAN_INTERVAL: 10}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] == {
+ glances.CONF_SCAN_INTERVAL: 10,
+ }
diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py
index 15e84b384c0..37609e151bd 100644
--- a/tests/components/google_translate/test_tts.py
+++ b/tests/components/google_translate/test_tts.py
@@ -65,7 +65,10 @@ class TestTTSGooglePlatform:
self.hass.services.call(
tts.DOMAIN,
"google_translate_say",
- {tts.ATTR_MESSAGE: "90% of I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
+ },
)
self.hass.block_till_done()
@@ -89,7 +92,10 @@ class TestTTSGooglePlatform:
self.hass.services.call(
tts.DOMAIN,
"google_translate_say",
- {tts.ATTR_MESSAGE: "90% of I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
+ },
)
self.hass.block_till_done()
@@ -115,6 +121,7 @@ class TestTTSGooglePlatform:
tts.DOMAIN,
"google_say",
{
+ "entity_id": "media_player.something",
tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
tts.ATTR_LANGUAGE: "de",
},
@@ -139,7 +146,10 @@ class TestTTSGooglePlatform:
self.hass.services.call(
tts.DOMAIN,
"google_translate_say",
- {tts.ATTR_MESSAGE: "90% of I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
+ },
)
self.hass.block_till_done()
@@ -161,7 +171,10 @@ class TestTTSGooglePlatform:
self.hass.services.call(
tts.DOMAIN,
"google_translate_say",
- {tts.ATTR_MESSAGE: "90% of I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
+ },
)
self.hass.block_till_done()
@@ -193,6 +206,7 @@ class TestTTSGooglePlatform:
tts.DOMAIN,
"google_say",
{
+ "entity_id": "media_player.something",
tts.ATTR_MESSAGE: (
"I person is on front of your door."
"I person is on front of your door."
@@ -203,7 +217,7 @@ class TestTTSGooglePlatform:
"I person is on front of your door."
"I person is on front of your door."
"I person is on front of your door."
- )
+ ),
},
)
self.hass.block_till_done()
diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py
index 354751be0d2..31aced7f807 100644
--- a/tests/components/heos/test_media_player.py
+++ b/tests/components/heos/test_media_player.py
@@ -506,7 +506,7 @@ async def test_select_radio_favorite(hass, config_entry, config, controller, fav
async def test_select_radio_favorite_command_error(
hass, config_entry, config, controller, favorites, caplog
):
- """Tests command error loged when playing favorite."""
+ """Tests command error logged when playing favorite."""
await setup_platform(hass, config_entry, config)
player = controller.players[1]
# Test set radio preset
diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py
index 6c2b7f78e24..38a76b7c3fb 100644
--- a/tests/components/homeassistant/test_init.py
+++ b/tests/components/homeassistant/test_init.py
@@ -4,6 +4,8 @@ import asyncio
import unittest
from unittest.mock import Mock, patch
+import pytest
+import voluptuous as vol
import yaml
from homeassistant import config
@@ -11,9 +13,12 @@ import homeassistant.components as comps
from homeassistant.components.homeassistant import (
SERVICE_CHECK_CONFIG,
SERVICE_RELOAD_CORE_CONFIG,
+ SERVICE_SET_LOCATION,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
EVENT_CORE_CONFIG_UPDATE,
SERVICE_HOMEASSISTANT_RESTART,
SERVICE_HOMEASSISTANT_STOP,
@@ -24,9 +29,8 @@ from homeassistant.const import (
STATE_ON,
)
import homeassistant.core as ha
-from homeassistant.exceptions import HomeAssistantError
+from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.helpers import entity
-import homeassistant.helpers.intent as intent
from homeassistant.setup import async_setup_component
from tests.common import (
@@ -249,95 +253,6 @@ class TestComponentsCore(unittest.TestCase):
assert not mock_stop.called
-async def test_turn_on_intent(hass):
- """Test HassTurnOn intent."""
- result = await async_setup_component(hass, "homeassistant", {})
- assert result
-
- hass.states.async_set("light.test_light", "off")
- calls = async_mock_service(hass, "light", SERVICE_TURN_ON)
-
- response = await intent.async_handle(
- hass, "test", "HassTurnOn", {"name": {"value": "test light"}}
- )
- await hass.async_block_till_done()
-
- assert response.speech["plain"]["speech"] == "Turned test light on"
- assert len(calls) == 1
- call = calls[0]
- assert call.domain == "light"
- assert call.service == "turn_on"
- assert call.data == {"entity_id": ["light.test_light"]}
-
-
-async def test_turn_off_intent(hass):
- """Test HassTurnOff intent."""
- result = await async_setup_component(hass, "homeassistant", {})
- assert result
-
- hass.states.async_set("light.test_light", "on")
- calls = async_mock_service(hass, "light", SERVICE_TURN_OFF)
-
- response = await intent.async_handle(
- hass, "test", "HassTurnOff", {"name": {"value": "test light"}}
- )
- await hass.async_block_till_done()
-
- assert response.speech["plain"]["speech"] == "Turned test light off"
- assert len(calls) == 1
- call = calls[0]
- assert call.domain == "light"
- assert call.service == "turn_off"
- assert call.data == {"entity_id": ["light.test_light"]}
-
-
-async def test_toggle_intent(hass):
- """Test HassToggle intent."""
- result = await async_setup_component(hass, "homeassistant", {})
- assert result
-
- hass.states.async_set("light.test_light", "off")
- calls = async_mock_service(hass, "light", SERVICE_TOGGLE)
-
- response = await intent.async_handle(
- hass, "test", "HassToggle", {"name": {"value": "test light"}}
- )
- await hass.async_block_till_done()
-
- assert response.speech["plain"]["speech"] == "Toggled test light"
- assert len(calls) == 1
- call = calls[0]
- assert call.domain == "light"
- assert call.service == "toggle"
- assert call.data == {"entity_id": ["light.test_light"]}
-
-
-async def test_turn_on_multiple_intent(hass):
- """Test HassTurnOn intent with multiple similar entities.
-
- This tests that matching finds the proper entity among similar names.
- """
- result = await async_setup_component(hass, "homeassistant", {})
- assert result
-
- hass.states.async_set("light.test_light", "off")
- hass.states.async_set("light.test_lights_2", "off")
- hass.states.async_set("light.test_lighter", "off")
- calls = async_mock_service(hass, "light", SERVICE_TURN_ON)
-
- response = await intent.async_handle(
- hass, "test", "HassTurnOn", {"name": {"value": "test lights"}}
- )
- await hass.async_block_till_done()
-
- assert response.speech["plain"]["speech"] == "Turned test lights 2 on"
- assert len(calls) == 1
- call = calls[0]
- assert call.domain == "light"
- assert call.service == "turn_on"
- assert call.data == {"entity_id": ["light.test_lights_2"]}
-
-
async def test_turn_on_to_not_block_for_domains_without_service(hass):
"""Test if turn_on is blocking domain with no service."""
await async_setup_component(hass, "homeassistant", {})
@@ -411,3 +326,63 @@ async def test_setting_location(hass):
assert len(events) == 1
assert hass.config.latitude == 30
assert hass.config.longitude == 40
+
+
+async def test_require_admin(hass, hass_read_only_user):
+ """Test services requiring admin."""
+ await async_setup_component(hass, "homeassistant", {})
+
+ for service in (
+ SERVICE_HOMEASSISTANT_RESTART,
+ SERVICE_HOMEASSISTANT_STOP,
+ SERVICE_CHECK_CONFIG,
+ SERVICE_RELOAD_CORE_CONFIG,
+ ):
+ with pytest.raises(Unauthorized):
+ await hass.services.async_call(
+ ha.DOMAIN,
+ service,
+ {},
+ context=ha.Context(user_id=hass_read_only_user.id),
+ blocking=True,
+ )
+ assert False, f"Should have raises for {service}"
+
+ with pytest.raises(Unauthorized):
+ await hass.services.async_call(
+ ha.DOMAIN,
+ SERVICE_SET_LOCATION,
+ {"latitude": 0, "longitude": 0},
+ context=ha.Context(user_id=hass_read_only_user.id),
+ blocking=True,
+ )
+
+
+async def test_turn_on_off_toggle_schema(hass, hass_read_only_user):
+ """Test the schemas for the turn on/off/toggle services."""
+ await async_setup_component(hass, "homeassistant", {})
+
+ for service in SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE:
+ for invalid in None, "nothing", ENTITY_MATCH_ALL, ENTITY_MATCH_NONE:
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ ha.DOMAIN,
+ service,
+ {"entity_id": invalid},
+ context=ha.Context(user_id=hass_read_only_user.id),
+ blocking=True,
+ )
+
+
+async def test_not_allowing_recursion(hass, caplog):
+ """Test we do not allow recursion."""
+ await async_setup_component(hass, "homeassistant", {})
+
+ for service in SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE:
+ await hass.services.async_call(
+ ha.DOMAIN, service, {"entity_id": "homeassistant.light"}, blocking=True,
+ )
+ assert (
+ f"Called service homeassistant.{service} with invalid entity IDs homeassistant.light"
+ in caplog.text
+ ), service
diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py
index c423f66c7b8..127d7044be9 100644
--- a/tests/components/homeassistant/test_scene.py
+++ b/tests/components/homeassistant/test_scene.py
@@ -258,3 +258,33 @@ async def test_entities_in_scene(hass):
("scene.scene_3", ["light.kitchen", "light.living_room"]),
):
assert ha_scene.entities_in_scene(hass, scene_id) == entities
+
+
+async def test_config(hass):
+ """Test passing config in YAML."""
+ assert await async_setup_component(
+ hass,
+ "scene",
+ {
+ "scene": [
+ {
+ "id": "scene_id",
+ "name": "Scene Icon",
+ "icon": "mdi:party",
+ "entities": {"light.kitchen": "on"},
+ },
+ {
+ "name": "Scene No Icon",
+ "entities": {"light.kitchen": {"state": "on"}},
+ },
+ ]
+ },
+ )
+
+ icon = hass.states.get("scene.scene_icon")
+ assert icon is not None
+ assert icon.attributes["icon"] == "mdi:party"
+
+ no_icon = hass.states.get("scene.scene_no_icon")
+ assert no_icon is not None
+ assert "icon" not in no_icon.attributes
diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py
index f67e0e2478d..ddca9698987 100644
--- a/tests/components/homekit/test_accessories.py
+++ b/tests/components/homekit/test_accessories.py
@@ -246,7 +246,7 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog):
async def test_missing_linked_battery_sensor(hass, hk_driver, caplog):
- """Test battery service with mising linked_battery_sensor."""
+ """Test battery service with missing linked_battery_sensor."""
entity_id = "homekit.accessory"
linked_battery = "sensor.battery"
hass.states.async_set(entity_id, "open")
diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py
index 39ad429d6ef..3b0662dc16d 100644
--- a/tests/components/homekit_controller/test_alarm_control_panel.py
+++ b/tests/components/homekit_controller/test_alarm_control_panel.py
@@ -16,7 +16,7 @@ def create_security_system_service():
targ_state.value = 0
# According to the spec, a battery-level characteristic is normally
- # part of a seperate service. However as the code was written (which
+ # part of a separate service. However as the code was written (which
# predates this test) the battery level would have to be part of the lock
# service as it is here.
targ_state = service.add_characteristic("battery-level")
diff --git a/tests/components/homekit_controller/test_lock.py b/tests/components/homekit_controller/test_lock.py
index 3b17ad13e41..d47b77a37eb 100644
--- a/tests/components/homekit_controller/test_lock.py
+++ b/tests/components/homekit_controller/test_lock.py
@@ -16,7 +16,7 @@ def create_lock_service():
targ_state.value = 0
# According to the spec, a battery-level characteristic is normally
- # part of a seperate service. However as the code was written (which
+ # part of a separate service. However as the code was written (which
# predates this test) the battery level would have to be part of the lock
# service as it is here.
targ_state = service.add_characteristic("battery-level")
diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py
index b0b06447f8a..502e9d1b73e 100644
--- a/tests/components/homematicip_cloud/conftest.py
+++ b/tests/components/homematicip_cloud/conftest.py
@@ -1,5 +1,5 @@
"""Initializer helpers for HomematicIP fake server."""
-from asynctest import CoroutineMock, MagicMock, Mock, patch
+from asynctest import CoroutineMock, MagicMock, Mock
from homematicip.aio.auth import AsyncAuth
from homematicip.aio.connection import AsyncConnection
from homematicip.aio.home import AsyncHome
@@ -19,9 +19,8 @@ from homeassistant.components.homematicip_cloud.const import (
from homeassistant.components.homematicip_cloud.hap import HomematicipHAP
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
-from homeassistant.setup import async_setup_component
-from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeTemplate
+from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeFactory
from tests.common import MockConfigEntry
@@ -66,46 +65,12 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry:
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(
+@pytest.fixture(name="default_mock_hap_factory")
+async def default_mock_hap_factory_fixture(
hass: HomeAssistantType, mock_connection, hmip_config_entry
) -> 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,
-) -> HomematicipHAP:
- """Create a mocked homematic access point."""
- home_name = hmip_config_entry.data["name"]
- mock_home = (
- HomeTemplate(connection=mock_connection, home_name=home_name)
- .init_home()
- .get_async_home_mock()
- )
-
- hmip_config_entry.add_to_hass(hass)
- with patch(
- "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.get_hap",
- return_value=mock_home,
- ):
- assert await async_setup_component(hass, HMIPC_DOMAIN, {}) is True
-
- await hass.async_block_till_done()
-
- hap = hass.data[HMIPC_DOMAIN][HAPID]
- mock_home.on_update(hap.async_update)
- mock_home.on_create(hap.async_create_entity)
- return hap
+ return HomeFactory(hass, mock_connection, hmip_config_entry)
@pytest.fixture(name="hmip_config")
@@ -130,13 +95,14 @@ def dummy_config_fixture() -> ConfigType:
@pytest.fixture(name="mock_hap_with_service")
async def mock_hap_with_service_fixture(
- hass: HomeAssistantType, default_mock_hap, dummy_config
+ hass: HomeAssistantType, default_mock_hap_factory, dummy_config
) -> HomematicipHAP:
"""Create a fake homematic access point with hass services."""
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap()
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
+ hass.data[HMIPC_DOMAIN] = {HAPID: mock_hap}
+ return mock_hap
@pytest.fixture(name="simple_mock_home")
@@ -144,6 +110,7 @@ def simple_mock_home_fixture() -> AsyncHome:
"""Return a simple AsyncHome Mock."""
return Mock(
spec=AsyncHome,
+ name="Demo",
devices=[],
groups=[],
location=Mock(),
diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py
index 42ff2061698..403dbd873be 100644
--- a/tests/components/homematicip_cloud/helper.py
+++ b/tests/components/homematicip_cloud/helper.py
@@ -1,7 +1,7 @@
"""Helper for HomematicIP Cloud Tests."""
import json
-from asynctest import Mock
+from asynctest import Mock, patch
from homematicip.aio.class_maps import (
TYPE_CLASS_MAP,
TYPE_GROUP_MAP,
@@ -12,10 +12,15 @@ from homematicip.aio.group import AsyncGroup
from homematicip.aio.home import AsyncHome
from homematicip.home import Home
+from homeassistant import config_entries
+from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
from homeassistant.components.homematicip_cloud.device import (
ATTR_IS_GROUP,
ATTR_MODEL_TYPE,
)
+from homeassistant.components.homematicip_cloud.hap import HomematicipHAP
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.setup import async_setup_component
from tests.common import load_fixture
@@ -23,11 +28,10 @@ HAPID = "3014F7110000000000000001"
HAPPIN = "5678"
AUTH_TOKEN = "1234"
HOME_JSON = "homematicip_cloud.json"
+FIXTURE_DATA = load_fixture(HOME_JSON)
-def get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
-):
+def get_and_check_entity_basics(hass, 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
@@ -35,13 +39,13 @@ def get_and_check_entity_basics(
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)
+ hmip_device = 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
+ assert ha_state.attributes[ATTR_IS_GROUP]
return ha_state, hmip_device
@@ -67,6 +71,51 @@ async def async_manipulate_test_data(
await hass.async_block_till_done()
+class HomeFactory:
+ """Factory to create a HomematicIP Cloud Home."""
+
+ def __init__(
+ self,
+ hass: HomeAssistantType,
+ mock_connection,
+ hmip_config_entry: config_entries.ConfigEntry,
+ ):
+ """Initialize the Factory."""
+ self.hass = hass
+ self.mock_connection = mock_connection
+ self.hmip_config_entry = hmip_config_entry
+
+ async def async_get_mock_hap(
+ self, test_devices=[], test_groups=[]
+ ) -> HomematicipHAP:
+ """Create a mocked homematic access point."""
+ home_name = self.hmip_config_entry.data["name"]
+ mock_home = (
+ HomeTemplate(
+ connection=self.mock_connection,
+ home_name=home_name,
+ test_devices=test_devices,
+ test_groups=test_groups,
+ )
+ .init_home()
+ .get_async_home_mock()
+ )
+
+ self.hmip_config_entry.add_to_hass(self.hass)
+ with patch(
+ "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.get_hap",
+ return_value=mock_home,
+ ):
+ assert await async_setup_component(self.hass, HMIPC_DOMAIN, {})
+
+ await self.hass.async_block_till_done()
+
+ hap = self.hass.data[HMIPC_DOMAIN][HAPID]
+ mock_home.on_update(hap.async_update)
+ mock_home.on_create(hap.async_create_entity)
+ return hap
+
+
class HomeTemplate(Home):
"""
Home template as builder for home mock.
@@ -84,17 +133,36 @@ class HomeTemplate(Home):
_typeGroupMap = TYPE_GROUP_MAP
_typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP
- def __init__(self, connection=None, home_name=""):
+ def __init__(self, connection=None, home_name="", test_devices=[], test_groups=[]):
"""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
+ self.test_devices = test_devices
+ self.test_groups = test_groups
- def init_home(self, json_path=HOME_JSON):
+ def _cleanup_json(self, json):
+ if self.test_devices is not None:
+ new_devices = {}
+ for json_device in json["devices"].items():
+ if json_device[1]["label"] in self.test_devices:
+ new_devices.update([json_device])
+ json["devices"] = new_devices
+
+ if self.test_groups is not None:
+ new_groups = {}
+ for json_group in json["groups"].items():
+ if json_group[1]["label"] in self.test_groups:
+ new_groups.update([json_group])
+ json["groups"] = new_groups
+
+ return json
+
+ def init_home(self):
"""Init template with json."""
- self.init_json_state = json.loads(load_fixture(HOME_JSON))
+ self.init_json_state = self._cleanup_json(json.loads(FIXTURE_DATA))
self.update_home(json_state=self.init_json_state, clearConfig=True)
return self
diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py
index cf85e805143..23e5beb40eb 100644
--- a/tests/components/homematicip_cloud/test_alarm_control_panel.py
+++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py
@@ -31,37 +31,41 @@ async def _async_manipulate_security_zones(
internal_zone = home.search_group_by_id(internal_zone_id)
internal_zone.active = internal_active
+ home.from_json(json)
+ home._get_functionalHomes(json)
+ home._load_functionalChannels()
home.fire_update_event(json)
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 await async_setup_component(
+ hass,
+ ALARM_CONTROL_PANEL_DOMAIN,
+ {ALARM_CONTROL_PANEL_DOMAIN: {"platform": HMIPC_DOMAIN}},
)
+
assert not hass.data.get(HMIPC_DOMAIN)
-async def test_hmip_alarm_control_panel(hass, default_mock_hap):
+async def test_hmip_alarm_control_panel(hass, default_mock_hap_factory):
"""Test HomematicipAlarmControlPanel."""
entity_id = "alarm_control_panel.hmip_alarm_control_panel"
entity_name = "HmIP Alarm Control Panel"
device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_groups=["EXTERNAL", "INTERNAL"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "disarmed"
assert not hmip_device
- home = default_mock_hap.home
+ home = mock_hap.home
await hass.services.async_call(
"alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True
diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py
index 1cfe06ff701..a66dd6d49ea 100644
--- a/tests/components/homematicip_cloud/test_binary_sensor.py
+++ b/tests/components/homematicip_cloud/test_binary_sensor.py
@@ -30,25 +30,23 @@ 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 await async_setup_component(
+ hass, BINARY_SENSOR_DOMAIN, {BINARY_SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}},
)
assert not hass.data.get(HMIPC_DOMAIN)
-async def test_hmip_acceleration_sensor(hass, default_mock_hap):
+async def test_hmip_acceleration_sensor(hass, default_mock_hap_factory):
"""Test HomematicipAccelerationSensor."""
entity_id = "binary_sensor.garagentor"
entity_name = "Garagentor"
device_model = "HmIP-SAM"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -75,14 +73,17 @@ async def test_hmip_acceleration_sensor(hass, default_mock_hap):
assert len(hmip_device.mock_calls) == service_call_counter + 2
-async def test_hmip_contact_interface(hass, default_mock_hap):
+async def test_hmip_contact_interface(hass, default_mock_hap_factory):
"""Test HomematicipContactInterface."""
entity_id = "binary_sensor.kontakt_schnittstelle_unterputz_1_fach"
entity_name = "Kontakt-Schnittstelle Unterputz – 1-fach"
device_model = "HmIP-FCI1"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -95,14 +96,17 @@ async def test_hmip_contact_interface(hass, default_mock_hap):
assert ha_state.state == STATE_OFF
-async def test_hmip_shutter_contact(hass, default_mock_hap):
+async def test_hmip_shutter_contact(hass, default_mock_hap_factory):
"""Test HomematicipShutterContact."""
entity_id = "binary_sensor.fenstergriffsensor"
entity_name = "Fenstergriffsensor"
device_model = "HmIP-SRH"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -124,14 +128,17 @@ async def test_hmip_shutter_contact(hass, default_mock_hap):
assert ha_state.attributes[ATTR_SABOTAGE]
-async def test_hmip_motion_detector(hass, default_mock_hap):
+async def test_hmip_motion_detector(hass, default_mock_hap_factory):
"""Test HomematicipMotionDetector."""
entity_id = "binary_sensor.bewegungsmelder_fur_55er_rahmen_innen"
entity_name = "Bewegungsmelder für 55er Rahmen – innen"
device_model = "HmIP-SMI55"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -140,14 +147,17 @@ async def test_hmip_motion_detector(hass, default_mock_hap):
assert ha_state.state == STATE_ON
-async def test_hmip_presence_detector(hass, default_mock_hap):
+async def test_hmip_presence_detector(hass, default_mock_hap_factory):
"""Test HomematicipPresenceDetector."""
entity_id = "binary_sensor.spi_1"
entity_name = "SPI_1"
device_model = "HmIP-SPI"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -161,14 +171,19 @@ async def test_hmip_presence_detector(hass, default_mock_hap):
assert ha_state.attributes[ATTR_EVENT_DELAY]
-async def test_hmip_pluggable_mains_failure_surveillance_sensor(hass, default_mock_hap):
+async def test_hmip_pluggable_mains_failure_surveillance_sensor(
+ hass, default_mock_hap_factory
+):
"""Test HomematicipPresenceDetector."""
- entity_id = "binary_sensor.netzausfall"
- entity_name = "Netzausfall"
+ entity_id = "binary_sensor.netzausfalluberwachung"
+ entity_name = "Netzausfallüberwachung"
device_model = "HmIP-PMFS"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -177,14 +192,17 @@ async def test_hmip_pluggable_mains_failure_surveillance_sensor(hass, default_mo
assert ha_state.state == STATE_OFF
-async def test_hmip_smoke_detector(hass, default_mock_hap):
+async def test_hmip_smoke_detector(hass, default_mock_hap_factory):
"""Test HomematicipSmokeDetector."""
entity_id = "binary_sensor.rauchwarnmelder"
entity_name = "Rauchwarnmelder"
device_model = "HmIP-SWSD"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -196,16 +214,24 @@ async def test_hmip_smoke_detector(hass, default_mock_hap):
)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_ON
+ await async_manipulate_test_data(
+ hass, hmip_device, "smokeDetectorAlarmType", None,
+ )
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
-async def test_hmip_water_detector(hass, default_mock_hap):
+async def test_hmip_water_detector(hass, default_mock_hap_factory):
"""Test HomematicipWaterDetector."""
entity_id = "binary_sensor.wassersensor"
entity_name = "Wassersensor"
device_model = "HmIP-SWD"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -230,14 +256,17 @@ async def test_hmip_water_detector(hass, default_mock_hap):
assert ha_state.state == STATE_OFF
-async def test_hmip_storm_sensor(hass, default_mock_hap):
+async def test_hmip_storm_sensor(hass, default_mock_hap_factory):
"""Test HomematicipStormSensor."""
entity_id = "binary_sensor.weather_sensor_plus_storm"
entity_name = "Weather Sensor – plus Storm"
device_model = "HmIP-SWO-PL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Weather Sensor – plus"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -246,14 +275,17 @@ async def test_hmip_storm_sensor(hass, default_mock_hap):
assert ha_state.state == STATE_ON
-async def test_hmip_rain_sensor(hass, default_mock_hap):
+async def test_hmip_rain_sensor(hass, default_mock_hap_factory):
"""Test HomematicipRainSensor."""
entity_id = "binary_sensor.wettersensor_pro_raining"
entity_name = "Wettersensor - pro Raining"
device_model = "HmIP-SWO-PR"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Wettersensor - pro"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -262,14 +294,17 @@ async def test_hmip_rain_sensor(hass, default_mock_hap):
assert ha_state.state == STATE_ON
-async def test_hmip_sunshine_sensor(hass, default_mock_hap):
+async def test_hmip_sunshine_sensor(hass, default_mock_hap_factory):
"""Test HomematicipSunshineSensor."""
entity_id = "binary_sensor.wettersensor_pro_sunshine"
entity_name = "Wettersensor - pro Sunshine"
device_model = "HmIP-SWO-PR"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Wettersensor - pro"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -279,14 +314,17 @@ async def test_hmip_sunshine_sensor(hass, default_mock_hap):
assert ha_state.state == STATE_OFF
-async def test_hmip_battery_sensor(hass, default_mock_hap):
+async def test_hmip_battery_sensor(hass, default_mock_hap_factory):
"""Test HomematicipSunshineSensor."""
entity_id = "binary_sensor.wohnungsture_battery"
entity_name = "Wohnungstüre Battery"
device_model = "HMIP-SWDO"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Wohnungstüre"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -295,14 +333,17 @@ async def test_hmip_battery_sensor(hass, default_mock_hap):
assert ha_state.state == STATE_ON
-async def test_hmip_security_zone_sensor_group(hass, default_mock_hap):
+async def test_hmip_security_zone_sensor_group(hass, default_mock_hap_factory):
"""Test HomematicipSecurityZoneSensorGroup."""
entity_id = "binary_sensor.internal_securityzone"
entity_name = "INTERNAL SecurityZone"
device_model = "HmIP-SecurityZone"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_groups=["INTERNAL"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -327,14 +368,15 @@ async def test_hmip_security_zone_sensor_group(hass, default_mock_hap):
assert ha_state.attributes[ATTR_WINDOW_STATE] == WindowState.OPEN
-async def test_hmip_security_sensor_group(hass, default_mock_hap):
+async def test_hmip_security_sensor_group(hass, default_mock_hap_factory):
"""Test HomematicipSecuritySensorGroup."""
entity_id = "binary_sensor.buro_sensors"
entity_name = "Büro Sensors"
device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_groups=["Büro"])
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
await async_manipulate_test_data(
diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py
index db052929474..52ca13aad62 100644
--- a/tests/components/homematicip_cloud/test_climate.py
+++ b/tests/components/homematicip_cloud/test_climate.py
@@ -19,6 +19,7 @@ from homeassistant.components.climate.const import (
PRESET_AWAY,
PRESET_BOOST,
PRESET_ECO,
+ PRESET_NONE,
)
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
from homeassistant.components.homematicip_cloud.climate import (
@@ -32,23 +33,24 @@ from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basi
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 await async_setup_component(
+ hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: {"platform": HMIPC_DOMAIN}}
)
assert not hass.data.get(HMIPC_DOMAIN)
-async def test_hmip_heating_group_heat(hass, default_mock_hap):
+async def test_hmip_heating_group_heat(hass, default_mock_hap_factory):
"""Test HomematicipHeatingGroup."""
entity_id = "climate.badezimmer"
entity_name = "Badezimmer"
device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Wandthermostat", "Heizkörperthermostat3"],
+ test_groups=[entity_name],
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == HVAC_MODE_AUTO
@@ -142,7 +144,7 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap):
await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO")
await async_manipulate_test_data(
hass,
- default_mock_hap.home.get_functionalHome(IndoorClimateHome),
+ mock_hap.home.get_functionalHome(IndoorClimateHome),
"absenceType",
AbsenceType.VACATION,
fire_device=hmip_device,
@@ -153,7 +155,7 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap):
await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO")
await async_manipulate_test_data(
hass,
- default_mock_hap.home.get_functionalHome(IndoorClimateHome),
+ mock_hap.home.get_functionalHome(IndoorClimateHome),
"absenceType",
AbsenceType.PERIOD,
fire_device=hmip_device,
@@ -172,7 +174,7 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap):
assert hmip_device.mock_calls[-1][0] == "set_active_profile"
assert hmip_device.mock_calls[-1][1] == (1,)
- default_mock_hap.home.get_functionalHome(
+ mock_hap.home.get_functionalHome(
IndoorClimateHome
).absenceType = AbsenceType.PERMANENT
await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO")
@@ -230,14 +232,17 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap):
assert ha_state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
-async def test_hmip_heating_group_cool(hass, default_mock_hap):
+async def test_hmip_heating_group_cool(hass, default_mock_hap_factory):
"""Test HomematicipHeatingGroup."""
entity_id = "climate.badezimmer"
entity_name = "Badezimmer"
device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_groups=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
hmip_device.activeProfile = hmip_device.profiles[3]
@@ -347,14 +352,17 @@ async def test_hmip_heating_group_cool(hass, default_mock_hap):
assert hmip_device.mock_calls[-1][1] == (4,)
-async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap):
+async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap_factory):
"""Test HomematicipHeatingGroup."""
entity_id = "climate.schlafzimmer"
entity_name = "Schlafzimmer"
device_model = None
-
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Wandthermostat", "Heizkörperthermostat", "Pc"],
+ test_groups=[entity_name],
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert hmip_device
@@ -368,6 +376,28 @@ async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap):
assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "P2"]
+async def test_hmip_heating_group_heat_with_radiator(hass, default_mock_hap_factory):
+ """Test HomematicipHeatingGroup."""
+ entity_id = "climate.vorzimmer"
+ entity_name = "Vorzimmer"
+ device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Heizkörperthermostat2"], test_groups=[entity_name],
+ )
+ 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 == HVAC_MODE_AUTO
+ assert ha_state.attributes["current_temperature"] == 20
+ 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[ATTR_PRESET_MODE] is None
+ assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_NONE, PRESET_BOOST]
+
+
async def test_hmip_climate_services(hass, mock_hap_with_service):
"""Test HomematicipHeatingGroup."""
@@ -480,14 +510,17 @@ async def test_hmip_climate_services(hass, mock_hap_with_service):
assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access
-async def test_hmip_heating_group_services(hass, mock_hap_with_service):
+async def test_hmip_heating_group_services(hass, default_mock_hap_factory):
"""Test HomematicipHeatingGroup services."""
entity_id = "climate.badezimmer"
entity_name = "Badezimmer"
device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_groups=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, mock_hap_with_service, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state
@@ -512,6 +545,5 @@ async def test_hmip_heating_group_services(hass, mock_hap_with_service):
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) # pylint: disable=protected-access
- == 12
+ len(hmip_device._connection.mock_calls) == 4 # pylint: disable=protected-access
)
diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py
index 5b267628ae3..7da1a94bdd7 100644
--- a/tests/components/homematicip_cloud/test_cover.py
+++ b/tests/components/homematicip_cloud/test_cover.py
@@ -15,28 +15,27 @@ 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 await async_setup_component(
+ hass, COVER_DOMAIN, {COVER_DOMAIN: {"platform": HMIPC_DOMAIN}}
)
assert not hass.data.get(HMIPC_DOMAIN)
-async def test_hmip_cover_shutter(hass, default_mock_hap):
+async def test_hmip_cover_shutter(hass, default_mock_hap_factory):
"""Test HomematicipCoverShutte."""
- entity_id = "cover.sofa_links"
- entity_name = "Sofa links"
- device_model = "HmIP-FBL"
+ entity_id = "cover.broll_1"
+ entity_name = "BROLL_1"
+ device_model = "HmIP-BROLL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, 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(
@@ -49,7 +48,6 @@ async def test_hmip_cover_shutter(hass, default_mock_hap):
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",
@@ -64,7 +62,6 @@ async def test_hmip_cover_shutter(hass, default_mock_hap):
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
@@ -76,7 +73,6 @@ async def test_hmip_cover_shutter(hass, default_mock_hap):
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
@@ -90,14 +86,17 @@ async def test_hmip_cover_shutter(hass, default_mock_hap):
assert ha_state.state == STATE_UNKNOWN
-async def test_hmip_cover_slats(hass, default_mock_hap):
+async def test_hmip_cover_slats(hass, default_mock_hap_factory):
"""Test HomematicipCoverSlats."""
entity_id = "cover.sofa_links"
entity_name = "Sofa links"
device_model = "HmIP-FBL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_CLOSED
@@ -152,19 +151,26 @@ async def test_hmip_cover_slats(hass, default_mock_hap):
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, "slatsLevel", None)
+ ha_state = hass.states.get(entity_id)
+ assert not ha_state.attributes.get(ATTR_CURRENT_TILT_POSITION)
+
await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_UNKNOWN
-async def test_hmip_garage_door_tormatic(hass, default_mock_hap):
+async def test_hmip_garage_door_tormatic(hass, default_mock_hap_factory):
"""Test HomematicipCoverShutte."""
entity_id = "cover.garage_door_module"
entity_name = "Garage Door Module"
device_model = "HmIP-MOD-TM"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "closed"
@@ -199,3 +205,139 @@ async def test_hmip_garage_door_tormatic(hass, default_mock_hap):
assert len(hmip_device.mock_calls) == service_call_counter + 5
assert hmip_device.mock_calls[-1][0] == "send_door_command"
assert hmip_device.mock_calls[-1][1] == (DoorCommand.STOP,)
+
+
+async def test_hmip_cover_shutter_group(hass, default_mock_hap_factory):
+ """Test HomematicipCoverShutteGroup."""
+ entity_id = "cover.rollos_shuttergroup"
+ entity_name = "Rollos ShutterGroup"
+ device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_groups=["Rollos"])
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "closed"
+ assert ha_state.attributes[ATTR_CURRENT_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
+
+ 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
+
+ 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
+
+ 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_UNKNOWN
+
+
+async def test_hmip_cover_slats_group(hass, default_mock_hap_factory):
+ """Test slats with HomematicipCoverShutteGroup."""
+ entity_id = "cover.rollos_shuttergroup"
+ entity_name = "Rollos ShutterGroup"
+ device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_groups=["Rollos"])
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, mock_hap, entity_id, entity_name, device_model
+ )
+ await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 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
+ service_call_counter = len(hmip_device.mock_calls)
+
+ await hass.services.async_call(
+ "cover",
+ "set_cover_position",
+ {"entity_id": entity_id, "position": "50"},
+ blocking=True,
+ )
+ await hass.services.async_call(
+ "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True
+ )
+
+ assert len(hmip_device.mock_calls) == service_call_counter + 2
+ 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.5)
+ 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] == 50
+ 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 + 5
+ 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] == 50
+ 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 + 7
+ 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] == 50
+ 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 + 9
+ assert hmip_device.mock_calls[-1][0] == "set_shutter_stop"
+ assert hmip_device.mock_calls[-1][1] == ()
diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py
index 4ce6283d64d..c678bee5e32 100644
--- a/tests/components/homematicip_cloud/test_device.py
+++ b/tests/components/homematicip_cloud/test_device.py
@@ -7,18 +7,34 @@ from homeassistant.components.homematicip_cloud.hap import HomematicipHAP
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 HAPID, async_manipulate_test_data, get_and_check_entity_basics
+from .helper import (
+ HAPID,
+ HomeFactory,
+ async_manipulate_test_data,
+ get_and_check_entity_basics,
+)
-async def test_hmip_remove_device(hass, default_mock_hap):
+async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory):
+ """Ensure that all supported devices could be loaded."""
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=None, test_groups=None
+ )
+
+ assert len(mock_hap.hmip_device_by_entity_id) == 183
+
+
+async def test_hmip_remove_device(hass, default_mock_hap_factory):
"""Test Remove of hmip device."""
entity_id = "light.treppe"
entity_name = "Treppe"
device_model = "HmIP-BSL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -29,7 +45,7 @@ async def test_hmip_remove_device(hass, default_mock_hap):
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)
+ pre_mapping_count = len(mock_hap.hmip_device_by_entity_id)
hmip_device.fire_remove_event()
@@ -37,17 +53,20 @@ async def test_hmip_remove_device(hass, default_mock_hap):
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
+ assert len(mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3
-async def test_hmip_add_device(hass, default_mock_hap, hmip_config_entry):
+async def test_hmip_add_device(hass, default_mock_hap_factory, hmip_config_entry):
"""Test Remove of hmip device."""
entity_id = "light.treppe"
entity_name = "Treppe"
device_model = "HmIP-BSL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -58,25 +77,25 @@ async def test_hmip_add_device(hass, default_mock_hap, hmip_config_entry):
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)
+ pre_mapping_count = len(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
+ assert len(mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3
reloaded_hap = HomematicipHAP(hass, hmip_config_entry)
with patch(
"homeassistant.components.homematicip_cloud.HomematicipHAP",
return_value=reloaded_hap,
), patch.object(reloaded_hap, "async_connect"), patch.object(
- reloaded_hap, "get_hap", return_value=default_mock_hap.home
+ reloaded_hap, "get_hap", return_value=mock_hap.home
), patch(
"homeassistant.components.homematicip_cloud.hap.asyncio.sleep"
):
- default_mock_hap.home.fire_create_event(event_type=EventType.DEVICE_ADDED)
+ mock_hap.home.fire_create_event(event_type=EventType.DEVICE_ADDED)
await hass.async_block_till_done()
assert len(device_registry.devices) == pre_device_count
@@ -85,14 +104,15 @@ async def test_hmip_add_device(hass, default_mock_hap, hmip_config_entry):
assert len(new_hap.hmip_device_by_entity_id) == pre_mapping_count
-async def test_hmip_remove_group(hass, default_mock_hap):
+async def test_hmip_remove_group(hass, default_mock_hap_factory):
"""Test Remove of hmip group."""
entity_id = "switch.strom_group"
entity_name = "Strom Group"
device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_groups=["Strom"])
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -103,59 +123,67 @@ async def test_hmip_remove_group(hass, default_mock_hap):
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)
+ pre_mapping_count = len(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
+ assert len(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):
+async def test_all_devices_unavailable_when_hap_not_connected(
+ hass, default_mock_hap_factory
+):
"""Test make all devices unavaulable when hap is not connected."""
entity_id = "light.treppe"
entity_name = "Treppe"
device_model = "HmIP-BSL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
assert hmip_device
- assert default_mock_hap.home.connected
+ assert mock_hap.home.connected
- await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False)
+ await async_manipulate_test_data(hass, 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):
+async def test_hap_reconnected(hass, default_mock_hap_factory):
"""Test reconnect hap."""
entity_id = "light.treppe"
entity_name = "Treppe"
device_model = "HmIP-BSL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
assert hmip_device
- assert default_mock_hap.home.connected
+ assert mock_hap.home.connected
- await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False)
+ await async_manipulate_test_data(hass, 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=protected-access
- await async_manipulate_test_data(hass, default_mock_hap.home, "connected", True)
+ mock_hap._accesspoint_connected = False # pylint: disable=protected-access
+ await async_manipulate_test_data(hass, mock_hap.home, "connected", True)
await hass.async_block_till_done()
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_ON
@@ -169,7 +197,9 @@ async def test_hap_with_name(hass, mock_connection, hmip_config_entry):
device_model = "HmIP-BSL"
hmip_config_entry.data["name"] = home_name
- mock_hap = await get_mock_hap(hass, mock_connection, hmip_config_entry)
+ mock_hap = await HomeFactory(
+ hass, mock_connection, hmip_config_entry
+ ).async_get_mock_hap(test_devices=["Treppe"])
assert mock_hap
ha_state, hmip_device = get_and_check_entity_basics(
@@ -181,14 +211,17 @@ async def test_hap_with_name(hass, mock_connection, hmip_config_entry):
assert ha_state.attributes["friendly_name"] == entity_name
-async def test_hmip_reset_energy_counter_services(hass, mock_hap_with_service):
+async def test_hmip_reset_energy_counter_services(hass, default_mock_hap_factory):
"""Test reset_energy_counter service."""
entity_id = "switch.pc"
entity_name = "Pc"
device_model = "HMIP-PSM"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, mock_hap_with_service, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state
@@ -205,4 +238,4 @@ async def test_hmip_reset_energy_counter_services(hass, mock_hap_with_service):
"homematicip_cloud", "reset_energy_counter", {"entity_id": "all"}, blocking=True
)
assert hmip_device.mock_calls[-1][0] == "reset_energy_counter"
- assert len(hmip_device._connection.mock_calls) == 12 # pylint: disable=W0212
+ assert len(hmip_device._connection.mock_calls) == 4 # pylint: disable=W0212
diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py
index e42dfe8fb4e..1dd5b2fc789 100644
--- a/tests/components/homematicip_cloud/test_hap.py
+++ b/tests/components/homematicip_cloud/test_hap.py
@@ -101,15 +101,16 @@ async def test_hap_setup_connection_error():
with patch.object(hap, "get_hap", side_effect=HmipcConnectionError), pytest.raises(
ConfigEntryNotReady
):
- await hap.async_setup()
+ assert not await hap.async_setup()
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(hass, default_mock_hap):
+async def test_hap_reset_unloads_entry_if_setup(hass, default_mock_hap_factory):
"""Test calling reset while the entry has been setup."""
- assert hass.data[HMIPC_DOMAIN][HAPID] == default_mock_hap
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap()
+ assert hass.data[HMIPC_DOMAIN][HAPID] == mock_hap
config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN)
assert len(config_entries) == 1
# hap_reset is called during unload
@@ -131,21 +132,22 @@ async def test_hap_create(hass, hmip_config_entry, simple_mock_home):
assert await hap.async_setup()
-async def test_hap_create_exception(hass, hmip_config_entry, simple_mock_home):
+async def test_hap_create_exception(hass, hmip_config_entry):
"""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,
+ "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state",
+ side_effect=Exception,
+ ):
+ assert not await hap.async_setup()
+
+ with patch(
+ "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state",
+ side_effect=HmipConnectionError,
), pytest.raises(ConfigEntryNotReady):
await hap.async_setup()
diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py
index ee63dba3c97..ef7f5fa24ae 100644
--- a/tests/components/homematicip_cloud/test_init.py
+++ b/tests/components/homematicip_cloud/test_init.py
@@ -1,6 +1,7 @@
"""Test HomematicIP Cloud setup process."""
from asynctest import CoroutineMock, Mock, patch
+from homematicip.base.base_connection import HmipConnectionError
from homeassistant.components.homematicip_cloud.const import (
CONF_ACCESSPOINT,
@@ -11,7 +12,12 @@ from homeassistant.components.homematicip_cloud.const import (
HMIPC_NAME,
)
from homeassistant.components.homematicip_cloud.hap import HomematicipHAP
-from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
+from homeassistant.config_entries import (
+ ENTRY_STATE_LOADED,
+ ENTRY_STATE_NOT_LOADED,
+ ENTRY_STATE_SETUP_ERROR,
+ ENTRY_STATE_SETUP_RETRY,
+)
from homeassistant.const import CONF_NAME
from homeassistant.setup import async_setup_component
@@ -31,10 +37,7 @@ async def test_config_with_accesspoint_passed_to_config_entry(hass):
# no acccesspoint exists
assert not hass.data.get(HMIPC_DOMAIN)
- assert (
- await async_setup_component(hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config})
- is True
- )
+ assert await async_setup_component(hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config})
# config_entry created for access point
config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN)
@@ -70,10 +73,7 @@ async def test_config_already_registered_not_passed_to_config_entry(hass):
CONF_AUTHTOKEN: "123",
CONF_NAME: "name",
}
- assert (
- await async_setup_component(hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config})
- is True
- )
+ assert await async_setup_component(hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config})
# no new config_entry created / still one config_entry
config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN)
@@ -87,6 +87,34 @@ async def test_config_already_registered_not_passed_to_config_entry(hass):
assert config_entries[0].unique_id == "ABC123"
+async def test_load_entry_fails_due_to_connection_error(hass, hmip_config_entry):
+ """Test load entry fails due to connection error."""
+ hmip_config_entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state",
+ side_effect=HmipConnectionError,
+ ):
+ assert await async_setup_component(hass, HMIPC_DOMAIN, {})
+
+ assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id]
+ assert hmip_config_entry.state == ENTRY_STATE_SETUP_RETRY
+
+
+async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry):
+ """Test load entry fails due to generic exception."""
+ hmip_config_entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state",
+ side_effect=Exception,
+ ):
+ assert await async_setup_component(hass, HMIPC_DOMAIN, {})
+
+ assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id]
+ assert hmip_config_entry.state == ENTRY_STATE_SETUP_ERROR
+
+
async def test_unload_entry(hass):
"""Test being able to unload an entry."""
mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"}
@@ -101,7 +129,7 @@ async def test_unload_entry(hass):
instance.home.currentAPVersion = "mock-ap-version"
instance.async_reset = CoroutineMock(return_value=True)
- assert await async_setup_component(hass, HMIPC_DOMAIN, {}) is True
+ assert await async_setup_component(hass, HMIPC_DOMAIN, {})
assert mock_hap.return_value.mock_calls[0][0] == "async_setup"
@@ -127,3 +155,71 @@ async def test_hmip_dump_hap_config_services(hass, mock_hap_with_service):
assert home.mock_calls[-1][0] == "download_configuration"
assert home.mock_calls
assert write_mock.mock_calls
+
+
+async def test_setup_services_and_unload_services(hass):
+ """Test setup services and unload services."""
+ mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"}
+ MockConfigEntry(domain=HMIPC_DOMAIN, data=mock_config).add_to_hass(hass)
+
+ with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap:
+ instance = mock_hap.return_value
+ instance.async_setup = CoroutineMock(return_value=True)
+ instance.home.id = "1"
+ instance.home.modelType = "mock-type"
+ instance.home.name = "mock-name"
+ instance.home.currentAPVersion = "mock-ap-version"
+ instance.async_reset = CoroutineMock(return_value=True)
+
+ assert await async_setup_component(hass, HMIPC_DOMAIN, {})
+
+ # Check services are created
+ hmipc_services = hass.services.async_services()[HMIPC_DOMAIN]
+ assert len(hmipc_services) == 8
+
+ config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN)
+ assert len(config_entries) == 1
+
+ await hass.config_entries.async_unload(config_entries[0].entry_id)
+ # Check services are removed
+ assert not hass.services.async_services().get(HMIPC_DOMAIN)
+
+
+async def test_setup_two_haps_unload_one_by_one(hass):
+ """Test setup two access points and unload one by one and check services."""
+
+ # Setup AP1
+ mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"}
+ MockConfigEntry(domain=HMIPC_DOMAIN, data=mock_config).add_to_hass(hass)
+ # Setup AP2
+ mock_config2 = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC1234", HMIPC_NAME: "name2"}
+ MockConfigEntry(domain=HMIPC_DOMAIN, data=mock_config2).add_to_hass(hass)
+
+ with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap:
+ instance = mock_hap.return_value
+ instance.async_setup = CoroutineMock(return_value=True)
+ instance.home.id = "1"
+ instance.home.modelType = "mock-type"
+ instance.home.name = "mock-name"
+ instance.home.currentAPVersion = "mock-ap-version"
+ instance.async_reset = CoroutineMock(return_value=True)
+
+ assert await async_setup_component(hass, HMIPC_DOMAIN, {})
+
+ hmipc_services = hass.services.async_services()[HMIPC_DOMAIN]
+ assert len(hmipc_services) == 8
+
+ config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN)
+ assert len(config_entries) == 2
+ # unload the first AP
+ await hass.config_entries.async_unload(config_entries[0].entry_id)
+
+ # services still exists
+ hmipc_services = hass.services.async_services()[HMIPC_DOMAIN]
+ assert len(hmipc_services) == 8
+
+ # unload the second AP
+ await hass.config_entries.async_unload(config_entries[1].entry_id)
+
+ # Check services are removed
+ assert not hass.services.async_services().get(HMIPC_DOMAIN)
diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py
index 632a6aac449..8909e469ee9 100644
--- a/tests/components/homematicip_cloud/test_light.py
+++ b/tests/components/homematicip_cloud/test_light.py
@@ -19,23 +19,23 @@ 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 await async_setup_component(
+ hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": HMIPC_DOMAIN}}
)
assert not hass.data.get(HMIPC_DOMAIN)
-async def test_hmip_light(hass, default_mock_hap):
+async def test_hmip_light(hass, default_mock_hap_factory):
"""Test HomematicipLight."""
entity_id = "light.treppe"
entity_name = "Treppe"
device_model = "HmIP-BSL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -64,14 +64,17 @@ async def test_hmip_light(hass, default_mock_hap):
assert ha_state.state == STATE_ON
-async def test_hmip_notification_light(hass, default_mock_hap):
+async def test_hmip_notification_light(hass, default_mock_hap_factory):
"""Test HomematicipNotificationLight."""
entity_id = "light.treppe_top_notification"
entity_name = "Treppe Top Notification"
device_model = "HmIP-BSL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Treppe"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -152,14 +155,17 @@ async def test_hmip_notification_light(hass, default_mock_hap):
assert not ha_state.attributes.get(ATTR_BRIGHTNESS)
-async def test_hmip_dimmer(hass, default_mock_hap):
+async def test_hmip_dimmer(hass, default_mock_hap_factory):
"""Test HomematicipDimmer."""
entity_id = "light.schlafzimmerlicht"
entity_name = "Schlafzimmerlicht"
device_model = "HmIP-BDT"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
@@ -201,14 +207,17 @@ async def test_hmip_dimmer(hass, default_mock_hap):
assert not ha_state.attributes.get(ATTR_BRIGHTNESS)
-async def test_hmip_light_measuring(hass, default_mock_hap):
+async def test_hmip_light_measuring(hass, default_mock_hap_factory):
"""Test HomematicipLightMeasuring."""
entity_id = "light.flur_oben"
entity_name = "Flur oben"
device_model = "HmIP-BSM"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py
index f0a81c69074..2ca36e228cc 100644
--- a/tests/components/homematicip_cloud/test_sensor.py
+++ b/tests/components/homematicip_cloud/test_sensor.py
@@ -30,23 +30,23 @@ 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 await async_setup_component(
+ hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}}
)
assert not hass.data.get(HMIPC_DOMAIN)
-async def test_hmip_accesspoint_status(hass, default_mock_hap):
+async def test_hmip_accesspoint_status(hass, default_mock_hap_factory):
"""Test HomematicipSwitch."""
entity_id = "sensor.access_point"
entity_name = "Access Point"
device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert hmip_device
assert ha_state.state == "8.0"
@@ -58,14 +58,17 @@ async def test_hmip_accesspoint_status(hass, default_mock_hap):
assert ha_state.state == "17.3"
-async def test_hmip_heating_thermostat(hass, default_mock_hap):
+async def test_hmip_heating_thermostat(hass, default_mock_hap_factory):
"""Test HomematicipHeatingThermostat."""
entity_id = "sensor.heizkorperthermostat_heating"
entity_name = "Heizkörperthermostat Heating"
device_model = "HMIP-eTRV"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Heizkörperthermostat"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "0"
@@ -89,14 +92,17 @@ async def test_hmip_heating_thermostat(hass, default_mock_hap):
assert ha_state.attributes["icon"] == "mdi:battery-outline"
-async def test_hmip_humidity_sensor(hass, default_mock_hap):
+async def test_hmip_humidity_sensor(hass, default_mock_hap_factory):
"""Test HomematicipHumiditySensor."""
entity_id = "sensor.bwth_1_humidity"
entity_name = "BWTH 1 Humidity"
device_model = "HmIP-BWTH"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["BWTH 1"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "40"
@@ -109,14 +115,17 @@ async def test_hmip_humidity_sensor(hass, default_mock_hap):
assert ha_state.attributes[ATTR_RSSI_PEER] == -77
-async def test_hmip_temperature_sensor1(hass, default_mock_hap):
+async def test_hmip_temperature_sensor1(hass, default_mock_hap_factory):
"""Test HomematicipTemperatureSensor."""
entity_id = "sensor.bwth_1_temperature"
entity_name = "BWTH 1 Temperature"
device_model = "HmIP-BWTH"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["BWTH 1"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "21.0"
@@ -131,14 +140,17 @@ async def test_hmip_temperature_sensor1(hass, default_mock_hap):
assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10
-async def test_hmip_temperature_sensor2(hass, default_mock_hap):
+async def test_hmip_temperature_sensor2(hass, default_mock_hap_factory):
"""Test HomematicipTemperatureSensor."""
entity_id = "sensor.heizkorperthermostat_temperature"
entity_name = "Heizkörperthermostat Temperature"
device_model = "HMIP-eTRV"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Heizkörperthermostat"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "20.0"
@@ -153,14 +165,42 @@ async def test_hmip_temperature_sensor2(hass, default_mock_hap):
assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10
-async def test_hmip_power_sensor(hass, default_mock_hap):
+async def test_hmip_temperature_sensor3(hass, default_mock_hap_factory):
+ """Test HomematicipTemperatureSensor."""
+ entity_id = "sensor.raumbediengerat_analog_temperature"
+ entity_name = "Raumbediengerät Analog Temperature"
+ device_model = "ALPHA-IP-RBGa"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Raumbediengerät Analog"]
+ )
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == "23.3"
+ assert ha_state.attributes[ATTR_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(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_factory):
"""Test HomematicipPowerSensor."""
entity_id = "sensor.flur_oben_power"
entity_name = "Flur oben Power"
device_model = "HmIP-BSM"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Flur oben"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "0.0"
@@ -187,14 +227,17 @@ async def test_hmip_power_sensor(hass, default_mock_hap):
assert ha_state.attributes[ATTR_CONFIG_PENDING]
-async def test_hmip_illuminance_sensor1(hass, default_mock_hap):
+async def test_hmip_illuminance_sensor1(hass, default_mock_hap_factory):
"""Test HomematicipIlluminanceSensor."""
entity_id = "sensor.wettersensor_illuminance"
entity_name = "Wettersensor Illuminance"
device_model = "HmIP-SWO-B"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Wettersensor"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "4890.0"
@@ -204,14 +247,17 @@ async def test_hmip_illuminance_sensor1(hass, default_mock_hap):
assert ha_state.state == "231"
-async def test_hmip_illuminance_sensor2(hass, default_mock_hap):
+async def test_hmip_illuminance_sensor2(hass, default_mock_hap_factory):
"""Test HomematicipIlluminanceSensor."""
entity_id = "sensor.lichtsensor_nord_illuminance"
entity_name = "Lichtsensor Nord Illuminance"
device_model = "HmIP-SLO"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Lichtsensor Nord"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "807.3"
@@ -224,14 +270,17 @@ async def test_hmip_illuminance_sensor2(hass, default_mock_hap):
assert ha_state.attributes[ATTR_LOWEST_ILLUMINATION] == 785.2
-async def test_hmip_windspeed_sensor(hass, default_mock_hap):
+async def test_hmip_windspeed_sensor(hass, default_mock_hap_factory):
"""Test HomematicipWindspeedSensor."""
entity_id = "sensor.wettersensor_pro_windspeed"
entity_name = "Wettersensor - pro Windspeed"
device_model = "HmIP-SWO-PR"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Wettersensor - pro"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "2.6"
@@ -268,14 +317,17 @@ async def test_hmip_windspeed_sensor(hass, default_mock_hap):
assert ha_state.attributes[ATTR_WIND_DIRECTION] == txt
-async def test_hmip_today_rain_sensor(hass, default_mock_hap):
+async def test_hmip_today_rain_sensor(hass, default_mock_hap_factory):
"""Test HomematicipTodayRainSensor."""
entity_id = "sensor.weather_sensor_plus_today_rain"
entity_name = "Weather Sensor – plus Today Rain"
device_model = "HmIP-SWO-PL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=["Weather Sensor – plus"]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "3.9"
@@ -285,14 +337,17 @@ async def test_hmip_today_rain_sensor(hass, default_mock_hap):
assert ha_state.state == "14.2"
-async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap):
+async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap_factory):
"""Test HomematicipPassageDetectorDeltaCounter."""
entity_id = "sensor.spdr_1"
entity_name = "SPDR_1"
device_model = "HmIP-SPDR"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "164"
diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py
index b8ca7b4b67e..0cd01154753 100644
--- a/tests/components/homematicip_cloud/test_switch.py
+++ b/tests/components/homematicip_cloud/test_switch.py
@@ -16,23 +16,23 @@ 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 await async_setup_component(
+ hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": HMIPC_DOMAIN}}
)
assert not hass.data.get(HMIPC_DOMAIN)
-async def test_hmip_switch(hass, default_mock_hap):
+async def test_hmip_switch(hass, default_mock_hap_factory):
"""Test HomematicipSwitch."""
entity_id = "switch.schrank"
entity_name = "Schrank"
device_model = "HMIP-PS"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -59,14 +59,17 @@ async def test_hmip_switch(hass, default_mock_hap):
assert ha_state.state == STATE_ON
-async def test_hmip_switch_measuring(hass, default_mock_hap):
+async def test_hmip_switch_measuring(hass, default_mock_hap_factory):
"""Test HomematicipSwitchMeasuring."""
entity_id = "switch.pc"
entity_name = "Pc"
device_model = "HMIP-PSM"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -100,14 +103,15 @@ async def test_hmip_switch_measuring(hass, default_mock_hap):
assert not ha_state.attributes.get(ATTR_TODAY_ENERGY_KWH)
-async def test_hmip_group_switch(hass, default_mock_hap):
+async def test_hmip_group_switch(hass, default_mock_hap_factory):
"""Test HomematicipGroupSwitch."""
entity_id = "switch.strom_group"
entity_name = "Strom Group"
device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_groups=["Strom"])
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
@@ -139,14 +143,22 @@ async def test_hmip_group_switch(hass, default_mock_hap):
assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE]
-async def test_hmip_multi_switch(hass, default_mock_hap):
+async def test_hmip_multi_switch(hass, default_mock_hap_factory):
"""Test HomematicipMultiSwitch."""
entity_id = "switch.jalousien_1_kizi_2_schlazi_channel1"
entity_name = "Jalousien - 1 KiZi, 2 SchlaZi Channel1"
device_model = "HmIP-PCBS2"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[
+ "Jalousien - 1 KiZi, 2 SchlaZi",
+ "Multi IO Box",
+ "Heizungsaktor",
+ "ioBroker",
+ ]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py
index 9427a2d05bf..e3370e77ffe 100644
--- a/tests/components/homematicip_cloud/test_weather.py
+++ b/tests/components/homematicip_cloud/test_weather.py
@@ -15,23 +15,23 @@ 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 await async_setup_component(
+ hass, WEATHER_DOMAIN, {WEATHER_DOMAIN: {"platform": HMIPC_DOMAIN}}
)
assert not hass.data.get(HMIPC_DOMAIN)
-async def test_hmip_weather_sensor(hass, default_mock_hap):
+async def test_hmip_weather_sensor(hass, default_mock_hap_factory):
"""Test HomematicipWeatherSensor."""
entity_id = "weather.weather_sensor_plus"
entity_name = "Weather Sensor – plus"
device_model = "HmIP-SWO-PL"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == ""
@@ -45,14 +45,17 @@ async def test_hmip_weather_sensor(hass, default_mock_hap):
assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1
-async def test_hmip_weather_sensor_pro(hass, default_mock_hap):
+async def test_hmip_weather_sensor_pro(hass, default_mock_hap_factory):
"""Test HomematicipWeatherSensorPro."""
entity_id = "weather.wettersensor_pro"
entity_name = "Wettersensor - pro"
device_model = "HmIP-SWO-PR"
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "sunny"
@@ -67,14 +70,15 @@ async def test_hmip_weather_sensor_pro(hass, default_mock_hap):
assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1
-async def test_hmip_home_weather(hass, default_mock_hap):
+async def test_hmip_home_weather(hass, default_mock_hap_factory):
"""Test HomematicipHomeWeather."""
entity_id = "weather.weather_1010_wien_osterreich"
entity_name = "Weather 1010 Wien, Österreich"
device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap()
ha_state, hmip_device = get_and_check_entity_basics(
- hass, default_mock_hap, entity_id, entity_name, device_model
+ hass, mock_hap, entity_id, entity_name, device_model
)
assert hmip_device
assert ha_state.state == "partlycloudy"
@@ -85,11 +89,7 @@ async def test_hmip_home_weather(hass, default_mock_hap):
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,
+ hass, mock_hap.home.weather, "temperature", 28.3, fire_device=mock_hap.home
)
ha_state = hass.states.get(entity_id)
diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py
index b1f6785b0a7..1ca2eca664e 100644
--- a/tests/components/hue/test_config_flow.py
+++ b/tests/components/hue/test_config_flow.py
@@ -2,7 +2,9 @@
import asyncio
from unittest.mock import Mock
+from aiohttp import client_exceptions
import aiohue
+from aiohue.discovery import URL_NUPNP
from asynctest import CoroutineMock, patch
import pytest
import voluptuous as vol
@@ -77,6 +79,7 @@ async def test_flow_works(hass):
assert result["data"] == {
"host": "1.2.3.4",
"username": "home-assistant#test-home",
+ "allow_hue_groups": False,
}
assert len(mock_bridge.initialize.mock_calls) == 1
@@ -84,7 +87,7 @@ async def test_flow_works(hass):
async def test_flow_no_discovered_bridges(hass, aioclient_mock):
"""Test config flow discovers no bridges."""
- aioclient_mock.get(const.API_NUPNP, json=[])
+ aioclient_mock.get(URL_NUPNP, json=[])
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": "user"}
@@ -95,9 +98,7 @@ async def test_flow_no_discovered_bridges(hass, aioclient_mock):
async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock):
"""Test config flow discovers only already configured bridges."""
- aioclient_mock.get(
- const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}]
- )
+ aioclient_mock.get(URL_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}])
MockConfigEntry(
domain="hue", unique_id="bla", data={"host": "1.2.3.4"}
).add_to_hass(hass)
@@ -111,9 +112,7 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock):
async def test_flow_one_bridge_discovered(hass, aioclient_mock):
"""Test config flow discovers one bridge."""
- aioclient_mock.get(
- const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}]
- )
+ aioclient_mock.get(URL_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}])
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": "user"}
@@ -130,7 +129,7 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock):
).add_to_hass(hass)
aioclient_mock.get(
- const.API_NUPNP,
+ URL_NUPNP,
json=[
{"internalipaddress": "1.2.3.4", "id": "bla"},
{"internalipaddress": "5.6.7.8", "id": "beer"},
@@ -153,7 +152,7 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock):
async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock):
"""Test config flow discovers two bridges."""
aioclient_mock.get(
- const.API_NUPNP,
+ URL_NUPNP,
json=[
{"internalipaddress": "1.2.3.4", "id": "bla"},
{"internalipaddress": "5.6.7.8", "id": "beer"},
@@ -215,7 +214,7 @@ async def test_flow_link_timeout(hass):
async def test_flow_link_unknown_error(hass):
- """Test if a unknown error happend during the linking processes."""
+ """Test if a unknown error happened during the linking processes."""
mock_bridge = get_mock_bridge(mock_create_user=CoroutineMock(side_effect=OSError),)
with patch(
"homeassistant.components.hue.config_flow.discover_nupnp",
@@ -259,7 +258,7 @@ async def test_flow_link_button_not_pressed(hass):
async def test_flow_link_unknown_host(hass):
"""Test config flow ."""
mock_bridge = get_mock_bridge(
- mock_create_user=CoroutineMock(side_effect=aiohue.RequestError),
+ mock_create_user=CoroutineMock(side_effect=client_exceptions.ClientOSError),
)
with patch(
"homeassistant.components.hue.config_flow.discover_nupnp",
@@ -442,6 +441,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass):
assert result["data"] == {
"host": "2.2.2.2",
"username": "username-abc",
+ "allow_hue_groups": False,
}
entries = hass.config_entries.async_entries("hue")
assert len(entries) == 2
diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py
index 375d5da4456..d9131dad226 100644
--- a/tests/components/hue/test_init.py
+++ b/tests/components/hue/test_init.py
@@ -37,7 +37,7 @@ async def test_setup_defined_hosts_known_auth(hass):
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True,
},
- {hue.CONF_HOST: "1.1.1.1", "filename": "bla"},
+ {hue.CONF_HOST: "1.1.1.1"},
]
}
},
@@ -59,7 +59,6 @@ async def test_setup_defined_hosts_known_auth(hass):
hue.CONF_HOST: "1.1.1.1",
hue.CONF_ALLOW_HUE_GROUPS: True,
hue.CONF_ALLOW_UNREACHABLE: False,
- "filename": "bla",
},
}
diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py
index df3fe5f8998..72546891a63 100644
--- a/tests/components/hue/test_light.py
+++ b/tests/components/hue/test_light.py
@@ -206,8 +206,8 @@ def mock_bridge(hass):
return bridge.mock_group_responses.popleft()
return None
- async def async_request_call(coro):
- await coro
+ async def async_request_call(task):
+ await task()
bridge.async_request_call = async_request_call
bridge.api.config.apiversion = "9.9.9"
@@ -703,9 +703,10 @@ def test_available():
colorgamuttype=LIGHT_GAMUT_TYPE,
colorgamut=LIGHT_GAMUT,
),
- coordinator=Mock(failed_last_update=False),
+ coordinator=Mock(last_update_success=True),
bridge=Mock(allow_unreachable=False),
is_group=False,
+ supported_features=hue_light.SUPPORT_HUE_EXTENDED,
)
assert light.available is False
@@ -717,9 +718,10 @@ def test_available():
colorgamuttype=LIGHT_GAMUT_TYPE,
colorgamut=LIGHT_GAMUT,
),
- coordinator=Mock(failed_last_update=False),
+ coordinator=Mock(last_update_success=True),
bridge=Mock(allow_unreachable=True),
is_group=False,
+ supported_features=hue_light.SUPPORT_HUE_EXTENDED,
)
assert light.available is True
@@ -731,9 +733,10 @@ def test_available():
colorgamuttype=LIGHT_GAMUT_TYPE,
colorgamut=LIGHT_GAMUT,
),
- coordinator=Mock(failed_last_update=False),
+ coordinator=Mock(last_update_success=True),
bridge=Mock(allow_unreachable=False),
is_group=True,
+ supported_features=hue_light.SUPPORT_HUE_EXTENDED,
)
assert light.available is True
@@ -748,9 +751,10 @@ def test_hs_color():
colorgamuttype=LIGHT_GAMUT_TYPE,
colorgamut=LIGHT_GAMUT,
),
- coordinator=Mock(failed_last_update=False),
+ coordinator=Mock(last_update_success=True),
bridge=Mock(),
is_group=False,
+ supported_features=hue_light.SUPPORT_HUE_EXTENDED,
)
assert light.hs_color is None
@@ -762,9 +766,10 @@ def test_hs_color():
colorgamuttype=LIGHT_GAMUT_TYPE,
colorgamut=LIGHT_GAMUT,
),
- coordinator=Mock(failed_last_update=False),
+ coordinator=Mock(last_update_success=True),
bridge=Mock(),
is_group=False,
+ supported_features=hue_light.SUPPORT_HUE_EXTENDED,
)
assert light.hs_color is None
@@ -776,9 +781,192 @@ def test_hs_color():
colorgamuttype=LIGHT_GAMUT_TYPE,
colorgamut=LIGHT_GAMUT,
),
- coordinator=Mock(failed_last_update=False),
+ coordinator=Mock(last_update_success=True),
bridge=Mock(),
is_group=False,
+ supported_features=hue_light.SUPPORT_HUE_EXTENDED,
)
assert light.hs_color == color.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT)
+
+
+async def test_group_features(hass, mock_bridge):
+ """Test group features."""
+
+ color_temp_type = "Color temperature light"
+ extended_color_type = "Extended color light"
+
+ group_response = {
+ "1": {
+ "name": "Group 1",
+ "lights": ["1", "2"],
+ "type": "Room",
+ "action": {
+ "on": True,
+ "bri": 254,
+ "hue": 10000,
+ "sat": 254,
+ "effect": "none",
+ "xy": [0.5, 0.5],
+ "ct": 250,
+ "alert": "select",
+ "colormode": "ct",
+ },
+ "state": {"any_on": True, "all_on": False},
+ },
+ "2": {
+ "name": "Group 2",
+ "lights": ["3", "4"],
+ "type": "Room",
+ "action": {
+ "on": True,
+ "bri": 153,
+ "hue": 4345,
+ "sat": 254,
+ "effect": "none",
+ "xy": [0.5, 0.5],
+ "ct": 250,
+ "alert": "select",
+ "colormode": "ct",
+ },
+ "state": {"any_on": True, "all_on": False},
+ },
+ "3": {
+ "name": "Group 3",
+ "lights": ["1", "3"],
+ "type": "Room",
+ "action": {
+ "on": True,
+ "bri": 153,
+ "hue": 4345,
+ "sat": 254,
+ "effect": "none",
+ "xy": [0.5, 0.5],
+ "ct": 250,
+ "alert": "select",
+ "colormode": "ct",
+ },
+ "state": {"any_on": True, "all_on": False},
+ },
+ }
+
+ light_1 = {
+ "state": {
+ "on": True,
+ "bri": 144,
+ "ct": 467,
+ "alert": "none",
+ "effect": "none",
+ "reachable": True,
+ },
+ "capabilities": {
+ "control": {
+ "colorgamuttype": "A",
+ "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]],
+ }
+ },
+ "type": color_temp_type,
+ "name": "Hue Lamp 1",
+ "modelid": "LCT001",
+ "swversion": "66009461",
+ "manufacturername": "Philips",
+ "uniqueid": "456",
+ }
+ light_2 = {
+ "state": {
+ "on": False,
+ "bri": 0,
+ "ct": 0,
+ "alert": "none",
+ "effect": "none",
+ "colormode": "xy",
+ "reachable": True,
+ },
+ "capabilities": {
+ "control": {
+ "colorgamuttype": "A",
+ "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]],
+ }
+ },
+ "type": color_temp_type,
+ "name": "Hue Lamp 2",
+ "modelid": "LCT001",
+ "swversion": "66009461",
+ "manufacturername": "Philips",
+ "uniqueid": "456",
+ }
+ light_3 = {
+ "state": {
+ "on": False,
+ "bri": 0,
+ "hue": 0,
+ "sat": 0,
+ "xy": [0, 0],
+ "ct": 0,
+ "alert": "none",
+ "effect": "none",
+ "colormode": "hs",
+ "reachable": True,
+ },
+ "capabilities": {
+ "control": {
+ "colorgamuttype": "A",
+ "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]],
+ }
+ },
+ "type": extended_color_type,
+ "name": "Hue Lamp 3",
+ "modelid": "LCT001",
+ "swversion": "66009461",
+ "manufacturername": "Philips",
+ "uniqueid": "123",
+ }
+ light_4 = {
+ "state": {
+ "on": True,
+ "bri": 100,
+ "hue": 13088,
+ "sat": 210,
+ "xy": [0.5, 0.4],
+ "ct": 420,
+ "alert": "none",
+ "effect": "none",
+ "colormode": "hs",
+ "reachable": True,
+ },
+ "capabilities": {
+ "control": {
+ "colorgamuttype": "A",
+ "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]],
+ }
+ },
+ "type": extended_color_type,
+ "name": "Hue Lamp 4",
+ "modelid": "LCT001",
+ "swversion": "66009461",
+ "manufacturername": "Philips",
+ "uniqueid": "123",
+ }
+ light_response = {
+ "1": light_1,
+ "2": light_2,
+ "3": light_3,
+ "4": light_4,
+ }
+
+ mock_bridge.allow_groups = True
+ mock_bridge.mock_light_responses.append(light_response)
+ mock_bridge.mock_group_responses.append(group_response)
+ await setup_bridge(hass, mock_bridge)
+
+ color_temp_feature = hue_light.SUPPORT_HUE["Color temperature light"]
+ extended_color_feature = hue_light.SUPPORT_HUE["Extended color light"]
+
+ group_1 = hass.states.get("light.group_1")
+ assert group_1.attributes["supported_features"] == color_temp_feature
+
+ group_2 = hass.states.get("light.group_2")
+ assert group_2.attributes["supported_features"] == extended_color_feature
+
+ group_3 = hass.states.get("light.group_3")
+ assert group_3.attributes["supported_features"] == extended_color_feature
diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py
index 78255116831..ca83da725fa 100644
--- a/tests/components/hue/test_sensor_base.py
+++ b/tests/components/hue/test_sensor_base.py
@@ -279,8 +279,8 @@ def create_mock_bridge(hass):
return bridge.mock_sensor_responses.popleft()
return None
- async def async_request_call(coro):
- await coro
+ async def async_request_call(task):
+ await task()
bridge.async_request_call = async_request_call
bridge.api.config.apiversion = "9.9.9"
diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py
index 67a23a61d8b..fa67bb2f8bf 100644
--- a/tests/components/input_datetime/test_init.py
+++ b/tests/components/input_datetime/test_init.py
@@ -307,7 +307,7 @@ async def test_restore_state(hass):
async def test_default_value(hass):
- """Test default value if none has been set via inital or restore state."""
+ """Test default value if none has been set via initial or restore state."""
await async_setup_component(
hass,
DOMAIN,
diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py
index 56344b6affe..723736f35bc 100644
--- a/tests/components/intent/test_init.py
+++ b/tests/components/intent/test_init.py
@@ -2,6 +2,7 @@
import pytest
from homeassistant.components.cover import SERVICE_OPEN_COVER
+from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.helpers import intent
from homeassistant.setup import async_setup_component
@@ -74,3 +75,96 @@ async def test_cover_intents_loading(hass):
assert call.domain == "cover"
assert call.service == "open_cover"
assert call.data == {"entity_id": "cover.garage_door"}
+
+
+async def test_turn_on_intent(hass):
+ """Test HassTurnOn intent."""
+ result = await async_setup_component(hass, "homeassistant", {})
+ result = await async_setup_component(hass, "intent", {})
+ assert result
+
+ hass.states.async_set("light.test_light", "off")
+ calls = async_mock_service(hass, "light", SERVICE_TURN_ON)
+
+ response = await intent.async_handle(
+ hass, "test", "HassTurnOn", {"name": {"value": "test light"}}
+ )
+ await hass.async_block_till_done()
+
+ assert response.speech["plain"]["speech"] == "Turned test light on"
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == "light"
+ assert call.service == "turn_on"
+ assert call.data == {"entity_id": ["light.test_light"]}
+
+
+async def test_turn_off_intent(hass):
+ """Test HassTurnOff intent."""
+ result = await async_setup_component(hass, "homeassistant", {})
+ result = await async_setup_component(hass, "intent", {})
+ assert result
+
+ hass.states.async_set("light.test_light", "on")
+ calls = async_mock_service(hass, "light", SERVICE_TURN_OFF)
+
+ response = await intent.async_handle(
+ hass, "test", "HassTurnOff", {"name": {"value": "test light"}}
+ )
+ await hass.async_block_till_done()
+
+ assert response.speech["plain"]["speech"] == "Turned test light off"
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == "light"
+ assert call.service == "turn_off"
+ assert call.data == {"entity_id": ["light.test_light"]}
+
+
+async def test_toggle_intent(hass):
+ """Test HassToggle intent."""
+ result = await async_setup_component(hass, "homeassistant", {})
+ result = await async_setup_component(hass, "intent", {})
+ assert result
+
+ hass.states.async_set("light.test_light", "off")
+ calls = async_mock_service(hass, "light", SERVICE_TOGGLE)
+
+ response = await intent.async_handle(
+ hass, "test", "HassToggle", {"name": {"value": "test light"}}
+ )
+ await hass.async_block_till_done()
+
+ assert response.speech["plain"]["speech"] == "Toggled test light"
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == "light"
+ assert call.service == "toggle"
+ assert call.data == {"entity_id": ["light.test_light"]}
+
+
+async def test_turn_on_multiple_intent(hass):
+ """Test HassTurnOn intent with multiple similar entities.
+
+ This tests that matching finds the proper entity among similar names.
+ """
+ result = await async_setup_component(hass, "homeassistant", {})
+ result = await async_setup_component(hass, "intent", {})
+ assert result
+
+ hass.states.async_set("light.test_light", "off")
+ hass.states.async_set("light.test_lights_2", "off")
+ hass.states.async_set("light.test_lighter", "off")
+ calls = async_mock_service(hass, "light", SERVICE_TURN_ON)
+
+ response = await intent.async_handle(
+ hass, "test", "HassTurnOn", {"name": {"value": "test lights"}}
+ )
+ await hass.async_block_till_done()
+
+ assert response.speech["plain"]["speech"] == "Turned test lights 2 on"
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == "light"
+ assert call.service == "turn_on"
+ assert call.data == {"entity_id": ["light.test_lights_2"]}
diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py
index ead4654cba2..7a6e1160f24 100644
--- a/tests/components/ipma/test_weather.py
+++ b/tests/components/ipma/test_weather.py
@@ -4,6 +4,14 @@ from unittest.mock import patch
from homeassistant.components import weather
from homeassistant.components.weather import (
+ ATTR_FORECAST,
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW,
+ ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
+ ATTR_FORECAST_WIND_SPEED,
ATTR_WEATHER_HUMIDITY,
ATTR_WEATHER_PRESSURE,
ATTR_WEATHER_TEMPERATURE,
@@ -12,6 +20,7 @@ from homeassistant.components.weather import (
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.setup import async_setup_component
+from homeassistant.util.dt import now
from tests.common import MockConfigEntry, mock_coro
@@ -71,16 +80,16 @@ class MockLocation:
"2020-01-15T07:51:00",
9,
"S",
- None,
+ "10",
),
Forecast(
"7.7",
- "2020-01-15T02:00:00",
+ now().utcnow().strftime("%Y-%m-%dT%H:%M:%S"),
1,
"86.9",
None,
None,
- "-99.0",
+ "80.0",
10.6,
"2020-01-15T07:51:00",
10,
@@ -122,7 +131,9 @@ async def test_setup_configuration(hass):
return_value=mock_coro(MockLocation()),
):
assert await async_setup_component(
- hass, weather.DOMAIN, {"weather": {"name": "HomeTown", "platform": "ipma"}}
+ hass,
+ weather.DOMAIN,
+ {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}},
)
await hass.async_block_till_done()
@@ -158,3 +169,53 @@ async def test_setup_config_flow(hass):
assert data.get(ATTR_WEATHER_WIND_SPEED) == 3.94
assert data.get(ATTR_WEATHER_WIND_BEARING) == "NW"
assert state.attributes.get("friendly_name") == "HomeTown"
+
+
+async def test_daily_forecast(hass):
+ """Test for successfully getting daily forecast."""
+ with patch(
+ "homeassistant.components.ipma.weather.async_get_location",
+ return_value=mock_coro(MockLocation()),
+ ):
+ assert await async_setup_component(
+ hass,
+ weather.DOMAIN,
+ {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "daily"}},
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("weather.hometown")
+ assert state.state == "rainy"
+
+ forecast = state.attributes.get(ATTR_FORECAST)[0]
+ assert forecast.get(ATTR_FORECAST_TIME) == "2020-01-15T00:00:00"
+ assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy"
+ assert forecast.get(ATTR_FORECAST_TEMP) == 16.2
+ assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6
+ assert forecast.get(ATTR_FORECAST_PRECIPITATION) == "100.0"
+ assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "10"
+ assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S"
+
+
+async def test_hourly_forecast(hass):
+ """Test for successfully getting daily forecast."""
+ with patch(
+ "homeassistant.components.ipma.weather.async_get_location",
+ return_value=mock_coro(MockLocation()),
+ ):
+ assert await async_setup_component(
+ hass,
+ weather.DOMAIN,
+ {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}},
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("weather.hometown")
+ assert state.state == "rainy"
+
+ forecast = state.attributes.get(ATTR_FORECAST)[0]
+ assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy"
+ assert forecast.get(ATTR_FORECAST_TEMP) == 7.7
+ assert forecast.get(ATTR_FORECAST_PRECIPITATION) == "80.0"
+ assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "32.7"
+ assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S"
diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py
index e630702c6b2..59b6dc01313 100644
--- a/tests/components/jewish_calendar/test_sensor.py
+++ b/tests/components/jewish_calendar/test_sensor.py
@@ -568,3 +568,37 @@ async def test_omer_sensor(hass, test_time, result):
await hass.async_block_till_done()
assert hass.states.get("sensor.test_day_of_the_omer").state == result
+
+
+DAFYOMI_PARAMS = [
+ (dt(2014, 4, 28, 0), "Beitzah 29"),
+ (dt(2020, 1, 4, 0), "Niddah 73"),
+ (dt(2020, 1, 5, 0), "Berachos 2"),
+ (dt(2020, 3, 7, 0), "Berachos 64"),
+ (dt(2020, 3, 8, 0), "Shabbos 2"),
+]
+DAFYOMI_TEST_IDS = [
+ "randomly_picked_date",
+ "end_of_cycle13",
+ "start_of_cycle14",
+ "cycle14_end_of_berachos",
+ "cycle14_start_of_shabbos",
+]
+
+
+@pytest.mark.parametrize(["test_time", "result"], DAFYOMI_PARAMS, ids=DAFYOMI_TEST_IDS)
+async def test_dafyomi_sensor(hass, test_time, result):
+ """Test Daf Yomi sensor output."""
+ test_time = hass.config.time_zone.localize(test_time)
+
+ with alter_time(test_time):
+ assert await async_setup_component(
+ hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"name": "test"}}
+ )
+ await hass.async_block_till_done()
+
+ future = dt_util.utcnow() + timedelta(seconds=30)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("sensor.test_daf_yomi").state == result
diff --git a/tests/components/konnected/__init__.py b/tests/components/konnected/__init__.py
new file mode 100644
index 00000000000..c5de5224a5d
--- /dev/null
+++ b/tests/components/konnected/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Konnected component."""
diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py
new file mode 100644
index 00000000000..3638f40735b
--- /dev/null
+++ b/tests/components/konnected/test_config_flow.py
@@ -0,0 +1,1150 @@
+"""Tests for Konnected Alarm Panel config flow."""
+from asynctest import patch
+import pytest
+
+from homeassistant.components import konnected
+from homeassistant.components.konnected import config_flow
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(name="mock_panel")
+async def mock_panel_fixture():
+ """Mock a Konnected Panel bridge."""
+ with patch("konnected.Client", autospec=True) as konn_client:
+
+ def mock_constructor(host, port, websession):
+ """Fake the panel constructor."""
+ konn_client.host = host
+ konn_client.port = port
+ return konn_client
+
+ konn_client.side_effect = mock_constructor
+ konn_client.ClientError = config_flow.CannotConnect
+ yield konn_client
+
+
+async def test_flow_works(hass, mock_panel):
+ """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"
+
+ mock_panel.get_status.return_value = {
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected",
+ }
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"port": 1234, "host": "1.2.3.4"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+ assert result["description_placeholders"] == {
+ "model": "Konnected Alarm Panel",
+ "id": "112233445566",
+ "host": "1.2.3.4",
+ "port": 1234,
+ }
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"]["host"] == "1.2.3.4"
+ assert result["data"]["port"] == 1234
+ assert result["data"]["model"] == "Konnected"
+ assert len(result["data"]["access_token"]) == 20 # confirm generated token size
+ assert result["data"]["default_options"] == config_flow.OPTIONS_SCHEMA(
+ {config_flow.CONF_IO: {}}
+ )
+
+
+async def test_pro_flow_works(hass, mock_panel):
+ """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"
+
+ mock_panel.get_status.return_value = {
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected Pro",
+ }
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"port": 1234, "host": "1.2.3.4"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+ assert result["description_placeholders"] == {
+ "model": "Konnected Alarm Panel Pro",
+ "id": "112233445566",
+ "host": "1.2.3.4",
+ "port": 1234,
+ }
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"]["host"] == "1.2.3.4"
+ assert result["data"]["port"] == 1234
+ assert result["data"]["model"] == "Konnected Pro"
+ assert len(result["data"]["access_token"]) == 20 # confirm generated token size
+ assert result["data"]["default_options"] == config_flow.OPTIONS_SCHEMA(
+ {config_flow.CONF_IO: {}}
+ )
+
+
+async def test_ssdp(hass, mock_panel):
+ """Test a panel being discovered."""
+ mock_panel.get_status.return_value = {
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected",
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "ssdp"},
+ data={
+ "ssdp_location": "http://1.2.3.4:1234/Device.xml",
+ "manufacturer": config_flow.KONN_MANUFACTURER,
+ "modelName": config_flow.KONN_MODEL,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+ assert result["description_placeholders"] == {
+ "model": "Konnected Alarm Panel",
+ "id": "112233445566",
+ "host": "1.2.3.4",
+ "port": 1234,
+ }
+
+
+async def test_import_no_host_user_finish(hass, mock_panel):
+ """Test importing a panel with no host info."""
+ mock_panel.get_status.return_value = {
+ "mac": "aa:bb:cc:dd:ee:ff",
+ "model": "Konnected Pro",
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "import"},
+ data={
+ "default_options": {
+ "blink": True,
+ "discovery": True,
+ "io": {
+ "1": "Disabled",
+ "10": "Disabled",
+ "11": "Disabled",
+ "12": "Disabled",
+ "2": "Disabled",
+ "3": "Disabled",
+ "4": "Disabled",
+ "5": "Disabled",
+ "6": "Disabled",
+ "7": "Disabled",
+ "8": "Disabled",
+ "9": "Disabled",
+ "alarm1": "Disabled",
+ "alarm2_out2": "Disabled",
+ "out": "Disabled",
+ "out1": "Disabled",
+ },
+ },
+ "id": "aabbccddeeff",
+ },
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "import_confirm"
+ assert result["description_placeholders"]["id"] == "aabbccddeeff"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ # confirm user is prompted to enter host
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"host": "1.1.1.1", "port": 1234}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+ assert result["description_placeholders"] == {
+ "model": "Konnected Alarm Panel Pro",
+ "id": "aabbccddeeff",
+ "host": "1.1.1.1",
+ "port": 1234,
+ }
+
+ # final confirmation
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "create_entry"
+
+
+async def test_import_ssdp_host_user_finish(hass, mock_panel):
+ """Test importing a panel with no host info which ssdp discovers."""
+ mock_panel.get_status.return_value = {
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected Pro",
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "import"},
+ data={
+ "default_options": {
+ "blink": True,
+ "discovery": True,
+ "io": {
+ "1": "Disabled",
+ "10": "Disabled",
+ "11": "Disabled",
+ "12": "Disabled",
+ "2": "Disabled",
+ "3": "Disabled",
+ "4": "Disabled",
+ "5": "Disabled",
+ "6": "Disabled",
+ "7": "Disabled",
+ "8": "Disabled",
+ "9": "Disabled",
+ "alarm1": "Disabled",
+ "alarm2_out2": "Disabled",
+ "out": "Disabled",
+ "out1": "Disabled",
+ },
+ },
+ "id": "112233445566",
+ },
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "import_confirm"
+ assert result["description_placeholders"]["id"] == "112233445566"
+
+ # discover the panel via ssdp
+ ssdp_result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "ssdp"},
+ data={
+ "ssdp_location": "http://0.0.0.0:1234/Device.xml",
+ "manufacturer": config_flow.KONN_MANUFACTURER,
+ "modelName": config_flow.KONN_MODEL_PRO,
+ },
+ )
+ assert ssdp_result["type"] == "abort"
+ assert ssdp_result["reason"] == "already_in_progress"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+ assert result["description_placeholders"] == {
+ "model": "Konnected Alarm Panel Pro",
+ "id": "112233445566",
+ "host": "0.0.0.0",
+ "port": 1234,
+ }
+
+ # final confirmation
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "create_entry"
+
+
+async def test_ssdp_already_configured(hass, mock_panel):
+ """Test if a discovered panel has already been configured."""
+ MockConfigEntry(
+ domain="konnected",
+ data={"host": "0.0.0.0", "port": 1234},
+ unique_id="112233445566",
+ ).add_to_hass(hass)
+ mock_panel.get_status.return_value = {
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected Pro",
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "ssdp"},
+ data={
+ "ssdp_location": "http://0.0.0.0:1234/Device.xml",
+ "manufacturer": config_flow.KONN_MANUFACTURER,
+ "modelName": config_flow.KONN_MODEL_PRO,
+ },
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_ssdp_host_update(hass, mock_panel):
+ """Test if a discovered panel has already been configured but changed host."""
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "11223344556677889900",
+ "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}),
+ }
+ )
+
+ device_options = config_flow.OPTIONS_SCHEMA(
+ {
+ "io": {
+ "2": "Binary Sensor",
+ "6": "Binary Sensor",
+ "10": "Binary Sensor",
+ "3": "Digital Sensor",
+ "7": "Digital Sensor",
+ "11": "Digital Sensor",
+ "4": "Switchable Output",
+ "out1": "Switchable Output",
+ "alarm1": "Switchable Output",
+ },
+ "binary_sensors": [
+ {"zone": "2", "type": "door"},
+ {"zone": "6", "type": "window", "name": "winder", "inverse": True},
+ {"zone": "10", "type": "door"},
+ ],
+ "sensors": [
+ {"zone": "3", "type": "dht"},
+ {"zone": "7", "type": "ds18b20", "name": "temper"},
+ {"zone": "11", "type": "dht"},
+ ],
+ "switches": [
+ {"zone": "4"},
+ {
+ "zone": "8",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"zone": "out1"},
+ {"zone": "alarm1"},
+ ],
+ }
+ )
+
+ MockConfigEntry(
+ domain="konnected",
+ data=device_config,
+ options=device_options,
+ unique_id="112233445566",
+ ).add_to_hass(hass)
+ mock_panel.get_status.return_value = {
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected Pro",
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "ssdp"},
+ data={
+ "ssdp_location": "http://1.1.1.1:1234/Device.xml",
+ "manufacturer": config_flow.KONN_MANUFACTURER,
+ "modelName": config_flow.KONN_MODEL_PRO,
+ },
+ )
+ assert result["type"] == "abort"
+
+ # confirm the host value was updated
+ entry = hass.config_entries.async_entries(config_flow.DOMAIN)[0]
+ assert entry.data["host"] == "1.1.1.1"
+ assert entry.data["port"] == 1234
+
+
+async def test_import_existing_config(hass, mock_panel):
+ """Test importing a host with an existing config file."""
+ mock_panel.get_status.return_value = {
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected Pro",
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "import"},
+ data=konnected.DEVICE_SCHEMA_YAML(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "binary_sensors": [
+ {"zone": "2", "type": "door"},
+ {"zone": 6, "type": "window", "name": "winder", "inverse": True},
+ {"zone": "10", "type": "door"},
+ ],
+ "sensors": [
+ {"zone": "3", "type": "dht"},
+ {"zone": 7, "type": "ds18b20", "name": "temper"},
+ {"zone": "11", "type": "dht"},
+ ],
+ "switches": [
+ {"zone": "4"},
+ {
+ "zone": 8,
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"zone": "out1"},
+ {"zone": "alarm1"},
+ ],
+ }
+ ),
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": result["data"]["access_token"],
+ "default_options": {
+ "io": {
+ "1": "Disabled",
+ "5": "Disabled",
+ "9": "Disabled",
+ "12": "Disabled",
+ "out": "Disabled",
+ "alarm2_out2": "Disabled",
+ "2": "Binary Sensor",
+ "6": "Binary Sensor",
+ "10": "Binary Sensor",
+ "3": "Digital Sensor",
+ "7": "Digital Sensor",
+ "11": "Digital Sensor",
+ "4": "Switchable Output",
+ "8": "Switchable Output",
+ "out1": "Switchable Output",
+ "alarm1": "Switchable Output",
+ },
+ "blink": True,
+ "discovery": True,
+ "binary_sensors": [
+ {"zone": "2", "type": "door", "inverse": False},
+ {"zone": "6", "type": "window", "name": "winder", "inverse": True},
+ {"zone": "10", "type": "door", "inverse": False},
+ ],
+ "sensors": [
+ {"zone": "3", "type": "dht", "poll_interval": 3},
+ {"zone": "7", "type": "ds18b20", "name": "temper", "poll_interval": 3},
+ {"zone": "11", "type": "dht", "poll_interval": 3},
+ ],
+ "switches": [
+ {"activation": "high", "zone": "4"},
+ {
+ "zone": "8",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"activation": "high", "zone": "out1"},
+ {"activation": "high", "zone": "alarm1"},
+ ],
+ },
+ }
+
+
+async def test_import_existing_config_entry(hass, mock_panel):
+ """Test importing a host that has an existing config entry."""
+ MockConfigEntry(
+ domain="konnected",
+ data={
+ "host": "0.0.0.0",
+ "port": 1111,
+ "id": "112233445566",
+ "extra": "something",
+ },
+ unique_id="112233445566",
+ ).add_to_hass(hass)
+
+ mock_panel.get_status.return_value = {
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected Pro",
+ }
+
+ # utilize a global access token this time
+ hass.data[config_flow.DOMAIN] = {"access_token": "SUPERSECRETTOKEN"}
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "import"},
+ data={
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "default_options": {
+ "blink": True,
+ "discovery": True,
+ "io": {
+ "1": "Disabled",
+ "10": "Binary Sensor",
+ "11": "Disabled",
+ "12": "Disabled",
+ "2": "Binary Sensor",
+ "3": "Disabled",
+ "4": "Disabled",
+ "5": "Disabled",
+ "6": "Binary Sensor",
+ "7": "Disabled",
+ "8": "Disabled",
+ "9": "Disabled",
+ "alarm1": "Disabled",
+ "alarm2_out2": "Disabled",
+ "out": "Disabled",
+ "out1": "Disabled",
+ },
+ "binary_sensors": [
+ {"inverse": False, "type": "door", "zone": "2"},
+ {"inverse": True, "type": "Window", "name": "winder", "zone": "6"},
+ {"inverse": False, "type": "door", "zone": "10"},
+ ],
+ },
+ },
+ )
+
+ assert result["type"] == "abort"
+
+ # We should have updated the entry
+ assert len(hass.config_entries.async_entries("konnected")) == 1
+ assert hass.config_entries.async_entries("konnected")[0].data == {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "SUPERSECRETTOKEN",
+ "extra": "something",
+ }
+
+
+async def test_import_pin_config(hass, mock_panel):
+ """Test importing a host with an existing config file that specifies pin configs."""
+ mock_panel.get_status.return_value = {
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected Pro",
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "import"},
+ data=konnected.DEVICE_SCHEMA_YAML(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "binary_sensors": [
+ {"pin": 1, "type": "door"},
+ {"pin": "2", "type": "window", "name": "winder", "inverse": True},
+ {"zone": "3", "type": "door"},
+ ],
+ "sensors": [
+ {"zone": 4, "type": "dht"},
+ {"pin": "7", "type": "ds18b20", "name": "temper"},
+ ],
+ "switches": [
+ {
+ "pin": "8",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"zone": "6"},
+ ],
+ }
+ ),
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": result["data"]["access_token"],
+ "default_options": {
+ "io": {
+ "7": "Disabled",
+ "8": "Disabled",
+ "9": "Disabled",
+ "10": "Disabled",
+ "11": "Disabled",
+ "12": "Disabled",
+ "out1": "Disabled",
+ "alarm1": "Disabled",
+ "alarm2_out2": "Disabled",
+ "1": "Binary Sensor",
+ "2": "Binary Sensor",
+ "3": "Binary Sensor",
+ "4": "Digital Sensor",
+ "5": "Digital Sensor",
+ "6": "Switchable Output",
+ "out": "Switchable Output",
+ },
+ "blink": True,
+ "discovery": True,
+ "binary_sensors": [
+ {"zone": "1", "type": "door", "inverse": False},
+ {"zone": "2", "type": "window", "name": "winder", "inverse": True},
+ {"zone": "3", "type": "door", "inverse": False},
+ ],
+ "sensors": [
+ {"zone": "4", "type": "dht", "poll_interval": 3},
+ {"zone": "5", "type": "ds18b20", "name": "temper", "poll_interval": 3},
+ ],
+ "switches": [
+ {
+ "zone": "out",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"activation": "high", "zone": "6"},
+ ],
+ },
+ }
+
+
+async def test_option_flow(hass, mock_panel):
+ """Test config flow options."""
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected",
+ "access_token": "11223344556677889900",
+ "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}),
+ }
+ )
+
+ device_options = config_flow.OPTIONS_SCHEMA({"io": {}})
+
+ entry = MockConfigEntry(
+ domain="konnected",
+ data=device_config,
+ options=device_options,
+ unique_id="112233445566",
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id, context={"source": "test"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_io"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "1": "Disabled",
+ "2": "Binary Sensor",
+ "3": "Digital Sensor",
+ "4": "Switchable Output",
+ "5": "Disabled",
+ "6": "Binary Sensor",
+ "out": "Switchable Output",
+ },
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_binary"
+ assert result["description_placeholders"] == {
+ "zone": "Zone 2",
+ }
+
+ # zone 2
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"type": "door"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_binary"
+ assert result["description_placeholders"] == {
+ "zone": "Zone 6",
+ }
+
+ # zone 6
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={"type": "window", "name": "winder", "inverse": True},
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_digital"
+ assert result["description_placeholders"] == {
+ "zone": "Zone 3",
+ }
+
+ # zone 3
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"type": "dht"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_switch"
+ assert result["description_placeholders"] == {
+ "zone": "Zone 4",
+ }
+
+ # zone 4
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_switch"
+ assert result["description_placeholders"] == {
+ "zone": "OUT",
+ }
+
+ # zone out
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_misc"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"blink": True},
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ "io": {
+ "2": "Binary Sensor",
+ "3": "Digital Sensor",
+ "4": "Switchable Output",
+ "6": "Binary Sensor",
+ "out": "Switchable Output",
+ },
+ "blink": True,
+ "binary_sensors": [
+ {"zone": "2", "type": "door", "inverse": False},
+ {"zone": "6", "type": "window", "name": "winder", "inverse": True},
+ ],
+ "sensors": [{"zone": "3", "type": "dht", "poll_interval": 3}],
+ "switches": [
+ {"activation": "high", "zone": "4"},
+ {
+ "zone": "out",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ ],
+ }
+
+
+async def test_option_flow_pro(hass, mock_panel):
+ """Test config flow options for pro board."""
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "11223344556677889900",
+ "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}),
+ }
+ )
+
+ device_options = config_flow.OPTIONS_SCHEMA({"io": {}})
+
+ entry = MockConfigEntry(
+ domain="konnected",
+ data=device_config,
+ options=device_options,
+ unique_id="112233445566",
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id, context={"source": "test"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_io"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "1": "Disabled",
+ "2": "Binary Sensor",
+ "3": "Digital Sensor",
+ "4": "Switchable Output",
+ "5": "Disabled",
+ "6": "Binary Sensor",
+ "7": "Digital Sensor",
+ },
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_io_ext"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "8": "Switchable Output",
+ "9": "Disabled",
+ "10": "Binary Sensor",
+ "11": "Digital Sensor",
+ "12": "Disabled",
+ "out1": "Switchable Output",
+ "alarm1": "Switchable Output",
+ "alarm2_out2": "Disabled",
+ },
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_binary"
+
+ # zone 2
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"type": "door"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_binary"
+
+ # zone 6
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={"type": "window", "name": "winder", "inverse": True},
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_binary"
+
+ # zone 10
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"type": "door"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_digital"
+
+ # zone 3
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"type": "dht"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_digital"
+
+ # zone 7
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"type": "ds18b20", "name": "temper"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_digital"
+
+ # zone 11
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"type": "dht"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_switch"
+
+ # zone 4
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_switch"
+
+ # zone 8
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_switch"
+
+ # zone out1
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_switch"
+
+ # zone alarm1
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_misc"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"blink": True},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ "io": {
+ "10": "Binary Sensor",
+ "11": "Digital Sensor",
+ "2": "Binary Sensor",
+ "3": "Digital Sensor",
+ "4": "Switchable Output",
+ "6": "Binary Sensor",
+ "7": "Digital Sensor",
+ "8": "Switchable Output",
+ "alarm1": "Switchable Output",
+ "out1": "Switchable Output",
+ },
+ "blink": True,
+ "binary_sensors": [
+ {"zone": "2", "type": "door", "inverse": False},
+ {"zone": "6", "type": "window", "name": "winder", "inverse": True},
+ {"zone": "10", "type": "door", "inverse": False},
+ ],
+ "sensors": [
+ {"zone": "3", "type": "dht", "poll_interval": 3},
+ {"zone": "7", "type": "ds18b20", "name": "temper", "poll_interval": 3},
+ {"zone": "11", "type": "dht", "poll_interval": 3},
+ ],
+ "switches": [
+ {"activation": "high", "zone": "4"},
+ {
+ "zone": "8",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"activation": "high", "zone": "out1"},
+ {"activation": "high", "zone": "alarm1"},
+ ],
+ }
+
+
+async def test_option_flow_import(hass, mock_panel):
+ """Test config flow options imported from configuration.yaml."""
+ device_options = config_flow.OPTIONS_SCHEMA(
+ {
+ "io": {
+ "1": "Binary Sensor",
+ "2": "Digital Sensor",
+ "3": "Switchable Output",
+ },
+ "binary_sensors": [
+ {"zone": "1", "type": "window", "name": "winder", "inverse": True},
+ ],
+ "sensors": [{"zone": "2", "type": "ds18b20", "name": "temper"}],
+ "switches": [
+ {
+ "zone": "3",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ ],
+ }
+ )
+
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "11223344556677889900",
+ "default_options": device_options,
+ }
+ )
+
+ entry = MockConfigEntry(
+ domain="konnected", data=device_config, unique_id="112233445566"
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id, context={"source": "test"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_io"
+
+ # confirm the defaults are set based on current config - we"ll spot check this throughout
+ schema = result["data_schema"]({})
+ assert schema["1"] == "Binary Sensor"
+ assert schema["2"] == "Digital Sensor"
+ assert schema["3"] == "Switchable Output"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "1": "Binary Sensor",
+ "2": "Digital Sensor",
+ "3": "Switchable Output",
+ },
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_io_ext"
+ schema = result["data_schema"]({})
+ assert schema["8"] == "Disabled"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={},
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_binary"
+
+ # zone 1
+ schema = result["data_schema"]({})
+ assert schema["type"] == "window"
+ assert schema["name"] == "winder"
+ assert schema["inverse"] is True
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"type": "door"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_digital"
+
+ # zone 2
+ schema = result["data_schema"]({})
+ assert schema["type"] == "ds18b20"
+ assert schema["name"] == "temper"
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"type": "dht"},
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_switch"
+
+ # zone 3
+ schema = result["data_schema"]({})
+ assert schema["name"] == "switcher"
+ assert schema["activation"] == "low"
+ assert schema["momentary"] == 50
+ assert schema["pause"] == 100
+ assert schema["repeat"] == 4
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"activation": "high"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_misc"
+
+ schema = result["data_schema"]({})
+ assert schema["blink"] is True
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"blink": False},
+ )
+
+ # verify the updated fields
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ "io": {"1": "Binary Sensor", "2": "Digital Sensor", "3": "Switchable Output"},
+ "blink": False,
+ "binary_sensors": [
+ {"zone": "1", "type": "door", "inverse": True, "name": "winder"},
+ ],
+ "sensors": [
+ {"zone": "2", "type": "dht", "poll_interval": 3, "name": "temper"},
+ ],
+ "switches": [
+ {
+ "zone": "3",
+ "name": "switcher",
+ "activation": "high",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ ],
+ }
+
+
+async def test_option_flow_existing(hass, mock_panel):
+ """Test config flow options with existing already in place."""
+ device_options = config_flow.OPTIONS_SCHEMA(
+ {
+ "io": {
+ "1": "Binary Sensor",
+ "2": "Digital Sensor",
+ "3": "Switchable Output",
+ },
+ "binary_sensors": [
+ {"zone": "1", "type": "window", "name": "winder", "inverse": True},
+ ],
+ "sensors": [{"zone": "2", "type": "ds18b20", "name": "temper"}],
+ "switches": [
+ {
+ "zone": "3",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ ],
+ }
+ )
+
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "11223344556677889900",
+ "default_options": config_flow.OPTIONS_SCHEMA({"io": {}}),
+ }
+ )
+
+ entry = MockConfigEntry(
+ domain="konnected",
+ data=device_config,
+ options=device_options,
+ unique_id="112233445566",
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id, context={"source": "test"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "options_io"
+
+ # confirm the defaults are pulled in from the existing options
+ schema = result["data_schema"]({})
+ assert schema["1"] == "Binary Sensor"
+ assert schema["2"] == "Digital Sensor"
+ assert schema["3"] == "Switchable Output"
diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py
new file mode 100644
index 00000000000..907f83cd981
--- /dev/null
+++ b/tests/components/konnected/test_init.py
@@ -0,0 +1,669 @@
+"""Test Konnected setup process."""
+from asynctest import patch
+import pytest
+
+from homeassistant.components import konnected
+from homeassistant.components.konnected import config_flow
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(name="mock_panel")
+async def mock_panel_fixture():
+ """Mock a Konnected Panel bridge."""
+ with patch("konnected.Client", autospec=True) as konn_client:
+
+ def mock_constructor(host, port, websession):
+ """Fake the panel constructor."""
+ konn_client.host = host
+ konn_client.port = port
+ return konn_client
+
+ konn_client.side_effect = mock_constructor
+ konn_client.ClientError = config_flow.CannotConnect
+ konn_client.get_status.return_value = {
+ "hwVersion": "2.3.0",
+ "swVersion": "2.3.1",
+ "heap": 10000,
+ "uptime": 12222,
+ "ip": "192.168.1.90",
+ "port": 9123,
+ "sensors": [],
+ "actuators": [],
+ "dht_sensors": [],
+ "ds18b20_sensors": [],
+ "mac": "11:22:33:44:55:66",
+ "settings": {},
+ }
+ yield konn_client
+
+
+async def test_config_schema(hass):
+ """Test that config schema is imported properly."""
+ config = {
+ konnected.DOMAIN: {
+ konnected.CONF_ACCESS_TOKEN: "abcdefgh",
+ konnected.CONF_DEVICES: [{konnected.CONF_ID: "aabbccddeeff"}],
+ }
+ }
+ assert konnected.CONFIG_SCHEMA(config) == {
+ "konnected": {
+ "access_token": "abcdefgh",
+ "devices": [
+ {
+ "default_options": {
+ "blink": True,
+ "discovery": True,
+ "io": {
+ "1": "Disabled",
+ "10": "Disabled",
+ "11": "Disabled",
+ "12": "Disabled",
+ "2": "Disabled",
+ "3": "Disabled",
+ "4": "Disabled",
+ "5": "Disabled",
+ "6": "Disabled",
+ "7": "Disabled",
+ "8": "Disabled",
+ "9": "Disabled",
+ "alarm1": "Disabled",
+ "alarm2_out2": "Disabled",
+ "out": "Disabled",
+ "out1": "Disabled",
+ },
+ },
+ "id": "aabbccddeeff",
+ }
+ ],
+ }
+ }
+
+ # check with host info
+ config = {
+ konnected.DOMAIN: {
+ konnected.CONF_ACCESS_TOKEN: "abcdefgh",
+ konnected.CONF_DEVICES: [
+ {konnected.CONF_ID: "aabbccddeeff", "host": "192.168.1.1", "port": 1234}
+ ],
+ }
+ }
+ assert konnected.CONFIG_SCHEMA(config) == {
+ "konnected": {
+ "access_token": "abcdefgh",
+ "devices": [
+ {
+ "default_options": {
+ "blink": True,
+ "discovery": True,
+ "io": {
+ "1": "Disabled",
+ "10": "Disabled",
+ "11": "Disabled",
+ "12": "Disabled",
+ "2": "Disabled",
+ "3": "Disabled",
+ "4": "Disabled",
+ "5": "Disabled",
+ "6": "Disabled",
+ "7": "Disabled",
+ "8": "Disabled",
+ "9": "Disabled",
+ "alarm1": "Disabled",
+ "alarm2_out2": "Disabled",
+ "out": "Disabled",
+ "out1": "Disabled",
+ },
+ },
+ "id": "aabbccddeeff",
+ "host": "192.168.1.1",
+ "port": 1234,
+ }
+ ],
+ }
+ }
+
+ # check pin to zone
+ config = {
+ konnected.DOMAIN: {
+ konnected.CONF_ACCESS_TOKEN: "abcdefgh",
+ konnected.CONF_DEVICES: [
+ {
+ konnected.CONF_ID: "aabbccddeeff",
+ "binary_sensors": [
+ {"pin": 2, "type": "door"},
+ {"zone": 1, "type": "door"},
+ ],
+ }
+ ],
+ }
+ }
+ assert konnected.CONFIG_SCHEMA(config) == {
+ "konnected": {
+ "access_token": "abcdefgh",
+ "devices": [
+ {
+ "default_options": {
+ "blink": True,
+ "discovery": True,
+ "io": {
+ "1": "Binary Sensor",
+ "10": "Disabled",
+ "11": "Disabled",
+ "12": "Disabled",
+ "2": "Binary Sensor",
+ "3": "Disabled",
+ "4": "Disabled",
+ "5": "Disabled",
+ "6": "Disabled",
+ "7": "Disabled",
+ "8": "Disabled",
+ "9": "Disabled",
+ "alarm1": "Disabled",
+ "alarm2_out2": "Disabled",
+ "out": "Disabled",
+ "out1": "Disabled",
+ },
+ "binary_sensors": [
+ {"inverse": False, "type": "door", "zone": "2"},
+ {"inverse": False, "type": "door", "zone": "1"},
+ ],
+ },
+ "id": "aabbccddeeff",
+ }
+ ],
+ }
+ }
+
+
+async def test_setup_with_no_config(hass):
+ """Test that we do not discover anything or try to set up a Konnected panel."""
+ assert await async_setup_component(hass, konnected.DOMAIN, {})
+
+ # No flows started
+ assert len(hass.config_entries.flow.async_progress()) == 0
+
+ # Nothing saved from configuration.yaml
+ assert hass.data[konnected.DOMAIN][konnected.CONF_ACCESS_TOKEN] is None
+ assert hass.data[konnected.DOMAIN][konnected.CONF_API_HOST] is None
+ assert konnected.YAML_CONFIGS not in hass.data[konnected.DOMAIN]
+
+
+async def test_setup_defined_hosts_known_auth(hass):
+ """Test we don't initiate a config entry if configured panel is known."""
+ MockConfigEntry(
+ domain="konnected",
+ unique_id="112233445566",
+ data={"host": "0.0.0.0", "id": "112233445566"},
+ ).add_to_hass(hass)
+ MockConfigEntry(
+ domain="konnected",
+ unique_id="aabbccddeeff",
+ data={"host": "1.2.3.4", "id": "aabbccddeeff"},
+ ).add_to_hass(hass)
+
+ assert (
+ await async_setup_component(
+ hass,
+ konnected.DOMAIN,
+ {
+ konnected.DOMAIN: {
+ konnected.CONF_ACCESS_TOKEN: "abcdefgh",
+ konnected.CONF_DEVICES: [
+ {
+ config_flow.CONF_ID: "aabbccddeeff",
+ config_flow.CONF_HOST: "0.0.0.0",
+ config_flow.CONF_PORT: 1234,
+ },
+ ],
+ }
+ },
+ )
+ is True
+ )
+
+ assert hass.data[konnected.DOMAIN][konnected.CONF_ACCESS_TOKEN] == "abcdefgh"
+ assert konnected.YAML_CONFIGS not in hass.data[konnected.DOMAIN]
+
+ # Flow aborted
+ assert len(hass.config_entries.flow.async_progress()) == 0
+
+
+async def test_setup_defined_hosts_no_known_auth(hass):
+ """Test we initiate config entry if config panel is not known."""
+ assert (
+ await async_setup_component(
+ hass,
+ konnected.DOMAIN,
+ {
+ konnected.DOMAIN: {
+ konnected.CONF_ACCESS_TOKEN: "abcdefgh",
+ konnected.CONF_DEVICES: [{konnected.CONF_ID: "aabbccddeeff"}],
+ }
+ },
+ )
+ is True
+ )
+
+ # Flow started for discovered bridge
+ assert len(hass.config_entries.flow.async_progress()) == 1
+
+
+async def test_setup_multiple(hass):
+ """Test we initiate config entry for multiple panels."""
+ assert (
+ await async_setup_component(
+ hass,
+ konnected.DOMAIN,
+ {
+ konnected.DOMAIN: {
+ konnected.CONF_ACCESS_TOKEN: "arandomstringvalue",
+ konnected.CONF_API_HOST: "http://192.168.86.32:8123",
+ konnected.CONF_DEVICES: [
+ {
+ konnected.CONF_ID: "aabbccddeeff",
+ "binary_sensors": [
+ {
+ "zone": 4,
+ "type": "motion",
+ "name": "Hallway Motion",
+ },
+ {
+ "zone": 5,
+ "type": "window",
+ "name": "Master Bedroom Window",
+ },
+ {
+ "zone": 6,
+ "type": "window",
+ "name": "Downstairs Windows",
+ },
+ ],
+ "switches": [{"zone": "out", "name": "siren"}],
+ },
+ {
+ konnected.CONF_ID: "445566778899",
+ "binary_sensors": [
+ {"zone": 1, "type": "motion", "name": "Front"},
+ {"zone": 2, "type": "window", "name": "Back"},
+ ],
+ "switches": [
+ {
+ "zone": "out",
+ "name": "Buzzer",
+ "momentary": 65,
+ "pause": 55,
+ "repeat": 4,
+ },
+ ],
+ },
+ ],
+ }
+ },
+ )
+ is True
+ )
+
+ # Flow started for discovered bridge
+ assert len(hass.config_entries.flow.async_progress()) == 2
+
+ # Globals saved
+ assert (
+ hass.data[konnected.DOMAIN][konnected.CONF_ACCESS_TOKEN] == "arandomstringvalue"
+ )
+ assert (
+ hass.data[konnected.DOMAIN][konnected.CONF_API_HOST]
+ == "http://192.168.86.32:8123"
+ )
+
+
+async def test_config_passed_to_config_entry(hass):
+ """Test that configured options for a host are loaded via config entry."""
+ entry = MockConfigEntry(
+ domain=konnected.DOMAIN,
+ data={config_flow.CONF_ID: "aabbccddeeff", config_flow.CONF_HOST: "0.0.0.0"},
+ )
+ entry.add_to_hass(hass)
+ with patch.object(konnected, "AlarmPanel", autospec=True) as mock_int:
+ assert (
+ await async_setup_component(
+ hass,
+ konnected.DOMAIN,
+ {
+ konnected.DOMAIN: {
+ konnected.CONF_ACCESS_TOKEN: "abcdefgh",
+ konnected.CONF_DEVICES: [{konnected.CONF_ID: "aabbccddeeff"}],
+ }
+ },
+ )
+ is True
+ )
+
+ assert len(mock_int.mock_calls) == 3
+ p_hass, p_entry = mock_int.mock_calls[0][1]
+
+ assert p_hass is hass
+ assert p_entry is entry
+
+
+async def test_unload_entry(hass, mock_panel):
+ """Test being able to unload an entry."""
+ entry = MockConfigEntry(
+ domain=konnected.DOMAIN, data={konnected.CONF_ID: "aabbccddeeff"}
+ )
+ entry.add_to_hass(hass)
+
+ assert await async_setup_component(hass, konnected.DOMAIN, {}) is True
+ assert hass.data[konnected.DOMAIN]["devices"].get("aabbccddeeff") is not None
+ assert await konnected.async_unload_entry(hass, entry)
+ assert hass.data[konnected.DOMAIN]["devices"] == {}
+
+
+async def test_api(hass, aiohttp_client, mock_panel):
+ """Test callback view."""
+ await async_setup_component(hass, "http", {"http": {}})
+
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "abcdefgh",
+ "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}),
+ }
+ )
+
+ device_options = config_flow.OPTIONS_SCHEMA(
+ {
+ "io": {
+ "1": "Binary Sensor",
+ "2": "Binary Sensor",
+ "3": "Binary Sensor",
+ "4": "Digital Sensor",
+ "5": "Digital Sensor",
+ "6": "Switchable Output",
+ "out": "Switchable Output",
+ },
+ "binary_sensors": [
+ {"zone": "1", "type": "door"},
+ {"zone": "2", "type": "window", "name": "winder", "inverse": True},
+ {"zone": "3", "type": "door"},
+ ],
+ "sensors": [
+ {"zone": "4", "type": "dht"},
+ {"zone": "5", "type": "ds18b20", "name": "temper"},
+ ],
+ "switches": [
+ {
+ "zone": "out",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"zone": "6"},
+ ],
+ }
+ )
+
+ entry = MockConfigEntry(
+ domain="konnected",
+ title="Konnected Alarm Panel",
+ data=device_config,
+ options=device_options,
+ )
+ entry.add_to_hass(hass)
+
+ assert (
+ await async_setup_component(
+ hass,
+ konnected.DOMAIN,
+ {konnected.DOMAIN: {konnected.CONF_ACCESS_TOKEN: "globaltoken"}},
+ )
+ is True
+ )
+
+ client = await aiohttp_client(hass.http.app)
+
+ # Test the get endpoint for switch status polling
+ resp = await client.get("/api/konnected")
+ assert resp.status == 404 # no device provided
+
+ resp = await client.get("/api/konnected/223344556677")
+ assert resp.status == 404 # unknown device provided
+
+ resp = await client.get("/api/konnected/device/112233445566")
+ assert resp.status == 404 # no zone provided
+ result = await resp.json()
+ assert result == {"message": "Switch on zone or pin unknown not configured"}
+
+ resp = await client.get("/api/konnected/device/112233445566?zone=8")
+ assert resp.status == 404 # invalid zone
+ result = await resp.json()
+ assert result == {"message": "Switch on zone or pin 8 not configured"}
+
+ resp = await client.get("/api/konnected/device/112233445566?pin=12")
+ assert resp.status == 404 # invalid pin
+ result = await resp.json()
+ assert result == {"message": "Switch on zone or pin 12 not configured"}
+
+ resp = await client.get("/api/konnected/device/112233445566?zone=out")
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"state": 1, "zone": "out"}
+
+ resp = await client.get("/api/konnected/device/112233445566?pin=8")
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"state": 1, "pin": "8"}
+
+ # Test the post endpoint for sensor updates
+ resp = await client.post("/api/konnected/device", json={"zone": "1", "state": 1})
+ assert resp.status == 404
+
+ resp = await client.post(
+ "/api/konnected/device/112233445566", json={"zone": "1", "state": 1}
+ )
+ assert resp.status == 401
+ result = await resp.json()
+ assert result == {"message": "unauthorized"}
+
+ resp = await client.post(
+ "/api/konnected/device/223344556677",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "1", "state": 1},
+ )
+ assert resp.status == 400
+
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "15", "state": 1},
+ )
+ assert resp.status == 400
+ result = await resp.json()
+ assert result == {"message": "unregistered sensor/actuator"}
+
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "1", "state": 1},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer globaltoken"},
+ json={"zone": "1", "state": 1},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "4", "temp": 22, "humi": 20},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+
+ # Test the put endpoint for sensor updates
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "1", "state": 1},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+
+
+async def test_state_updates(hass, aiohttp_client, mock_panel):
+ """Test callback view."""
+ await async_setup_component(hass, "http", {"http": {}})
+
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "abcdefgh",
+ "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}),
+ }
+ )
+
+ device_options = config_flow.OPTIONS_SCHEMA(
+ {
+ "io": {
+ "1": "Binary Sensor",
+ "2": "Binary Sensor",
+ "3": "Binary Sensor",
+ "4": "Digital Sensor",
+ "5": "Digital Sensor",
+ "6": "Switchable Output",
+ "out": "Switchable Output",
+ },
+ "binary_sensors": [
+ {"zone": "1", "type": "door"},
+ {"zone": "2", "type": "window", "name": "winder", "inverse": True},
+ {"zone": "3", "type": "door"},
+ ],
+ "sensors": [
+ {"zone": "4", "type": "dht"},
+ {"zone": "5", "type": "ds18b20", "name": "temper"},
+ ],
+ "switches": [
+ {
+ "zone": "out",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"zone": "6"},
+ ],
+ }
+ )
+
+ entry = MockConfigEntry(
+ domain="konnected",
+ title="Konnected Alarm Panel",
+ data=device_config,
+ options=device_options,
+ )
+ entry.add_to_hass(hass)
+
+ assert (
+ await async_setup_component(
+ hass,
+ konnected.DOMAIN,
+ {konnected.DOMAIN: {konnected.CONF_ACCESS_TOKEN: "1122334455"}},
+ )
+ is True
+ )
+
+ client = await aiohttp_client(hass.http.app)
+
+ # Test updating a binary sensor
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "1", "state": 0},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+ await hass.async_block_till_done()
+ assert hass.states.get("binary_sensor.konnected_445566_zone_1").state == "off"
+
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "1", "state": 1},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+ await hass.async_block_till_done()
+ assert hass.states.get("binary_sensor.konnected_445566_zone_1").state == "on"
+
+ # Test updating sht sensor
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "4", "temp": 22, "humi": 20},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.konnected_445566_sensor_4_humidity").state == "20"
+ assert (
+ hass.states.get("sensor.konnected_445566_sensor_4_temperature").state == "22.0"
+ )
+
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "4", "temp": 25, "humi": 23},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.konnected_445566_sensor_4_humidity").state == "23"
+ assert (
+ hass.states.get("sensor.konnected_445566_sensor_4_temperature").state == "25.0"
+ )
+
+ # Test updating ds sensor
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "5", "temp": 32, "addr": 1},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.temper_temperature").state == "32.0"
+
+ resp = await client.post(
+ "/api/konnected/device/112233445566",
+ headers={"Authorization": "Bearer abcdefgh"},
+ json={"zone": "5", "temp": 42, "addr": 1},
+ )
+ assert resp.status == 200
+ result = await resp.json()
+ assert result == {"message": "ok"}
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.temper_temperature").state == "42.0"
diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py
new file mode 100644
index 00000000000..f1ae8a4357c
--- /dev/null
+++ b/tests/components/konnected/test_panel.py
@@ -0,0 +1,553 @@
+"""Test Konnected setup process."""
+from asynctest import patch
+import pytest
+
+from homeassistant.components.konnected import config_flow, panel
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(name="mock_panel")
+async def mock_panel_fixture():
+ """Mock a Konnected Panel bridge."""
+ with patch("konnected.Client", autospec=True) as konn_client:
+
+ def mock_constructor(host, port, websession):
+ """Fake the panel constructor."""
+ konn_client.host = host
+ konn_client.port = port
+ return konn_client
+
+ konn_client.side_effect = mock_constructor
+ konn_client.ClientError = config_flow.CannotConnect
+ konn_client.get_status.return_value = {
+ "hwVersion": "2.3.0",
+ "swVersion": "2.3.1",
+ "heap": 10000,
+ "uptime": 12222,
+ "ip": "192.168.1.90",
+ "port": 9123,
+ "sensors": [],
+ "actuators": [],
+ "dht_sensors": [],
+ "ds18b20_sensors": [],
+ "mac": "11:22:33:44:55:66",
+ "model": "Konnected Pro", # `model` field only included in pro
+ "settings": {},
+ }
+ yield konn_client
+
+
+async def test_create_and_setup(hass, mock_panel):
+ """Test that we create a Konnected Panel and save the data."""
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "11223344556677889900",
+ "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}),
+ }
+ )
+
+ device_options = config_flow.OPTIONS_SCHEMA(
+ {
+ "io": {
+ "1": "Binary Sensor",
+ "2": "Binary Sensor",
+ "3": "Binary Sensor",
+ "4": "Digital Sensor",
+ "5": "Digital Sensor",
+ "6": "Switchable Output",
+ "out": "Switchable Output",
+ },
+ "binary_sensors": [
+ {"zone": "1", "type": "door"},
+ {"zone": "2", "type": "window", "name": "winder", "inverse": True},
+ {"zone": "3", "type": "door"},
+ ],
+ "sensors": [
+ {"zone": "4", "type": "dht"},
+ {"zone": "5", "type": "ds18b20", "name": "temper"},
+ ],
+ "switches": [
+ {
+ "zone": "out",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"zone": "6"},
+ ],
+ }
+ )
+
+ entry = MockConfigEntry(
+ domain="konnected",
+ title="Konnected Alarm Panel",
+ data=device_config,
+ options=device_options,
+ )
+ entry.add_to_hass(hass)
+
+ # override get_status to reflect non-pro board
+ mock_panel.get_status.return_value = {
+ "hwVersion": "2.3.0",
+ "swVersion": "2.3.1",
+ "heap": 10000,
+ "uptime": 12222,
+ "ip": "192.168.1.90",
+ "port": 9123,
+ "sensors": [],
+ "actuators": [],
+ "dht_sensors": [],
+ "ds18b20_sensors": [],
+ "mac": "11:22:33:44:55:66",
+ "settings": {},
+ }
+
+ # setup the integration and inspect panel behavior
+ assert (
+ await async_setup_component(
+ hass,
+ panel.DOMAIN,
+ {
+ panel.DOMAIN: {
+ panel.CONF_ACCESS_TOKEN: "arandomstringvalue",
+ panel.CONF_API_HOST: "http://192.168.1.1:8123",
+ }
+ },
+ )
+ is True
+ )
+
+ # confirm panel instance was created and configured
+ # hass.data is the only mechanism to get a reference to the created panel instance
+ device = hass.data[panel.DOMAIN][panel.CONF_DEVICES]["112233445566"]["panel"]
+ await device.update_switch("1", 0)
+
+ # confirm the correct api is used
+ # pylint: disable=no-member
+ assert mock_panel.put_device.call_count == 1
+ assert mock_panel.put_zone.call_count == 0
+
+ # confirm the settings are sent to the panel
+ # pylint: disable=no-member
+ assert mock_panel.put_settings.call_args_list[0][1] == {
+ "sensors": [{"pin": "1"}, {"pin": "2"}, {"pin": "5"}],
+ "actuators": [{"trigger": 0, "pin": "8"}, {"trigger": 1, "pin": "9"}],
+ "dht_sensors": [{"poll_interval": 3, "pin": "6"}],
+ "ds18b20_sensors": [{"pin": "7"}],
+ "auth_token": "11223344556677889900",
+ "blink": True,
+ "discovery": True,
+ "endpoint": "http://192.168.1.1:8123/api/konnected",
+ }
+
+ # confirm the device settings are saved in hass.data
+ assert device.stored_configuration == {
+ "binary_sensors": {
+ "1": {
+ "inverse": False,
+ "name": "Konnected 445566 Zone 1",
+ "state": None,
+ "type": "door",
+ },
+ "2": {"inverse": True, "name": "winder", "state": None, "type": "window"},
+ "3": {
+ "inverse": False,
+ "name": "Konnected 445566 Zone 3",
+ "state": None,
+ "type": "door",
+ },
+ },
+ "blink": True,
+ "panel": device,
+ "discovery": True,
+ "host": "1.2.3.4",
+ "port": 1234,
+ "sensors": [
+ {
+ "name": "Konnected 445566 Sensor 4",
+ "poll_interval": 3,
+ "type": "dht",
+ "zone": "4",
+ },
+ {"name": "temper", "poll_interval": 3, "type": "ds18b20", "zone": "5"},
+ ],
+ "switches": [
+ {
+ "activation": "low",
+ "momentary": 50,
+ "name": "switcher",
+ "pause": 100,
+ "repeat": 4,
+ "state": None,
+ "zone": "out",
+ },
+ {
+ "activation": "high",
+ "momentary": None,
+ "name": "Konnected 445566 Actuator 6",
+ "pause": None,
+ "repeat": None,
+ "state": None,
+ "zone": "6",
+ },
+ ],
+ }
+
+
+async def test_create_and_setup_pro(hass, mock_panel):
+ """Test that we create a Konnected Pro Panel and save the data."""
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "11223344556677889900",
+ "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}),
+ }
+ )
+
+ device_options = config_flow.OPTIONS_SCHEMA(
+ {
+ "io": {
+ "2": "Binary Sensor",
+ "6": "Binary Sensor",
+ "10": "Binary Sensor",
+ "3": "Digital Sensor",
+ "7": "Digital Sensor",
+ "11": "Digital Sensor",
+ "4": "Switchable Output",
+ "8": "Switchable Output",
+ "out1": "Switchable Output",
+ "alarm1": "Switchable Output",
+ },
+ "binary_sensors": [
+ {"zone": "2", "type": "door"},
+ {"zone": "6", "type": "window", "name": "winder", "inverse": True},
+ {"zone": "10", "type": "door"},
+ ],
+ "sensors": [
+ {"zone": "3", "type": "dht"},
+ {"zone": "7", "type": "ds18b20", "name": "temper"},
+ {"zone": "11", "type": "dht", "poll_interval": 5},
+ ],
+ "switches": [
+ {"zone": "4"},
+ {
+ "zone": "8",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"zone": "out1"},
+ {"zone": "alarm1"},
+ ],
+ }
+ )
+
+ entry = MockConfigEntry(
+ domain="konnected",
+ title="Konnected Pro Alarm Panel",
+ data=device_config,
+ options=device_options,
+ )
+ entry.add_to_hass(hass)
+
+ # setup the integration and inspect panel behavior
+ assert (
+ await async_setup_component(
+ hass,
+ panel.DOMAIN,
+ {
+ panel.DOMAIN: {
+ panel.CONF_ACCESS_TOKEN: "arandomstringvalue",
+ panel.CONF_API_HOST: "http://192.168.1.1:8123",
+ }
+ },
+ )
+ is True
+ )
+
+ # confirm panel instance was created and configured
+ # hass.data is the only mechanism to get a reference to the created panel instance
+ device = hass.data[panel.DOMAIN][panel.CONF_DEVICES]["112233445566"]["panel"]
+ await device.update_switch("2", 1)
+
+ # confirm the correct api is used
+ # pylint: disable=no-member
+ assert mock_panel.put_device.call_count == 0
+ assert mock_panel.put_zone.call_count == 1
+
+ # confirm the settings are sent to the panel
+ # pylint: disable=no-member
+ assert mock_panel.put_settings.call_args_list[0][1] == {
+ "sensors": [{"zone": "2"}, {"zone": "6"}, {"zone": "10"}],
+ "actuators": [
+ {"trigger": 1, "zone": "4"},
+ {"trigger": 0, "zone": "8"},
+ {"trigger": 1, "zone": "out1"},
+ {"trigger": 1, "zone": "alarm1"},
+ ],
+ "dht_sensors": [
+ {"poll_interval": 3, "zone": "3"},
+ {"poll_interval": 5, "zone": "11"},
+ ],
+ "ds18b20_sensors": [{"zone": "7"}],
+ "auth_token": "11223344556677889900",
+ "blink": True,
+ "discovery": True,
+ "endpoint": "http://192.168.1.1:8123/api/konnected",
+ }
+
+ # confirm the device settings are saved in hass.data
+ assert device.stored_configuration == {
+ "binary_sensors": {
+ "10": {
+ "inverse": False,
+ "name": "Konnected 445566 Zone 10",
+ "state": None,
+ "type": "door",
+ },
+ "2": {
+ "inverse": False,
+ "name": "Konnected 445566 Zone 2",
+ "state": None,
+ "type": "door",
+ },
+ "6": {"inverse": True, "name": "winder", "state": None, "type": "window"},
+ },
+ "blink": True,
+ "panel": device,
+ "discovery": True,
+ "host": "1.2.3.4",
+ "port": 1234,
+ "sensors": [
+ {
+ "name": "Konnected 445566 Sensor 3",
+ "poll_interval": 3,
+ "type": "dht",
+ "zone": "3",
+ },
+ {"name": "temper", "poll_interval": 3, "type": "ds18b20", "zone": "7"},
+ {
+ "name": "Konnected 445566 Sensor 11",
+ "poll_interval": 5,
+ "type": "dht",
+ "zone": "11",
+ },
+ ],
+ "switches": [
+ {
+ "activation": "high",
+ "momentary": None,
+ "name": "Konnected 445566 Actuator 4",
+ "pause": None,
+ "repeat": None,
+ "state": None,
+ "zone": "4",
+ },
+ {
+ "activation": "low",
+ "momentary": 50,
+ "name": "switcher",
+ "pause": 100,
+ "repeat": 4,
+ "state": None,
+ "zone": "8",
+ },
+ {
+ "activation": "high",
+ "momentary": None,
+ "name": "Konnected 445566 Actuator out1",
+ "pause": None,
+ "repeat": None,
+ "state": None,
+ "zone": "out1",
+ },
+ {
+ "activation": "high",
+ "momentary": None,
+ "name": "Konnected 445566 Actuator alarm1",
+ "pause": None,
+ "repeat": None,
+ "state": None,
+ "zone": "alarm1",
+ },
+ ],
+ }
+
+
+async def test_default_options(hass, mock_panel):
+ """Test that we create a Konnected Panel and save the data."""
+ device_config = config_flow.CONFIG_ENTRY_SCHEMA(
+ {
+ "host": "1.2.3.4",
+ "port": 1234,
+ "id": "112233445566",
+ "model": "Konnected Pro",
+ "access_token": "11223344556677889900",
+ "default_options": config_flow.OPTIONS_SCHEMA(
+ {
+ "io": {
+ "1": "Binary Sensor",
+ "2": "Binary Sensor",
+ "3": "Binary Sensor",
+ "4": "Digital Sensor",
+ "5": "Digital Sensor",
+ "6": "Switchable Output",
+ "out": "Switchable Output",
+ },
+ "binary_sensors": [
+ {"zone": "1", "type": "door"},
+ {
+ "zone": "2",
+ "type": "window",
+ "name": "winder",
+ "inverse": True,
+ },
+ {"zone": "3", "type": "door"},
+ ],
+ "sensors": [
+ {"zone": "4", "type": "dht"},
+ {"zone": "5", "type": "ds18b20", "name": "temper"},
+ ],
+ "switches": [
+ {
+ "zone": "out",
+ "name": "switcher",
+ "activation": "low",
+ "momentary": 50,
+ "pause": 100,
+ "repeat": 4,
+ },
+ {"zone": "6"},
+ ],
+ }
+ ),
+ }
+ )
+
+ entry = MockConfigEntry(
+ domain="konnected",
+ title="Konnected Alarm Panel",
+ data=device_config,
+ options={},
+ )
+ entry.add_to_hass(hass)
+
+ # override get_status to reflect non-pro board
+ mock_panel.get_status.return_value = {
+ "hwVersion": "2.3.0",
+ "swVersion": "2.3.1",
+ "heap": 10000,
+ "uptime": 12222,
+ "ip": "192.168.1.90",
+ "port": 9123,
+ "sensors": [],
+ "actuators": [],
+ "dht_sensors": [],
+ "ds18b20_sensors": [],
+ "mac": "11:22:33:44:55:66",
+ "settings": {},
+ }
+
+ # setup the integration and inspect panel behavior
+ assert (
+ await async_setup_component(
+ hass,
+ panel.DOMAIN,
+ {
+ panel.DOMAIN: {
+ panel.CONF_ACCESS_TOKEN: "arandomstringvalue",
+ panel.CONF_API_HOST: "http://192.168.1.1:8123",
+ }
+ },
+ )
+ is True
+ )
+
+ # confirm panel instance was created and configured.
+ # hass.data is the only mechanism to get a reference to the created panel instance
+ device = hass.data[panel.DOMAIN][panel.CONF_DEVICES]["112233445566"]["panel"]
+ await device.update_switch("1", 0)
+
+ # confirm the correct api is used
+ # pylint: disable=no-member
+ assert mock_panel.put_device.call_count == 1
+ assert mock_panel.put_zone.call_count == 0
+
+ # confirm the settings are sent to the panel
+ # pylint: disable=no-member
+ assert mock_panel.put_settings.call_args_list[0][1] == {
+ "sensors": [{"pin": "1"}, {"pin": "2"}, {"pin": "5"}],
+ "actuators": [{"trigger": 0, "pin": "8"}, {"trigger": 1, "pin": "9"}],
+ "dht_sensors": [{"poll_interval": 3, "pin": "6"}],
+ "ds18b20_sensors": [{"pin": "7"}],
+ "auth_token": "11223344556677889900",
+ "blink": True,
+ "discovery": True,
+ "endpoint": "http://192.168.1.1:8123/api/konnected",
+ }
+
+ # confirm the device settings are saved in hass.data
+ assert device.stored_configuration == {
+ "binary_sensors": {
+ "1": {
+ "inverse": False,
+ "name": "Konnected 445566 Zone 1",
+ "state": None,
+ "type": "door",
+ },
+ "2": {"inverse": True, "name": "winder", "state": None, "type": "window"},
+ "3": {
+ "inverse": False,
+ "name": "Konnected 445566 Zone 3",
+ "state": None,
+ "type": "door",
+ },
+ },
+ "blink": True,
+ "panel": device,
+ "discovery": True,
+ "host": "1.2.3.4",
+ "port": 1234,
+ "sensors": [
+ {
+ "name": "Konnected 445566 Sensor 4",
+ "poll_interval": 3,
+ "type": "dht",
+ "zone": "4",
+ },
+ {"name": "temper", "poll_interval": 3, "type": "ds18b20", "zone": "5"},
+ ],
+ "switches": [
+ {
+ "activation": "low",
+ "momentary": 50,
+ "name": "switcher",
+ "pause": 100,
+ "repeat": 4,
+ "state": None,
+ "zone": "out",
+ },
+ {
+ "activation": "high",
+ "momentary": None,
+ "name": "Konnected 445566 Actuator 6",
+ "pause": None,
+ "repeat": None,
+ "state": None,
+ "zone": "6",
+ },
+ ],
+ }
diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py
index a737396cca8..3ac8171ce7d 100644
--- a/tests/components/light/test_device_action.py
+++ b/tests/components/light/test_device_action.py
@@ -2,7 +2,7 @@
import pytest
import homeassistant.components.automation as automation
-from homeassistant.components.light import DOMAIN
+from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS
from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
@@ -42,7 +42,13 @@ async def test_get_actions(hass, device_reg, entity_reg):
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",
+ "5678",
+ device_id=device_entry.id,
+ supported_features=SUPPORT_BRIGHTNESS,
+ )
expected_actions = [
{
"domain": DOMAIN,
@@ -62,6 +68,18 @@ async def test_get_actions(hass, device_reg, entity_reg):
"device_id": device_entry.id,
"entity_id": f"{DOMAIN}.test_5678",
},
+ {
+ "domain": DOMAIN,
+ "type": "brightness_increase",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "brightness_decrease",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert actions == expected_actions
@@ -108,6 +126,30 @@ async def test_action(hass, calls):
"type": "toggle",
},
},
+ {
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_brightness_increase",
+ },
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "brightness_increase",
+ },
+ },
+ {
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_brightness_decrease",
+ },
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "brightness_decrease",
+ },
+ },
]
},
)
@@ -138,3 +180,19 @@ async def test_action(hass, calls):
hass.bus.async_fire("test_event3")
await hass.async_block_till_done()
assert hass.states.get(ent1.entity_id).state == STATE_ON
+
+ turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on")
+
+ hass.bus.async_fire("test_brightness_increase")
+ await hass.async_block_till_done()
+
+ assert len(turn_on_calls) == 1
+ assert turn_on_calls[0].data["entity_id"] == ent1.entity_id
+ assert turn_on_calls[0].data["brightness_step_pct"] == 10
+
+ hass.bus.async_fire("test_brightness_decrease")
+ await hass.async_block_till_done()
+
+ assert len(turn_on_calls) == 2
+ assert turn_on_calls[1].data["entity_id"] == ent1.entity_id
+ assert turn_on_calls[1].data["brightness_step_pct"] == -10
diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py
index 676fa4ec849..49bc626a957 100644
--- a/tests/components/light/test_init.py
+++ b/tests/components/light/test_init.py
@@ -462,3 +462,37 @@ async def test_light_turn_on_auth(hass, hass_admin_user):
True,
core.Context(user_id=hass_admin_user.id),
)
+
+
+async def test_light_brightness_step(hass):
+ """Test that light context works."""
+ platform = getattr(hass.components, "test.light")
+ platform.init()
+ entity = platform.ENTITIES[0]
+ entity.supported_features = light.SUPPORT_BRIGHTNESS
+ entity.brightness = 100
+ assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
+
+ state = hass.states.get(entity.entity_id)
+ assert state is not None
+ assert state.attributes["brightness"] == 100
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": entity.entity_id, "brightness_step": -10},
+ True,
+ )
+
+ _, data = entity.last_call("turn_on")
+ assert data["brightness"] == 90, data
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": entity.entity_id, "brightness_step_pct": 10},
+ True,
+ )
+
+ _, data = entity.last_call("turn_on")
+ assert data["brightness"] == 125, data
diff --git a/tests/components/linky/conftest.py b/tests/components/linky/conftest.py
new file mode 100644
index 00000000000..f77f01a4ae7
--- /dev/null
+++ b/tests/components/linky/conftest.py
@@ -0,0 +1,11 @@
+"""Linky generic test utils."""
+from unittest.mock import patch
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def patch_fakeuseragent():
+ """Stub out fake useragent dep that makes requests."""
+ with patch("pylinky.client.UserAgent", return_value="Test Browser"):
+ yield
diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py
index 9f1c62a8b13..82e7b3bc2ac 100644
--- a/tests/components/lovelace/test_init.py
+++ b/tests/components/lovelace/test_init.py
@@ -38,6 +38,13 @@ async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage):
assert response["result"] == {"yo": "hello"}
+ # Test with safe mode
+ hass.config.safe_mode = True
+ await client.send_json({"id": 8, "type": "lovelace/config"})
+ response = await client.receive_json()
+ assert not response["success"]
+ assert response["error"]["code"] == "config_not_found"
+
async def test_lovelace_from_storage_save_before_load(
hass, hass_ws_client, hass_storage
diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py
index 810998ec0b8..d8a96b2db52 100644
--- a/tests/components/marytts/test_tts.py
+++ b/tests/components/marytts/test_tts.py
@@ -66,7 +66,12 @@ class TestTTSMaryTTSPlatform:
with patch("http.client.HTTPConnection", return_value=conn):
self.hass.services.call(
- tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "marytts_say",
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "HomeAssistant",
+ },
)
self.hass.block_till_done()
@@ -93,7 +98,12 @@ class TestTTSMaryTTSPlatform:
with patch("http.client.HTTPConnection", return_value=conn):
self.hass.services.call(
- tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "marytts_say",
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "HomeAssistant",
+ },
)
self.hass.block_till_done()
@@ -123,7 +133,12 @@ class TestTTSMaryTTSPlatform:
with patch("http.client.HTTPConnection", return_value=conn):
self.hass.services.call(
- tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "marytts_say",
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "HomeAssistant",
+ },
)
self.hass.block_till_done()
diff --git a/tests/components/melcloud/__init__.py b/tests/components/melcloud/__init__.py
new file mode 100644
index 00000000000..f20383660d4
--- /dev/null
+++ b/tests/components/melcloud/__init__.py
@@ -0,0 +1 @@
+"""Tests for the MELCloud integration."""
diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py
new file mode 100644
index 00000000000..90c766f0831
--- /dev/null
+++ b/tests/components/melcloud/test_config_flow.py
@@ -0,0 +1,171 @@
+"""Test the MELCloud config flow."""
+import asyncio
+
+from aiohttp import ClientError, ClientResponseError
+from asynctest import patch
+import pymelcloud
+import pytest
+
+from homeassistant import config_entries
+from homeassistant.components.melcloud.const import DOMAIN
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture
+def mock_login():
+ """Mock pymelcloud login."""
+ with patch("pymelcloud.login") as mock:
+ mock.return_value = "test-token"
+ yield mock
+
+
+@pytest.fixture
+def mock_get_devices():
+ """Mock pymelcloud get_devices."""
+ with patch("pymelcloud.get_devices") as mock:
+ mock.return_value = {
+ pymelcloud.DEVICE_TYPE_ATA: [],
+ pymelcloud.DEVICE_TYPE_ATW: [],
+ }
+ yield mock
+
+
+@pytest.fixture
+def mock_request_info():
+ """Mock RequestInfo to create ClientResponseErrors."""
+ with patch("aiohttp.RequestInfo") as mock_ri:
+ mock_ri.return_value.real_url.return_value = ""
+ yield mock_ri
+
+
+async def test_form(hass, mock_login, mock_get_devices):
+ """Test we get the form."""
+ 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.melcloud.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.melcloud.async_setup_entry", return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"username": "test-email@test-domain.com", "password": "test-password"},
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "test-email@test-domain.com"
+ assert result2["data"] == {
+ "username": "test-email@test-domain.com",
+ "token": "test-token",
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+@pytest.mark.parametrize(
+ "error,reason",
+ [(ClientError(), "cannot_connect"), (asyncio.TimeoutError(), "cannot_connect")],
+)
+async def test_form_errors(hass, mock_login, mock_get_devices, error, reason):
+ """Test we handle cannot connect error."""
+ mock_login.side_effect = error
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data={"username": "test-email@test-domain.com", "password": "test-password"},
+ )
+
+ assert len(mock_login.mock_calls) == 1
+ assert result["type"] == "abort"
+ assert result["reason"] == reason
+
+
+@pytest.mark.parametrize(
+ "error,message",
+ [(401, "invalid_auth"), (403, "invalid_auth"), (500, "cannot_connect")],
+)
+async def test_form_response_errors(
+ hass, mock_login, mock_get_devices, mock_request_info, error, message
+):
+ """Test we handle response errors."""
+ mock_login.side_effect = ClientResponseError(mock_request_info(), (), status=error)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data={"username": "test-email@test-domain.com", "password": "test-password"},
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == message
+
+
+async def test_import_with_token(hass, mock_login, mock_get_devices):
+ """Test successful import."""
+ with patch(
+ "homeassistant.components.melcloud.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.melcloud.async_setup_entry", return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={"username": "test-email@test-domain.com", "token": "test-token"},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "test-email@test-domain.com"
+ assert result["data"] == {
+ "username": "test-email@test-domain.com",
+ "token": "test-token",
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_token_refresh(hass, mock_login, mock_get_devices):
+ """Re-configuration with existing username should refresh token."""
+ mock_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "username": "test-email@test-domain.com",
+ "token": "test-original-token",
+ },
+ unique_id="test-email@test-domain.com",
+ )
+ mock_entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.melcloud.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.melcloud.async_setup_entry", return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data={
+ "username": "test-email@test-domain.com",
+ "password": "test-password",
+ },
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 0
+ assert len(mock_setup_entry.mock_calls) == 0
+
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+
+ entry = entries[0]
+ assert entry.data["username"] == "test-email@test-domain.com"
+ assert entry.data["token"] == "test-token"
diff --git a/tests/components/meteo_france/__init__.py b/tests/components/meteo_france/__init__.py
new file mode 100644
index 00000000000..c4d4c446574
--- /dev/null
+++ b/tests/components/meteo_france/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Meteo-France component."""
diff --git a/tests/components/meteo_france/conftest.py b/tests/components/meteo_france/conftest.py
new file mode 100644
index 00000000000..088587ab2c2
--- /dev/null
+++ b/tests/components/meteo_france/conftest.py
@@ -0,0 +1,16 @@
+"""Meteo-France generic test utils."""
+from unittest.mock import patch
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def patch_requests():
+ """Stub out services that makes requests."""
+ patch_client = patch("homeassistant.components.meteo_france.meteofranceClient")
+ patch_weather_alert = patch(
+ "homeassistant.components.meteo_france.VigilanceMeteoFranceProxy"
+ )
+
+ with patch_client, patch_weather_alert:
+ yield
diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py
new file mode 100644
index 00000000000..f9ead2c1ef3
--- /dev/null
+++ b/tests/components/meteo_france/test_config_flow.py
@@ -0,0 +1,128 @@
+"""Tests for the Meteo-France config flow."""
+from unittest.mock import patch
+
+from meteofrance.client import meteofranceError
+import pytest
+
+from homeassistant import data_entry_flow
+from homeassistant.components.meteo_france.const import CONF_CITY, DOMAIN
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
+
+from tests.common import MockConfigEntry
+
+CITY_1_POSTAL = "74220"
+CITY_1_NAME = "La Clusaz"
+CITY_2_POSTAL_DISTRICT_1 = "69001"
+CITY_2_POSTAL_DISTRICT_4 = "69004"
+CITY_2_NAME = "Lyon"
+
+
+@pytest.fixture(name="client_1")
+def mock_controller_client_1():
+ """Mock a successful client."""
+ with patch(
+ "homeassistant.components.meteo_france.config_flow.meteofranceClient",
+ update=False,
+ ) as service_mock:
+ service_mock.return_value.get_data.return_value = {"name": CITY_1_NAME}
+ yield service_mock
+
+
+@pytest.fixture(name="client_2")
+def mock_controller_client_2():
+ """Mock a successful client."""
+ with patch(
+ "homeassistant.components.meteo_france.config_flow.meteofranceClient",
+ update=False,
+ ) as service_mock:
+ service_mock.return_value.get_data.return_value = {"name": CITY_2_NAME}
+ yield service_mock
+
+
+async def test_user(hass, client_1):
+ """Test user config."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ # test with all provided
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["result"].unique_id == CITY_1_NAME
+ assert result["title"] == CITY_1_NAME
+ assert result["data"][CONF_CITY] == CITY_1_POSTAL
+
+
+async def test_import(hass, client_1):
+ """Test import step."""
+ # import with all
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["result"].unique_id == CITY_1_NAME
+ assert result["title"] == CITY_1_NAME
+ assert result["data"][CONF_CITY] == CITY_1_POSTAL
+
+
+async def test_abort_if_already_setup(hass, client_1):
+ """Test we abort if already setup."""
+ MockConfigEntry(
+ domain=DOMAIN, data={CONF_CITY: CITY_1_POSTAL}, unique_id=CITY_1_NAME
+ ).add_to_hass(hass)
+
+ # Should fail, same CITY same postal code (import)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+ # Should fail, same CITY same postal code (flow)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_abort_if_already_setup_district(hass, client_2):
+ """Test we abort if already setup."""
+ MockConfigEntry(
+ domain=DOMAIN, data={CONF_CITY: CITY_2_POSTAL_DISTRICT_1}, unique_id=CITY_2_NAME
+ ).add_to_hass(hass)
+
+ # Should fail, same CITY different postal code (import)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={CONF_CITY: CITY_2_POSTAL_DISTRICT_4},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+ # Should fail, same CITY different postal code (flow)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={CONF_CITY: CITY_2_POSTAL_DISTRICT_4},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_client_failed(hass):
+ """Test when we have errors during client fetch."""
+ with patch(
+ "homeassistant.components.meteo_france.config_flow.meteofranceClient",
+ side_effect=meteofranceError(),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py
index 25f541e9287..37dbfad4d35 100644
--- a/tests/components/mikrotik/test_config_flow.py
+++ b/tests/components/mikrotik/test_config_flow.py
@@ -179,7 +179,7 @@ async def test_name_exists(hass, api):
async def test_connection_error(hass, conn_error):
- """Test error when connection is unsuccesful."""
+ """Test error when connection is unsuccessful."""
result = await hass.config_entries.flow.async_init(
mikrotik.DOMAIN, context={"source": "user"}
diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py
index bf2b19c735c..ea7e22239b2 100644
--- a/tests/components/mikrotik/test_init.py
+++ b/tests/components/mikrotik/test_init.py
@@ -16,7 +16,7 @@ async def test_setup_with_no_config(hass):
async def test_successful_config_entry(hass):
- """Test config entry successfull setup."""
+ """Test config entry successful setup."""
entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,)
entry.add_to_hass(hass)
mock_registry = Mock()
diff --git a/tests/components/minecraft_server/__init__.py b/tests/components/minecraft_server/__init__.py
new file mode 100644
index 00000000000..36a1bb3f69d
--- /dev/null
+++ b/tests/components/minecraft_server/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Minecraft Server integration."""
diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py
new file mode 100644
index 00000000000..30626fbdcb0
--- /dev/null
+++ b/tests/components/minecraft_server/test_config_flow.py
@@ -0,0 +1,194 @@
+"""Test the Minecraft Server config flow."""
+
+from asynctest import patch
+from mcstatus.pinger import PingResponse
+
+from homeassistant.components.minecraft_server.const import (
+ DEFAULT_NAME,
+ DEFAULT_PORT,
+ DOMAIN,
+)
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
+from homeassistant.data_entry_flow import (
+ RESULT_TYPE_ABORT,
+ RESULT_TYPE_CREATE_ENTRY,
+ RESULT_TYPE_FORM,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.common import MockConfigEntry
+
+STATUS_RESPONSE_RAW = {
+ "description": {"text": "Dummy Description"},
+ "version": {"name": "Dummy Version", "protocol": 123},
+ "players": {
+ "online": 3,
+ "max": 10,
+ "sample": [
+ {"name": "Player 1", "id": "1"},
+ {"name": "Player 2", "id": "2"},
+ {"name": "Player 3", "id": "3"},
+ ],
+ },
+}
+
+USER_INPUT = {
+ CONF_NAME: DEFAULT_NAME,
+ CONF_HOST: "mc.dummyserver.com",
+ CONF_PORT: DEFAULT_PORT,
+}
+
+USER_INPUT_IPV4 = {
+ CONF_NAME: DEFAULT_NAME,
+ CONF_HOST: "1.1.1.1",
+ CONF_PORT: DEFAULT_PORT,
+}
+
+USER_INPUT_IPV6 = {
+ CONF_NAME: DEFAULT_NAME,
+ CONF_HOST: "::ffff:0101:0101",
+ CONF_PORT: DEFAULT_PORT,
+}
+
+USER_INPUT_PORT_TOO_SMALL = {
+ CONF_NAME: DEFAULT_NAME,
+ CONF_HOST: "mc.dummyserver.com",
+ CONF_PORT: 1023,
+}
+
+USER_INPUT_PORT_TOO_LARGE = {
+ CONF_NAME: DEFAULT_NAME,
+ CONF_HOST: "mc.dummyserver.com",
+ CONF_PORT: 65536,
+}
+
+
+async def test_show_config_form(hass: HomeAssistantType) -> None:
+ """Test if initial configuration form is shown."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+
+async def test_invalid_ip(hass: HomeAssistantType) -> None:
+ """Test error in case of an invalid IP address."""
+ with patch("getmac.get_mac_address", return_value=None):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "invalid_ip"}
+
+
+async def test_same_host(hass: HomeAssistantType) -> None:
+ """Test abort in case of same host name."""
+ unique_id = f"{USER_INPUT[CONF_HOST]}-{USER_INPUT[CONF_PORT]}"
+ mock_config_entry = MockConfigEntry(
+ domain=DOMAIN, unique_id=unique_id, data=USER_INPUT
+ )
+ mock_config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_port_too_small(hass: HomeAssistantType) -> None:
+ """Test error in case of a too small port."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_SMALL
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "invalid_port"}
+
+
+async def test_port_too_large(hass: HomeAssistantType) -> None:
+ """Test error in case of a too large port."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_LARGE
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "invalid_port"}
+
+
+async def test_connection_failed(hass: HomeAssistantType) -> None:
+ """Test error in case of a failed connection."""
+ with patch("mcstatus.server.MinecraftServer.ping", side_effect=OSError):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_connection_succeeded_with_host(hass: HomeAssistantType) -> None:
+ """Test config entry in case of a successful connection with a host name."""
+ with patch("mcstatus.server.MinecraftServer.ping", return_value=50):
+ with patch(
+ "mcstatus.server.MinecraftServer.status",
+ return_value=PingResponse(STATUS_RESPONSE_RAW),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == f"{USER_INPUT[CONF_HOST]}:{USER_INPUT[CONF_PORT]}"
+ assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME]
+ assert result["data"][CONF_HOST] == USER_INPUT[CONF_HOST]
+ assert result["data"][CONF_PORT] == USER_INPUT[CONF_PORT]
+
+
+async def test_connection_succeeded_with_ip4(hass: HomeAssistantType) -> None:
+ """Test config entry in case of a successful connection with an IPv4 address."""
+ with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"):
+ with patch("mcstatus.server.MinecraftServer.ping", return_value=50):
+ with patch(
+ "mcstatus.server.MinecraftServer.status",
+ return_value=PingResponse(STATUS_RESPONSE_RAW),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert (
+ result["title"]
+ == f"{USER_INPUT_IPV4[CONF_HOST]}:{USER_INPUT_IPV4[CONF_PORT]}"
+ )
+ assert result["data"][CONF_NAME] == USER_INPUT_IPV4[CONF_NAME]
+ assert result["data"][CONF_HOST] == USER_INPUT_IPV4[CONF_HOST]
+ assert result["data"][CONF_PORT] == USER_INPUT_IPV4[CONF_PORT]
+
+
+async def test_connection_succeeded_with_ip6(hass: HomeAssistantType) -> None:
+ """Test config entry in case of a successful connection with an IPv6 address."""
+ with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"):
+ with patch("mcstatus.server.MinecraftServer.ping", return_value=50):
+ with patch(
+ "mcstatus.server.MinecraftServer.status",
+ return_value=PingResponse(STATUS_RESPONSE_RAW),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV6
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert (
+ result["title"]
+ == f"{USER_INPUT_IPV6[CONF_HOST]}:{USER_INPUT_IPV6[CONF_PORT]}"
+ )
+ assert result["data"][CONF_NAME] == USER_INPUT_IPV6[CONF_NAME]
+ assert result["data"][CONF_HOST] == USER_INPUT_IPV6[CONF_HOST]
+ assert result["data"][CONF_PORT] == USER_INPUT_IPV6[CONF_PORT]
diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py
index 0db9d42048f..65dc328186d 100644
--- a/tests/components/mobile_app/test_entity.py
+++ b/tests/components/mobile_app/test_entity.py
@@ -35,7 +35,7 @@ async def test_sensor(hass, create_registrations, webhook_client):
assert json == {"success": True}
await hass.async_block_till_done()
- entity = hass.states.get("sensor.battery_state")
+ entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.attributes["device_class"] == "battery"
@@ -43,7 +43,7 @@ async def test_sensor(hass, create_registrations, webhook_client):
assert entity.attributes["unit_of_measurement"] == "%"
assert entity.attributes["foo"] == "bar"
assert entity.domain == "sensor"
- assert entity.name == "Battery State"
+ assert entity.name == "Test 1 Battery State"
assert entity.state == "100"
update_resp = await webhook_client.post(
@@ -63,7 +63,7 @@ async def test_sensor(hass, create_registrations, webhook_client):
assert update_resp.status == 200
- updated_entity = hass.states.get("sensor.battery_state")
+ updated_entity = hass.states.get("sensor.test_1_battery_state")
assert updated_entity.state == "123"
dev_reg = await device_registry.async_get_registry(hass)
diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py
index 3df71c34781..39837543a47 100644
--- a/tests/components/mobile_app/test_webhook.py
+++ b/tests/components/mobile_app/test_webhook.py
@@ -1,5 +1,4 @@
"""Webhook tests for mobile_app."""
-
import logging
import pytest
@@ -17,6 +16,53 @@ from tests.common import async_mock_service
_LOGGER = logging.getLogger(__name__)
+def encrypt_payload(secret_key, payload):
+ """Return a encrypted payload given a key and dictionary of data."""
+ try:
+ from nacl.secret import SecretBox
+ from nacl.encoding import Base64Encoder
+ except (ImportError, OSError):
+ pytest.skip("libnacl/libsodium is not installed")
+ return
+
+ import json
+
+ keylen = SecretBox.KEY_SIZE
+ prepped_key = secret_key.encode("utf-8")
+ prepped_key = prepped_key[:keylen]
+ prepped_key = prepped_key.ljust(keylen, b"\0")
+
+ payload = json.dumps(payload).encode("utf-8")
+
+ return (
+ SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8")
+ )
+
+
+def decrypt_payload(secret_key, encrypted_data):
+ """Return a decrypted payload given a key and a string of encrypted data."""
+ try:
+ from nacl.secret import SecretBox
+ from nacl.encoding import Base64Encoder
+ except (ImportError, OSError):
+ pytest.skip("libnacl/libsodium is not installed")
+ return
+
+ import json
+
+ keylen = SecretBox.KEY_SIZE
+ prepped_key = secret_key.encode("utf-8")
+ prepped_key = prepped_key[:keylen]
+ prepped_key = prepped_key.ljust(keylen, b"\0")
+
+ decrypted_data = SecretBox(prepped_key).decrypt(
+ encrypted_data, encoder=Base64Encoder
+ )
+ decrypted_data = decrypted_data.decode("utf-8")
+
+ return json.loads(decrypted_data)
+
+
async def test_webhook_handle_render_template(create_registrations, webhook_client):
"""Test that we render templates properly."""
resp = await webhook_client.post(
@@ -166,23 +212,8 @@ async def test_webhook_returns_error_incorrect_json(
async def test_webhook_handle_decryption(webhook_client, create_registrations):
"""Test that we can encrypt/decrypt properly."""
- try:
- from nacl.secret import SecretBox
- from nacl.encoding import Base64Encoder
- except (ImportError, OSError):
- pytest.skip("libnacl/libsodium is not installed")
- return
-
- import json
-
- keylen = SecretBox.KEY_SIZE
- key = create_registrations[0]["secret"].encode("utf-8")
- key = key[:keylen]
- key = key.ljust(keylen, b"\0")
-
- payload = json.dumps(RENDER_TEMPLATE["data"]).encode("utf-8")
-
- data = SecretBox(key).encrypt(payload, encoder=Base64Encoder).decode("utf-8")
+ key = create_registrations[0]["secret"]
+ data = encrypt_payload(key, RENDER_TEMPLATE["data"])
container = {"type": "render_template", "encrypted": True, "encrypted_data": data}
@@ -195,12 +226,9 @@ async def test_webhook_handle_decryption(webhook_client, create_registrations):
webhook_json = await resp.json()
assert "encrypted_data" in webhook_json
- decrypted_data = SecretBox(key).decrypt(
- webhook_json["encrypted_data"], encoder=Base64Encoder
- )
- decrypted_data = decrypted_data.decode("utf-8")
+ decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"])
- assert json.loads(decrypted_data) == {"one": "Hello world"}
+ assert decrypted_data == {"one": "Hello world"}
async def test_webhook_requires_encryption(webhook_client, create_registrations):
@@ -219,7 +247,7 @@ async def test_webhook_requires_encryption(webhook_client, create_registrations)
async def test_webhook_update_location(hass, webhook_client, create_registrations):
- """Test that encrypted registrations only accept encrypted data."""
+ """Test that location can be updated."""
resp = await webhook_client.post(
"/api/webhook/{}".format(create_registrations[1]["webhook_id"]),
json={
@@ -236,3 +264,52 @@ async def test_webhook_update_location(hass, webhook_client, create_registration
assert state.attributes["longitude"] == 2.0
assert state.attributes["gps_accuracy"] == 10
assert state.attributes["altitude"] == -10
+
+
+async def test_webhook_enable_encryption(hass, webhook_client, create_registrations):
+ """Test that encryption can be added to a reg initially created without."""
+ webhook_id = create_registrations[1]["webhook_id"]
+
+ enable_enc_resp = await webhook_client.post(
+ "/api/webhook/{}".format(webhook_id), json={"type": "enable_encryption"},
+ )
+
+ assert enable_enc_resp.status == 200
+
+ enable_enc_json = await enable_enc_resp.json()
+ assert len(enable_enc_json) == 1
+ assert CONF_SECRET in enable_enc_json
+
+ key = enable_enc_json["secret"]
+
+ enc_required_resp = await webhook_client.post(
+ "/api/webhook/{}".format(webhook_id), json=RENDER_TEMPLATE,
+ )
+
+ assert enc_required_resp.status == 400
+
+ enc_required_json = await enc_required_resp.json()
+ assert "error" in enc_required_json
+ assert enc_required_json["success"] is False
+ assert enc_required_json["error"]["code"] == "encryption_required"
+
+ enc_data = encrypt_payload(key, RENDER_TEMPLATE["data"])
+
+ container = {
+ "type": "render_template",
+ "encrypted": True,
+ "encrypted_data": enc_data,
+ }
+
+ enc_resp = await webhook_client.post(
+ "/api/webhook/{}".format(webhook_id), json=container
+ )
+
+ assert enc_resp.status == 200
+
+ enc_json = await enc_resp.json()
+ assert "encrypted_data" in enc_json
+
+ decrypted_data = decrypt_payload(key, enc_json["encrypted_data"])
+
+ assert decrypted_data == {"one": "Hello world"}
diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py
index 9f13cba8907..16d8f9a1936 100644
--- a/tests/components/modbus/test_modbus_sensor.py
+++ b/tests/components/modbus/test_modbus_sensor.py
@@ -17,8 +17,8 @@ from homeassistant.components.modbus.sensor import (
DATA_TYPE_FLOAT,
DATA_TYPE_INT,
DATA_TYPE_UINT,
- REGISTER_TYPE_HOLDING,
- REGISTER_TYPE_INPUT,
+ DEFAULT_REGISTER_TYPE_HOLDING,
+ DEFAULT_REGISTER_TYPE_INPUT,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import (
@@ -72,7 +72,7 @@ async def run_test(hass, mock_hub, register_config, register_words, expected):
# Setup inputs for the sensor
read_result = ReadResult(register_words)
- if register_config.get(CONF_REGISTER_TYPE) == REGISTER_TYPE_INPUT:
+ if register_config.get(CONF_REGISTER_TYPE) == DEFAULT_REGISTER_TYPE_INPUT:
mock_hub.read_input_registers.return_value = read_result
else:
mock_hub.read_holding_registers.return_value = read_result
@@ -310,7 +310,7 @@ async def test_two_word_input_register(hass, mock_hub):
"""Test reaging of input register."""
register_config = {
CONF_COUNT: 2,
- CONF_REGISTER_TYPE: REGISTER_TYPE_INPUT,
+ CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_INPUT,
CONF_DATA_TYPE: DATA_TYPE_UINT,
CONF_SCALE: 1,
CONF_OFFSET: 0,
@@ -329,7 +329,7 @@ async def test_two_word_holding_register(hass, mock_hub):
"""Test reaging of holding register."""
register_config = {
CONF_COUNT: 2,
- CONF_REGISTER_TYPE: REGISTER_TYPE_HOLDING,
+ CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_HOLDING,
CONF_DATA_TYPE: DATA_TYPE_UINT,
CONF_SCALE: 1,
CONF_OFFSET: 0,
@@ -348,7 +348,7 @@ async def test_float_data_type(hass, mock_hub):
"""Test floating point register data type."""
register_config = {
CONF_COUNT: 2,
- CONF_REGISTER_TYPE: REGISTER_TYPE_HOLDING,
+ CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_HOLDING,
CONF_DATA_TYPE: DATA_TYPE_FLOAT,
CONF_SCALE: 1,
CONF_OFFSET: 0,
diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py
index 889410927e5..aa72549152e 100644
--- a/tests/components/mqtt/test_config_flow.py
+++ b/tests/components/mqtt/test_config_flow.py
@@ -50,7 +50,7 @@ async def test_user_connection_works(hass, mock_try_connection, mock_finish_setu
async def test_user_connection_fails(hass, mock_try_connection, mock_finish_setup):
- """Test if connnection cannot be made."""
+ """Test if connection cannot be made."""
mock_try_connection.return_value = False
result = await hass.config_entries.flow.async_init(
diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py
index b15518961a4..128c18de8df 100644
--- a/tests/components/mqtt/test_cover.py
+++ b/tests/components/mqtt/test_cover.py
@@ -19,7 +19,9 @@ from homeassistant.const import (
SERVICE_TOGGLE,
SERVICE_TOGGLE_COVER_TILT,
STATE_CLOSED,
+ STATE_CLOSING,
STATE_OPEN,
+ STATE_OPENING,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
@@ -67,6 +69,93 @@ async def test_state_via_state_topic(hass, mqtt_mock):
assert state.state == STATE_OPEN
+async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_mock):
+ """Test the controlling opening and closing state via a custom payload."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "command_topic": "command-topic",
+ "qos": 0,
+ "payload_open": "OPEN",
+ "payload_close": "CLOSE",
+ "payload_stop": "STOP",
+ "state_opening": "34",
+ "state_closing": "--43",
+ }
+ },
+ )
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_UNKNOWN
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "state-topic", "34")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPENING
+
+ async_fire_mqtt_message(hass, "state-topic", "--43")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSING
+
+ async_fire_mqtt_message(hass, "state-topic", STATE_CLOSED)
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSED
+
+
+async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock):
+ """Test the state after setting the position using optimistic mode."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "position_topic": "position-topic",
+ "set_position_topic": "set-position-topic",
+ "qos": 0,
+ "payload_open": "OPEN",
+ "payload_close": "CLOSE",
+ "payload_stop": "STOP",
+ "optimistic": True,
+ }
+ },
+ )
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_UNKNOWN
+
+ await hass.services.async_call(
+ cover.DOMAIN,
+ SERVICE_SET_COVER_POSITION,
+ {ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: 0},
+ blocking=True,
+ )
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSED
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await hass.services.async_call(
+ cover.DOMAIN,
+ SERVICE_SET_COVER_POSITION,
+ {ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: 100},
+ blocking=True,
+ )
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPEN
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+
async def test_position_via_position_topic(hass, mqtt_mock):
"""Test the controlling state via topic."""
assert await async_setup_component(
diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py
new file mode 100644
index 00000000000..c3ba6eebadd
--- /dev/null
+++ b/tests/components/mqtt/test_device_trigger.py
@@ -0,0 +1,777 @@
+"""The tests for MQTT device triggers."""
+import json
+
+import pytest
+
+import homeassistant.components.automation as automation
+from homeassistant.components.mqtt import DOMAIN
+from homeassistant.components.mqtt.device_trigger import async_attach_trigger
+from homeassistant.components.mqtt.discovery import async_start
+from homeassistant.setup import async_setup_component
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ async_fire_mqtt_message,
+ async_get_device_automations,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+)
+
+
+@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 service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock):
+ """Test we get the expected triggers from a discovered mqtt device."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1)
+ await hass.async_block_till_done()
+
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+ expected_triggers = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ ]
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, expected_triggers)
+
+
+async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock):
+ """Test we don't get unknown triggers."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ # Discover a sensor (without device triggers)
+ data1 = (
+ '{ "device":{"identifiers":["0AFFD2"]},'
+ ' "state_topic": "foobar/sensor",'
+ ' "unique_id": "unique" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data1)
+ await hass.async_block_till_done()
+
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla1",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": ("short_press")},
+ },
+ },
+ ]
+ },
+ )
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, [])
+
+
+async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock):
+ """Test getting non existing triggers."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ # Discover a sensor (without device triggers)
+ data1 = (
+ '{ "device":{"identifiers":["0AFFD2"]},'
+ ' "state_topic": "foobar/sensor",'
+ ' "unique_id": "unique" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data1)
+ await hass.async_block_till_done()
+
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, [])
+
+
+async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock):
+ """Test bad discovery message."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ # Test sending bad data
+ data0 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payloads": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data0)
+ await hass.async_block_till_done()
+ assert device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) is None
+
+ # Test sending correct data
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1)
+ await hass.async_block_till_done()
+
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+ expected_triggers = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ ]
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, expected_triggers)
+
+
+async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock):
+ """Test triggers can be updated and removed."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ data2 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_2" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1)
+ await hass.async_block_till_done()
+
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+ expected_triggers1 = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ ]
+ expected_triggers2 = [dict(expected_triggers1[0])]
+ expected_triggers2[0]["subtype"] = "button_2"
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, expected_triggers1)
+
+ # Update trigger
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data2)
+ await hass.async_block_till_done()
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, expected_triggers2)
+
+ # Remove trigger
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", "")
+ await hass.async_block_till_done()
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, [])
+
+
+async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock):
+ """Test triggers firing."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ data2 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "long_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_long_press",'
+ ' "subtype": "button_2" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2)
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla1",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": ("short_press")},
+ },
+ },
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla2",
+ "type": "button_1",
+ "subtype": "button_long_press",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": ("long_press")},
+ },
+ },
+ ]
+ },
+ )
+
+ # Fake short press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "short_press"
+
+ # Fake long press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "long_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "long_press"
+
+
+async def test_if_fires_on_mqtt_message_late_discover(
+ hass, device_reg, calls, mqtt_mock
+):
+ """Test triggers firing of MQTT device triggers discovered after setup."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ data0 = (
+ '{ "device":{"identifiers":["0AFFD2"]},'
+ ' "state_topic": "foobar/sensor",'
+ ' "unique_id": "unique" }'
+ )
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ data2 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "long_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_long_press",'
+ ' "subtype": "button_2" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0)
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla1",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": ("short_press")},
+ },
+ },
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla2",
+ "type": "button_1",
+ "subtype": "button_long_press",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": ("long_press")},
+ },
+ },
+ ]
+ },
+ )
+
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2)
+ await hass.async_block_till_done()
+
+ # Fake short press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "short_press"
+
+ # Fake long press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "long_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "long_press"
+
+
+async def test_if_fires_on_mqtt_message_after_update(
+ hass, device_reg, calls, mqtt_mock
+):
+ """Test triggers firing after update."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ data2 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "topic": "foobar/triggers/buttonOne",'
+ ' "type": "button_long_press",'
+ ' "subtype": "button_2" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla1",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": ("short_press")},
+ },
+ },
+ ]
+ },
+ )
+
+ # Fake short press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ # Update the trigger
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data2)
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ async_fire_mqtt_message(hass, "foobar/triggers/buttonOne", "")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+
+
+async def test_not_fires_on_mqtt_message_after_remove(
+ hass, device_reg, calls, mqtt_mock
+):
+ """Test triggers not firing after removal."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla1",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": ("short_press")},
+ },
+ },
+ ]
+ },
+ )
+
+ # Fake short press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ # Remove the trigger
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "")
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ # Rediscover the trigger
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+
+
+async def test_attach_remove(hass, device_reg, mqtt_mock):
+ """Test attach and removal of trigger."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ calls = []
+
+ def callback(trigger):
+ calls.append(trigger["trigger"]["payload"])
+
+ remove = await async_attach_trigger(
+ hass,
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla1",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ callback,
+ None,
+ )
+
+ # Fake short press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0] == "short_press"
+
+ # Remove the trigger
+ remove()
+ await hass.async_block_till_done()
+
+ # Verify the triggers are no longer active
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
+async def test_attach_remove_late(hass, device_reg, mqtt_mock):
+ """Test attach and removal of trigger ."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ data0 = (
+ '{ "device":{"identifiers":["0AFFD2"]},'
+ ' "state_topic": "foobar/sensor",'
+ ' "unique_id": "unique" }'
+ )
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0)
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ calls = []
+
+ def callback(trigger):
+ calls.append(trigger["trigger"]["payload"])
+
+ remove = await async_attach_trigger(
+ hass,
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla1",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ callback,
+ None,
+ )
+
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
+ await hass.async_block_till_done()
+
+ # Fake short press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0] == "short_press"
+
+ # Remove the trigger
+ remove()
+ await hass.async_block_till_done()
+
+ # Verify the triggers are no longer active
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
+async def test_attach_remove_late2(hass, device_reg, mqtt_mock):
+ """Test attach and removal of trigger ."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, config_entry)
+
+ data0 = (
+ '{ "device":{"identifiers":["0AFFD2"]},'
+ ' "state_topic": "foobar/sensor",'
+ ' "unique_id": "unique" }'
+ )
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ ' "payload": "short_press",'
+ ' "topic": "foobar/triggers/button1",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1" }'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0)
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ calls = []
+
+ def callback(trigger):
+ calls.append(trigger["trigger"]["payload"])
+
+ remove = await async_attach_trigger(
+ hass,
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla1",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ callback,
+ None,
+ )
+
+ # Remove the trigger
+ remove()
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
+ await hass.async_block_till_done()
+
+ # Verify the triggers are no longer active
+ async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock):
+ """Test MQTT device registry integration."""
+ entry = MockConfigEntry(domain=DOMAIN)
+ entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ data = json.dumps(
+ {
+ "automation_type": "trigger",
+ "topic": "test-topic",
+ "type": "foo",
+ "subtype": "bar",
+ "device": {
+ "identifiers": ["helloworld"],
+ "connections": [["mac", "02:5b:26:a8:dc:12"]],
+ "manufacturer": "Whatever",
+ "name": "Beer",
+ "model": "Glass",
+ "sw_version": "0.1-beta",
+ },
+ }
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({("mqtt", "helloworld")}, set())
+ assert device is not None
+ assert device.identifiers == {("mqtt", "helloworld")}
+ assert device.connections == {("mac", "02:5b:26:a8:dc:12")}
+ assert device.manufacturer == "Whatever"
+ assert device.name == "Beer"
+ assert device.model == "Glass"
+ assert device.sw_version == "0.1-beta"
+
+
+async def test_entity_device_info_update(hass, mqtt_mock):
+ """Test device registry update."""
+ entry = MockConfigEntry(domain=DOMAIN)
+ entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ config = {
+ "automation_type": "trigger",
+ "topic": "test-topic",
+ "type": "foo",
+ "subtype": "bar",
+ "device": {
+ "identifiers": ["helloworld"],
+ "connections": [["mac", "02:5b:26:a8:dc:12"]],
+ "manufacturer": "Whatever",
+ "name": "Beer",
+ "model": "Glass",
+ "sw_version": "0.1-beta",
+ },
+ }
+
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({("mqtt", "helloworld")}, set())
+ assert device is not None
+ assert device.name == "Beer"
+
+ config["device"]["name"] = "Milk"
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({("mqtt", "helloworld")}, set())
+ assert device is not None
+ assert device.name == "Milk"
diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py
index 6320be3b772..e09b4d786a6 100644
--- a/tests/components/mqtt/test_discovery.py
+++ b/tests/components/mqtt/test_discovery.py
@@ -229,7 +229,7 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog):
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "sw":"1.2.3.4",'
- ' "mf":"Noone"'
+ ' "mf":"None"'
" }"
"}"
)
@@ -250,7 +250,7 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog):
ABBREVIATIONS_WHITE_LIST = [
- # MQTT client/server settings
+ # MQTT client/server/trigger settings
"CONF_BIRTH_MESSAGE",
"CONF_BROKER",
"CONF_CERTIFICATE",
@@ -258,6 +258,7 @@ ABBREVIATIONS_WHITE_LIST = [
"CONF_CLIENT_ID",
"CONF_CLIENT_KEY",
"CONF_DISCOVERY",
+ "CONF_DISCOVERY_ID",
"CONF_DISCOVERY_PREFIX",
"CONF_EMBEDDED",
"CONF_KEEPALIVE",
diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py
index de6ffa51170..6e61dfac3ab 100644
--- a/tests/components/plex/mock_classes.py
+++ b/tests/components/plex/mock_classes.py
@@ -1,4 +1,6 @@
"""Mock classes used in tests."""
+import itertools
+
from homeassistant.components.plex.const import CONF_SERVER, CONF_SERVER_IDENTIFIER
from homeassistant.const import CONF_HOST, CONF_PORT
@@ -17,6 +19,12 @@ MOCK_SERVERS = [
},
]
+MOCK_MONITORED_USERS = {
+ "a": {"enabled": True},
+ "b": {"enabled": False},
+ "c": {"enabled": True},
+}
+
class MockResource:
"""Mock a PlexAccount resource."""
@@ -53,10 +61,26 @@ class MockPlexAccount:
return self._resources
+class MockPlexSystemAccount:
+ """Mock a PlexSystemAccount instance."""
+
+ def __init__(self):
+ """Initialize the object."""
+ self.name = "Dummy"
+ self.accountID = 1
+
+
class MockPlexServer:
"""Mock a PlexServer instance."""
- def __init__(self, index=0, ssl=True):
+ def __init__(
+ self,
+ index=0,
+ ssl=True,
+ load_users=True,
+ num_users=len(MOCK_MONITORED_USERS),
+ ignore_new_users=False,
+ ):
"""Initialize the object."""
host = MOCK_SERVERS[index][CONF_HOST]
port = MOCK_SERVERS[index][CONF_PORT]
@@ -68,8 +92,52 @@ class MockPlexServer:
]
prefix = "https" if ssl else "http"
self._baseurl = f"{prefix}://{host}:{port}"
+ self._systemAccount = MockPlexSystemAccount()
+ self._ignore_new_users = ignore_new_users
+ self._load_users = load_users
+ self._num_users = num_users
+
+ def systemAccounts(self):
+ """Mock the systemAccounts lookup method."""
+ return [self._systemAccount]
+
+ @property
+ def accounts(self):
+ """Mock the accounts property."""
+ return set(["a", "b", "c"])
+
+ @property
+ def owner(self):
+ """Mock the owner property."""
+ return "a"
@property
def url_in_use(self):
"""Return URL used by PlexServer."""
return self._baseurl
+
+ @property
+ def version(self):
+ """Mock version of PlexServer."""
+ return "1.0"
+
+ @property
+ def option_monitored_users(self):
+ """Mock loaded config option for monitored users."""
+ userdict = dict(itertools.islice(MOCK_MONITORED_USERS.items(), self._num_users))
+ return userdict if self._load_users else {}
+
+ @property
+ def option_ignore_new_shared_users(self):
+ """Mock loaded config option for ignoring new users."""
+ return self._ignore_new_users
+
+ @property
+ def option_show_all_controls(self):
+ """Mock loaded config option for showing all controls."""
+ return False
+
+ @property
+ def option_use_episode_art(self):
+ """Mock loaded config option for using episode art."""
+ return False
diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py
index 8f9342c4f72..b331444123a 100644
--- a/tests/components/plex/test_config_flow.py
+++ b/tests/components/plex/test_config_flow.py
@@ -1,4 +1,5 @@
"""Tests for Plex config flow."""
+import copy
from unittest.mock import patch
import asynctest
@@ -26,6 +27,7 @@ DEFAULT_OPTIONS = {
config_flow.MP_DOMAIN: {
config_flow.CONF_USE_EPISODE_ART: False,
config_flow.CONF_SHOW_ALL_CONTROLS: False,
+ config_flow.CONF_IGNORE_NEW_SHARED_USERS: False,
}
}
@@ -457,9 +459,20 @@ async def test_all_available_servers_configured(hass):
async def test_option_flow(hass):
- """Test config flow selection of one of two bridges."""
+ """Test config options flow selection."""
- entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=DEFAULT_OPTIONS)
+ mock_plex_server = MockPlexServer(load_users=False)
+
+ MOCK_SERVER_ID = MOCK_SERVERS[0][config_flow.CONF_SERVER_IDENTIFIER]
+ hass.data[config_flow.DOMAIN] = {
+ config_flow.SERVERS: {MOCK_SERVER_ID: mock_plex_server}
+ }
+
+ entry = MockConfigEntry(
+ domain=config_flow.DOMAIN,
+ data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVER_ID},
+ options=DEFAULT_OPTIONS,
+ )
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(
@@ -473,6 +486,8 @@ async def test_option_flow(hass):
user_input={
config_flow.CONF_USE_EPISODE_ART: True,
config_flow.CONF_SHOW_ALL_CONTROLS: True,
+ config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
+ config_flow.CONF_MONITORED_USERS: list(mock_plex_server.accounts),
},
)
assert result["type"] == "create_entry"
@@ -480,6 +495,105 @@ async def test_option_flow(hass):
config_flow.MP_DOMAIN: {
config_flow.CONF_USE_EPISODE_ART: True,
config_flow.CONF_SHOW_ALL_CONTROLS: True,
+ config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
+ config_flow.CONF_MONITORED_USERS: {
+ user: {"enabled": True} for user in mock_plex_server.accounts
+ },
+ }
+ }
+
+
+async def test_option_flow_loading_saved_users(hass):
+ """Test config options flow selection when loading existing user config."""
+
+ mock_plex_server = MockPlexServer(load_users=True)
+
+ MOCK_SERVER_ID = MOCK_SERVERS[0][config_flow.CONF_SERVER_IDENTIFIER]
+ hass.data[config_flow.DOMAIN] = {
+ config_flow.SERVERS: {MOCK_SERVER_ID: mock_plex_server}
+ }
+
+ entry = MockConfigEntry(
+ domain=config_flow.DOMAIN,
+ data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVER_ID},
+ options=DEFAULT_OPTIONS,
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id, context={"source": "test"}, data=None
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "plex_mp_settings"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ config_flow.CONF_USE_EPISODE_ART: True,
+ config_flow.CONF_SHOW_ALL_CONTROLS: True,
+ config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
+ config_flow.CONF_MONITORED_USERS: list(mock_plex_server.accounts),
+ },
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ config_flow.MP_DOMAIN: {
+ config_flow.CONF_USE_EPISODE_ART: True,
+ config_flow.CONF_SHOW_ALL_CONTROLS: True,
+ config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
+ config_flow.CONF_MONITORED_USERS: {
+ user: {"enabled": True} for user in mock_plex_server.accounts
+ },
+ }
+ }
+
+
+async def test_option_flow_new_users_available(hass):
+ """Test config options flow selection when new Plex accounts available."""
+
+ mock_plex_server = MockPlexServer(load_users=True, num_users=2)
+
+ MOCK_SERVER_ID = MOCK_SERVERS[0][config_flow.CONF_SERVER_IDENTIFIER]
+ hass.data[config_flow.DOMAIN] = {
+ config_flow.SERVERS: {MOCK_SERVER_ID: mock_plex_server}
+ }
+
+ OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS)
+ OPTIONS_WITH_USERS[config_flow.MP_DOMAIN][config_flow.CONF_MONITORED_USERS] = {
+ "a": {"enabled": True}
+ }
+
+ entry = MockConfigEntry(
+ domain=config_flow.DOMAIN,
+ data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVER_ID},
+ options=OPTIONS_WITH_USERS,
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id, context={"source": "test"}, data=None
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "plex_mp_settings"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ config_flow.CONF_USE_EPISODE_ART: True,
+ config_flow.CONF_SHOW_ALL_CONTROLS: True,
+ config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
+ config_flow.CONF_MONITORED_USERS: list(mock_plex_server.accounts),
+ },
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ config_flow.MP_DOMAIN: {
+ config_flow.CONF_USE_EPISODE_ART: True,
+ config_flow.CONF_SHOW_ALL_CONTROLS: True,
+ config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
+ config_flow.CONF_MONITORED_USERS: {
+ user: {"enabled": True} for user in mock_plex_server.accounts
+ },
}
}
diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py
index 114daa7d2f7..c18476a92a9 100644
--- a/tests/components/radarr/test_sensor.py
+++ b/tests/components/radarr/test_sensor.py
@@ -4,6 +4,7 @@ import unittest
import pytest
import homeassistant.components.radarr.sensor as radarr
+from homeassistant.const import DATA_GIGABYTES
from tests.common import get_test_home_assistant
@@ -218,7 +219,7 @@ class TestRadarrSetup(unittest.TestCase):
"platform": "radarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": [],
"monitored_conditions": ["diskspace"],
}
@@ -227,7 +228,7 @@ class TestRadarrSetup(unittest.TestCase):
device.update()
assert "263.10" == device.state
assert "mdi:harddisk" == device.icon
- assert "GB" == device.unit_of_measurement
+ assert DATA_GIGABYTES == device.unit_of_measurement
assert "Radarr Disk Space" == device.name
assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"]
@@ -238,7 +239,7 @@ class TestRadarrSetup(unittest.TestCase):
"platform": "radarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["diskspace"],
}
@@ -247,7 +248,7 @@ class TestRadarrSetup(unittest.TestCase):
device.update()
assert "263.10" == device.state
assert "mdi:harddisk" == device.icon
- assert "GB" == device.unit_of_measurement
+ assert DATA_GIGABYTES == device.unit_of_measurement
assert "Radarr Disk Space" == device.name
assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"]
@@ -258,7 +259,7 @@ class TestRadarrSetup(unittest.TestCase):
"platform": "radarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["commands"],
}
@@ -278,7 +279,7 @@ class TestRadarrSetup(unittest.TestCase):
"platform": "radarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["movies"],
}
@@ -298,7 +299,7 @@ class TestRadarrSetup(unittest.TestCase):
"platform": "radarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
}
@@ -325,7 +326,7 @@ class TestRadarrSetup(unittest.TestCase):
"platform": "radarr",
"api_key": "foo",
"days": "1",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
}
@@ -348,7 +349,7 @@ class TestRadarrSetup(unittest.TestCase):
"platform": "radarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["status"],
}
@@ -368,7 +369,7 @@ class TestRadarrSetup(unittest.TestCase):
"platform": "radarr",
"api_key": "foo",
"days": "1",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
"ssl": "true",
@@ -393,7 +394,7 @@ class TestRadarrSetup(unittest.TestCase):
"platform": "radarr",
"api_key": "foo",
"days": "1",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
}
diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py
index ae04066651f..a21ef578ca9 100644
--- a/tests/components/recorder/test_init.py
+++ b/tests/components/recorder/test_init.py
@@ -198,7 +198,14 @@ def test_recorder_setup_failure():
):
setup.side_effect = ImportError("driver not found")
rec = Recorder(
- hass, keep_days=7, purge_interval=2, uri="sqlite://", include={}, exclude={}
+ hass,
+ keep_days=7,
+ purge_interval=2,
+ uri="sqlite://",
+ db_max_retries=10,
+ db_retry_wait=3,
+ include={},
+ exclude={},
)
rec.start()
rec.join()
diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py
index 7edbfa065ad..30eeae9a8e3 100644
--- a/tests/components/rest/test_sensor.py
+++ b/tests/components/rest/test_sensor.py
@@ -6,10 +6,12 @@ import pytest
from pytest import raises
import requests
from requests.exceptions import RequestException, Timeout
+from requests.structures import CaseInsensitiveDict
import requests_mock
import homeassistant.components.rest.sensor as rest
import homeassistant.components.sensor as sensor
+from homeassistant.const import DATA_MEGABYTES
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.config_validation import template
from homeassistant.setup import setup_component
@@ -125,7 +127,7 @@ class TestRestSensorSetup(unittest.TestCase):
"method": "GET",
"value_template": "{{ value_json.key }}",
"name": "foo",
- "unit_of_measurement": "MB",
+ "unit_of_measurement": DATA_MEGABYTES,
"verify_ssl": "true",
"timeout": 30,
"authentication": "basic",
@@ -153,7 +155,7 @@ class TestRestSensorSetup(unittest.TestCase):
"value_template": "{{ value_json.key }}",
"payload": '{ "device": "toaster"}',
"name": "foo",
- "unit_of_measurement": "MB",
+ "unit_of_measurement": DATA_MEGABYTES,
"verify_ssl": "true",
"timeout": 30,
"authentication": "basic",
@@ -165,6 +167,33 @@ class TestRestSensorSetup(unittest.TestCase):
)
assert 2 == mock_req.call_count
+ @requests_mock.Mocker()
+ def test_setup_get_xml(self, mock_req):
+ """Test setup with valid configuration."""
+ mock_req.get("http://localhost", status_code=200)
+ with assert_setup_component(1, "sensor"):
+ assert setup_component(
+ self.hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "value_template": "{{ value_json.key }}",
+ "name": "foo",
+ "unit_of_measurement": DATA_MEGABYTES,
+ "verify_ssl": "true",
+ "timeout": 30,
+ "authentication": "basic",
+ "username": "my username",
+ "password": "my password",
+ "headers": {"Accept": "text/xml"},
+ }
+ },
+ )
+ assert 2 == mock_req.call_count
+
class TestRestSensor(unittest.TestCase):
"""Tests for REST sensor platform."""
@@ -177,13 +206,15 @@ class TestRestSensor(unittest.TestCase):
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
- '{ "key": "' + self.initial_state + '" }'
+ '{ "key": "' + self.initial_state + '" }',
+ CaseInsensitiveDict({"Content-Type": "application/json"}),
),
)
self.name = "foo"
- self.unit_of_measurement = "MB"
+ self.unit_of_measurement = DATA_MEGABYTES
self.device_class = None
self.value_template = template("{{ value_json.key }}")
+ self.json_attrs_path = None
self.value_template.hass = self.hass
self.force_update = False
self.resource_template = None
@@ -198,15 +229,17 @@ class TestRestSensor(unittest.TestCase):
[],
self.force_update,
self.resource_template,
+ self.json_attrs_path,
)
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
- def update_side_effect(self, data):
+ def update_side_effect(self, data, headers):
"""Side effect function for mocking RestData.update()."""
self.rest.data = data
+ self.rest.headers = headers
def test_name(self):
"""Test the name."""
@@ -228,7 +261,8 @@ class TestRestSensor(unittest.TestCase):
def test_update_when_value_is_none(self):
"""Test state gets updated to unknown when sensor returns no data."""
self.rest.update = Mock(
- "rest.RestData.update", side_effect=self.update_side_effect(None)
+ "rest.RestData.update",
+ side_effect=self.update_side_effect(None, CaseInsensitiveDict()),
)
self.sensor.update()
assert self.sensor.state is None
@@ -238,7 +272,10 @@ class TestRestSensor(unittest.TestCase):
"""Test state gets updated when sensor returns a new status."""
self.rest.update = Mock(
"rest.RestData.update",
- side_effect=self.update_side_effect('{ "key": "updated_state" }'),
+ side_effect=self.update_side_effect(
+ '{ "key": "updated_state" }',
+ CaseInsensitiveDict({"Content-Type": "application/json"}),
+ ),
)
self.sensor.update()
assert "updated_state" == self.sensor.state
@@ -247,7 +284,10 @@ class TestRestSensor(unittest.TestCase):
def test_update_with_no_template(self):
"""Test update when there is no value template."""
self.rest.update = Mock(
- "rest.RestData.update", side_effect=self.update_side_effect("plain_state")
+ "rest.RestData.update",
+ side_effect=self.update_side_effect(
+ "plain_state", CaseInsensitiveDict({"Content-Type": "application/json"})
+ ),
)
self.sensor = rest.RestSensor(
self.hass,
@@ -259,6 +299,7 @@ class TestRestSensor(unittest.TestCase):
[],
self.force_update,
self.resource_template,
+ self.json_attrs_path,
)
self.sensor.update()
assert "plain_state" == self.sensor.state
@@ -268,7 +309,10 @@ class TestRestSensor(unittest.TestCase):
"""Test attributes get extracted from a JSON result."""
self.rest.update = Mock(
"rest.RestData.update",
- side_effect=self.update_side_effect('{ "key": "some_json_value" }'),
+ side_effect=self.update_side_effect(
+ '{ "key": "some_json_value" }',
+ CaseInsensitiveDict({"Content-Type": "application/json"}),
+ ),
)
self.sensor = rest.RestSensor(
self.hass,
@@ -280,6 +324,7 @@ class TestRestSensor(unittest.TestCase):
["key"],
self.force_update,
self.resource_template,
+ self.json_attrs_path,
)
self.sensor.update()
assert "some_json_value" == self.sensor.device_state_attributes["key"]
@@ -288,7 +333,10 @@ class TestRestSensor(unittest.TestCase):
"""Test attributes get extracted from a JSON list[0] result."""
self.rest.update = Mock(
"rest.RestData.update",
- side_effect=self.update_side_effect('[{ "key": "another_value" }]'),
+ side_effect=self.update_side_effect(
+ '[{ "key": "another_value" }]',
+ CaseInsensitiveDict({"Content-Type": "application/json"}),
+ ),
)
self.sensor = rest.RestSensor(
self.hass,
@@ -300,6 +348,7 @@ class TestRestSensor(unittest.TestCase):
["key"],
self.force_update,
self.resource_template,
+ self.json_attrs_path,
)
self.sensor.update()
assert "another_value" == self.sensor.device_state_attributes["key"]
@@ -308,7 +357,10 @@ class TestRestSensor(unittest.TestCase):
def test_update_with_json_attrs_no_data(self, mock_logger):
"""Test attributes when no JSON result fetched."""
self.rest.update = Mock(
- "rest.RestData.update", side_effect=self.update_side_effect(None)
+ "rest.RestData.update",
+ side_effect=self.update_side_effect(
+ None, CaseInsensitiveDict({"Content-Type": "application/json"})
+ ),
)
self.sensor = rest.RestSensor(
self.hass,
@@ -320,6 +372,7 @@ class TestRestSensor(unittest.TestCase):
["key"],
self.force_update,
self.resource_template,
+ self.json_attrs_path,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
@@ -330,7 +383,10 @@ class TestRestSensor(unittest.TestCase):
"""Test attributes get extracted from a JSON result."""
self.rest.update = Mock(
"rest.RestData.update",
- side_effect=self.update_side_effect('["list", "of", "things"]'),
+ side_effect=self.update_side_effect(
+ '["list", "of", "things"]',
+ CaseInsensitiveDict({"Content-Type": "application/json"}),
+ ),
)
self.sensor = rest.RestSensor(
self.hass,
@@ -342,6 +398,7 @@ class TestRestSensor(unittest.TestCase):
["key"],
self.force_update,
self.resource_template,
+ self.json_attrs_path,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
@@ -352,7 +409,10 @@ class TestRestSensor(unittest.TestCase):
"""Test attributes get extracted from a JSON result."""
self.rest.update = Mock(
"rest.RestData.update",
- side_effect=self.update_side_effect("This is text rather than JSON data."),
+ side_effect=self.update_side_effect(
+ "This is text rather than JSON data.",
+ CaseInsensitiveDict({"Content-Type": "text/plain"}),
+ ),
)
self.sensor = rest.RestSensor(
self.hass,
@@ -364,6 +424,7 @@ class TestRestSensor(unittest.TestCase):
["key"],
self.force_update,
self.resource_template,
+ self.json_attrs_path,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
@@ -375,7 +436,8 @@ class TestRestSensor(unittest.TestCase):
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
- '{ "key": "json_state_updated_value" }'
+ '{ "key": "json_state_updated_value" }',
+ CaseInsensitiveDict({"Content-Type": "application/json"}),
),
)
self.sensor = rest.RestSensor(
@@ -388,6 +450,7 @@ class TestRestSensor(unittest.TestCase):
["key"],
self.force_update,
self.resource_template,
+ self.json_attrs_path,
)
self.sensor.update()
@@ -396,6 +459,136 @@ class TestRestSensor(unittest.TestCase):
"json_state_updated_value" == self.sensor.device_state_attributes["key"]
), self.force_update
+ def test_update_with_json_attrs_with_json_attrs_path(self):
+ """Test attributes get extracted from a JSON result with a template for the attributes."""
+ json_attrs_path = "$.toplevel.second_level"
+ value_template = template("{{ value_json.toplevel.master_value }}")
+ value_template.hass = self.hass
+
+ self.rest.update = Mock(
+ "rest.RestData.update",
+ side_effect=self.update_side_effect(
+ '{ "toplevel": {"master_value": "master", "second_level": {"some_json_key": "some_json_value", "some_json_key2": "some_json_value2" } } }',
+ CaseInsensitiveDict({"Content-Type": "application/json"}),
+ ),
+ )
+ self.sensor = rest.RestSensor(
+ self.hass,
+ self.rest,
+ self.name,
+ self.unit_of_measurement,
+ self.device_class,
+ value_template,
+ ["some_json_key", "some_json_key2"],
+ self.force_update,
+ self.resource_template,
+ json_attrs_path,
+ )
+
+ self.sensor.update()
+ assert "some_json_value" == self.sensor.device_state_attributes["some_json_key"]
+ assert (
+ "some_json_value2" == self.sensor.device_state_attributes["some_json_key2"]
+ )
+ assert "master" == self.sensor.state
+
+ def test_update_with_xml_convert_json_attrs_with_json_attrs_path(self):
+ """Test attributes get extracted from a JSON result that was converted from XML with a template for the attributes."""
+ json_attrs_path = "$.toplevel.second_level"
+ value_template = template("{{ value_json.toplevel.master_value }}")
+ value_template.hass = self.hass
+
+ self.rest.update = Mock(
+ "rest.RestData.update",
+ side_effect=self.update_side_effect(
+ "mastersome_json_valuesome_json_value2",
+ CaseInsensitiveDict({"Content-Type": "text/xml+svg"}),
+ ),
+ )
+ self.sensor = rest.RestSensor(
+ self.hass,
+ self.rest,
+ self.name,
+ self.unit_of_measurement,
+ self.device_class,
+ value_template,
+ ["some_json_key", "some_json_key2"],
+ self.force_update,
+ self.resource_template,
+ json_attrs_path,
+ )
+
+ self.sensor.update()
+ assert "some_json_value" == self.sensor.device_state_attributes["some_json_key"]
+ assert (
+ "some_json_value2" == self.sensor.device_state_attributes["some_json_key2"]
+ )
+ assert "master" == self.sensor.state
+
+ def test_update_with_xml_convert_json_attrs_with_jsonattr_template(self):
+ """Test attributes get extracted from a JSON result that was converted from XML."""
+ json_attrs_path = "$.response"
+ value_template = template("{{ value_json.response.bss.wlan }}")
+ value_template.hass = self.hass
+
+ self.rest.update = Mock(
+ "rest.RestData.update",
+ side_effect=self.update_side_effect(
+ '01255648alexander000bogus000000000upupupup000x0XF0x0XF 0',
+ CaseInsensitiveDict({"Content-Type": "text/xml"}),
+ ),
+ )
+ self.sensor = rest.RestSensor(
+ self.hass,
+ self.rest,
+ self.name,
+ self.unit_of_measurement,
+ self.device_class,
+ value_template,
+ ["led0", "led1", "temp0", "time0", "ver"],
+ self.force_update,
+ self.resource_template,
+ json_attrs_path,
+ )
+
+ self.sensor.update()
+ assert "0" == self.sensor.device_state_attributes["led0"]
+ assert "0" == self.sensor.device_state_attributes["led1"]
+ assert "0x0XF0x0XF" == self.sensor.device_state_attributes["temp0"]
+ assert "0" == self.sensor.device_state_attributes["time0"]
+ assert "12556" == self.sensor.device_state_attributes["ver"]
+ assert "bogus" == self.sensor.state
+
+ @patch("homeassistant.components.rest.sensor._LOGGER")
+ def test_update_with_xml_convert_bad_xml(self, mock_logger):
+ """Test attributes get extracted from a XML result with bad xml."""
+ value_template = template("{{ value_json.toplevel.master_value }}")
+ value_template.hass = self.hass
+
+ self.rest.update = Mock(
+ "rest.RestData.update",
+ side_effect=self.update_side_effect(
+ "this is not xml", CaseInsensitiveDict({"Content-Type": "text/xml"})
+ ),
+ )
+ self.sensor = rest.RestSensor(
+ self.hass,
+ self.rest,
+ self.name,
+ self.unit_of_measurement,
+ self.device_class,
+ value_template,
+ ["key"],
+ self.force_update,
+ self.resource_template,
+ self.json_attrs_path,
+ )
+
+ self.sensor.update()
+ assert {} == self.sensor.device_state_attributes
+ assert mock_logger.warning.called
+ assert mock_logger.debug.called
+
class TestRestData(unittest.TestCase):
"""Tests for RestData."""
diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py
index 18c4f946318..2a67cf5348d 100644
--- a/tests/components/rflink/test_binary_sensor.py
+++ b/tests/components/rflink/test_binary_sensor.py
@@ -130,6 +130,7 @@ async def test_off_delay(hass, monkeypatch):
async_fire_time_changed(hass, future)
event_callback(on_event)
await hass.async_block_till_done()
+ await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test2")
assert state.state == STATE_ON
assert len(events) == 1
@@ -140,6 +141,7 @@ async def test_off_delay(hass, monkeypatch):
async_fire_time_changed(hass, future)
event_callback(on_event)
await hass.async_block_till_done()
+ await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test2")
assert state.state == STATE_ON
assert len(events) == 2
@@ -149,6 +151,7 @@ async def test_off_delay(hass, monkeypatch):
with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
+ await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test2")
assert state.state == STATE_ON
assert len(events) == 2
@@ -158,6 +161,7 @@ async def test_off_delay(hass, monkeypatch):
with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
+ await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test2")
assert state.state == STATE_OFF
assert len(events) == 3
diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py
index dc286502068..e10cdc20143 100644
--- a/tests/components/rflink/test_cover.py
+++ b/tests/components/rflink/test_cover.py
@@ -144,6 +144,7 @@ async def test_firing_bus_event(hass, monkeypatch):
# test event for new unconfigured sensor
event_callback({"id": "protocol_0_0", "command": "down"})
await hass.async_block_till_done()
+ await hass.async_block_till_done()
assert calls[0].data == {"state": "down", "entity_id": DOMAIN + ".test"}
diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py
index 970c532f22e..87696191ac8 100644
--- a/tests/components/rflink/test_light.py
+++ b/tests/components/rflink/test_light.py
@@ -184,6 +184,7 @@ async def test_firing_bus_event(hass, monkeypatch):
# test event for new unconfigured sensor
event_callback({"id": "protocol_0_0", "command": "off"})
await hass.async_block_till_done()
+ await hass.async_block_till_done()
assert calls[0].data == {"state": "off", "entity_id": DOMAIN + ".test"}
@@ -298,18 +299,16 @@ async def test_signal_repetitions_cancelling(hass, monkeypatch):
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test"}
)
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DOMAIN + ".test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DOMAIN + ".test"}, blocking=True
)
- await hass.async_block_till_done()
-
- 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"
+ assert [call[0][1] for call in protocol.send_command_ack.call_args_list] == [
+ "off",
+ "on",
+ "on",
+ "on",
+ ]
async def test_type_toggle(hass, monkeypatch):
diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py
index d1fced33208..bcade409d3e 100644
--- a/tests/components/rflink/test_switch.py
+++ b/tests/components/rflink/test_switch.py
@@ -214,6 +214,7 @@ async def test_device_defaults(hass, monkeypatch):
# test event for new unconfigured sensor
event_callback({"id": "protocol_0_0", "command": "off"})
await hass.async_block_till_done()
+ await hass.async_block_till_done()
assert calls[0].data == {"state": "off", "entity_id": DOMAIN + ".test"}
diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py
index 809c71562c0..39d2c63ffdd 100644
--- a/tests/components/ring/test_init.py
+++ b/tests/components/ring/test_init.py
@@ -1,12 +1,10 @@
"""The tests for the Ring component."""
from asyncio import run_coroutine_threadsafe
-from copy import deepcopy
from datetime import timedelta
import unittest
import requests_mock
-from homeassistant import setup
import homeassistant.components.ring as ring
from tests.common import get_test_home_assistant, load_fixture
@@ -57,25 +55,3 @@ class TestRing(unittest.TestCase):
).result()
assert response
-
- @requests_mock.Mocker()
- def test_setup_component_no_login(self, mock):
- """Test the setup when no login is configured."""
- mock.post(
- "https://api.ring.com/clients_api/session",
- text=load_fixture("ring_session.json"),
- )
- conf = deepcopy(VALID_CONFIG)
- del conf["ring"]["username"]
- assert not setup.setup_component(self.hass, ring.DOMAIN, conf)
-
- @requests_mock.Mocker()
- def test_setup_component_no_pwd(self, mock):
- """Test the setup when no password is configured."""
- mock.post(
- "https://api.ring.com/clients_api/session",
- text=load_fixture("ring_session.json"),
- )
- conf = deepcopy(VALID_CONFIG)
- del conf["ring"]["password"]
- assert not setup.setup_component(self.hass, ring.DOMAIN, conf)
diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py
index 6cc727b1a1c..5a2687e4cf9 100644
--- a/tests/components/ring/test_light.py
+++ b/tests/components/ring/test_light.py
@@ -7,7 +7,7 @@ from tests.common import load_fixture
async def test_entity_registry(hass, requests_mock):
- """Tests that the devices are registed in the entity registry."""
+ """Tests that the devices are registered in the entity registry."""
await setup_platform(hass, LIGHT_DOMAIN)
entity_registry = await hass.helpers.entity_registry.async_get_registry()
diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py
index e2a86014f1c..6979fafc01d 100644
--- a/tests/components/ring/test_switch.py
+++ b/tests/components/ring/test_switch.py
@@ -7,7 +7,7 @@ from tests.common import load_fixture
async def test_entity_registry(hass, requests_mock):
- """Tests that the devices are registed in the entity registry."""
+ """Tests that the devices are registered in the entity registry."""
await setup_platform(hass, SWITCH_DOMAIN)
entity_registry = await hass.helpers.entity_registry.async_get_registry()
diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py
index 9c8ec3a9a09..91ee8a7205f 100644
--- a/tests/components/samsungtv/test_config_flow.py
+++ b/tests/components/samsungtv/test_config_flow.py
@@ -4,6 +4,7 @@ from unittest.mock import call, patch
from asynctest import mock
import pytest
from samsungctl.exceptions import AccessDenied, UnhandledResponse
+from websocket import WebSocketProtocolException
from homeassistant.components.samsungtv.const import (
CONF_MANUFACTURER,
@@ -42,7 +43,7 @@ AUTODETECT_WEBSOCKET = {
"method": "websocket",
"port": None,
"host": "fake_host",
- "timeout": 31,
+ "timeout": 1,
}
AUTODETECT_LEGACY = {
"name": "HomeAssistant",
@@ -245,6 +246,28 @@ async def test_ssdp_not_supported(hass):
assert result["reason"] == "not_supported"
+async def test_ssdp_not_supported_2(hass):
+ """Test starting a flow from discovery for not supported device."""
+ with patch(
+ "homeassistant.components.samsungtv.config_flow.Remote",
+ side_effect=WebSocketProtocolException("Boom"),
+ ), patch("homeassistant.components.samsungtv.config_flow.socket"):
+
+ # confirm to add the entry
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+
+ # device not supported
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input="whatever"
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "not_supported"
+
+
async def test_ssdp_not_successful(hass):
"""Test starting a flow from discovery but no device found."""
with patch(
diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py
index 9d64f5298f4..dbaa5e6e117 100644
--- a/tests/components/script/test_init.py
+++ b/tests/components/script/test_init.py
@@ -446,3 +446,24 @@ async def test_extraction_functions(hass):
"device-in-both",
"device-in-last",
}
+
+
+async def test_config(hass):
+ """Test passing info in config."""
+ assert await async_setup_component(
+ hass,
+ "script",
+ {
+ "script": {
+ "test_script": {
+ "alias": "Script Name",
+ "icon": "mdi:party",
+ "sequence": [],
+ }
+ }
+ },
+ )
+
+ test_script = hass.states.get("script.test_script")
+ assert test_script.name == "Script Name"
+ assert test_script.attributes["icon"] == "mdi:party"
diff --git a/tests/components/signal_messenger/__init__.py b/tests/components/signal_messenger/__init__.py
new file mode 100644
index 00000000000..e3b556f6c18
--- /dev/null
+++ b/tests/components/signal_messenger/__init__.py
@@ -0,0 +1 @@
+"""Tests for the signal_messenger component."""
diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py
new file mode 100644
index 00000000000..dbfd19795e8
--- /dev/null
+++ b/tests/components/signal_messenger/test_notify.py
@@ -0,0 +1,122 @@
+"""The tests for the signal_messenger platform."""
+
+import os
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from pysignalclirestapi import SignalCliRestApi
+import requests_mock
+
+import homeassistant.components.signal_messenger.notify as signalmessenger
+from homeassistant.setup import async_setup_component
+
+BASE_COMPONENT = "notify"
+
+
+async def test_signal_messenger_init(hass):
+ """Test that service loads successfully."""
+
+ config = {
+ BASE_COMPONENT: {
+ "name": "test",
+ "platform": "signal_messenger",
+ "url": "http://127.0.0.1:8080",
+ "number": "+43443434343",
+ "recipients": ["+435565656565"],
+ }
+ }
+
+ with patch("pysignalclirestapi.SignalCliRestApi.send_message", return_value=None):
+ assert await async_setup_component(hass, BASE_COMPONENT, config)
+ await hass.async_block_till_done()
+
+ # Test that service loads successfully
+ assert hass.services.has_service(BASE_COMPONENT, "test")
+
+
+class TestSignalMesssenger(unittest.TestCase):
+ """Test the signal_messenger notify."""
+
+ def setUp(self):
+ """Set up things to be run when tests are started."""
+ recipients = ["+435565656565"]
+ number = "+43443434343"
+ client = SignalCliRestApi("http://127.0.0.1:8080", number)
+ self._signalmessenger = signalmessenger.SignalNotificationService(
+ recipients, client
+ )
+
+ @requests_mock.Mocker()
+ def test_send_message(self, mock):
+ """Test send message."""
+ message = "Testing Signal Messenger platform :)"
+ mock.register_uri(
+ "POST", "http://127.0.0.1:8080/v2/send", status_code=201,
+ )
+ mock.register_uri(
+ "GET",
+ "http://127.0.0.1:8080/v1/about",
+ status_code=200,
+ json={"versions": ["v1", "v2"]},
+ )
+ with self.assertLogs(
+ "homeassistant.components.signal_messenger.notify", level="DEBUG"
+ ) as context:
+ self._signalmessenger.send_message(message)
+ self.assertIn("Sending signal message", context.output[0])
+ self.assertTrue(mock.called)
+ self.assertEqual(mock.call_count, 2)
+
+ @requests_mock.Mocker()
+ def test_send_message_should_show_deprecation_warning(self, mock):
+ """Test send message."""
+ message = "Testing Signal Messenger platform with attachment :)"
+ mock.register_uri(
+ "POST", "http://127.0.0.1:8080/v2/send", status_code=201,
+ )
+ mock.register_uri(
+ "GET",
+ "http://127.0.0.1:8080/v1/about",
+ status_code=200,
+ json={"versions": ["v1", "v2"]},
+ )
+ with self.assertLogs(
+ "homeassistant.components.signal_messenger.notify", level="WARNING"
+ ) as context:
+ with tempfile.NamedTemporaryFile(
+ suffix=".png", prefix=os.path.basename(__file__)
+ ) as tf:
+ data = {"data": {"attachment": tf.name}}
+ self._signalmessenger.send_message(message, **data)
+ self.assertIn(
+ "The 'attachment' option is deprecated, please replace it with 'attachments'. This option will become invalid in version 0.108.",
+ context.output[0],
+ )
+ self.assertTrue(mock.called)
+ self.assertEqual(mock.call_count, 2)
+
+ @requests_mock.Mocker()
+ def test_send_message_with_attachment(self, mock):
+ """Test send message."""
+ message = "Testing Signal Messenger platform :)"
+ mock.register_uri(
+ "POST", "http://127.0.0.1:8080/v2/send", status_code=201,
+ )
+ mock.register_uri(
+ "GET",
+ "http://127.0.0.1:8080/v1/about",
+ status_code=200,
+ json={"versions": ["v1", "v2"]},
+ )
+ with self.assertLogs(
+ "homeassistant.components.signal_messenger.notify", level="DEBUG"
+ ) as context:
+ with tempfile.NamedTemporaryFile(
+ suffix=".png", prefix=os.path.basename(__file__)
+ ) as tf:
+ data = {"data": {"attachments": [tf.name]}}
+ self._signalmessenger.send_message(message, **data)
+ self.assertIn("Sending signal message", context.output[0])
+ self.assertTrue(mock.called)
+ self.assertEqual(mock.call_count, 2)
diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py
index 92557f9d543..952e82c01be 100644
--- a/tests/components/smhi/test_weather.py
+++ b/tests/components/smhi/test_weather.py
@@ -75,7 +75,7 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None:
async def test_setup_plattform(hass):
- """Test that setup plattform does nothing."""
+ """Test that setup platform does nothing."""
assert await weather_smhi.async_setup_platform(hass, None, None) is None
diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py
index 46f40dd80ef..759639362e4 100644
--- a/tests/components/solaredge/test_config_flow.py
+++ b/tests/components/solaredge/test_config_flow.py
@@ -18,7 +18,7 @@ API_KEY = "a1b2c3d4e5f6g7h8"
@pytest.fixture(name="test_api")
def mock_controller():
- """Mock a successfull Solaredge API."""
+ """Mock a successful Solaredge API."""
api = Mock()
api.get_details.return_value = {"details": {"status": "active"}}
with patch("solaredge.Solaredge", return_value=api):
diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py
index cd05cf13185..7828290560a 100644
--- a/tests/components/solarlog/test_config_flow.py
+++ b/tests/components/solarlog/test_config_flow.py
@@ -46,7 +46,7 @@ async def test_form(hass):
@pytest.fixture(name="test_connect")
def mock_controller():
- """Mock a successfull _host_in_configuration_exists."""
+ """Mock a successful _host_in_configuration_exists."""
with patch(
"homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection",
side_effect=lambda *_: mock_coro(True),
diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py
index 38382dc70ab..300d201079b 100644
--- a/tests/components/sonarr/test_sensor.py
+++ b/tests/components/sonarr/test_sensor.py
@@ -6,6 +6,7 @@ import unittest
import pytest
import homeassistant.components.sonarr.sensor as sonarr
+from homeassistant.const import DATA_GIGABYTES
from tests.common import get_test_home_assistant
@@ -497,7 +498,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": [],
"monitored_conditions": ["diskspace"],
}
@@ -506,7 +507,7 @@ class TestSonarrSetup(unittest.TestCase):
device.update()
assert "263.10" == device.state
assert "mdi:harddisk" == device.icon
- assert "GB" == device.unit_of_measurement
+ assert DATA_GIGABYTES == device.unit_of_measurement
assert "Sonarr Disk Space" == device.name
assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"]
@@ -517,7 +518,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["diskspace"],
}
@@ -526,7 +527,7 @@ class TestSonarrSetup(unittest.TestCase):
device.update()
assert "263.10" == device.state
assert "mdi:harddisk" == device.icon
- assert "GB" == device.unit_of_measurement
+ assert DATA_GIGABYTES == device.unit_of_measurement
assert "Sonarr Disk Space" == device.name
assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"]
@@ -537,7 +538,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["commands"],
}
@@ -557,7 +558,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["queue"],
}
@@ -577,7 +578,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["series"],
}
@@ -599,7 +600,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["wanted"],
}
@@ -621,7 +622,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
}
@@ -645,7 +646,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "1",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
}
@@ -665,7 +666,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "2",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["status"],
}
@@ -685,7 +686,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "1",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
"ssl": "true",
@@ -707,7 +708,7 @@ class TestSonarrSetup(unittest.TestCase):
"platform": "sonarr",
"api_key": "foo",
"days": "1",
- "unit": "GB",
+ "unit": DATA_GIGABYTES,
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
}
diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py
index 8789db1ca1f..b18f9efda97 100644
--- a/tests/components/soundtouch/test_media_player.py
+++ b/tests/components/soundtouch/test_media_player.py
@@ -1,34 +1,121 @@
"""Test the Soundtouch component."""
-import logging
-import unittest
-from unittest import mock
+from unittest.mock import call
-from libsoundtouch.device import Config, Preset, SoundTouchDevice as STD, Status, Volume
+from asynctest import patch
+from libsoundtouch.device import (
+ Config,
+ Preset,
+ SoundTouchDevice as STD,
+ Status,
+ Volume,
+ ZoneSlave,
+ ZoneStatus,
+)
+import pytest
+from homeassistant.components.media_player.const import (
+ ATTR_MEDIA_CONTENT_ID,
+ ATTR_MEDIA_CONTENT_TYPE,
+)
from homeassistant.components.soundtouch import media_player as soundtouch
+from homeassistant.components.soundtouch.const import DOMAIN
+from homeassistant.components.soundtouch.media_player import DATA_SOUNDTOUCH
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
+from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.setup import async_setup_component
-from tests.common import get_test_home_assistant
+# pylint: disable=super-init-not-called
-class MockService:
- """Mock Soundtouch service."""
-
- def __init__(self, master, slaves):
- """Create a new service."""
- self.data = {"master": master, "slaves": slaves}
+DEVICE_1_IP = "192.168.0.1"
+DEVICE_2_IP = "192.168.0.2"
-def _mock_soundtouch_device(*args, **kwargs):
- return MockDevice()
+def get_config(host=DEVICE_1_IP, port=8090, name="soundtouch"):
+ """Return a default component."""
+ return {"platform": DOMAIN, "host": host, "port": port, "name": name}
+
+
+DEVICE_1_CONFIG = {**get_config(), "name": "soundtouch_1"}
+DEVICE_2_CONFIG = {**get_config(), "host": DEVICE_2_IP, "name": "soundtouch_2"}
+
+
+@pytest.fixture(name="one_device")
+def one_device_fixture():
+ """Mock one master device."""
+ device_1 = MockDevice()
+ device_patch = patch(
+ "homeassistant.components.soundtouch.media_player.soundtouch_device",
+ return_value=device_1,
+ )
+ with device_patch as device:
+ yield device
+
+
+@pytest.fixture(name="two_zones")
+def two_zones_fixture():
+ """Mock one master and one slave."""
+ device_1 = MockDevice(
+ MockZoneStatus(
+ is_master=True,
+ master_id=1,
+ master_ip=DEVICE_1_IP,
+ slaves=[MockZoneSlave(DEVICE_2_IP)],
+ )
+ )
+ device_2 = MockDevice(
+ MockZoneStatus(
+ is_master=False,
+ master_id=1,
+ master_ip=DEVICE_1_IP,
+ slaves=[MockZoneSlave(DEVICE_2_IP)],
+ )
+ )
+ devices = {DEVICE_1_IP: device_1, DEVICE_2_IP: device_2}
+ device_patch = patch(
+ "homeassistant.components.soundtouch.media_player.soundtouch_device",
+ side_effect=lambda host, _: devices[host],
+ )
+ with device_patch as device:
+ yield device
+
+
+@pytest.fixture(name="mocked_status")
+def status_fixture():
+ """Mock the device status."""
+ status_patch = patch(
+ "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPlaying
+ )
+ with status_patch as status:
+ yield status
+
+
+@pytest.fixture(name="mocked_volume")
+def volume_fixture():
+ """Mock the device volume."""
+ volume_patch = patch("libsoundtouch.device.SoundTouchDevice.volume")
+ with volume_patch as volume:
+ yield volume
+
+
+async def setup_soundtouch(hass, config):
+ """Set up soundtouch integration."""
+ assert await async_setup_component(hass, "media_player", {"media_player": config})
+ await hass.async_block_till_done()
+ await hass.async_start()
class MockDevice(STD):
"""Mock device."""
- def __init__(self):
+ def __init__(self, zone_status=None):
"""Init the class."""
- self._config = MockConfig
+ self._config = MockConfig()
+ self._zone_status = zone_status or MockZoneStatus()
+
+ def zone_status(self, refresh=True):
+ """Zone status mock object."""
+ return self._zone_status
class MockConfig(Config):
@@ -39,6 +126,26 @@ class MockConfig(Config):
self._name = "name"
+class MockZoneStatus(ZoneStatus):
+ """Mock zone status."""
+
+ def __init__(self, is_master=True, master_id=None, master_ip=None, slaves=None):
+ """Init the class."""
+ self._is_master = is_master
+ self._master_id = master_id
+ self._master_ip = master_ip
+ self._slaves = slaves or []
+
+
+class MockZoneSlave(ZoneSlave):
+ """Mock zone slave."""
+
+ def __init__(self, device_ip=None, role=None):
+ """Init the class."""
+ self._ip = device_ip
+ self._role = role
+
+
def _mocked_presets(*args, **kwargs):
"""Return a list of mocked presets."""
return [MockPreset("1")]
@@ -59,6 +166,7 @@ class MockVolume(Volume):
def __init__(self):
"""Init class."""
self._actual = 12
+ self._muted = False
class MockVolumeMuted(Volume):
@@ -130,697 +238,623 @@ class MockStatusPause(Status):
"""Init the class."""
self._source = ""
self._play_status = "PAUSE_STATE"
+ self._image = "image.url"
+ self._artist = None
+ self._track = None
+ self._album = None
+ self._duration = None
+ self._station_name = None
-def default_component():
- """Return a default component."""
- return {"host": "192.168.0.1", "port": 8090, "name": "soundtouch"}
-
-
-class TestSoundtouchMediaPlayer(unittest.TestCase):
- """Bose Soundtouch test class."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- logging.disable(logging.CRITICAL)
-
- def tearDown(self): # pylint: disable=invalid-name
- """Stop everything that was started."""
- logging.disable(logging.NOTSET)
- self.hass.stop()
-
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=None,
+async def test_ensure_setup_config(mocked_status, mocked_volume, hass, one_device):
+ """Test setup OK with custom config."""
+ await setup_soundtouch(
+ hass, get_config(host="192.168.1.44", port=8888, name="custom_sound")
)
- def test_ensure_setup_config(self, mocked_soundtouch_device):
- """Test setup OK with custom config."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert len(all_devices) == 1
- assert all_devices[0].name == "soundtouch"
- assert all_devices[0].config["port"] == 8090
- assert mocked_soundtouch_device.call_count == 1
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=None,
+ assert one_device.call_count == 1
+ assert one_device.call_args == call("192.168.1.44", 8888)
+ assert len(hass.states.async_all()) == 1
+ state = hass.states.get("media_player.custom_sound")
+ assert state.name == "custom_sound"
+
+
+async def test_ensure_setup_discovery(mocked_status, mocked_volume, hass, one_device):
+ """Test setup with discovery."""
+ new_device = {
+ "port": "8090",
+ "host": "192.168.1.1",
+ "properties": {},
+ "hostname": "hostname.local",
+ }
+ await async_load_platform(
+ hass, "media_player", DOMAIN, new_device, {"media_player": {}}
)
- def test_ensure_setup_discovery(self, mocked_soundtouch_device):
- """Test setup with discovery."""
- new_device = {
- "port": "8090",
- "host": "192.168.1.1",
- "properties": {},
- "hostname": "hostname.local",
- }
- soundtouch.setup_platform(self.hass, None, mock.MagicMock(), new_device)
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert len(all_devices) == 1
- assert all_devices[0].config["port"] == 8090
- assert all_devices[0].config["host"] == "192.168.1.1"
- assert mocked_soundtouch_device.call_count == 1
+ await hass.async_block_till_done()
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=None,
+ assert one_device.call_count == 1
+ assert one_device.call_args == call("192.168.1.1", 8090)
+ assert len(hass.states.async_all()) == 1
+
+
+async def test_ensure_setup_discovery_no_duplicate(
+ mocked_status, mocked_volume, hass, one_device
+):
+ """Test setup OK if device already exists."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert len(hass.states.async_all()) == 1
+
+ new_device = {
+ "port": "8090",
+ "host": "192.168.1.1",
+ "properties": {},
+ "hostname": "hostname.local",
+ }
+ await async_load_platform(
+ hass, "media_player", DOMAIN, new_device, {"media_player": DEVICE_1_CONFIG}
)
- def test_ensure_setup_discovery_no_duplicate(self, mocked_soundtouch_device):
- """Test setup OK if device already exists."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]) == 1
- new_device = {
- "port": "8090",
- "host": "192.168.1.1",
- "properties": {},
- "hostname": "hostname.local",
- }
- soundtouch.setup_platform(
- self.hass, None, mock.MagicMock(), new_device # New device
- )
- assert len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]) == 2
- existing_device = {
- "port": "8090",
- "host": "192.168.0.1",
- "properties": {},
- "hostname": "hostname.local",
- }
- soundtouch.setup_platform(
- self.hass, None, mock.MagicMock(), existing_device # Existing device
- )
- assert mocked_soundtouch_device.call_count == 2
- assert len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]) == 2
+ await hass.async_block_till_done()
+ assert one_device.call_count == 2
+ assert len(hass.states.async_all()) == 2
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ existing_device = {
+ "port": "8090",
+ "host": "192.168.0.1",
+ "properties": {},
+ "hostname": "hostname.local",
+ }
+ await async_load_platform(
+ hass, "media_player", DOMAIN, existing_device, {"media_player": DEVICE_1_CONFIG}
)
- def test_update(self, mocked_soundtouch_device, mocked_status, mocked_volume):
- """Test update device state."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- self.hass.data[soundtouch.DATA_SOUNDTOUCH][0].update()
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 2
+ await hass.async_block_till_done()
+ assert one_device.call_count == 2
+ assert len(hass.states.async_all()) == 2
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch(
- "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPlaying
+
+async def test_playing_media(mocked_status, mocked_volume, hass, one_device):
+ """Test playing media info."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ entity_1_state = hass.states.get("media_player.soundtouch_1")
+ assert entity_1_state.state == STATE_PLAYING
+ assert entity_1_state.attributes["media_title"] == "artist - track"
+ assert entity_1_state.attributes["media_track"] == "track"
+ assert entity_1_state.attributes["media_artist"] == "artist"
+ assert entity_1_state.attributes["media_album_name"] == "album"
+ assert entity_1_state.attributes["media_duration"] == 1
+
+
+async def test_playing_unknown_media(mocked_status, mocked_volume, hass, one_device):
+ """Test playing media info."""
+ mocked_status.side_effect = MockStatusUnknown
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ entity_1_state = hass.states.get("media_player.soundtouch_1")
+ assert entity_1_state.state == STATE_PLAYING
+
+
+async def test_playing_radio(mocked_status, mocked_volume, hass, one_device):
+ """Test playing radio info."""
+ mocked_status.side_effect = MockStatusPlayingRadio
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ entity_1_state = hass.states.get("media_player.soundtouch_1")
+ assert entity_1_state.state == STATE_PLAYING
+ assert entity_1_state.attributes["media_title"] == "station"
+
+
+async def test_get_volume_level(mocked_status, mocked_volume, hass, one_device):
+ """Test volume level."""
+ mocked_volume.side_effect = MockVolume
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ entity_1_state = hass.states.get("media_player.soundtouch_1")
+ assert entity_1_state.attributes["volume_level"] == 0.12
+
+
+async def test_get_state_off(mocked_status, mocked_volume, hass, one_device):
+ """Test state device is off."""
+ mocked_status.side_effect = MockStatusStandby
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ entity_1_state = hass.states.get("media_player.soundtouch_1")
+ assert entity_1_state.state == STATE_OFF
+
+
+async def test_get_state_pause(mocked_status, mocked_volume, hass, one_device):
+ """Test state device is paused."""
+ mocked_status.side_effect = MockStatusPause
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ entity_1_state = hass.states.get("media_player.soundtouch_1")
+ assert entity_1_state.state == STATE_PAUSED
+
+
+async def test_is_muted(mocked_status, mocked_volume, hass, one_device):
+ """Test device volume is muted."""
+ mocked_volume.side_effect = MockVolumeMuted
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ entity_1_state = hass.states.get("media_player.soundtouch_1")
+ assert entity_1_state.attributes["is_volume_muted"]
+
+
+async def test_media_commands(mocked_status, mocked_volume, hass, one_device):
+ """Test supported media commands."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ entity_1_state = hass.states.get("media_player.soundtouch_1")
+ assert entity_1_state.attributes["supported_features"] == 18365
+
+
+@patch("libsoundtouch.device.SoundTouchDevice.power_off")
+async def test_should_turn_off(
+ mocked_power_off, mocked_status, mocked_volume, hass, one_device
+):
+ """Test device is turned off."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player", "turn_off", {"entity_id": "media_player.soundtouch_1"}, True,
)
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ assert mocked_status.call_count == 2
+ assert mocked_power_off.call_count == 1
+
+
+@patch("libsoundtouch.device.SoundTouchDevice.power_on")
+async def test_should_turn_on(
+ mocked_power_on, mocked_status, mocked_volume, hass, one_device
+):
+ """Test device is turned on."""
+ mocked_status.side_effect = MockStatusStandby
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player", "turn_on", {"entity_id": "media_player.soundtouch_1"}, True,
)
- def test_playing_media(
- self, mocked_soundtouch_device, mocked_status, mocked_volume
- ):
- """Test playing media info."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert all_devices[0].state == STATE_PLAYING
- assert all_devices[0].media_image_url == "image.url"
- assert all_devices[0].media_title == "artist - track"
- assert all_devices[0].media_track == "track"
- assert all_devices[0].media_artist == "artist"
- assert all_devices[0].media_album_name == "album"
- assert all_devices[0].media_duration == 1
+ assert mocked_status.call_count == 2
+ assert mocked_power_on.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch(
- "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusUnknown
+
+@patch("libsoundtouch.device.SoundTouchDevice.volume_up")
+async def test_volume_up(
+ mocked_volume_up, mocked_status, mocked_volume, hass, one_device
+):
+ """Test volume up."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player", "volume_up", {"entity_id": "media_player.soundtouch_1"}, True,
)
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ assert mocked_volume.call_count == 2
+ assert mocked_volume_up.call_count == 1
+
+
+@patch("libsoundtouch.device.SoundTouchDevice.volume_down")
+async def test_volume_down(
+ mocked_volume_down, mocked_status, mocked_volume, hass, one_device
+):
+ """Test volume down."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player", "volume_down", {"entity_id": "media_player.soundtouch_1"}, True,
)
- def test_playing_unknown_media(
- self, mocked_soundtouch_device, mocked_status, mocked_volume
- ):
- """Test playing media info."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert all_devices[0].media_title is None
+ assert mocked_volume.call_count == 2
+ assert mocked_volume_down.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch(
- "libsoundtouch.device.SoundTouchDevice.status",
- side_effect=MockStatusPlayingRadio,
+
+@patch("libsoundtouch.device.SoundTouchDevice.set_volume")
+async def test_set_volume_level(
+ mocked_set_volume, mocked_status, mocked_volume, hass, one_device
+):
+ """Test set volume level."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player",
+ "volume_set",
+ {"entity_id": "media_player.soundtouch_1", "volume_level": 0.17},
+ True,
)
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ assert mocked_volume.call_count == 2
+ mocked_set_volume.assert_called_with(17)
+
+
+@patch("libsoundtouch.device.SoundTouchDevice.mute")
+async def test_mute(mocked_mute, mocked_status, mocked_volume, hass, one_device):
+ """Test mute volume."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player",
+ "volume_mute",
+ {"entity_id": "media_player.soundtouch_1", "is_volume_muted": True},
+ True,
)
- def test_playing_radio(
- self, mocked_soundtouch_device, mocked_status, mocked_volume
- ):
- """Test playing radio info."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert all_devices[0].state == STATE_PLAYING
- assert all_devices[0].media_image_url == "image.url"
- assert all_devices[0].media_title == "station"
- assert all_devices[0].media_track is None
- assert all_devices[0].media_artist is None
- assert all_devices[0].media_album_name is None
- assert all_devices[0].media_duration is None
+ assert mocked_volume.call_count == 2
+ assert mocked_mute.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume", side_effect=MockVolume)
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+
+@patch("libsoundtouch.device.SoundTouchDevice.play")
+async def test_play(mocked_play, mocked_status, mocked_volume, hass, one_device):
+ """Test play command."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player", "media_play", {"entity_id": "media_player.soundtouch_1"}, True,
)
- def test_get_volume_level(
- self, mocked_soundtouch_device, mocked_status, mocked_volume
- ):
- """Test volume level."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert all_devices[0].volume_level == 0.12
+ assert mocked_status.call_count == 2
+ assert mocked_play.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch(
- "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusStandby
+
+@patch("libsoundtouch.device.SoundTouchDevice.pause")
+async def test_pause(mocked_pause, mocked_status, mocked_volume, hass, one_device):
+ """Test pause command."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player", "media_pause", {"entity_id": "media_player.soundtouch_1"}, True,
)
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ assert mocked_status.call_count == 2
+ assert mocked_pause.call_count == 1
+
+
+@patch("libsoundtouch.device.SoundTouchDevice.play_pause")
+async def test_play_pause(
+ mocked_play_pause, mocked_status, mocked_volume, hass, one_device
+):
+ """Test play/pause."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player",
+ "media_play_pause",
+ {"entity_id": "media_player.soundtouch_1"},
+ True,
)
- def test_get_state_off(
- self, mocked_soundtouch_device, mocked_status, mocked_volume
- ):
- """Test state device is off."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert all_devices[0].state == STATE_OFF
+ assert mocked_status.call_count == 2
+ assert mocked_play_pause.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch(
- "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPause
+
+@patch("libsoundtouch.device.SoundTouchDevice.previous_track")
+@patch("libsoundtouch.device.SoundTouchDevice.next_track")
+async def test_next_previous_track(
+ mocked_next_track,
+ mocked_previous_track,
+ mocked_status,
+ mocked_volume,
+ hass,
+ one_device,
+):
+ """Test next/previous track."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player",
+ "media_next_track",
+ {"entity_id": "media_player.soundtouch_1"},
+ True,
)
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ assert mocked_status.call_count == 2
+ assert mocked_next_track.call_count == 1
+
+ await hass.services.async_call(
+ "media_player",
+ "media_previous_track",
+ {"entity_id": "media_player.soundtouch_1"},
+ True,
)
- def test_get_state_pause(
- self, mocked_soundtouch_device, mocked_status, mocked_volume
- ):
- """Test state device is paused."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert all_devices[0].state == STATE_PAUSED
+ assert mocked_status.call_count == 3
+ assert mocked_previous_track.call_count == 1
- @mock.patch(
- "libsoundtouch.device.SoundTouchDevice.volume", side_effect=MockVolumeMuted
+
+@patch("libsoundtouch.device.SoundTouchDevice.select_preset")
+@patch("libsoundtouch.device.SoundTouchDevice.presets", side_effect=_mocked_presets)
+async def test_play_media(
+ mocked_presets, mocked_select_preset, mocked_status, mocked_volume, hass, one_device
+):
+ """Test play preset 1."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player",
+ "play_media",
+ {
+ "entity_id": "media_player.soundtouch_1",
+ ATTR_MEDIA_CONTENT_TYPE: "PLAYLIST",
+ ATTR_MEDIA_CONTENT_ID: 1,
+ },
+ True,
)
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ assert mocked_presets.call_count == 1
+ assert mocked_select_preset.call_count == 1
+
+ await hass.services.async_call(
+ "media_player",
+ "play_media",
+ {
+ "entity_id": "media_player.soundtouch_1",
+ ATTR_MEDIA_CONTENT_TYPE: "PLAYLIST",
+ ATTR_MEDIA_CONTENT_ID: 2,
+ },
+ True,
)
- def test_is_muted(self, mocked_soundtouch_device, mocked_status, mocked_volume):
- """Test device volume is muted."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert all_devices[0].is_volume_muted is True
+ assert mocked_presets.call_count == 2
+ assert mocked_select_preset.call_count == 1
- @mock.patch("homeassistant.components.soundtouch.media_player.soundtouch_device")
- def test_media_commands(self, mocked_soundtouch_device):
- """Test supported media commands."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- assert mocked_soundtouch_device.call_count == 1
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert all_devices[0].supported_features == 18365
- @mock.patch("libsoundtouch.device.SoundTouchDevice.power_off")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+@patch("libsoundtouch.device.SoundTouchDevice.play_url")
+async def test_play_media_url(
+ mocked_play_url, mocked_status, mocked_volume, hass, one_device
+):
+ """Test play preset 1."""
+ await setup_soundtouch(hass, DEVICE_1_CONFIG)
+
+ assert one_device.call_count == 1
+ assert mocked_status.call_count == 1
+ assert mocked_volume.call_count == 1
+
+ await hass.services.async_call(
+ "media_player",
+ "play_media",
+ {
+ "entity_id": "media_player.soundtouch_1",
+ ATTR_MEDIA_CONTENT_TYPE: "MUSIC",
+ ATTR_MEDIA_CONTENT_ID: "http://fqdn/file.mp3",
+ },
+ True,
)
- def test_should_turn_off(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_off
- ):
- """Test device is turned off."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].turn_off()
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 1
- assert mocked_power_off.call_count == 1
+ mocked_play_url.assert_called_with("http://fqdn/file.mp3")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.power_on")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+
+@patch("libsoundtouch.device.SoundTouchDevice.create_zone")
+async def test_play_everywhere(
+ mocked_create_zone, mocked_status, mocked_volume, hass, two_zones
+):
+ """Test play everywhere."""
+ mocked_device = two_zones
+ await setup_soundtouch(hass, [DEVICE_1_CONFIG, DEVICE_2_CONFIG])
+
+ assert mocked_device.call_count == 2
+ assert mocked_status.call_count == 2
+ assert mocked_volume.call_count == 2
+
+ # one master, one slave => create zone
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_PLAY_EVERYWHERE,
+ {"master": "media_player.soundtouch_1"},
+ True,
)
- def test_should_turn_on(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_on
- ):
- """Test device is turned on."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].turn_on()
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 1
- assert mocked_power_on.call_count == 1
+ assert mocked_create_zone.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume_up")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ # unknown master, create zone must not be called
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_PLAY_EVERYWHERE,
+ {"master": "media_player.entity_X"},
+ True,
)
- def test_volume_up(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_up
- ):
- """Test volume up."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].volume_up()
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 2
- assert mocked_volume_up.call_count == 1
+ assert mocked_create_zone.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume_down")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ # no slaves, create zone must not be called
+ for entity in list(hass.data[DATA_SOUNDTOUCH]):
+ if entity.entity_id == "media_player.soundtouch_1":
+ continue
+ hass.data[DATA_SOUNDTOUCH].remove(entity)
+ await entity.async_remove()
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_PLAY_EVERYWHERE,
+ {"master": "media_player.soundtouch_1"},
+ True,
)
- def test_volume_down(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_down
- ):
- """Test volume down."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].volume_down()
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 2
- assert mocked_volume_down.call_count == 1
+ assert mocked_create_zone.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.set_volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+
+@patch("libsoundtouch.device.SoundTouchDevice.create_zone")
+async def test_create_zone(
+ mocked_create_zone, mocked_status, mocked_volume, hass, two_zones
+):
+ """Test creating a zone."""
+ mocked_device = two_zones
+ await setup_soundtouch(hass, [DEVICE_1_CONFIG, DEVICE_2_CONFIG])
+
+ assert mocked_device.call_count == 2
+ assert mocked_status.call_count == 2
+ assert mocked_volume.call_count == 2
+
+ # one master, one slave => create zone
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_CREATE_ZONE,
+ {
+ "master": "media_player.soundtouch_1",
+ "slaves": ["media_player.soundtouch_2"],
+ },
+ True,
)
- def test_set_volume_level(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_set_volume
- ):
- """Test set volume level."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].set_volume_level(0.17)
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 2
- mocked_set_volume.assert_called_with(17)
+ assert mocked_create_zone.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.mute")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ # unknown master, create zone must not be called
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_CREATE_ZONE,
+ {"master": "media_player.entity_X", "slaves": ["media_player.soundtouch_2"]},
+ True,
)
- def test_mute(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_mute
- ):
- """Test mute volume."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].mute_volume(None)
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 2
- assert mocked_mute.call_count == 1
+ assert mocked_create_zone.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.play")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ # no slaves, create zone must not be called
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_CREATE_ZONE,
+ {"master": "media_player.soundtouch_1", "slaves": []},
+ True,
)
- def test_play(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play
- ):
- """Test play command."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].media_play()
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 1
- assert mocked_play.call_count == 1
+ assert mocked_create_zone.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.pause")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+
+@patch("libsoundtouch.device.SoundTouchDevice.remove_zone_slave")
+async def test_remove_zone_slave(
+ mocked_remove_zone_slave, mocked_status, mocked_volume, hass, two_zones
+):
+ """Test adding a slave to an existing zone."""
+ mocked_device = two_zones
+ await setup_soundtouch(hass, [DEVICE_1_CONFIG, DEVICE_2_CONFIG])
+
+ assert mocked_device.call_count == 2
+ assert mocked_status.call_count == 2
+ assert mocked_volume.call_count == 2
+
+ # remove one slave
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_REMOVE_ZONE_SLAVE,
+ {
+ "master": "media_player.soundtouch_1",
+ "slaves": ["media_player.soundtouch_2"],
+ },
+ True,
)
- def test_pause(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_pause
- ):
- """Test pause command."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].media_pause()
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 1
- assert mocked_pause.call_count == 1
+ assert mocked_remove_zone_slave.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.play_pause")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ # unknown master. add zone slave is not called
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_REMOVE_ZONE_SLAVE,
+ {"master": "media_player.entity_X", "slaves": ["media_player.soundtouch_2"]},
+ True,
)
- def test_play_pause_play(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_pause
- ):
- """Test play/pause."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].media_play_pause()
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 1
- assert mocked_play_pause.call_count == 1
+ assert mocked_remove_zone_slave.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.previous_track")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.next_track")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ # no slave to add, add zone slave is not called
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_REMOVE_ZONE_SLAVE,
+ {"master": "media_player.soundtouch_1", "slaves": []},
+ True,
)
- def test_next_previous_track(
- self,
- mocked_soundtouch_device,
- mocked_status,
- mocked_volume,
- mocked_next_track,
- mocked_previous_track,
- ):
- """Test next/previous track."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices[0].media_next_track()
- assert mocked_status.call_count == 2
- assert mocked_next_track.call_count == 1
- all_devices[0].media_previous_track()
- assert mocked_status.call_count == 3
- assert mocked_previous_track.call_count == 1
+ assert mocked_remove_zone_slave.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.select_preset")
- @mock.patch(
- "libsoundtouch.device.SoundTouchDevice.presets", side_effect=_mocked_presets
+
+@patch("libsoundtouch.device.SoundTouchDevice.add_zone_slave")
+async def test_add_zone_slave(
+ mocked_add_zone_slave, mocked_status, mocked_volume, hass, two_zones,
+):
+ """Test removing a slave from a zone."""
+ mocked_device = two_zones
+ await setup_soundtouch(hass, [DEVICE_1_CONFIG, DEVICE_2_CONFIG])
+
+ assert mocked_device.call_count == 2
+ assert mocked_status.call_count == 2
+ assert mocked_volume.call_count == 2
+
+ # add one slave
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_ADD_ZONE_SLAVE,
+ {
+ "master": "media_player.soundtouch_1",
+ "slaves": ["media_player.soundtouch_2"],
+ },
+ True,
)
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ assert mocked_add_zone_slave.call_count == 1
+
+ # unknown master, add zone slave is not called
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_ADD_ZONE_SLAVE,
+ {"master": "media_player.entity_X", "slaves": ["media_player.soundtouch_2"]},
+ True,
)
- def test_play_media(
- self,
- mocked_soundtouch_device,
- mocked_status,
- mocked_volume,
- mocked_presets,
- mocked_select_preset,
- ):
- """Test play preset 1."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices[0].play_media("PLAYLIST", 1)
- assert mocked_presets.call_count == 1
- assert mocked_select_preset.call_count == 1
- all_devices[0].play_media("PLAYLIST", 2)
- assert mocked_presets.call_count == 2
- assert mocked_select_preset.call_count == 1
+ assert mocked_add_zone_slave.call_count == 1
- @mock.patch("libsoundtouch.device.SoundTouchDevice.play_url")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
+ # no slave to add, add zone slave is not called
+ await hass.services.async_call(
+ soundtouch.DOMAIN,
+ soundtouch.SERVICE_ADD_ZONE_SLAVE,
+ {"master": "media_player.soundtouch_1", "slaves": ["media_player.entity_X"]},
+ True,
)
- def test_play_media_url(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_url
- ):
- """Test play preset 1."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- assert mocked_soundtouch_device.call_count == 1
- assert mocked_status.call_count == 1
- assert mocked_volume.call_count == 1
- all_devices[0].play_media("MUSIC", "http://fqdn/file.mp3")
- mocked_play_url.assert_called_with("http://fqdn/file.mp3")
-
- @mock.patch("libsoundtouch.device.SoundTouchDevice.create_zone")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
- )
- def test_play_everywhere(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone
- ):
- """Test play everywhere."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].entity_id = "media_player.entity_1"
- all_devices[1].entity_id = "media_player.entity_2"
- assert mocked_soundtouch_device.call_count == 2
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 2
-
- # one master, one slave => create zone
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_PLAY_EVERYWHERE,
- {"master": "media_player.entity_1"},
- True,
- )
- assert mocked_create_zone.call_count == 1
-
- # unknown master. create zone is must not be called
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_PLAY_EVERYWHERE,
- {"master": "media_player.entity_X"},
- True,
- )
- assert mocked_create_zone.call_count == 1
-
- # no slaves, create zone must not be called
- all_devices.pop(1)
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_PLAY_EVERYWHERE,
- {"master": "media_player.entity_1"},
- True,
- )
- assert mocked_create_zone.call_count == 1
-
- @mock.patch("libsoundtouch.device.SoundTouchDevice.create_zone")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
- )
- def test_create_zone(
- self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone
- ):
- """Test creating a zone."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].entity_id = "media_player.entity_1"
- all_devices[1].entity_id = "media_player.entity_2"
- assert mocked_soundtouch_device.call_count == 2
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 2
-
- # one master, one slave => create zone
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_CREATE_ZONE,
- {"master": "media_player.entity_1", "slaves": ["media_player.entity_2"]},
- True,
- )
- assert mocked_create_zone.call_count == 1
-
- # unknown master. create zone is must not be called
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_CREATE_ZONE,
- {"master": "media_player.entity_X", "slaves": ["media_player.entity_2"]},
- True,
- )
- assert mocked_create_zone.call_count == 1
-
- # no slaves, create zone must not be called
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_CREATE_ZONE,
- {"master": "media_player.entity_X", "slaves": []},
- True,
- )
- assert mocked_create_zone.call_count == 1
-
- @mock.patch("libsoundtouch.device.SoundTouchDevice.remove_zone_slave")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
- )
- def test_remove_zone_slave(
- self,
- mocked_soundtouch_device,
- mocked_status,
- mocked_volume,
- mocked_remove_zone_slave,
- ):
- """Test adding a slave to an existing zone."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].entity_id = "media_player.entity_1"
- all_devices[1].entity_id = "media_player.entity_2"
- assert mocked_soundtouch_device.call_count == 2
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 2
-
- # remove one slave
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_REMOVE_ZONE_SLAVE,
- {"master": "media_player.entity_1", "slaves": ["media_player.entity_2"]},
- True,
- )
- assert mocked_remove_zone_slave.call_count == 1
-
- # unknown master. add zone slave is not called
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_REMOVE_ZONE_SLAVE,
- {"master": "media_player.entity_X", "slaves": ["media_player.entity_2"]},
- True,
- )
- assert mocked_remove_zone_slave.call_count == 1
-
- # no slave to add, add zone slave is not called
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_REMOVE_ZONE_SLAVE,
- {"master": "media_player.entity_1", "slaves": []},
- True,
- )
- assert mocked_remove_zone_slave.call_count == 1
-
- @mock.patch("libsoundtouch.device.SoundTouchDevice.add_zone_slave")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.volume")
- @mock.patch("libsoundtouch.device.SoundTouchDevice.status")
- @mock.patch(
- "homeassistant.components.soundtouch.media_player.soundtouch_device",
- side_effect=_mock_soundtouch_device,
- )
- def test_add_zone_slave(
- self,
- mocked_soundtouch_device,
- mocked_status,
- mocked_volume,
- mocked_add_zone_slave,
- ):
- """Test removing a slave from a zone."""
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock())
- all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- all_devices[0].entity_id = "media_player.entity_1"
- all_devices[1].entity_id = "media_player.entity_2"
- assert mocked_soundtouch_device.call_count == 2
- assert mocked_status.call_count == 2
- assert mocked_volume.call_count == 2
-
- # add one slave
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_ADD_ZONE_SLAVE,
- {"master": "media_player.entity_1", "slaves": ["media_player.entity_2"]},
- True,
- )
- assert mocked_add_zone_slave.call_count == 1
-
- # unknown master. add zone slave is not called
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_ADD_ZONE_SLAVE,
- {"master": "media_player.entity_X", "slaves": ["media_player.entity_2"]},
- True,
- )
- assert mocked_add_zone_slave.call_count == 1
-
- # no slave to add, add zone slave is not called
- self.hass.services.call(
- soundtouch.DOMAIN,
- soundtouch.SERVICE_ADD_ZONE_SLAVE,
- {"master": "media_player.entity_1", "slaves": ["media_player.entity_X"]},
- True,
- )
- assert mocked_add_zone_slave.call_count == 1
+ assert mocked_add_zone_slave.call_count == 1
diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py
index eac75a3b4e7..82748c122ab 100644
--- a/tests/components/startca/test_sensor.py
+++ b/tests/components/startca/test_sensor.py
@@ -1,6 +1,7 @@
"""Tests for the Start.ca sensor platform."""
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.startca.sensor import StartcaData
+from homeassistant.const import DATA_GIGABYTES
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -55,47 +56,47 @@ async def test_capped_setup(hass, aioclient_mock):
assert state.state == "76.24"
state = hass.states.get("sensor.start_ca_usage")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_data_limit")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "400"
state = hass.states.get("sensor.start_ca_used_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_used_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_used_total")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "311.43"
state = hass.states.get("sensor.start_ca_grace_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_grace_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_grace_total")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "311.43"
state = hass.states.get("sensor.start_ca_total_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_total_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_remaining")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "95.05"
@@ -150,47 +151,47 @@ async def test_unlimited_setup(hass, aioclient_mock):
assert state.state == "0"
state = hass.states.get("sensor.start_ca_usage")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "0.0"
state = hass.states.get("sensor.start_ca_data_limit")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "inf"
state = hass.states.get("sensor.start_ca_used_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "0.0"
state = hass.states.get("sensor.start_ca_used_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "0.0"
state = hass.states.get("sensor.start_ca_used_total")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "0.0"
state = hass.states.get("sensor.start_ca_grace_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_grace_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_grace_total")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "311.43"
state = hass.states.get("sensor.start_ca_total_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_total_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_remaining")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "inf"
diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py
index 293f8d1e4cf..888f56efb29 100644
--- a/tests/components/stream/test_hls.py
+++ b/tests/components/stream/test_hls.py
@@ -47,7 +47,7 @@ async def test_hls_stream(hass, hass_client):
# Stop stream, if it hasn't quit already
stream.stop()
- # Ensure playlist not accessable after stream ends
+ # Ensure playlist not accessible after stream ends
fail_response = await http_client.get(parsed_url.path)
assert fail_response.status == 404
@@ -84,7 +84,7 @@ async def test_stream_timeout(hass, hass_client):
future = dt_util.utcnow() + timedelta(minutes=5)
async_fire_time_changed(hass, future)
- # Ensure playlist not accessable
+ # Ensure playlist not accessible
fail_response = await http_client.get(parsed_url.path)
assert fail_response.status == 404
diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py
index 0b1c4f91781..0ad87b59a81 100644
--- a/tests/components/system_log/test_init.py
+++ b/tests/components/system_log/test_init.py
@@ -30,6 +30,7 @@ def _generate_and_log_exception(exception, log):
def assert_log(log, exception, message, level):
"""Assert that specified values are in a specific log entry."""
+ assert log["name"] == "test_logger"
assert exception in log["exception"]
assert message == log["message"]
assert level == log["level"]
diff --git a/tests/components/teksavvy/test_sensor.py b/tests/components/teksavvy/test_sensor.py
index 30bb98911f8..641112e6362 100644
--- a/tests/components/teksavvy/test_sensor.py
+++ b/tests/components/teksavvy/test_sensor.py
@@ -1,6 +1,7 @@
"""Tests for the TekSavvy sensor platform."""
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.teksavvy.sensor import TekSavvyData
+from homeassistant.const import DATA_GIGABYTES
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -45,31 +46,31 @@ async def test_capped_setup(hass, aioclient_mock):
await async_setup_component(hass, "sensor", {"sensor": config})
state = hass.states.get("sensor.teksavvy_data_limit")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "400"
state = hass.states.get("sensor.teksavvy_off_peak_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "36.24"
state = hass.states.get("sensor.teksavvy_off_peak_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "1.58"
state = hass.states.get("sensor.teksavvy_off_peak_total")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "37.82"
state = hass.states.get("sensor.teksavvy_on_peak_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "226.75"
state = hass.states.get("sensor.teksavvy_on_peak_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "8.82"
state = hass.states.get("sensor.teksavvy_on_peak_total")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "235.57"
state = hass.states.get("sensor.teksavvy_usage_ratio")
@@ -77,11 +78,11 @@ async def test_capped_setup(hass, aioclient_mock):
assert state.state == "56.69"
state = hass.states.get("sensor.teksavvy_usage")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "226.75"
state = hass.states.get("sensor.teksavvy_remaining")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "173.25"
@@ -126,35 +127,35 @@ async def test_unlimited_setup(hass, aioclient_mock):
await async_setup_component(hass, "sensor", {"sensor": config})
state = hass.states.get("sensor.teksavvy_data_limit")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "inf"
state = hass.states.get("sensor.teksavvy_off_peak_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "36.24"
state = hass.states.get("sensor.teksavvy_off_peak_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "1.58"
state = hass.states.get("sensor.teksavvy_off_peak_total")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "37.82"
state = hass.states.get("sensor.teksavvy_on_peak_download")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "226.75"
state = hass.states.get("sensor.teksavvy_on_peak_upload")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "8.82"
state = hass.states.get("sensor.teksavvy_on_peak_total")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "235.57"
state = hass.states.get("sensor.teksavvy_usage")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "226.75"
state = hass.states.get("sensor.teksavvy_usage_ratio")
@@ -162,7 +163,7 @@ async def test_unlimited_setup(hass, aioclient_mock):
assert state.state == "0"
state = hass.states.get("sensor.teksavvy_remaining")
- assert state.attributes.get("unit_of_measurement") == "GB"
+ assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
assert state.state == "inf"
diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py
index f4972ada2c7..6ee265de8d5 100644
--- a/tests/components/tellduslive/test_config_flow.py
+++ b/tests/components/tellduslive/test_config_flow.py
@@ -239,7 +239,7 @@ async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive):
async def test_discovery_already_configured(hass, mock_tellduslive):
- """Test abort if alredy configured fires from discovery."""
+ """Test abort if already configured fires from discovery."""
MockConfigEntry(domain="tellduslive", data={"host": "some-host"}).add_to_hass(hass)
flow = init_config_flow(hass)
diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py
index a0cccdcb18e..9980691085b 100644
--- a/tests/components/template/test_cover.py
+++ b/tests/components/template/test_cover.py
@@ -885,7 +885,7 @@ async def test_availability_template(hass, calls):
async def test_availability_without_availability_template(hass, calls):
- """Test that component is availble if there is no."""
+ """Test that component is available if there is no."""
assert await setup.async_setup_component(
hass,
"cover",
diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py
index 3e1ec207169..dccca97a1cc 100644
--- a/tests/components/template/test_light.py
+++ b/tests/components/template/test_light.py
@@ -4,7 +4,11 @@ import logging
import pytest
from homeassistant import setup
-from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_COLOR_TEMP,
+ ATTR_HS_COLOR,
+)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import callback
@@ -816,6 +820,123 @@ class TestTemplateLight:
assert state.attributes["entity_picture"] == "/local/light.png"
+ def test_color_action_no_template(self):
+ """Test setting color with optimistic template."""
+ assert setup.setup_component(
+ self.hass,
+ "light",
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{1 == 1}}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_color": [
+ {
+ "service": "test.automation",
+ "data_template": {
+ "entity_id": "test.test_state",
+ "h": "{{h}}",
+ "s": "{{s}}",
+ },
+ },
+ {
+ "service": "test.automation",
+ "data_template": {
+ "entity_id": "test.test_state",
+ "s": "{{s}}",
+ "h": "{{h}}",
+ },
+ },
+ ],
+ }
+ },
+ }
+ },
+ )
+ self.hass.start()
+ self.hass.block_till_done()
+
+ state = self.hass.states.get("light.test_template_light")
+ assert state.attributes.get("hs_color") is None
+
+ common.turn_on(
+ self.hass, "light.test_template_light", **{ATTR_HS_COLOR: (40, 50)}
+ )
+ self.hass.block_till_done()
+ assert len(self.calls) == 2
+ assert self.calls[0].data["h"] == "40"
+ assert self.calls[0].data["s"] == "50"
+ assert self.calls[1].data["h"] == "40"
+ assert self.calls[1].data["s"] == "50"
+
+ state = self.hass.states.get("light.test_template_light")
+ _LOGGER.info(str(state.attributes))
+ assert state is not None
+ assert self.calls[0].data["h"] == "40"
+ assert self.calls[0].data["s"] == "50"
+ assert self.calls[1].data["h"] == "40"
+ assert self.calls[1].data["s"] == "50"
+
+ @pytest.mark.parametrize(
+ "expected_hs,template",
+ [
+ ((360, 100), "{{(360, 100)}}"),
+ ((359.9, 99.9), "{{(359.9, 99.9)}}"),
+ (None, "{{(361, 100)}}"),
+ (None, "{{(360, 101)}}"),
+ (None, "{{x - 12}}"),
+ ],
+ )
+ def test_color_template(self, expected_hs, template):
+ """Test the template for the color."""
+ with assert_setup_component(1, "light"):
+ assert setup.setup_component(
+ self.hass,
+ "light",
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{ 1 == 1 }}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_color": [
+ {
+ "service": "input_number.set_value",
+ "data_template": {
+ "entity_id": "input_number.h",
+ "color_temp": "{{h}}",
+ },
+ }
+ ],
+ "color_template": template,
+ }
+ },
+ }
+ },
+ )
+ self.hass.start()
+ self.hass.block_till_done()
+ state = self.hass.states.get("light.test_template_light")
+ assert state is not None
+ assert state.attributes.get("hs_color") == expected_hs
+
async def test_available_template_with_entities(hass):
"""Test availability templates with values from other entities."""
diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py
index 7b7e822ce58..477583f23fb 100644
--- a/tests/components/tesla/test_config_flow.py
+++ b/tests/components/tesla/test_config_flow.py
@@ -4,7 +4,7 @@ from unittest.mock import patch
from teslajsonpy import TeslaException
from homeassistant import config_entries, data_entry_flow, setup
-from homeassistant.components.tesla.const import DOMAIN
+from homeassistant.components.tesla.const import DOMAIN, MIN_SCAN_INTERVAL
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_PASSWORD,
@@ -40,8 +40,8 @@ async def test_form(hass):
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "test@email.com"
assert result2["data"] == {
- "token": "test-refresh-token",
- "access_token": "test-access-token",
+ CONF_TOKEN: "test-refresh-token",
+ CONF_ACCESS_TOKEN: "test-access-token",
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
@@ -157,4 +157,4 @@ async def test_option_flow_input_floor(hass):
result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1}
)
assert result["type"] == "create_entry"
- assert result["data"] == {CONF_SCAN_INTERVAL: 300}
+ assert result["data"] == {CONF_SCAN_INTERVAL: MIN_SCAN_INTERVAL}
diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py
index 03581d16c09..1da0c16d43c 100644
--- a/tests/components/tod/test_binary_sensor.py
+++ b/tests/components/tod/test_binary_sensor.py
@@ -24,7 +24,7 @@ class TestBinarySensorTod(unittest.TestCase):
def setup_method(self, method):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
- self.hass.config.latitute = 50.27583
+ self.hass.config.latitude = 50.27583
self.hass.config.longitude = 18.98583
def teardown_method(self, method):
diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py
index 6aafe29901d..62c4bc3a065 100644
--- a/tests/components/tts/test_init.py
+++ b/tests/components/tts/test_init.py
@@ -95,7 +95,10 @@ class TestTTS:
self.hass.services.call(
tts.DOMAIN,
"demo_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
+ },
)
self.hass.block_till_done()
@@ -103,13 +106,13 @@ class TestTTS:
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC
assert calls[0].data[
ATTR_MEDIA_CONTENT_ID
- ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format(
+ ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format(
self.hass.config.api.base_url
)
assert os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3",
)
)
@@ -125,7 +128,10 @@ class TestTTS:
self.hass.services.call(
tts.DOMAIN,
"demo_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
+ },
)
self.hass.block_till_done()
@@ -133,13 +139,13 @@ class TestTTS:
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC
assert calls[0].data[
ATTR_MEDIA_CONTENT_ID
- ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format(
+ ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format(
self.hass.config.api.base_url
)
assert os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3",
)
)
@@ -163,7 +169,8 @@ class TestTTS:
tts.DOMAIN,
"demo_say",
{
- tts.ATTR_MESSAGE: "I person is on front of your door.",
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
tts.ATTR_LANGUAGE: "de",
},
)
@@ -173,13 +180,13 @@ class TestTTS:
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC
assert calls[0].data[
ATTR_MEDIA_CONTENT_ID
- ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format(
+ ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format(
self.hass.config.api.base_url
)
assert os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3",
)
)
@@ -196,7 +203,8 @@ class TestTTS:
tts.DOMAIN,
"demo_say",
{
- tts.ATTR_MESSAGE: "I person is on front of your door.",
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
tts.ATTR_LANGUAGE: "lang",
},
)
@@ -206,7 +214,7 @@ class TestTTS:
assert not os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_lang_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3",
)
)
@@ -223,7 +231,8 @@ class TestTTS:
tts.DOMAIN,
"demo_say",
{
- tts.ATTR_MESSAGE: "I person is on front of your door.",
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
tts.ATTR_LANGUAGE: "de",
tts.ATTR_OPTIONS: {"voice": "alex"},
},
@@ -236,13 +245,13 @@ class TestTTS:
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC
assert calls[0].data[
ATTR_MEDIA_CONTENT_ID
- ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format(
+ ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format(
self.hass.config.api.base_url, opt_hash
)
assert os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_de_{0}_demo.mp3".format(
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format(
opt_hash
),
)
@@ -265,7 +274,8 @@ class TestTTS:
tts.DOMAIN,
"demo_say",
{
- tts.ATTR_MESSAGE: "I person is on front of your door.",
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
tts.ATTR_LANGUAGE: "de",
},
)
@@ -277,13 +287,13 @@ class TestTTS:
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC
assert calls[0].data[
ATTR_MEDIA_CONTENT_ID
- ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format(
+ ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format(
self.hass.config.api.base_url, opt_hash
)
assert os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_de_{0}_demo.mp3".format(
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format(
opt_hash
),
)
@@ -302,7 +312,8 @@ class TestTTS:
tts.DOMAIN,
"demo_say",
{
- tts.ATTR_MESSAGE: "I person is on front of your door.",
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
tts.ATTR_LANGUAGE: "de",
tts.ATTR_OPTIONS: {"speed": 1},
},
@@ -315,7 +326,7 @@ class TestTTS:
assert not os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_de_{0}_demo.mp3".format(
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format(
opt_hash
),
)
@@ -333,7 +344,10 @@ class TestTTS:
self.hass.services.call(
tts.DOMAIN,
"demo_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
+ },
)
self.hass.block_till_done()
@@ -341,7 +355,7 @@ class TestTTS:
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC
assert (
calls[0].data[ATTR_MEDIA_CONTENT_ID] == "http://fnord"
- "/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd"
+ "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
"_en_-_demo.mp3"
)
@@ -357,7 +371,10 @@ class TestTTS:
self.hass.services.call(
tts.DOMAIN,
"demo_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
+ },
)
self.hass.block_till_done()
@@ -365,7 +382,7 @@ class TestTTS:
assert os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3",
)
)
@@ -375,7 +392,7 @@ class TestTTS:
assert not os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3",
)
)
@@ -393,7 +410,10 @@ class TestTTS:
self.hass.services.call(
tts.DOMAIN,
"demo_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
+ },
)
self.hass.block_till_done()
@@ -401,7 +421,7 @@ class TestTTS:
req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID])
_, demo_data = self.demo_provider.get_tts_audio("bla", "en")
demo_data = tts.SpeechManager.write_tags(
- "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3",
demo_data,
self.demo_provider,
"AI person is in front of your door.",
@@ -425,7 +445,10 @@ class TestTTS:
self.hass.services.call(
tts.DOMAIN,
"demo_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
+ },
)
self.hass.block_till_done()
@@ -433,10 +456,10 @@ class TestTTS:
req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID])
_, demo_data = self.demo_provider.get_tts_audio("bla", "de")
demo_data = tts.SpeechManager.write_tags(
- "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3",
demo_data,
self.demo_provider,
- "I person is on front of your door.",
+ "There is someone at the door.",
"de",
None,
)
@@ -453,7 +476,7 @@ class TestTTS:
self.hass.start()
url = (
- "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3"
+ "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3"
).format(self.hass.config.api.base_url)
req = requests.get(url)
@@ -487,7 +510,10 @@ class TestTTS:
self.hass.services.call(
tts.DOMAIN,
"demo_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
+ },
)
self.hass.block_till_done()
@@ -495,7 +521,7 @@ class TestTTS:
assert not os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3",
)
)
@@ -512,7 +538,8 @@ class TestTTS:
tts.DOMAIN,
"demo_say",
{
- tts.ATTR_MESSAGE: "I person is on front of your door.",
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
tts.ATTR_CACHE: False,
},
)
@@ -522,7 +549,7 @@ class TestTTS:
assert not os.path.isfile(
os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3",
)
)
@@ -533,7 +560,7 @@ class TestTTS:
_, demo_data = self.demo_provider.get_tts_audio("bla", "en")
cache_file = os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3",
)
os.mkdir(self.default_tts_cache)
@@ -552,14 +579,17 @@ class TestTTS:
self.hass.services.call(
tts.DOMAIN,
"demo_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
+ },
)
self.hass.block_till_done()
assert len(calls) == 1
assert calls[0].data[
ATTR_MEDIA_CONTENT_ID
- ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format(
+ ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format(
self.hass.config.api.base_url
)
@@ -579,7 +609,10 @@ class TestTTS:
self.hass.services.call(
tts.DOMAIN,
"demo_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "There is someone at the door.",
+ },
)
self.hass.block_till_done()
@@ -590,7 +623,7 @@ class TestTTS:
_, demo_data = self.demo_provider.get_tts_audio("bla", "en")
cache_file = os.path.join(
self.default_tts_cache,
- "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3",
+ "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3",
)
os.mkdir(self.default_tts_cache)
@@ -605,7 +638,7 @@ class TestTTS:
self.hass.start()
url = (
- "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3"
+ "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3"
).format(self.hass.config.api.base_url)
req = requests.get(url)
@@ -622,14 +655,15 @@ async def test_setup_component_and_web_get_url(hass, hass_client):
client = await hass_client()
url = "/api/tts_get_url"
- data = {"platform": "demo", "message": "I person is on front of your door."}
+ data = {"platform": "demo", "message": "There is someone at the door."}
req = await client.post(url, json=data)
assert req.status == 200
response = await req.json()
assert response.get("url") == (
- "{}/api/tts_proxy/265944c108cbb00b2a62"
- "1be5930513e03a0bb2cd_en_-_demo.mp3".format(hass.config.api.base_url)
+ "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format(
+ hass.config.api.base_url
+ )
)
tts_cache = hass.config.path(tts.DEFAULT_CACHE_DIR)
@@ -646,7 +680,7 @@ async def test_setup_component_and_web_get_url_bad_config(hass, hass_client):
client = await hass_client()
url = "/api/tts_get_url"
- data = {"message": "I person is on front of your door."}
+ data = {"message": "There is someone at the door."}
req = await client.post(url, json=data)
assert req.status == 400
diff --git a/tests/components/twitch/__init__.py b/tests/components/twitch/__init__.py
new file mode 100644
index 00000000000..ec26cf264ef
--- /dev/null
+++ b/tests/components/twitch/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Twitch component."""
diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py
new file mode 100644
index 00000000000..6c656f874d0
--- /dev/null
+++ b/tests/components/twitch/test_twitch.py
@@ -0,0 +1,174 @@
+"""The tests for an update of the Twitch component."""
+from unittest.mock import MagicMock, patch
+
+from requests import HTTPError
+from twitch.resources import Channel, Follow, Stream, Subscription, User
+
+from homeassistant.components import sensor
+from homeassistant.setup import async_setup_component
+
+ENTITY_ID = "sensor.channel123"
+CONFIG = {
+ sensor.DOMAIN: {
+ "platform": "twitch",
+ "client_id": "1234",
+ "channels": ["channel123"],
+ }
+}
+CONFIG_WITH_OAUTH = {
+ sensor.DOMAIN: {
+ "platform": "twitch",
+ "client_id": "1234",
+ "channels": ["channel123"],
+ "token": "9876",
+ }
+}
+
+USER_ID = User({"id": 123, "display_name": "channel123", "logo": "logo.png"})
+STREAM_OBJECT_ONLINE = Stream(
+ {
+ "channel": {"game": "Good Game", "status": "Title"},
+ "preview": {"medium": "stream-medium.png"},
+ }
+)
+CHANNEL_OBJECT = Channel({"followers": 42, "views": 24})
+OAUTH_USER_ID = User({"id": 987})
+SUB_ACTIVE = Subscription({"created_at": "2020-01-20T21:22:42", "is_gift": False})
+FOLLOW_ACTIVE = Follow({"created_at": "2020-01-20T21:22:42"})
+
+
+async def test_init(hass):
+ """Test initial config."""
+
+ channels = MagicMock()
+ channels.get_by_id.return_value = CHANNEL_OBJECT
+ streams = MagicMock()
+ streams.get_stream_by_user.return_value = None
+
+ twitch_mock = MagicMock()
+ twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID]
+ twitch_mock.channels = channels
+ twitch_mock.streams = streams
+
+ with patch(
+ "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock
+ ):
+ assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True
+
+ sensor_state = hass.states.get(ENTITY_ID)
+ assert sensor_state.state == "offline"
+ assert sensor_state.name == "channel123"
+ assert sensor_state.attributes["icon"] == "mdi:twitch"
+ assert sensor_state.attributes["friendly_name"] == "channel123"
+ assert sensor_state.attributes["views"] == 24
+ assert sensor_state.attributes["followers"] == 42
+
+
+async def test_offline(hass):
+ """Test offline state."""
+
+ twitch_mock = MagicMock()
+ twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID]
+ twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT
+ twitch_mock.streams.get_stream_by_user.return_value = None
+
+ with patch(
+ "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock,
+ ):
+ assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True
+
+ sensor_state = hass.states.get(ENTITY_ID)
+ assert sensor_state.state == "offline"
+ assert sensor_state.attributes["entity_picture"] == "logo.png"
+
+
+async def test_streaming(hass):
+ """Test streaming state."""
+
+ twitch_mock = MagicMock()
+ twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID]
+ twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT
+ twitch_mock.streams.get_stream_by_user.return_value = STREAM_OBJECT_ONLINE
+
+ with patch(
+ "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock,
+ ):
+ assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True
+
+ sensor_state = hass.states.get(ENTITY_ID)
+ assert sensor_state.state == "streaming"
+ assert sensor_state.attributes["entity_picture"] == "stream-medium.png"
+ assert sensor_state.attributes["game"] == "Good Game"
+ assert sensor_state.attributes["title"] == "Title"
+
+
+async def test_oauth_without_sub_and_follow(hass):
+ """Test state with oauth."""
+
+ twitch_mock = MagicMock()
+ twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID]
+ twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT
+ twitch_mock._oauth_token = True # A replacement for the token
+ twitch_mock.users.get.return_value = OAUTH_USER_ID
+ twitch_mock.users.check_subscribed_to_channel.side_effect = HTTPError()
+ twitch_mock.users.check_follows_channel.side_effect = HTTPError()
+
+ with patch(
+ "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock,
+ ):
+ assert (
+ await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) is True
+ )
+
+ sensor_state = hass.states.get(ENTITY_ID)
+ assert sensor_state.attributes["subscribed"] is False
+ assert sensor_state.attributes["following"] is False
+
+
+async def test_oauth_with_sub(hass):
+ """Test state with oauth and sub."""
+
+ twitch_mock = MagicMock()
+ twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID]
+ twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT
+ twitch_mock._oauth_token = True # A replacement for the token
+ twitch_mock.users.get.return_value = OAUTH_USER_ID
+ twitch_mock.users.check_subscribed_to_channel.return_value = SUB_ACTIVE
+ twitch_mock.users.check_follows_channel.side_effect = HTTPError()
+
+ with patch(
+ "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock,
+ ):
+ assert (
+ await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) is True
+ )
+
+ sensor_state = hass.states.get(ENTITY_ID)
+ assert sensor_state.attributes["subscribed"] is True
+ assert sensor_state.attributes["subscribed_since"] == "2020-01-20T21:22:42"
+ assert sensor_state.attributes["subscription_is_gifted"] is False
+ assert sensor_state.attributes["following"] is False
+
+
+async def test_oauth_with_follow(hass):
+ """Test state with oauth and follow."""
+
+ twitch_mock = MagicMock()
+ twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID]
+ twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT
+ twitch_mock._oauth_token = True # A replacement for the token
+ twitch_mock.users.get.return_value = OAUTH_USER_ID
+ twitch_mock.users.check_subscribed_to_channel.side_effect = HTTPError()
+ twitch_mock.users.check_follows_channel.return_value = FOLLOW_ACTIVE
+
+ with patch(
+ "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock,
+ ):
+ assert (
+ await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) is True
+ )
+
+ sensor_state = hass.states.get(ENTITY_ID)
+ assert sensor_state.attributes["subscribed"] is False
+ assert sensor_state.attributes["following"] is True
+ assert sensor_state.attributes["following_since"] == "2020-01-20T21:22:42"
diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py
new file mode 100644
index 00000000000..189b80c1932
--- /dev/null
+++ b/tests/components/unifi/conftest.py
@@ -0,0 +1,13 @@
+"""Fixtures for UniFi methods."""
+from asynctest import patch
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def mock_discovery():
+ """No real network traffic allowed."""
+ with patch(
+ "homeassistant.components.unifi.config_flow.async_discover_unifi",
+ return_value=None,
+ ) as mock:
+ yield mock
diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py
index cc8896d55ce..64d1ab9775e 100644
--- a/tests/components/unifi/test_config_flow.py
+++ b/tests/components/unifi/test_config_flow.py
@@ -2,6 +2,7 @@
import aiounifi
from asynctest import patch
+from homeassistant import data_entry_flow
from homeassistant.components import unifi
from homeassistant.components.unifi import config_flow
from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID
@@ -13,17 +14,29 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
+from .test_controller import setup_unifi_integration
+
from tests.common import MockConfigEntry
+WLANS = [{"name": "SSID 1"}, {"name": "SSID 2"}]
-async def test_flow_works(hass, aioclient_mock):
+
+async def test_flow_works(hass, aioclient_mock, mock_discovery):
"""Test config flow."""
+ mock_discovery.return_value = "1"
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": "user"}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
+ assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == {
+ CONF_HOST: "unifi",
+ CONF_USERNAME: "",
+ CONF_PASSWORD: "",
+ CONF_PORT: 8443,
+ CONF_VERIFY_SSL: False,
+ }
aioclient_mock.post(
"https://1.2.3.4:1234/api/login",
@@ -228,36 +241,39 @@ async def test_flow_fails_unknown_problem(hass, aioclient_mock):
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)
+ controller = await setup_unifi_integration(hass, wlans_response=WLANS)
- flow = await hass.config_entries.options.async_create_flow(
- entry.entry_id, context={"source": "test"}, data=None
+ result = await hass.config_entries.options.async_init(
+ controller.config_entry.entry_id
)
- result = await flow.async_step_init()
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "device_tracker"
- result = await flow.async_step_device_tracker(
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
user_input={
config_flow.CONF_TRACK_CLIENTS: False,
config_flow.CONF_TRACK_WIRED_CLIENTS: False,
config_flow.CONF_TRACK_DEVICES: False,
+ config_flow.CONF_SSID_FILTER: ["SSID 1"],
config_flow.CONF_DETECTION_TIME: 100,
- }
+ },
)
- assert result["type"] == "form"
+
+ assert result["type"] == data_entry_flow.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}
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True}
)
- assert result["type"] == "create_entry"
+
+ assert result["type"] == data_entry_flow.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_SSID_FILTER: ["SSID 1"],
config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True,
}
diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py
index 74137cf8a3a..daec8cddf5d 100644
--- a/tests/components/unifi/test_controller.py
+++ b/tests/components/unifi/test_controller.py
@@ -63,6 +63,7 @@ async def setup_unifi_integration(
clients_response=None,
devices_response=None,
clients_all_response=None,
+ wlans_response=None,
known_wireless_clients=None,
controllers=None,
):
@@ -98,6 +99,10 @@ async def setup_unifi_integration(
if clients_all_response:
mock_client_all_responses.append(clients_all_response)
+ mock_wlans_responses = deque()
+ if wlans_response:
+ mock_wlans_responses.append(wlans_response)
+
mock_requests = []
async def mock_request(self, method, path, json=None):
@@ -109,11 +114,16 @@ async def setup_unifi_integration(
return mock_device_responses.popleft()
if path == "s/{site}/rest/user" and mock_client_all_responses:
return mock_client_all_responses.popleft()
+ if path == "s/{site}/rest/wlanconf" and mock_wlans_responses:
+ return mock_wlans_responses.popleft()
return {}
+ # "aiounifi.Controller.start_websocket", return_value=True
with patch("aiounifi.Controller.login", return_value=True), patch(
"aiounifi.Controller.sites", return_value=sites
- ), patch("aiounifi.Controller.request", new=mock_request):
+ ), patch("aiounifi.Controller.request", new=mock_request), patch.object(
+ aiounifi.websocket.WSClient, "start", return_value=True
+ ):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@@ -125,6 +135,7 @@ async def setup_unifi_integration(
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_wlans_responses = mock_wlans_responses
controller.mock_requests = mock_requests
return controller
@@ -233,47 +244,28 @@ async def test_reset_after_successful_setup(hass):
assert len(controller.listeners) == 0
-async def test_failed_update_failed_login(hass):
- """Running update can handle a failed login."""
+async def test_wireless_client_event_calls_update_wireless_devices(hass):
+ """Call update_wireless_devices method when receiving wireless client event."""
controller = await setup_unifi_integration(hass)
- with patch.object(
- 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()
+ with patch(
+ "homeassistant.components.unifi.controller.UniFiController.update_wireless_clients",
+ return_value=None,
+ ) as wireless_clients_mock:
+ controller.api.websocket._data = {
+ "meta": {"rc": "ok", "message": "events"},
+ "data": [
+ {
+ "datetime": "2020-01-20T19:37:04Z",
+ "key": aiounifi.events.WIRELESS_CLIENT_CONNECTED,
+ "msg": "User[11:22:33:44:55:66] has connected to WLAN",
+ "time": 1579549024893,
+ }
+ ],
+ }
+ controller.api.session_handler("data")
- 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)
-
- 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)
-
- with patch.object(
- controller.api.clients, "update", side_effect=aiounifi.AiounifiException
- ):
- await controller.async_update()
- await hass.async_block_till_done()
-
- assert controller.available is False
-
- await controller.async_update()
- await hass.async_block_till_done()
- assert controller.available is True
+ assert wireless_clients_mock.assert_called_once
async def test_get_controller(hass):
@@ -307,7 +299,7 @@ async def test_get_controller_controller_unavailable(hass):
async def test_get_controller_unknown_error(hass):
- """Check that get_controller can handle unkown errors."""
+ """Check that get_controller can handle unknown errors."""
with patch(
"aiounifi.Controller.login", side_effect=aiounifi.AiounifiException
), pytest.raises(unifi.errors.AuthenticationRequired):
diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py
index f123772a6ca..608e72b483a 100644
--- a/tests/components/unifi/test_device_tracker.py
+++ b/tests/components/unifi/test_device_tracker.py
@@ -2,6 +2,8 @@
from copy import copy
from datetime import timedelta
+from aiounifi.controller import SIGNAL_CONNECTION_STATE
+from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING
from asynctest import patch
from homeassistant import config_entries
@@ -9,6 +11,7 @@ from homeassistant.components import unifi
import homeassistant.components.device_tracker as device_tracker
from homeassistant.components.unifi.const import (
CONF_SSID_FILTER,
+ CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
CONF_TRACK_WIRED_CLIENTS,
)
@@ -112,7 +115,7 @@ async def test_tracked_devices(hass):
devices_response=[DEVICE_1, DEVICE_2],
known_wireless_clients=(CLIENT_4["mac"],),
)
- assert len(hass.states.async_all()) == 5
+ assert len(hass.states.async_all()) == 6
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None
@@ -123,7 +126,8 @@ async def test_tracked_devices(hass):
assert client_2.state == "not_home"
client_3 = hass.states.get("device_tracker.client_3")
- assert client_3 is None
+ assert client_3 is not None
+ assert client_3.state == "not_home"
# Wireless client with wired bug, if bug active on restart mark device away
client_4 = hass.states.get("device_tracker.client_4")
@@ -134,13 +138,15 @@ async def test_tracked_devices(hass):
assert device_1 is not None
assert device_1.state == "not_home"
+ # State change signalling works
client_1_copy = copy(CLIENT_1)
client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
+ event = {"meta": {"message": "sta:sync"}, "data": [client_1_copy]}
+ controller.api.message_handler(event)
device_1_copy = copy(DEVICE_1)
device_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
- controller.mock_client_responses.append([client_1_copy])
- controller.mock_device_responses.append([device_1_copy])
- await controller.async_update()
+ event = {"meta": {"message": "device:sync"}, "data": [device_1_copy]}
+ controller.api.message_handler(event)
await hass.async_block_till_done()
client_1 = hass.states.get("device_tracker.client_1")
@@ -149,33 +155,215 @@ async def test_tracked_devices(hass):
device_1 = hass.states.get("device_tracker.device_1")
assert device_1.state == "home"
+ # Disabled device is unavailable
device_1_copy = copy(DEVICE_1)
device_1_copy["disabled"] = True
- controller.mock_client_responses.append({})
- controller.mock_device_responses.append([device_1_copy])
- await controller.async_update()
+ event = {"meta": {"message": "device:sync"}, "data": [device_1_copy]}
+ controller.api.message_handler(event)
await hass.async_block_till_done()
device_1 = hass.states.get("device_tracker.device_1")
assert device_1.state == STATE_UNAVAILABLE
- controller.config_entry.add_update_listener(controller.async_options_updated)
- hass.config_entries.async_update_entry(
- controller.config_entry,
- options={
- CONF_SSID_FILTER: [],
- CONF_TRACK_WIRED_CLIENTS: False,
- CONF_TRACK_DEVICES: False,
- },
+
+async def test_controller_state_change(hass):
+ """Verify entities state reflect on controller becoming unavailable."""
+ controller = await setup_unifi_integration(
+ hass, clients_response=[CLIENT_1], devices_response=[DEVICE_1],
+ )
+ assert len(hass.states.async_all()) == 3
+
+ # Controller unavailable
+ controller.async_unifi_signalling_callback(
+ SIGNAL_CONNECTION_STATE, STATE_DISCONNECTED
)
await hass.async_block_till_done()
+
client_1 = hass.states.get("device_tracker.client_1")
- assert client_1
+ assert client_1.state == STATE_UNAVAILABLE
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1.state == STATE_UNAVAILABLE
+
+ # Controller available
+ controller.async_unifi_signalling_callback(SIGNAL_CONNECTION_STATE, STATE_RUNNING)
+ await hass.async_block_till_done()
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1.state == "not_home"
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1.state == "not_home"
+
+
+async def test_option_track_clients(hass):
+ """Test the tracking of clients can be turned off."""
+ controller = await setup_unifi_integration(
+ hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1],
+ )
+ assert len(hass.states.async_all()) == 4
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1 is not None
+
+ client_2 = hass.states.get("device_tracker.wired_client")
+ assert client_2 is not None
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1 is not None
+
+ hass.config_entries.async_update_entry(
+ controller.config_entry, options={CONF_TRACK_CLIENTS: False},
+ )
+ await hass.async_block_till_done()
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1 is None
+
client_2 = hass.states.get("device_tracker.wired_client")
assert client_2 is None
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1 is not None
+
+ hass.config_entries.async_update_entry(
+ controller.config_entry, options={CONF_TRACK_CLIENTS: True},
+ )
+ await hass.async_block_till_done()
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1 is not None
+
+ client_2 = hass.states.get("device_tracker.wired_client")
+ assert client_2 is not None
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1 is not None
+
+
+async def test_option_track_wired_clients(hass):
+ """Test the tracking of wired clients can be turned off."""
+ controller = await setup_unifi_integration(
+ hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1],
+ )
+ assert len(hass.states.async_all()) == 4
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1 is not None
+
+ client_2 = hass.states.get("device_tracker.wired_client")
+ assert client_2 is not None
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1 is not None
+
+ hass.config_entries.async_update_entry(
+ controller.config_entry, options={CONF_TRACK_WIRED_CLIENTS: False},
+ )
+ await hass.async_block_till_done()
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1 is not None
+
+ client_2 = hass.states.get("device_tracker.wired_client")
+ assert client_2 is None
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1 is not None
+
+ hass.config_entries.async_update_entry(
+ controller.config_entry, options={CONF_TRACK_WIRED_CLIENTS: True},
+ )
+ await hass.async_block_till_done()
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1 is not None
+
+ client_2 = hass.states.get("device_tracker.wired_client")
+ assert client_2 is not None
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1 is not None
+
+
+async def test_option_track_devices(hass):
+ """Test the tracking of devices can be turned off."""
+ controller = await setup_unifi_integration(
+ hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1],
+ )
+ assert len(hass.states.async_all()) == 4
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1 is not None
+
+ client_2 = hass.states.get("device_tracker.wired_client")
+ assert client_2 is not None
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1 is not None
+
+ hass.config_entries.async_update_entry(
+ controller.config_entry, options={CONF_TRACK_DEVICES: False},
+ )
+ await hass.async_block_till_done()
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1 is not None
+
+ client_2 = hass.states.get("device_tracker.wired_client")
+ assert client_2 is not None
+
device_1 = hass.states.get("device_tracker.device_1")
assert device_1 is None
+ hass.config_entries.async_update_entry(
+ controller.config_entry, options={CONF_TRACK_DEVICES: True},
+ )
+ await hass.async_block_till_done()
+
+ client_1 = hass.states.get("device_tracker.client_1")
+ assert client_1 is not None
+
+ client_2 = hass.states.get("device_tracker.wired_client")
+ assert client_2 is not None
+
+ device_1 = hass.states.get("device_tracker.device_1")
+ assert device_1 is not None
+
+
+async def test_option_ssid_filter(hass):
+ """Test the SSID filter works."""
+ controller = await setup_unifi_integration(
+ hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_3],
+ )
+ assert len(hass.states.async_all()) == 2
+
+ # SSID filter active
+ client_3 = hass.states.get("device_tracker.client_3")
+ assert client_3.state == "not_home"
+
+ client_3_copy = copy(CLIENT_3)
+ client_3_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
+ event = {"meta": {"message": "sta:sync"}, "data": [client_3_copy]}
+ controller.api.message_handler(event)
+ await hass.async_block_till_done()
+
+ # SSID filter active even though time stamp should mark as home
+ client_3 = hass.states.get("device_tracker.client_3")
+ assert client_3.state == "not_home"
+
+ # Remove SSID filter
+ hass.config_entries.async_update_entry(
+ controller.config_entry, options={CONF_SSID_FILTER: []},
+ )
+ event = {"meta": {"message": "sta:sync"}, "data": [client_3_copy]}
+ controller.api.message_handler(event)
+ await hass.async_block_till_done()
+
+ # SSID no longer filtered
+ client_3 = hass.states.get("device_tracker.client_3")
+ assert client_3.state == "home"
+
async def test_wireless_client_go_wired_issue(hass):
"""Test the solution to catch wireless device go wired UniFi issue.
@@ -194,9 +382,8 @@ async def test_wireless_client_go_wired_issue(hass):
client_1_client["is_wired"] = True
client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
- controller.mock_client_responses.append([client_1_client])
- controller.mock_device_responses.append({})
- await controller.async_update()
+ event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]}
+ controller.api.message_handler(event)
await hass.async_block_till_done()
client_1 = hass.states.get("device_tracker.client_1")
@@ -207,9 +394,8 @@ async def test_wireless_client_go_wired_issue(hass):
"utcnow",
return_value=(dt_util.utcnow() + timedelta(minutes=5)),
):
- controller.mock_client_responses.append([client_1_client])
- controller.mock_device_responses.append({})
- await controller.async_update()
+ event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]}
+ controller.api.message_handler(event)
await hass.async_block_till_done()
client_1 = hass.states.get("device_tracker.client_1")
@@ -217,9 +403,8 @@ async def test_wireless_client_go_wired_issue(hass):
client_1_client["is_wired"] = False
client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
- controller.mock_client_responses.append([client_1_client])
- controller.mock_device_responses.append({})
- await controller.async_update()
+ event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]}
+ controller.api.message_handler(event)
await hass.async_block_till_done()
client_1 = hass.states.get("device_tracker.client_1")
@@ -269,7 +454,7 @@ async def test_restoring_client(hass):
async def test_dont_track_clients(hass):
- """Test dont track clients config works."""
+ """Test don't track clients config works."""
await setup_unifi_integration(
hass,
options={unifi.controller.CONF_TRACK_CLIENTS: False},
@@ -287,7 +472,7 @@ async def test_dont_track_clients(hass):
async def test_dont_track_devices(hass):
- """Test dont track devices config works."""
+ """Test don't track devices config works."""
await setup_unifi_integration(
hass,
options={unifi.controller.CONF_TRACK_DEVICES: False},
@@ -305,7 +490,7 @@ async def test_dont_track_devices(hass):
async def test_dont_track_wired_clients(hass):
- """Test dont track wired clients config works."""
+ """Test don't track wired clients config works."""
await setup_unifi_integration(
hass,
options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False},
diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py
index 1f5a3852e16..12f9c1bfd17 100644
--- a/tests/components/unifi/test_init.py
+++ b/tests/components/unifi/test_init.py
@@ -4,6 +4,8 @@ from unittest.mock import Mock, patch
from homeassistant.components import unifi
from homeassistant.setup import async_setup_component
+from .test_controller import setup_unifi_integration
+
from tests.common import MockConfigEntry, mock_coro
@@ -42,67 +44,15 @@ async def test_setup_with_config(hass):
async def test_successful_config_entry(hass):
"""Test that configured options for a host are loaded via config entry."""
- entry = MockConfigEntry(
- domain=unifi.DOMAIN,
- data={
- "controller": {
- "host": "0.0.0.0",
- "username": "user",
- "password": "pass",
- "port": 80,
- "site": "default",
- "verify_ssl": True,
- },
- "poe_control": True,
- },
- )
- entry.add_to_hass(hass)
- mock_registry = Mock()
- with patch.object(unifi, "UniFiController") as mock_controller, patch(
- "homeassistant.helpers.device_registry.async_get_registry",
- return_value=mock_coro(mock_registry),
- ):
- mock_controller.return_value.async_setup.return_value = mock_coro(True)
- mock_controller.return_value.mac = "00:11:22:33:44:55"
- assert await unifi.async_setup_entry(hass, entry) is True
-
- assert len(mock_controller.mock_calls) == 2
- p_hass, p_entry = mock_controller.mock_calls[0][1]
-
- assert p_hass is hass
- assert p_entry is entry
-
- assert len(mock_registry.mock_calls) == 1
- assert mock_registry.mock_calls[0][2] == {
- "config_entry_id": entry.entry_id,
- "connections": {("mac", "00:11:22:33:44:55")},
- "manufacturer": unifi.ATTR_MANUFACTURER,
- "model": "UniFi Controller",
- "name": "UniFi Controller",
- }
+ await setup_unifi_integration(hass)
+ assert hass.data[unifi.DOMAIN]
async def test_controller_fail_setup(hass):
"""Test that a failed setup still stores controller."""
- entry = MockConfigEntry(
- domain=unifi.DOMAIN,
- data={
- "controller": {
- "host": "0.0.0.0",
- "username": "user",
- "password": "pass",
- "port": 80,
- "site": "default",
- "verify_ssl": True,
- },
- "poe_control": True,
- },
- )
- entry.add_to_hass(hass)
-
with patch.object(unifi, "UniFiController") as mock_cntrlr:
mock_cntrlr.return_value.async_setup.return_value = mock_coro(False)
- assert await unifi.async_setup_entry(hass, entry) is False
+ await setup_unifi_integration(hass)
assert hass.data[unifi.DOMAIN] == {}
@@ -140,33 +90,8 @@ async def test_controller_no_mac(hass):
async def test_unload_entry(hass):
"""Test being able to unload an entry."""
- entry = MockConfigEntry(
- domain=unifi.DOMAIN,
- data={
- "controller": {
- "host": "0.0.0.0",
- "username": "user",
- "password": "pass",
- "port": 80,
- "site": "default",
- "verify_ssl": True,
- },
- "poe_control": True,
- },
- )
- entry.add_to_hass(hass)
+ controller = await setup_unifi_integration(hass)
+ assert hass.data[unifi.DOMAIN]
- with patch.object(unifi, "UniFiController") as mock_controller, patch(
- "homeassistant.helpers.device_registry.async_get_registry",
- return_value=mock_coro(Mock()),
- ):
- mock_controller.return_value.async_setup.return_value = mock_coro(True)
- mock_controller.return_value.mac = "00:11:22:33:44:55"
- assert await unifi.async_setup_entry(hass, entry) is True
-
- assert len(mock_controller.return_value.mock_calls) == 1
-
- mock_controller.return_value.async_reset.return_value = mock_coro(True)
- assert await unifi.async_unload_entry(hass, entry)
- assert len(mock_controller.return_value.async_reset.mock_calls) == 1
- assert hass.data[unifi.DOMAIN] == {}
+ assert await unifi.async_unload_entry(hass, controller.config_entry)
+ assert not hass.data[unifi.DOMAIN]
diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py
index 723d6871636..7d0600f5885 100644
--- a/tests/components/unifi/test_sensor.py
+++ b/tests/components/unifi/test_sensor.py
@@ -54,7 +54,7 @@ async def test_no_clients(hass):
hass, options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True},
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1
@@ -70,7 +70,7 @@ async def test_sensors(hass):
clients_response=CLIENTS,
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 5
wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
@@ -90,8 +90,32 @@ async def test_sensors(hass):
clients[1]["rx_bytes"] = 2345000000
clients[1]["tx_bytes"] = 6789000000
- controller.mock_client_responses.append(clients)
- await controller.async_update()
+ event = {"meta": {"message": "sta:sync"}, "data": clients}
+ controller.api.message_handler(event)
+ 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"
+
+ hass.config_entries.async_update_entry(
+ controller.config_entry,
+ options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: False},
+ )
+ await hass.async_block_till_done()
+
+ wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
+ assert wireless_client_rx is None
+
+ wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
+ assert wireless_client_tx is None
+
+ hass.config_entries.async_update_entry(
+ controller.config_entry,
+ options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True},
+ )
await hass.async_block_till_done()
wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py
index cc4c41bcbfd..a2b609078de 100644
--- a/tests/components/unifi/test_switch.py
+++ b/tests/components/unifi/test_switch.py
@@ -207,7 +207,7 @@ async def test_no_clients(hass):
},
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1
@@ -223,7 +223,7 @@ async def test_controller_not_client(hass):
devices_response=[DEVICE_1],
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1
cloudkey = hass.states.get("switch.cloud_key")
assert cloudkey is None
@@ -244,7 +244,7 @@ async def test_not_admin(hass):
devices_response=[DEVICE_1],
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1
@@ -262,7 +262,7 @@ async def test_switches(hass):
clients_all_response=[BLOCKED, UNBLOCKED, CLIENT_1],
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 4
switch_1 = hass.states.get("switch.poe_client_1")
@@ -297,18 +297,22 @@ async def test_new_client_discovered_on_block_control(hass):
clients_all_response=[BLOCKED],
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 2
- controller.mock_client_all_responses.append([BLOCKED])
+ controller.api.websocket._data = {
+ "meta": {"message": "sta:sync"},
+ "data": [BLOCKED],
+ }
+ controller.api.session_handler("data")
# 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(controller.mock_requests) == 5
assert len(hass.states.async_all()) == 2
- assert controller.mock_requests[3] == {
+ assert controller.mock_requests[4] == {
"json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"},
"method": "post",
"path": "s/{site}/cmd/stamgr/",
@@ -317,8 +321,8 @@ async def test_new_client_discovered_on_block_control(hass):
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] == {
+ assert len(controller.mock_requests) == 6
+ assert controller.mock_requests[5] == {
"json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"},
"method": "post",
"path": "s/{site}/cmd/stamgr/",
@@ -337,19 +341,22 @@ async def test_new_client_discovered_on_poe_control(hass):
devices_response=[DEVICE_1],
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 2
- controller.mock_client_responses.append([CLIENT_1, CLIENT_2])
- controller.mock_device_responses.append([DEVICE_1])
+ controller.api.websocket._data = {
+ "meta": {"message": "sta:sync"},
+ "data": [CLIENT_2],
+ }
+ controller.api.session_handler("data")
# 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(controller.mock_requests) == 6
+ assert len(controller.mock_requests) == 5
assert len(hass.states.async_all()) == 3
- assert controller.mock_requests[3] == {
+ assert controller.mock_requests[4] == {
"json": {
"port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}]
},
@@ -360,8 +367,8 @@ async def test_new_client_discovered_on_poe_control(hass):
await hass.services.async_call(
"switch", "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True
)
- assert len(controller.mock_requests) == 9
- assert controller.mock_requests[3] == {
+ assert len(controller.mock_requests) == 6
+ assert controller.mock_requests[4] == {
"json": {
"port_overrides": [
{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"}
@@ -386,7 +393,7 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass):
hass, clients_response=POE_SWITCH_CLIENTS, devices_response=[DEVICE_1],
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 4
switch_1 = hass.states.get("switch.poe_client_1")
@@ -437,7 +444,7 @@ async def test_restoring_client(hass):
clients_all_response=[CLIENT_1],
)
- assert len(controller.mock_requests) == 3
+ assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 3
device_1 = hass.states.get("switch.client_1")
diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py
index 07b5cb059bf..10fa026db29 100644
--- a/tests/components/updater/test_init.py
+++ b/tests/components/updater/test_init.py
@@ -1,20 +1,15 @@
"""The tests for the Updater component."""
import asyncio
-from datetime import timedelta
-from unittest.mock import Mock, patch
+from unittest.mock import Mock
+from asynctest import patch
import pytest
from homeassistant.components import updater
+from homeassistant.helpers.update_coordinator import UpdateFailed
from homeassistant.setup import async_setup_component
-import homeassistant.util.dt as dt_util
-from tests.common import (
- MockDependency,
- async_fire_time_changed,
- mock_component,
- mock_coro,
-)
+from tests.common import MockDependency, mock_component, mock_coro
NEW_VERSION = "10000.0"
MOCK_VERSION = "10.0"
@@ -32,95 +27,39 @@ def mock_distro():
yield
+@pytest.fixture(autouse=True)
+def mock_version():
+ """Mock current version."""
+ with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
+ yield
+
+
@pytest.fixture(name="mock_get_newest_version")
def mock_get_newest_version_fixture():
"""Fixture to mock get_newest_version."""
- with patch("homeassistant.components.updater.get_newest_version") as mock:
+ with patch(
+ "homeassistant.components.updater.get_newest_version",
+ return_value=(NEW_VERSION, RELEASE_NOTES),
+ ) as mock:
yield mock
-@pytest.fixture(name="mock_get_uuid")
+@pytest.fixture(name="mock_get_uuid", autouse=True)
def mock_get_uuid_fixture():
"""Fixture to mock get_uuid."""
with patch("homeassistant.components.updater._load_uuid") as mock:
yield mock
-@pytest.fixture(name="mock_utcnow")
-def mock_utcnow_fixture():
- """Fixture to mock utcnow."""
- with patch("homeassistant.components.updater.dt_util") as mock:
- yield mock.utcnow
-
-
-async def test_new_version_shows_entity_startup(
- hass, mock_get_uuid, mock_get_newest_version
-):
- """Test if binary sensor is unavailable at first."""
- mock_get_uuid.return_value = MOCK_HUUID
- mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES))
-
- res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
- assert res, "Updater failed to set up"
-
- await hass.async_block_till_done()
- assert hass.states.is_state("binary_sensor.updater", "unavailable")
- assert "newest_version" not in hass.states.get("binary_sensor.updater").attributes
- assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes
-
-
-async def test_rename_entity(hass, mock_get_uuid, mock_get_newest_version, mock_utcnow):
- """Test if renaming the binary sensor works correctly."""
- mock_get_uuid.return_value = MOCK_HUUID
- mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES))
-
- now = dt_util.utcnow()
- later = now + timedelta(hours=1)
- mock_utcnow.return_value = now
-
- res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
- assert res, "Updater failed to set up"
-
- await hass.async_block_till_done()
- assert hass.states.is_state("binary_sensor.updater", "unavailable")
- assert hass.states.get("binary_sensor.new_entity_id") is None
-
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- entity_registry.async_update_entity(
- "binary_sensor.updater", new_entity_id="binary_sensor.new_entity_id"
- )
-
- await hass.async_block_till_done()
- assert hass.states.is_state("binary_sensor.new_entity_id", "unavailable")
- assert hass.states.get("binary_sensor.updater") is None
-
- with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
- async_fire_time_changed(hass, later)
- await hass.async_block_till_done()
-
- assert hass.states.is_state("binary_sensor.new_entity_id", "on")
- assert hass.states.get("binary_sensor.updater") is None
-
-
async def test_new_version_shows_entity_true(
- hass, mock_get_uuid, mock_get_newest_version, mock_utcnow
+ hass, mock_get_uuid, mock_get_newest_version
):
"""Test if sensor is true if new version is available."""
mock_get_uuid.return_value = MOCK_HUUID
- mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES))
- now = dt_util.utcnow()
- later = now + timedelta(hours=1)
- mock_utcnow.return_value = now
-
- res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
- assert res, "Updater failed to set up"
+ assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
await hass.async_block_till_done()
- with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
- async_fire_time_changed(hass, later)
- await hass.async_block_till_done()
-
assert hass.states.is_state("binary_sensor.updater", "on")
assert (
hass.states.get("binary_sensor.updater").attributes["newest_version"]
@@ -133,23 +72,15 @@ async def test_new_version_shows_entity_true(
async def test_same_version_shows_entity_false(
- hass, mock_get_uuid, mock_get_newest_version, mock_utcnow
+ hass, mock_get_uuid, mock_get_newest_version
):
"""Test if sensor is false if no new version is available."""
mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, ""))
- now = dt_util.utcnow()
- later = now + timedelta(hours=1)
- mock_utcnow.return_value = now
-
- res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
- assert res, "Updater failed to set up"
+ assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
await hass.async_block_till_done()
- with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
- async_fire_time_changed(hass, later)
- await hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.updater", "off")
assert (
@@ -159,29 +90,18 @@ async def test_same_version_shows_entity_false(
assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes
-async def test_disable_reporting(
- hass, mock_get_uuid, mock_get_newest_version, mock_utcnow
-):
+async def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version):
"""Test we do not gather analytics when disable reporting is active."""
mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, ""))
- now = dt_util.utcnow()
- later = now + timedelta(hours=1)
- mock_utcnow.return_value = now
-
- res = await async_setup_component(
+ assert await async_setup_component(
hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": False}}
)
- assert res, "Updater failed to set up"
-
await hass.async_block_till_done()
- with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
- async_fire_time_changed(hass, later)
- await hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.updater", "off")
- res = await updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG)
+ await updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG)
call = mock_get_newest_version.mock_calls[0][1]
assert call[0] is hass
assert call[1] is None
@@ -215,9 +135,10 @@ async def test_error_fetching_new_version_timeout(hass):
with patch(
"homeassistant.helpers.system_info.async_get_system_info",
Mock(return_value=mock_coro({"fake": "bla"})),
- ), patch("async_timeout.timeout", side_effect=asyncio.TimeoutError):
- res = await updater.get_newest_version(hass, MOCK_HUUID, False)
- assert res is None
+ ), patch("async_timeout.timeout", side_effect=asyncio.TimeoutError), pytest.raises(
+ UpdateFailed
+ ):
+ await updater.get_newest_version(hass, MOCK_HUUID, False)
async def test_error_fetching_new_version_bad_json(hass, aioclient_mock):
@@ -227,9 +148,8 @@ async def test_error_fetching_new_version_bad_json(hass, aioclient_mock):
with patch(
"homeassistant.helpers.system_info.async_get_system_info",
Mock(return_value=mock_coro({"fake": "bla"})),
- ):
- res = await updater.get_newest_version(hass, MOCK_HUUID, False)
- assert res is None
+ ), pytest.raises(UpdateFailed):
+ await updater.get_newest_version(hass, MOCK_HUUID, False)
async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock):
@@ -245,31 +165,21 @@ async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock)
with patch(
"homeassistant.helpers.system_info.async_get_system_info",
Mock(return_value=mock_coro({"fake": "bla"})),
- ):
- res = await updater.get_newest_version(hass, MOCK_HUUID, False)
- assert res is None
+ ), pytest.raises(UpdateFailed):
+ await updater.get_newest_version(hass, MOCK_HUUID, False)
async def test_new_version_shows_entity_after_hour_hassio(
- hass, mock_get_uuid, mock_get_newest_version, mock_utcnow
+ hass, mock_get_uuid, mock_get_newest_version
):
"""Test if binary sensor gets updated if new version is available / Hass.io."""
mock_get_uuid.return_value = MOCK_HUUID
- mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES))
mock_component(hass, "hassio")
hass.data["hassio_hass_version"] = "999.0"
- now = dt_util.utcnow()
- later = now + timedelta(hours=1)
- mock_utcnow.return_value = now
-
- res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
- assert res, "Updater failed to set up"
+ assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
await hass.async_block_till_done()
- with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
- async_fire_time_changed(hass, later)
- await hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.updater", "on")
assert (
diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py
index 66273e01f43..daeffb4ed1d 100644
--- a/tests/components/velbus/test_config_flow.py
+++ b/tests/components/velbus/test_config_flow.py
@@ -22,7 +22,7 @@ def mock_controller_assert():
@pytest.fixture(name="controller")
def mock_controller():
- """Mock a successfull velbus controller."""
+ """Mock a successful velbus controller."""
controller = Mock()
with patch("velbus.Controller", return_value=controller):
yield controller
diff --git a/tests/components/vilfo/__init__.py b/tests/components/vilfo/__init__.py
new file mode 100644
index 00000000000..680b556fc12
--- /dev/null
+++ b/tests/components/vilfo/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Vilfo Router integration."""
diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py
new file mode 100644
index 00000000000..d73d15df8dd
--- /dev/null
+++ b/tests/components/vilfo/test_config_flow.py
@@ -0,0 +1,184 @@
+"""Test the Vilfo Router config flow."""
+from unittest.mock import patch
+
+import vilfo
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.vilfo.const import DOMAIN
+from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC
+
+from tests.common import mock_coro
+
+
+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"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch("vilfo.Client.ping", return_value=None), patch(
+ "vilfo.Client.get_board_information", return_value=None,
+ ), patch(
+ "homeassistant.components.vilfo.async_setup", return_value=mock_coro(True)
+ ) as mock_setup, patch(
+ "homeassistant.components.vilfo.async_setup_entry",
+ return_value=mock_coro(True),
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == "testadmin.vilfo.com"
+ assert result2["data"] == {
+ "host": "testadmin.vilfo.com",
+ "access_token": "test-token",
+ }
+
+ 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_invalid_auth(hass):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch("vilfo.Client.ping", return_value=None), patch(
+ "vilfo.Client.get_board_information",
+ side_effect=vilfo.exceptions.AuthenticationException,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "testadmin.vilfo.com", "access_token": "test-token"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "testadmin.vilfo.com", "access_token": "test-token"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+ with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException):
+ result3 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "testadmin.vilfo.com", "access_token": "test-token"},
+ )
+
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result3["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_wrong_host(hass):
+ """Test we handle wrong host errors."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data={"host": "this is an invalid hostname", "access_token": "test-token"},
+ )
+
+ assert result["errors"] == {"host": "wrong_host"}
+
+
+async def test_form_already_configured(hass):
+ """Test that we handle already configured exceptions appropriately."""
+ first_flow_result1 = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch("vilfo.Client.ping", return_value=None), patch(
+ "vilfo.Client.get_board_information", return_value=None,
+ ):
+ first_flow_result2 = await hass.config_entries.flow.async_configure(
+ first_flow_result1["flow_id"],
+ {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"},
+ )
+
+ second_flow_result1 = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch("vilfo.Client.ping", return_value=None), patch(
+ "vilfo.Client.get_board_information", return_value=None,
+ ):
+ second_flow_result2 = await hass.config_entries.flow.async_configure(
+ second_flow_result1["flow_id"],
+ {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"},
+ )
+
+ assert first_flow_result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert second_flow_result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert second_flow_result2["reason"] == "already_configured"
+
+
+async def test_form_unexpected_exception(hass):
+ """Test that we handle unexpected exceptions."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch("vilfo.Client.ping", side_effect=Exception):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "testadmin.vilfo.com", "access_token": "test-token"},
+ )
+
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_validate_input_returns_data(hass):
+ """Test we handle the MAC address being resolved or not."""
+ mock_data = {"host": "testadmin.vilfo.com", "access_token": "test-token"}
+ mock_data_with_ip = {"host": "192.168.0.1", "access_token": "test-token"}
+ mock_mac = "FF-00-00-00-00-00"
+
+ with patch("vilfo.Client.ping", return_value=None), patch(
+ "vilfo.Client.get_board_information", return_value=None
+ ):
+ result = await hass.components.vilfo.config_flow.validate_input(
+ hass, data=mock_data
+ )
+
+ assert result["title"] == mock_data["host"]
+ assert result[CONF_HOST] == mock_data["host"]
+ assert result[CONF_MAC] is None
+ assert result[CONF_ID] == mock_data["host"]
+
+ with patch("vilfo.Client.ping", return_value=None), patch(
+ "vilfo.Client.get_board_information", return_value=None
+ ), patch("vilfo.Client.resolve_mac_address", return_value=mock_mac):
+ result2 = await hass.components.vilfo.config_flow.validate_input(
+ hass, data=mock_data
+ )
+ result3 = await hass.components.vilfo.config_flow.validate_input(
+ hass, data=mock_data_with_ip
+ )
+
+ assert result2["title"] == mock_data["host"]
+ assert result2[CONF_HOST] == mock_data["host"]
+ assert result2[CONF_MAC] == mock_mac
+ assert result2[CONF_ID] == mock_mac
+
+ assert result3["title"] == mock_data_with_ip["host"]
+ assert result3[CONF_HOST] == mock_data_with_ip["host"]
+ assert result3[CONF_MAC] == mock_mac
+ assert result3[CONF_ID] == mock_mac
diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py
new file mode 100644
index 00000000000..581ea7cdd5c
--- /dev/null
+++ b/tests/components/vizio/conftest.py
@@ -0,0 +1,107 @@
+"""Configure py.test."""
+from asynctest import patch
+import pytest
+from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME
+
+from .const import CURRENT_INPUT, INPUT_LIST, MODEL, UNIQUE_ID, VERSION
+
+
+class MockInput:
+ """Mock Vizio device input."""
+
+ def __init__(self, name):
+ """Initialize mock Vizio device input."""
+ self.meta_name = name
+ self.name = name
+
+
+def get_mock_inputs(input_list):
+ """Return list of MockInput."""
+ return [MockInput(input) for input in input_list]
+
+
+@pytest.fixture(name="skip_notifications", autouse=True)
+def skip_notifications_fixture():
+ """Skip notification calls."""
+ with patch("homeassistant.components.persistent_notification.async_create"), patch(
+ "homeassistant.components.persistent_notification.async_dismiss"
+ ):
+ yield
+
+
+@pytest.fixture(name="vizio_connect")
+def vizio_connect_fixture():
+ """Mock valid vizio device and entry setup."""
+ with patch(
+ "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id",
+ return_value=UNIQUE_ID,
+ ):
+ yield
+
+
+@pytest.fixture(name="vizio_bypass_setup")
+def vizio_bypass_setup_fixture():
+ """Mock component setup."""
+ with patch("homeassistant.components.vizio.async_setup_entry", return_value=True):
+ yield
+
+
+@pytest.fixture(name="vizio_bypass_update")
+def vizio_bypass_update_fixture():
+ """Mock component update."""
+ with patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.can_connect",
+ return_value=True,
+ ), patch("homeassistant.components.vizio.media_player.VizioDevice.async_update"):
+ yield
+
+
+@pytest.fixture(name="vizio_guess_device_type")
+def vizio_guess_device_type_fixture():
+ """Mock vizio async_guess_device_type function."""
+ with patch(
+ "homeassistant.components.vizio.config_flow.async_guess_device_type",
+ return_value="speaker",
+ ):
+ yield
+
+
+@pytest.fixture(name="vizio_cant_connect")
+def vizio_cant_connect_fixture():
+ """Mock vizio device can't connect."""
+ with patch(
+ "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
+ return_value=False,
+ ):
+ yield
+
+
+@pytest.fixture(name="vizio_update")
+def vizio_update_fixture():
+ """Mock valid updates to vizio device."""
+ with patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.can_connect",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_current_volume",
+ return_value=int(MAX_VOLUME[DEVICE_CLASS_SPEAKER] / 2),
+ ), patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_current_input",
+ return_value=CURRENT_INPUT,
+ ), patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list",
+ return_value=get_mock_inputs(INPUT_LIST),
+ ), patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_power_state",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_model",
+ return_value=MODEL,
+ ), patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_version",
+ return_value=VERSION,
+ ):
+ yield
diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py
new file mode 100644
index 00000000000..537db445a85
--- /dev/null
+++ b/tests/components/vizio/const.py
@@ -0,0 +1,79 @@
+"""Constants for the Vizio integration tests."""
+import logging
+
+from homeassistant.components.media_player import (
+ DEVICE_CLASS_SPEAKER,
+ DEVICE_CLASS_TV,
+ DOMAIN as MP_DOMAIN,
+)
+from homeassistant.components.vizio.const import CONF_VOLUME_STEP
+from homeassistant.const import (
+ CONF_ACCESS_TOKEN,
+ CONF_DEVICE_CLASS,
+ CONF_HOST,
+ CONF_NAME,
+ CONF_PORT,
+ CONF_TYPE,
+)
+from homeassistant.util import slugify
+
+_LOGGER = logging.getLogger(__name__)
+
+NAME = "Vizio"
+NAME2 = "Vizio2"
+HOST = "192.168.1.1:9000"
+HOST2 = "192.168.1.2:9000"
+ACCESS_TOKEN = "deadbeef"
+VOLUME_STEP = 2
+UNIQUE_ID = "testid"
+MODEL = "model"
+VERSION = "version"
+
+MOCK_USER_VALID_TV_CONFIG = {
+ CONF_NAME: NAME,
+ CONF_HOST: HOST,
+ CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
+ CONF_ACCESS_TOKEN: ACCESS_TOKEN,
+}
+
+MOCK_OPTIONS = {
+ CONF_VOLUME_STEP: VOLUME_STEP,
+}
+
+MOCK_IMPORT_VALID_TV_CONFIG = {
+ CONF_NAME: NAME,
+ CONF_HOST: HOST,
+ CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
+ CONF_ACCESS_TOKEN: ACCESS_TOKEN,
+ CONF_VOLUME_STEP: VOLUME_STEP,
+}
+
+MOCK_INVALID_TV_CONFIG = {
+ CONF_NAME: NAME,
+ CONF_HOST: HOST,
+ CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
+}
+
+MOCK_SPEAKER_CONFIG = {
+ CONF_NAME: NAME,
+ CONF_HOST: HOST,
+ CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER,
+}
+
+VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local."
+ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}"
+ZEROCONF_HOST = HOST.split(":")[0]
+ZEROCONF_PORT = HOST.split(":")[1]
+
+MOCK_ZEROCONF_SERVICE_INFO = {
+ CONF_TYPE: VIZIO_ZEROCONF_SERVICE_TYPE,
+ CONF_NAME: ZEROCONF_NAME,
+ CONF_HOST: ZEROCONF_HOST,
+ CONF_PORT: ZEROCONF_PORT,
+ "properties": {"name": "SB4031-D5"},
+}
+
+CURRENT_INPUT = "HDMI"
+INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"]
+
+ENTITY_ID = f"{MP_DOMAIN}.{slugify(NAME)}"
diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py
index cf6cdb6afdb..9683ebd87b0 100644
--- a/tests/components/vizio/test_config_flow.py
+++ b/tests/components/vizio/test_config_flow.py
@@ -1,7 +1,4 @@
"""Tests for Vizio config flow."""
-import logging
-
-from asynctest import patch
import pytest
import voluptuous as vol
@@ -20,113 +17,26 @@ from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_HOST,
CONF_NAME,
- CONF_PORT,
- CONF_TYPE,
)
from homeassistant.helpers.typing import HomeAssistantType
+from .const import (
+ ACCESS_TOKEN,
+ HOST,
+ HOST2,
+ MOCK_IMPORT_VALID_TV_CONFIG,
+ MOCK_INVALID_TV_CONFIG,
+ MOCK_SPEAKER_CONFIG,
+ MOCK_USER_VALID_TV_CONFIG,
+ MOCK_ZEROCONF_SERVICE_INFO,
+ NAME,
+ NAME2,
+ UNIQUE_ID,
+ VOLUME_STEP,
+)
+
from tests.common import MockConfigEntry
-_LOGGER = logging.getLogger(__name__)
-
-NAME = "Vizio"
-NAME2 = "Vizio2"
-HOST = "192.168.1.1:9000"
-HOST2 = "192.168.1.2:9000"
-ACCESS_TOKEN = "deadbeef"
-VOLUME_STEP = 2
-UNIQUE_ID = "testid"
-
-MOCK_USER_VALID_TV_CONFIG = {
- CONF_NAME: NAME,
- CONF_HOST: HOST,
- CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
- CONF_ACCESS_TOKEN: ACCESS_TOKEN,
-}
-
-MOCK_IMPORT_VALID_TV_CONFIG = {
- CONF_NAME: NAME,
- CONF_HOST: HOST,
- CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
- CONF_ACCESS_TOKEN: ACCESS_TOKEN,
- CONF_VOLUME_STEP: VOLUME_STEP,
-}
-
-MOCK_INVALID_TV_CONFIG = {
- CONF_NAME: NAME,
- CONF_HOST: HOST,
- CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
-}
-
-MOCK_SPEAKER_CONFIG = {
- CONF_NAME: NAME,
- CONF_HOST: HOST,
- CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER,
-}
-
-VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local."
-ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}"
-ZEROCONF_HOST = HOST.split(":")[0]
-ZEROCONF_PORT = HOST.split(":")[1]
-
-MOCK_ZEROCONF_ENTRY = {
- CONF_TYPE: VIZIO_ZEROCONF_SERVICE_TYPE,
- CONF_NAME: ZEROCONF_NAME,
- CONF_HOST: ZEROCONF_HOST,
- CONF_PORT: ZEROCONF_PORT,
- "properties": {"name": "SB4031-D5"},
-}
-
-
-@pytest.fixture(name="vizio_connect")
-def vizio_connect_fixture():
- """Mock valid vizio device and entry setup."""
- with patch(
- "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
- return_value=True,
- ), patch(
- "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id",
- return_value=UNIQUE_ID,
- ):
- yield
-
-
-@pytest.fixture(name="vizio_bypass_setup")
-def vizio_bypass_setup_fixture():
- """Mock component setup."""
- with patch("homeassistant.components.vizio.async_setup_entry", return_value=True):
- yield
-
-
-@pytest.fixture(name="vizio_bypass_update")
-def vizio_bypass_update_fixture():
- """Mock component update."""
- with patch(
- "homeassistant.components.vizio.media_player.VizioAsync.can_connect",
- return_value=True,
- ), patch("homeassistant.components.vizio.media_player.VizioDevice.async_update"):
- yield
-
-
-@pytest.fixture(name="vizio_guess_device_type")
-def vizio_guess_device_type_fixture():
- """Mock vizio async_guess_device_type function."""
- with patch(
- "homeassistant.components.vizio.config_flow.async_guess_device_type",
- return_value="speaker",
- ):
- yield
-
-
-@pytest.fixture(name="vizio_cant_connect")
-def vizio_cant_connect_fixture():
- """Mock vizio device cant connect."""
- with patch(
- "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
- return_value=False,
- ):
- yield
-
async def test_user_flow_minimum_fields(
hass: HomeAssistantType,
@@ -142,12 +52,7 @@ async def test_user_flow_minimum_fields(
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={
- CONF_NAME: NAME,
- CONF_HOST: HOST,
- CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER,
- },
+ result["flow_id"], user_input=MOCK_SPEAKER_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
@@ -172,13 +77,7 @@ async def test_user_flow_all_fields(
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={
- CONF_NAME: NAME,
- CONF_HOST: HOST,
- CONF_DEVICE_CLASS: DEVICE_CLASS_TV,
- CONF_ACCESS_TOKEN: ACCESS_TOKEN,
- },
+ result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
@@ -401,15 +300,15 @@ async def test_import_flow_update_options(
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
- data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG),
+ data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG),
)
await hass.async_block_till_done()
- assert result["result"].options == {CONF_VOLUME_STEP: VOLUME_STEP}
+ assert result["result"].options == {CONF_VOLUME_STEP: DEFAULT_VOLUME_STEP}
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
entry_id = result["result"].entry_id
- updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy()
+ updated_config = MOCK_SPEAKER_CONFIG.copy()
updated_config[CONF_VOLUME_STEP] = VOLUME_STEP + 1
result = await hass.config_entries.flow.async_init(
DOMAIN,
@@ -462,7 +361,7 @@ async def test_zeroconf_flow(
vizio_guess_device_type: pytest.fixture,
) -> None:
"""Test zeroconf config flow."""
- discovery_info = MOCK_ZEROCONF_ENTRY.copy()
+ discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info
)
@@ -471,7 +370,7 @@ async def test_zeroconf_flow(
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
- # Apply discovery updates to entry to mimick when user hits submit without changing
+ # Apply discovery updates to entry to mimic when user hits submit without changing
# defaults which were set from discovery parameters
user_input = result["data_schema"](discovery_info)
@@ -498,7 +397,7 @@ async def test_zeroconf_flow_already_configured(
entry.add_to_hass(hass)
# Try rediscovering same device
- discovery_info = MOCK_ZEROCONF_ENTRY.copy()
+ discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info
)
@@ -506,3 +405,29 @@ async def test_zeroconf_flow_already_configured(
# Flow should abort because device is already setup
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_setup"
+
+
+async def test_zeroconf_dupe_fail(
+ hass: HomeAssistantType,
+ vizio_connect: pytest.fixture,
+ vizio_bypass_setup: pytest.fixture,
+ vizio_guess_device_type: pytest.fixture,
+) -> None:
+ """Test zeroconf config flow when device gets discovered multiple times."""
+ discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy()
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info
+ )
+
+ # Form should always show even if all required properties are discovered
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy()
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info
+ )
+
+ # Flow should abort because device is already setup
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_in_progress"
diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py
new file mode 100644
index 00000000000..1be067e9570
--- /dev/null
+++ b/tests/components/vizio/test_init.py
@@ -0,0 +1,43 @@
+"""Tests for Vizio init."""
+import pytest
+
+from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN
+from homeassistant.components.vizio.const import DOMAIN
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.setup import async_setup_component
+
+from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID
+
+from tests.common import MockConfigEntry
+
+
+async def test_setup_component(
+ hass: HomeAssistantType,
+ vizio_connect: pytest.fixture,
+ vizio_update: pytest.fixture,
+) -> None:
+ """Test component setup."""
+ assert await async_setup_component(
+ hass, DOMAIN, {DOMAIN: MOCK_USER_VALID_TV_CONFIG}
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1
+
+
+async def test_load_and_unload(
+ hass: HomeAssistantType,
+ vizio_connect: pytest.fixture,
+ vizio_update: pytest.fixture,
+) -> None:
+ """Test loading and unloading entry."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID
+ )
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1
+
+ assert await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0
diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py
new file mode 100644
index 00000000000..bbbbca8c359
--- /dev/null
+++ b/tests/components/vizio/test_media_player.py
@@ -0,0 +1,297 @@
+"""Tests for Vizio config flow."""
+from datetime import timedelta
+from unittest.mock import call
+
+from asynctest import patch
+import pytest
+from pyvizio.const import (
+ DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER,
+ DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV,
+ MAX_VOLUME,
+)
+
+from homeassistant.components.media_player import (
+ ATTR_INPUT_SOURCE,
+ ATTR_MEDIA_VOLUME_LEVEL,
+ ATTR_MEDIA_VOLUME_MUTED,
+ DEVICE_CLASS_SPEAKER,
+ DEVICE_CLASS_TV,
+ DOMAIN as MP_DOMAIN,
+ SERVICE_MEDIA_NEXT_TRACK,
+ SERVICE_MEDIA_PREVIOUS_TRACK,
+ SERVICE_SELECT_SOURCE,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ SERVICE_VOLUME_DOWN,
+ SERVICE_VOLUME_MUTE,
+ SERVICE_VOLUME_SET,
+ SERVICE_VOLUME_UP,
+)
+from homeassistant.components.vizio.const import CONF_VOLUME_STEP, DOMAIN
+from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.util import dt as dt_util
+
+from .const import (
+ CURRENT_INPUT,
+ ENTITY_ID,
+ INPUT_LIST,
+ MOCK_SPEAKER_CONFIG,
+ MOCK_USER_VALID_TV_CONFIG,
+ NAME,
+ UNIQUE_ID,
+ VOLUME_STEP,
+)
+
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+async def _test_setup(
+ hass: HomeAssistantType, ha_device_class: str, vizio_power_state: bool
+) -> None:
+ """Test Vizio Device entity setup."""
+ if vizio_power_state:
+ ha_power_state = STATE_ON
+ elif vizio_power_state is False:
+ ha_power_state = STATE_OFF
+ else:
+ ha_power_state = STATE_UNAVAILABLE
+
+ if ha_device_class == DEVICE_CLASS_SPEAKER:
+ vizio_device_class = VIZIO_DEVICE_CLASS_SPEAKER
+ config_entry = MockConfigEntry(
+ domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID
+ )
+ else:
+ vizio_device_class = VIZIO_DEVICE_CLASS_TV
+ config_entry = MockConfigEntry(
+ domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID
+ )
+
+ with patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_current_volume",
+ return_value=int(MAX_VOLUME[vizio_device_class] / 2),
+ ), patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_power_state",
+ return_value=vizio_power_state,
+ ):
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ attr = hass.states.get(ENTITY_ID).attributes
+ assert attr["friendly_name"] == NAME
+ assert attr["device_class"] == ha_device_class
+
+ assert hass.states.get(ENTITY_ID).state == ha_power_state
+ if ha_power_state == STATE_ON:
+ assert attr["source_list"] == INPUT_LIST
+ assert attr["source"] == CURRENT_INPUT
+ assert (
+ attr["volume_level"]
+ == float(int(MAX_VOLUME[vizio_device_class] / 2))
+ / MAX_VOLUME[vizio_device_class]
+ )
+
+
+async def _test_setup_failure(hass: HomeAssistantType, config: str) -> None:
+ """Test generic Vizio entity setup failure."""
+ with patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.can_connect",
+ return_value=False,
+ ):
+ config_entry = MockConfigEntry(domain=DOMAIN, data=config, unique_id=UNIQUE_ID)
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0
+
+
+async def _test_service(
+ hass: HomeAssistantType,
+ vizio_func_name: str,
+ ha_service_name: str,
+ additional_service_data: dict,
+ *args,
+ **kwargs,
+) -> None:
+ """Test generic Vizio media player entity service."""
+ service_data = {ATTR_ENTITY_ID: ENTITY_ID}
+ if additional_service_data:
+ service_data.update(additional_service_data)
+
+ with patch(
+ f"homeassistant.components.vizio.media_player.VizioAsync.{vizio_func_name}"
+ ) as service_call:
+ await hass.services.async_call(
+ MP_DOMAIN, ha_service_name, service_data=service_data, blocking=True,
+ )
+ assert service_call.called
+
+ if args or kwargs:
+ assert service_call.call_args == call(*args, **kwargs)
+
+
+async def test_speaker_on(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture
+) -> None:
+ """Test Vizio Speaker entity setup when on."""
+ await _test_setup(hass, DEVICE_CLASS_SPEAKER, True)
+
+
+async def test_speaker_off(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture
+) -> None:
+ """Test Vizio Speaker entity setup when off."""
+ await _test_setup(hass, DEVICE_CLASS_SPEAKER, False)
+
+
+async def test_speaker_unavailable(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture
+) -> None:
+ """Test Vizio Speaker entity setup when unavailable."""
+ await _test_setup(hass, DEVICE_CLASS_SPEAKER, None)
+
+
+async def test_init_tv_on(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture
+) -> None:
+ """Test Vizio TV entity setup when on."""
+ await _test_setup(hass, DEVICE_CLASS_TV, True)
+
+
+async def test_init_tv_off(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture
+) -> None:
+ """Test Vizio TV entity setup when off."""
+ await _test_setup(hass, DEVICE_CLASS_TV, False)
+
+
+async def test_init_tv_unavailable(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture
+) -> None:
+ """Test Vizio TV entity setup when unavailable."""
+ await _test_setup(hass, DEVICE_CLASS_TV, None)
+
+
+async def test_setup_failure_speaker(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture
+) -> None:
+ """Test speaker entity setup failure."""
+ await _test_setup_failure(hass, MOCK_SPEAKER_CONFIG)
+
+
+async def test_setup_failure_tv(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture
+) -> None:
+ """Test TV entity setup failure."""
+ await _test_setup_failure(hass, MOCK_USER_VALID_TV_CONFIG)
+
+
+async def test_services(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture
+) -> None:
+ """Test all Vizio media player entity services."""
+ await _test_setup(hass, DEVICE_CLASS_TV, True)
+
+ await _test_service(hass, "pow_on", SERVICE_TURN_ON, None)
+ await _test_service(hass, "pow_off", SERVICE_TURN_OFF, None)
+ await _test_service(
+ hass, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True}
+ )
+ await _test_service(
+ hass, "mute_off", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False}
+ )
+ await _test_service(
+ hass, "set_input", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "USB"}, "USB"
+ )
+ await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, None)
+ await _test_service(hass, "vol_down", SERVICE_VOLUME_DOWN, None)
+ await _test_service(
+ hass, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1}
+ )
+ await _test_service(
+ hass, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0}
+ )
+ await _test_service(hass, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None)
+ await _test_service(hass, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None)
+
+
+async def test_options_update(
+ hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture
+) -> None:
+ """Test when config entry update event fires."""
+ await _test_setup(hass, DEVICE_CLASS_SPEAKER, True)
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert config_entry.options
+ new_options = config_entry.options.copy()
+ updated_options = {CONF_VOLUME_STEP: VOLUME_STEP}
+ new_options.update(updated_options)
+ hass.config_entries.async_update_entry(
+ entry=config_entry, options=new_options,
+ )
+ assert config_entry.options == updated_options
+ await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, None, num=VOLUME_STEP)
+
+
+async def _test_update_availability_switch(
+ hass: HomeAssistantType,
+ initial_power_state: bool,
+ final_power_state: bool,
+ caplog: pytest.fixture,
+) -> None:
+ now = dt_util.utcnow()
+ future_interval = timedelta(minutes=1)
+
+ # Setup device as if time is right now
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ await _test_setup(hass, DEVICE_CLASS_SPEAKER, initial_power_state)
+
+ # Clear captured logs so that only availability state changes are captured for
+ # future assertion
+ caplog.clear()
+
+ # Fast forward time to future twice to trigger update and assert vizio log message
+ for i in range(1, 3):
+ future = now + (future_interval * i)
+ with patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_power_state",
+ return_value=final_power_state,
+ ), patch("homeassistant.util.dt.utcnow", return_value=future), patch(
+ "homeassistant.util.utcnow", return_value=future
+ ):
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+ if final_power_state is None:
+ assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
+ else:
+ assert hass.states.get(ENTITY_ID).state != STATE_UNAVAILABLE
+
+ # Ensure connection status messages from vizio.media_player appear exactly once
+ # (on availability state change)
+ vizio_log_list = [
+ log
+ for log in caplog.records
+ if log.name == "homeassistant.components.vizio.media_player"
+ ]
+ assert len(vizio_log_list) == 1
+
+
+async def test_update_unavailable_to_available(
+ hass: HomeAssistantType,
+ vizio_connect: pytest.fixture,
+ vizio_update: pytest.fixture,
+ caplog: pytest.fixture,
+) -> None:
+ """Test device becomes available after being unavailable."""
+ await _test_update_availability_switch(hass, None, True, caplog)
+
+
+async def test_update_available_to_unavailable(
+ hass: HomeAssistantType,
+ vizio_connect: pytest.fixture,
+ vizio_update: pytest.fixture,
+ caplog: pytest.fixture,
+) -> None:
+ """Test device becomes unavailable after being available."""
+ await _test_update_availability_switch(hass, True, None, caplog)
diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py
index d2a7197fe1a..a65201735ae 100644
--- a/tests/components/voicerss/test_tts.py
+++ b/tests/components/voicerss/test_tts.py
@@ -67,7 +67,10 @@ class TestTTSVoiceRSSPlatform:
self.hass.services.call(
tts.DOMAIN,
"voicerss_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "I person is on front of your door.",
+ },
)
self.hass.block_till_done()
@@ -97,7 +100,10 @@ class TestTTSVoiceRSSPlatform:
self.hass.services.call(
tts.DOMAIN,
"voicerss_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "I person is on front of your door.",
+ },
)
self.hass.block_till_done()
@@ -121,6 +127,7 @@ class TestTTSVoiceRSSPlatform:
tts.DOMAIN,
"voicerss_say",
{
+ "entity_id": "media_player.something",
tts.ATTR_MESSAGE: "I person is on front of your door.",
tts.ATTR_LANGUAGE: "de-de",
},
@@ -145,7 +152,10 @@ class TestTTSVoiceRSSPlatform:
self.hass.services.call(
tts.DOMAIN,
"voicerss_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "I person is on front of your door.",
+ },
)
self.hass.block_till_done()
@@ -167,7 +177,10 @@ class TestTTSVoiceRSSPlatform:
self.hass.services.call(
tts.DOMAIN,
"voicerss_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "I person is on front of your door.",
+ },
)
self.hass.block_till_done()
@@ -194,7 +207,10 @@ class TestTTSVoiceRSSPlatform:
self.hass.services.call(
tts.DOMAIN,
"voicerss_say",
- {tts.ATTR_MESSAGE: "I person is on front of your door."},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "I person is on front of your door.",
+ },
)
self.hass.block_till_done()
diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py
index 4da60783c44..80fd05a41cc 100644
--- a/tests/components/vultr/test_sensor.py
+++ b/tests/components/vultr/test_sensor.py
@@ -10,7 +10,12 @@ import voluptuous as vol
from homeassistant.components import vultr as base_vultr
from homeassistant.components.vultr import CONF_SUBSCRIPTION
import homeassistant.components.vultr.sensor as vultr
-from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PLATFORM
+from homeassistant.const import (
+ CONF_MONITORED_CONDITIONS,
+ CONF_NAME,
+ CONF_PLATFORM,
+ DATA_GIGABYTES,
+)
from tests.common import get_test_home_assistant, load_fixture
from tests.components.vultr.test_init import VALID_CONFIG
@@ -83,7 +88,7 @@ class TestVultrSensorSetup(unittest.TestCase):
device.update()
- if device.unit_of_measurement == "GB": # Test Bandwidth Used
+ if device.unit_of_measurement == DATA_GIGABYTES: # Test Bandwidth Used
if device.subscription == "576965":
assert "Vultr my new server Current Bandwidth Used" == device.name
assert "mdi:chart-histogram" == device.icon
diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py
index 779e39c67ce..894968f5db4 100644
--- a/tests/components/wled/test_sensor.py
+++ b/tests/components/wled/test_sensor.py
@@ -8,10 +8,9 @@ from homeassistant.components.wled.const import (
ATTR_LED_COUNT,
ATTR_MAX_POWER,
CURRENT_MA,
- DATA_BYTES,
DOMAIN,
)
-from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT
+from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DATA_BYTES
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py
index edd5c058f12..182c629d795 100644
--- a/tests/components/yandextts/test_tts.py
+++ b/tests/components/yandextts/test_tts.py
@@ -67,7 +67,9 @@ class TestTTSYandexPlatform:
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
- tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "yandextts_say",
+ {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"},
)
self.hass.block_till_done()
@@ -103,7 +105,9 @@ class TestTTSYandexPlatform:
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
- tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "yandextts_say",
+ {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"},
)
self.hass.block_till_done()
@@ -135,7 +139,11 @@ class TestTTSYandexPlatform:
self.hass.services.call(
tts.DOMAIN,
"yandextts_say",
- {tts.ATTR_MESSAGE: "HomeAssistant", tts.ATTR_LANGUAGE: "ru-RU"},
+ {
+ "entity_id": "media_player.something",
+ tts.ATTR_MESSAGE: "HomeAssistant",
+ tts.ATTR_LANGUAGE: "ru-RU",
+ },
)
self.hass.block_till_done()
@@ -165,7 +173,9 @@ class TestTTSYandexPlatform:
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
- tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "yandextts_say",
+ {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"},
)
self.hass.block_till_done()
@@ -195,7 +205,9 @@ class TestTTSYandexPlatform:
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
- tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "yandextts_say",
+ {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"},
)
self.hass.block_till_done()
@@ -230,7 +242,9 @@ class TestTTSYandexPlatform:
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
- tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "yandextts_say",
+ {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"},
)
self.hass.block_till_done()
@@ -266,7 +280,9 @@ class TestTTSYandexPlatform:
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
- tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "yandextts_say",
+ {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"},
)
self.hass.block_till_done()
@@ -298,7 +314,9 @@ class TestTTSYandexPlatform:
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
- tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "yandextts_say",
+ {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"},
)
self.hass.block_till_done()
@@ -330,7 +348,9 @@ class TestTTSYandexPlatform:
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
- tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
+ tts.DOMAIN,
+ "yandextts_say",
+ {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"},
)
self.hass.block_till_done()
@@ -362,6 +382,7 @@ class TestTTSYandexPlatform:
tts.DOMAIN,
"yandextts_say",
{
+ "entity_id": "media_player.something",
tts.ATTR_MESSAGE: "HomeAssistant",
"options": {"emotion": "evil", "speed": 2},
},
diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py
index f42f2f6af2e..f1eec4da942 100644
--- a/tests/components/yessssms/test_notify.py
+++ b/tests/components/yessssms/test_notify.py
@@ -36,7 +36,7 @@ def init_valid_settings(hass, config):
@pytest.fixture(name="invalid_provider_settings")
def init_invalid_provider_settings(hass, config):
- """Set invalid provider data and initalize component."""
+ """Set invalid provider data and initialize component."""
config["notify"][CONF_PROVIDER] = "FantasyMobile" # invalid provider
return async_setup_component(hass, "notify", config)
diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py
index 9b6a8b5b55f..03b6ed21148 100644
--- a/tests/components/zha/common.py
+++ b/tests/components/zha/common.py
@@ -1,6 +1,6 @@
"""Common test objects."""
import time
-from unittest.mock import Mock, patch
+from unittest.mock import Mock
from asynctest import CoroutineMock
import zigpy.profiles.zha
@@ -10,28 +10,9 @@ 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,
- DATA_ZHA_BRIDGE_ID,
- DATA_ZHA_CONFIG,
- DATA_ZHA_DISPATCHERS,
-)
+import homeassistant.components.zha.core.const as zha_const
from homeassistant.util import slugify
-from tests.common import mock_coro
-
-
-class FakeApplication:
- """Fake application for mocking zigpy."""
-
- def __init__(self):
- """Init fake application."""
- self.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32")
- self.nwk = 0x087D
-
-
-APPLICATION = FakeApplication()
-
class FakeEndpoint:
"""Fake endpoint for moking zigpy."""
@@ -73,14 +54,15 @@ def patch_cluster(cluster):
cluster.read_attributes = CoroutineMock()
cluster.read_attributes_raw = Mock()
cluster.unbind = CoroutineMock(return_value=[0])
+ cluster.write_attributes = CoroutineMock(return_value=[0])
class FakeDevice:
"""Fake device for mocking zigpy."""
- def __init__(self, ieee, manufacturer, model, node_desc=None):
+ def __init__(self, app, ieee, manufacturer, model, node_desc=None):
"""Init fake device."""
- self._application = APPLICATION
+ self._application = app
self.ieee = zigpy.types.EUI64.convert(ieee)
self.nwk = 0xB79C
self.zdo = Mock()
@@ -90,6 +72,7 @@ class FakeDevice:
self.last_seen = time.time()
self.status = 2
self.initializing = False
+ self.skip_configuration = False
self.manufacturer = manufacturer
self.model = model
self.node_desc = zigpy.zdo.types.NodeDescriptor()
@@ -100,64 +83,12 @@ class FakeDevice:
self.node_desc = zigpy.zdo.types.NodeDescriptor.deserialize(node_desc)[0]
-def make_device(endpoints, ieee, manufacturer, model):
- """Make a fake device using the specified cluster classes."""
- device = FakeDevice(ieee, manufacturer, model)
- for epid, ep in endpoints.items():
- endpoint = FakeEndpoint(manufacturer, model, epid)
- endpoint.device = device
- device.endpoints[epid] = endpoint
- endpoint.device_type = ep["device_type"]
- profile_id = ep.get("profile_id")
- if profile_id:
- endpoint.profile_id = profile_id
-
- for cluster_id in ep.get("in_clusters", []):
- endpoint.add_input_cluster(cluster_id)
-
- for cluster_id in ep.get("out_clusters", []):
- endpoint.add_output_cluster(cluster_id)
-
- return device
-
-
-async def async_init_zigpy_device(
- hass,
- in_cluster_ids,
- out_cluster_ids,
- device_type,
- gateway,
- ieee="00:0d:6f:00:0a:90:69:e7",
- manufacturer="FakeManufacturer",
- model="FakeModel",
- is_new_join=False,
-):
- """Create and initialize a device.
-
- This creates a fake device and adds it to the "network". It can be used to
- test existing device functionality and new device pairing functionality.
- The is_new_join parameter influences whether or not the device will go
- through cluster binding and zigbee cluster configure reporting. That only
- happens when the device is paired to the network for the first time.
- """
- device = make_device(
- {
- 1: {
- "in_clusters": in_cluster_ids,
- "out_clusters": out_cluster_ids,
- "device_type": device_type,
- }
- },
- ieee,
- manufacturer,
- model,
- )
- if is_new_join:
- await gateway.async_device_initialized(device)
- else:
- await gateway.async_device_restored(device)
- await hass.async_block_till_done()
- return device
+def get_zha_gateway(hass):
+ """Return ZHA gateway from hass.data."""
+ try:
+ return hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]
+ except KeyError:
+ return None
def make_attribute(attrid, value, status=0):
@@ -169,14 +100,6 @@ def make_attribute(attrid, value, status=0):
return attr
-async def async_setup_entry(hass, config_entry):
- """Mock setup entry for zha."""
- hass.data[DATA_ZHA][DATA_ZHA_CONFIG] = {}
- hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = []
- hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = APPLICATION.ieee
- return True
-
-
async def find_entity_id(domain, zha_device, hass):
"""Find the entity id under the testing.
@@ -195,43 +118,13 @@ async def find_entity_id(domain, zha_device, hass):
return None
-async def async_enable_traffic(hass, zha_gateway, zha_devices):
+async def async_enable_traffic(hass, zha_devices):
"""Allow traffic to flow through the gateway and the zha device."""
for zha_device in zha_devices:
zha_device.update_available(True)
await hass.async_block_till_done()
-async def async_test_device_join(
- hass, zha_gateway, cluster_id, entity_id, device_type=None
-):
- """Test a newly joining device.
-
- This creates a new fake device and adds it to the network. It is meant to
- simulate pairing a new device to the network so that code pathways that
- only trigger during device joins can be tested.
- """
- # create zigpy device mocking out the zigbee network operations
- with patch(
- "zigpy.zcl.Cluster.configure_reporting",
- return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
- ):
- with patch(
- "zigpy.zcl.Cluster.bind",
- return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
- ):
- await async_init_zigpy_device(
- hass,
- [cluster_id, zigpy.zcl.clusters.general.Basic.cluster_id],
- [],
- device_type,
- zha_gateway,
- ieee="00:0d:6f:00:0a:90:69:f7",
- is_new_join=True,
- )
- 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:
@@ -239,3 +132,25 @@ def make_zcl_header(command_id: int, global_command: bool = True) -> zcl_f.ZCLHe
else:
frc = zcl_f.FrameControl(zcl_f.FrameType.CLUSTER_COMMAND)
return zcl_f.ZCLHeader(frc, tsn=1, command_id=command_id)
+
+
+def reset_clusters(clusters):
+ """Reset mocks on cluster."""
+ for cluster in clusters:
+ cluster.bind.reset_mock()
+ cluster.configure_reporting.reset_mock()
+ cluster.write_attributes.reset_mock()
+
+
+async def async_test_rejoin(hass, zigpy_device, clusters, report_counts, ep_id=1):
+ """Test device rejoins."""
+ reset_clusters(clusters)
+
+ zha_gateway = get_zha_gateway(hass)
+ await zha_gateway.async_device_initialized(zigpy_device)
+ await hass.async_block_till_done()
+ for cluster, reports in zip(clusters, report_counts):
+ assert cluster.bind.call_count == 1
+ assert cluster.bind.await_count == 1
+ assert cluster.configure_reporting.call_count == reports
+ assert cluster.configure_reporting.await_count == reports
diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py
index 18344172d29..26dd2b5da5c 100644
--- a/tests/components/zha/conftest.py
+++ b/tests/components/zha/conftest.py
@@ -1,77 +1,83 @@
"""Test configuration for the ZHA component."""
from unittest import mock
-from unittest.mock import patch
import asynctest
import pytest
import zigpy
from zigpy.application import ControllerApplication
+import zigpy.group
+import zigpy.types
-from homeassistant import config_entries
-from homeassistant.components.zha.core.const import COMPONENTS, DATA_ZHA, DOMAIN
-from homeassistant.components.zha.core.gateway import ZHAGateway
-from homeassistant.components.zha.core.store import async_get_registry
-from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
+import homeassistant.components.zha.core.const as zha_const
+import homeassistant.components.zha.core.registries as zha_regs
+from homeassistant.setup import async_setup_component
-from .common import FakeDevice, FakeEndpoint, async_setup_entry
+from .common import FakeDevice, FakeEndpoint, get_zha_gateway
+
+from tests.common import MockConfigEntry
FIXTURE_GRP_ID = 0x1001
FIXTURE_GRP_NAME = "fixture group"
-@pytest.fixture(name="config_entry")
-def config_entry_fixture(hass):
- """Fixture representing a config entry."""
- config_entry = config_entries.ConfigEntry(
- 1,
- DOMAIN,
- "Mock Title",
- {},
- "test",
- config_entries.CONN_CLASS_LOCAL_PUSH,
- system_options={},
- )
- return config_entry
-
-
-@pytest.fixture(name="zha_gateway")
-async def zha_gateway_fixture(hass, config_entry):
- """Fixture representing a zha gateway.
-
- Create a ZHAGateway object that can be used to interact with as if we
- had a real zigbee network running.
- """
- for component in COMPONENTS:
- hass.data[DATA_ZHA][component] = hass.data[DATA_ZHA].get(component, {})
- zha_storage = await async_get_registry(hass)
- dev_reg = await get_dev_reg(hass)
- gateway = ZHAGateway(hass, {}, config_entry)
- gateway.zha_storage = zha_storage
- gateway.ha_device_registry = dev_reg
- gateway.application_controller = mock.MagicMock(spec_set=ControllerApplication)
- groups = zigpy.group.Groups(gateway.application_controller)
- groups.add_listener(gateway)
+@pytest.fixture
+def zigpy_app_controller():
+ """Zigpy ApplicationController fixture."""
+ app = mock.MagicMock(spec_set=ControllerApplication)
+ app.startup = asynctest.CoroutineMock()
+ app.shutdown = asynctest.CoroutineMock()
+ groups = zigpy.group.Groups(app)
groups.add_group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, suppress_event=True)
- gateway.application_controller.configure_mock(groups=groups)
- gateway._initialize_groups()
- return gateway
+ app.configure_mock(groups=groups)
+ type(app).ieee = mock.PropertyMock()
+ app.ieee.return_value = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32")
+ type(app).nwk = mock.PropertyMock(return_value=zigpy.types.NWK(0x0000))
+ type(app).devices = mock.PropertyMock(return_value={})
+ return app
-@pytest.fixture(autouse=True)
-async def setup_zha(hass, config_entry):
- """Load the ZHA component.
+@pytest.fixture
+def zigpy_radio():
+ """Zigpy radio mock."""
+ radio = mock.MagicMock()
+ radio.connect = asynctest.CoroutineMock()
+ return radio
- This will init the ZHA component. It loads the component in HA so that
- we can test the domains that ZHA supports without actually having a zigbee
- network running.
- """
- # this prevents needing an actual radio and zigbee network available
- with patch("homeassistant.components.zha.async_setup_entry", async_setup_entry):
- hass.data[DATA_ZHA] = {}
- # init ZHA
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
+@pytest.fixture(name="config_entry")
+async def config_entry_fixture(hass):
+ """Fixture representing a config entry."""
+ entry = MockConfigEntry(
+ version=1,
+ domain=zha_const.DOMAIN,
+ data={
+ zha_const.CONF_BAUDRATE: zha_const.DEFAULT_BAUDRATE,
+ zha_const.CONF_RADIO_TYPE: "MockRadio",
+ zha_const.CONF_USB_PATH: "/dev/ttyUSB0",
+ },
+ )
+ entry.add_to_hass(hass)
+ return entry
+
+
+@pytest.fixture
+def setup_zha(hass, config_entry, zigpy_app_controller, zigpy_radio):
+ """Set up ZHA component."""
+ zha_config = {zha_const.DOMAIN: {zha_const.CONF_ENABLE_QUIRKS: False}}
+
+ radio_details = {
+ zha_const.ZHA_GW_RADIO: mock.MagicMock(return_value=zigpy_radio),
+ zha_const.CONTROLLER: mock.MagicMock(return_value=zigpy_app_controller),
+ zha_const.ZHA_GW_RADIO_DESCRIPTION: "mock radio",
+ }
+
+ async def _setup():
+ with mock.patch.dict(zha_regs.RADIO_TYPES, {"MockRadio": radio_details}):
+ status = await async_setup_component(hass, zha_const.DOMAIN, zha_config)
+ assert status is True
+ await hass.async_block_till_done()
+
+ return _setup
@pytest.fixture
@@ -91,7 +97,7 @@ def channel():
@pytest.fixture
-def zigpy_device_mock():
+def zigpy_device_mock(zigpy_app_controller):
"""Make a fake device using the specified cluster classes."""
def _mock_dev(
@@ -99,10 +105,12 @@ def zigpy_device_mock():
ieee="00:0d:6f:00:0a:90:69:e7",
manufacturer="FakeManufacturer",
model="FakeModel",
- node_desc=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00",
+ node_descriptor=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00",
):
"""Make a fake device using the specified cluster classes."""
- device = FakeDevice(ieee, manufacturer, model, node_desc)
+ device = FakeDevice(
+ zigpy_app_controller, ieee, manufacturer, model, node_descriptor
+ )
for epid, ep in endpoints.items():
endpoint = FakeEndpoint(manufacturer, model, epid)
endpoint.device = device
@@ -121,3 +129,36 @@ def zigpy_device_mock():
return device
return _mock_dev
+
+
+@pytest.fixture
+def zha_device_joined(hass, setup_zha):
+ """Return a newly joined ZHA device."""
+
+ async def _zha_device(zigpy_dev):
+ await setup_zha()
+ zha_gateway = get_zha_gateway(hass)
+ await zha_gateway.async_device_initialized(zigpy_dev)
+ await hass.async_block_till_done()
+ return zha_gateway.get_device(zigpy_dev.ieee)
+
+ return _zha_device
+
+
+@pytest.fixture
+def zha_device_restored(hass, zigpy_app_controller, setup_zha):
+ """Return a restored ZHA device."""
+
+ async def _zha_device(zigpy_dev):
+ zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev
+ await setup_zha()
+ zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]
+ return zha_gateway.get_device(zigpy_dev.ieee)
+
+ return _zha_device
+
+
+@pytest.fixture(params=["zha_device_joined", "zha_device_restored"])
+def zha_device_joined_restored(request):
+ """Join or restore ZHA device."""
+ return request.getfixturevalue(request.param)
diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py
index f01d27eb167..b67a39cd3ab 100644
--- a/tests/components/zha/test_api.py
+++ b/tests/components/zha/test_api.py
@@ -1,11 +1,9 @@
"""Test ZHA API."""
import pytest
-import zigpy
+import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
-from homeassistant.components.light import DOMAIN as light_domain
-from homeassistant.components.switch import DOMAIN
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 (
@@ -23,50 +21,67 @@ from homeassistant.components.zha.core.const import (
GROUP_NAME,
)
-from .common import async_init_zigpy_device
from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME
+IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7"
+IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8"
+
@pytest.fixture
-async def zha_client(hass, config_entry, zha_gateway, hass_ws_client):
+async def device_switch(hass, zigpy_device_mock, zha_device_joined):
+ """Test zha switch platform."""
+
+ zigpy_device = zigpy_device_mock(
+ {
+ 1: {
+ "in_clusters": [general.OnOff.cluster_id, general.Basic.cluster_id],
+ "out_clusters": [],
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
+ }
+ },
+ ieee=IEEE_SWITCH_DEVICE,
+ )
+ zha_device = await zha_device_joined(zigpy_device)
+ zha_device.set_available(True)
+ return zha_device
+
+
+@pytest.fixture
+async def device_groupable(hass, zigpy_device_mock, zha_device_joined):
+ """Test zha light platform."""
+
+ zigpy_device = zigpy_device_mock(
+ {
+ 1: {
+ "in_clusters": [
+ general.OnOff.cluster_id,
+ general.Basic.cluster_id,
+ general.Groups.cluster_id,
+ ],
+ "out_clusters": [],
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
+ }
+ },
+ ieee=IEEE_GROUPABLE_DEVICE,
+ )
+ zha_device = await zha_device_joined(zigpy_device)
+ zha_device.set_available(True)
+ return zha_device
+
+
+@pytest.fixture
+async def zha_client(hass, hass_ws_client, device_switch, device_groupable):
"""Test zha switch platform."""
# load the ZHA API
async_load_api(hass)
-
- # create zigpy device
- await async_init_zigpy_device(
- hass,
- [general.OnOff.cluster_id, general.Basic.cluster_id],
- [],
- None,
- zha_gateway,
- )
-
- await async_init_zigpy_device(
- hass,
- [general.OnOff.cluster_id, general.Basic.cluster_id, general.Groups.cluster_id],
- [],
- zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT,
- zha_gateway,
- manufacturer="FakeGroupManufacturer",
- model="FakeGroupModel",
- ieee="01:2d:6f:00:0a:90:69:e8",
- )
-
- # load up switch domain
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
- await hass.config_entries.async_forward_entry_setup(config_entry, light_domain)
- await hass.async_block_till_done()
-
return await hass_ws_client(hass)
-async def test_device_clusters(hass, config_entry, zha_gateway, zha_client):
+async def test_device_clusters(hass, zha_client):
"""Test getting device cluster info."""
await zha_client.send_json(
- {ID: 5, TYPE: "zha/devices/clusters", ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7"}
+ {ID: 5, TYPE: "zha/devices/clusters", ATTR_IEEE: IEEE_SWITCH_DEVICE}
)
msg = await zha_client.receive_json()
@@ -86,14 +101,14 @@ async def test_device_clusters(hass, config_entry, zha_gateway, zha_client):
assert cluster_info[ATTR_NAME] == "OnOff"
-async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_client):
+async def test_device_cluster_attributes(zha_client):
"""Test getting device cluster attributes."""
await zha_client.send_json(
{
ID: 5,
TYPE: "zha/devices/clusters/attributes",
ATTR_ENDPOINT_ID: 1,
- ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7",
+ ATTR_IEEE: IEEE_SWITCH_DEVICE,
ATTR_CLUSTER_ID: 6,
ATTR_CLUSTER_TYPE: CLUSTER_TYPE_IN,
}
@@ -109,14 +124,14 @@ async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_cl
assert attribute[ATTR_NAME] is not None
-async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_client):
+async def test_device_cluster_commands(zha_client):
"""Test getting device cluster commands."""
await zha_client.send_json(
{
ID: 5,
TYPE: "zha/devices/clusters/commands",
ATTR_ENDPOINT_ID: 1,
- ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7",
+ ATTR_IEEE: IEEE_SWITCH_DEVICE,
ATTR_CLUSTER_ID: 6,
ATTR_CLUSTER_TYPE: CLUSTER_TYPE_IN,
}
@@ -133,7 +148,7 @@ async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_clie
assert command[TYPE] is not None
-async def test_list_devices(hass, config_entry, zha_gateway, zha_client):
+async def test_list_devices(zha_client):
"""Test getting zha devices."""
await zha_client.send_json({ID: 5, TYPE: "zha/devices"})
@@ -164,7 +179,7 @@ async def test_list_devices(hass, config_entry, zha_gateway, zha_client):
assert device == device2
-async def test_device_not_found(hass, config_entry, zha_gateway, zha_client):
+async def test_device_not_found(zha_client):
"""Test not found response from get device API."""
await zha_client.send_json(
{ID: 6, TYPE: "zha/device", ATTR_IEEE: "28:6d:97:00:01:04:11:8c"}
@@ -176,7 +191,7 @@ async def test_device_not_found(hass, config_entry, zha_gateway, zha_client):
assert msg["error"]["code"] == const.ERR_NOT_FOUND
-async def test_list_groups(hass, config_entry, zha_gateway, zha_client):
+async def test_list_groups(zha_client):
"""Test getting zha zigbee groups."""
await zha_client.send_json({ID: 7, TYPE: "zha/groups"})
@@ -193,7 +208,7 @@ async def test_list_groups(hass, config_entry, zha_gateway, zha_client):
assert group["members"] == []
-async def test_get_group(hass, config_entry, zha_gateway, zha_client):
+async def test_get_group(zha_client):
"""Test getting a specific zha zigbee group."""
await zha_client.send_json({ID: 8, TYPE: "zha/group", GROUP_ID: FIXTURE_GRP_ID})
@@ -208,7 +223,7 @@ async def test_get_group(hass, config_entry, zha_gateway, zha_client):
assert group["members"] == []
-async def test_get_group_not_found(hass, config_entry, zha_gateway, zha_client):
+async def test_get_group_not_found(zha_client):
"""Test not found response from get group API."""
await zha_client.send_json({ID: 9, TYPE: "zha/group", GROUP_ID: 1234567})
@@ -220,14 +235,9 @@ async def test_get_group_not_found(hass, config_entry, zha_gateway, zha_client):
assert msg["error"]["code"] == const.ERR_NOT_FOUND
-async def test_list_groupable_devices(hass, config_entry, zha_gateway, zha_client):
+async def test_list_groupable_devices(zha_client, device_groupable):
"""Test getting zha devices that have a group cluster."""
- # Make device available
- zha_gateway.devices[
- zigpy.types.EUI64.convert("01:2d:6f:00:0a:90:69:e8")
- ].set_available(True)
-
await zha_client.send_json({ID: 10, TYPE: "zha/devices/groupable"})
msg = await zha_client.receive_json()
@@ -251,9 +261,7 @@ async def test_list_groupable_devices(hass, config_entry, zha_gateway, zha_clien
# Make sure there are no groupable devices when the device is unavailable
# Make device unavailable
- zha_gateway.devices[
- zigpy.types.EUI64.convert("01:2d:6f:00:0a:90:69:e8")
- ].set_available(False)
+ device_groupable.set_available(False)
await zha_client.send_json({ID: 11, TYPE: "zha/devices/groupable"})
@@ -265,7 +273,7 @@ async def test_list_groupable_devices(hass, config_entry, zha_gateway, zha_clien
assert len(devices) == 0
-async def test_add_group(hass, config_entry, zha_gateway, zha_client):
+async def test_add_group(zha_client):
"""Test adding and getting a new zha zigbee group."""
await zha_client.send_json({ID: 12, TYPE: "zha/group/add", GROUP_NAME: "new_group"})
@@ -291,7 +299,7 @@ async def test_add_group(hass, config_entry, zha_gateway, zha_client):
assert group["name"] == FIXTURE_GRP_NAME or group["name"] == "new_group"
-async def test_remove_group(hass, config_entry, zha_gateway, zha_client):
+async def test_remove_group(zha_client):
"""Test removing a new zha zigbee group."""
await zha_client.send_json({ID: 14, TYPE: "zha/groups"})
diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py
index 2765a465ace..a22bfa54dae 100644
--- a/tests/components/zha/test_binary_sensor.py
+++ b/tests/components/zha/test_binary_sensor.py
@@ -1,5 +1,5 @@
"""Test zha binary sensor."""
-import zigpy.zcl.clusters.general as general
+import pytest
import zigpy.zcl.clusters.measurement as measurement
import zigpy.zcl.clusters.security as security
import zigpy.zcl.foundation as zcl_f
@@ -9,76 +9,28 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from .common import (
async_enable_traffic,
- async_init_zigpy_device,
- async_test_device_join,
+ async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
)
+DEVICE_IAS = {
+ 1: {
+ "device_type": 1026,
+ "in_clusters": [security.IasZone.cluster_id],
+ "out_clusters": [],
+ }
+}
-async def test_binary_sensor(hass, config_entry, zha_gateway):
- """Test zha binary_sensor platform."""
- # create zigpy devices
- zigpy_device_zone = await async_init_zigpy_device(
- hass,
- [security.IasZone.cluster_id, general.Basic.cluster_id],
- [],
- None,
- zha_gateway,
- ieee="00:0d:6f:11:9a:90:69:e6",
- )
-
- zigpy_device_occupancy = await async_init_zigpy_device(
- hass,
- [measurement.OccupancySensing.cluster_id, general.Basic.cluster_id],
- [],
- None,
- zha_gateway,
- ieee="00:0d:6f:11:9a:90:69:e7",
- manufacturer="FakeOccupancy",
- model="FakeOccupancyModel",
- )
-
- # load up binary_sensor domain
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
-
- # on off binary_sensor
- zone_cluster = zigpy_device_zone.endpoints.get(1).ias_zone
- zone_zha_device = zha_gateway.get_device(zigpy_device_zone.ieee)
- zone_entity_id = await find_entity_id(DOMAIN, zone_zha_device, hass)
- assert zone_entity_id is not None
-
- # occupancy binary_sensor
- occupancy_cluster = zigpy_device_occupancy.endpoints.get(1).occupancy
- occupancy_zha_device = zha_gateway.get_device(zigpy_device_occupancy.ieee)
- occupancy_entity_id = await find_entity_id(DOMAIN, occupancy_zha_device, hass)
- assert occupancy_entity_id is not None
-
- # test that the sensors exist and are in the unavailable state
- assert hass.states.get(zone_entity_id).state == STATE_UNAVAILABLE
- assert hass.states.get(occupancy_entity_id).state == STATE_UNAVAILABLE
-
- await async_enable_traffic(
- hass, zha_gateway, [zone_zha_device, occupancy_zha_device]
- )
-
- # test that the sensors exist and are in the off state
- assert hass.states.get(zone_entity_id).state == STATE_OFF
- assert hass.states.get(occupancy_entity_id).state == STATE_OFF
-
- # test getting messages that trigger and reset the sensors
- await async_test_binary_sensor_on_off(hass, occupancy_cluster, occupancy_entity_id)
-
- # test IASZone binary sensors
- await async_test_iaszone_on_off(hass, zone_cluster, zone_entity_id)
-
- # test new sensor join
- await async_test_device_join(
- hass, zha_gateway, measurement.OccupancySensing.cluster_id, occupancy_entity_id
- )
+DEVICE_OCCUPANCY = {
+ 1: {
+ "device_type": 263,
+ "in_clusters": [measurement.OccupancySensing.cluster_id],
+ "out_clusters": [],
+ }
+}
async def async_test_binary_sensor_on_off(hass, cluster, entity_id):
@@ -109,3 +61,43 @@ async def async_test_iaszone_on_off(hass, cluster, entity_id):
cluster.listener_event("cluster_command", 1, 0, [0])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
+
+
+@pytest.mark.parametrize(
+ "device, on_off_test, cluster_name, reporting",
+ [
+ (DEVICE_IAS, async_test_iaszone_on_off, "ias_zone", (0,)),
+ (DEVICE_OCCUPANCY, async_test_binary_sensor_on_off, "occupancy", (1,)),
+ ],
+)
+async def test_binary_sensor(
+ hass,
+ zigpy_device_mock,
+ zha_device_joined_restored,
+ device,
+ on_off_test,
+ cluster_name,
+ reporting,
+):
+ """Test ZHA binary_sensor platform."""
+ zigpy_device = zigpy_device_mock(device)
+ zha_device = await zha_device_joined_restored(zigpy_device)
+ entity_id = await find_entity_id(DOMAIN, zha_device, hass)
+
+ assert entity_id is not None
+
+ # test that the sensors exist and are in the unavailable state
+ assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
+
+ await async_enable_traffic(hass, [zha_device])
+
+ # test that the sensors exist and are in the off state
+ assert hass.states.get(entity_id).state == STATE_OFF
+
+ # test getting messages that trigger and reset the sensors
+ cluster = getattr(zigpy_device.endpoints[1], cluster_name)
+ await on_off_test(hass, cluster, entity_id)
+
+ # test rejoin
+ await async_test_rejoin(hass, zigpy_device, [cluster], reporting)
+ assert hass.states.get(entity_id).state == STATE_OFF
diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py
index c5ad4d3fbc0..ee493ca01a7 100644
--- a/tests/components/zha/test_channels.py
+++ b/tests/components/zha/test_channels.py
@@ -6,6 +6,8 @@ import homeassistant.components.zha.core.channels as channels
import homeassistant.components.zha.core.device as zha_device
import homeassistant.components.zha.core.registries as registries
+from .common import get_zha_gateway
+
@pytest.fixture
def ieee():
@@ -19,6 +21,13 @@ def nwk():
return t.NWK(0xBEEF)
+@pytest.fixture
+async def zha_gateway(hass, setup_zha):
+ """Return ZhaGateway fixture."""
+ await setup_zha()
+ return get_zha_gateway(hass)
+
+
@pytest.mark.parametrize(
"cluster_id, bind_count, attrs",
[
@@ -63,7 +72,7 @@ def nwk():
],
)
async def test_in_channel_config(
- cluster_id, bind_count, attrs, zha_gateway, hass, zigpy_device_mock
+ cluster_id, bind_count, attrs, hass, zigpy_device_mock, zha_gateway
):
"""Test ZHA core channel configuration for input clusters."""
zigpy_dev = zigpy_device_mock(
diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py
index 9d1c019c718..e5883605e34 100644
--- a/tests/components/zha/test_cover.py
+++ b/tests/components/zha/test_cover.py
@@ -1,9 +1,10 @@
"""Test zha cover."""
from unittest.mock import MagicMock, call, patch
+import asynctest
+import pytest
import zigpy.types
import zigpy.zcl.clusters.closures as closures
-import zigpy.zcl.clusters.general as general
import zigpy.zcl.foundation as zcl_f
from homeassistant.components.cover import DOMAIN
@@ -11,8 +12,7 @@ from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE
from .common import (
async_enable_traffic,
- async_init_zigpy_device,
- async_test_device_join,
+ async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
@@ -21,17 +21,25 @@ from .common import (
from tests.common import mock_coro
-async def test_cover(hass, config_entry, zha_gateway):
- """Test zha cover platform."""
+@pytest.fixture
+def zigpy_cover_device(zigpy_device_mock):
+ """Zigpy cover device."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass,
- [closures.WindowCovering.cluster_id, general.Basic.cluster_id],
- [],
- None,
- zha_gateway,
- )
+ endpoints = {
+ 1: {
+ "device_type": 1026,
+ "in_clusters": [closures.WindowCovering.cluster_id],
+ "out_clusters": [],
+ }
+ }
+ return zigpy_device_mock(endpoints)
+
+
+@asynctest.patch(
+ "homeassistant.components.zha.core.channels.closures.WindowCovering.async_initialize"
+)
+async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device):
+ """Test zha cover platform."""
async def get_chan_attr(*args, **kwargs):
return 100
@@ -41,13 +49,11 @@ async def test_cover(hass, config_entry, zha_gateway):
new=MagicMock(side_effect=get_chan_attr),
) as get_attr_mock:
# load up cover domain
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
+ zha_device = await zha_device_joined_restored(zigpy_cover_device)
assert get_attr_mock.call_count == 2
assert get_attr_mock.call_args[0][0] == "current_position_lift_percentage"
- cluster = zigpy_device.endpoints.get(1).window_covering
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
+ cluster = zigpy_cover_device.endpoints.get(1).window_covering
entity_id = await find_entity_id(DOMAIN, zha_device, hass)
assert entity_id is not None
@@ -55,7 +61,7 @@ async def test_cover(hass, config_entry, zha_gateway):
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
+ await async_enable_traffic(hass, [zha_device])
await hass.async_block_till_done()
attr = make_attribute(8, 100)
@@ -124,6 +130,6 @@ async def test_cover(hass, config_entry, zha_gateway):
False, 0x2, (), expect_reply=True, manufacturer=None
)
- await async_test_device_join(
- hass, zha_gateway, closures.WindowCovering.cluster_id, entity_id
- )
+ # test rejoin
+ await async_test_rejoin(hass, zigpy_cover_device, [cluster], (1,))
+ assert hass.states.get(entity_id).state == STATE_OPEN
diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py
index c3195559d20..8866e6cff55 100644
--- a/tests/components/zha/test_device_action.py
+++ b/tests/components/zha/test_device_action.py
@@ -11,12 +11,10 @@ from homeassistant.components.device_automation import (
_async_get_device_automations as async_get_device_automations,
)
from homeassistant.components.zha import DOMAIN
-from homeassistant.components.zha.core.const import CHANNEL_ON_OFF
+from homeassistant.components.zha.core.const import CHANNEL_EVENT_RELAY
from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.setup import async_setup_component
-from .common import async_enable_traffic, async_init_zigpy_device
-
from tests.common import async_mock_service, mock_coro
SHORT_PRESS = "remote_button_short_press"
@@ -25,33 +23,30 @@ COMMAND_SINGLE = "single"
@pytest.fixture
-def calls(hass):
- """Track calls to a mock service."""
- return async_mock_service(hass, "zha", "warning_device_warn")
+async def device_ias(hass, zigpy_device_mock, zha_device_joined_restored):
+ """IAS device fixture."""
-
-async def test_get_actions(hass, config_entry, zha_gateway):
- """Test we get the expected actions from a zha device."""
-
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass,
- [
- general.Basic.cluster_id,
- security.IasZone.cluster_id,
- security.IasWd.cluster_id,
- ],
- [],
- None,
- zha_gateway,
+ clusters = [general.Basic, security.IasZone, security.IasWd]
+ zigpy_device = zigpy_device_mock(
+ {
+ 1: {
+ "in_clusters": [c.cluster_id for c in clusters],
+ "out_clusters": [general.OnOff.cluster_id],
+ "device_type": 0,
+ }
+ },
)
- await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor")
+ zha_device = await zha_device_joined_restored(zigpy_device)
+ zha_device.update_available(True)
await hass.async_block_till_done()
- hass.config_entries._entries.append(config_entry)
+ return zigpy_device, zha_device
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
- ieee_address = str(zha_device.ieee)
+
+async def test_get_actions(hass, device_ias):
+ """Test we get the expected actions from a zha device."""
+
+ ieee_address = str(device_ias[0].ieee)
ha_device_registry = await async_get_registry(hass)
reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set())
@@ -66,40 +61,19 @@ async def test_get_actions(hass, config_entry, zha_gateway):
assert actions == expected_actions
-async def test_action(hass, config_entry, zha_gateway, calls):
+async def test_action(hass, device_ias):
"""Test for executing a zha device action."""
-
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass,
- [
- general.Basic.cluster_id,
- security.IasZone.cluster_id,
- security.IasWd.cluster_id,
- ],
- [general.OnOff.cluster_id],
- None,
- zha_gateway,
- )
+ zigpy_device, zha_device = device_ias
zigpy_device.device_automation_triggers = {
(SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}
}
- await hass.config_entries.async_forward_entry_setup(config_entry, "switch")
- await hass.async_block_till_done()
-
- hass.config_entries._entries.append(config_entry)
-
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
ieee_address = str(zha_device.ieee)
ha_device_registry = await async_get_registry(hass)
reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set())
- # allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
-
with patch(
"zigpy.zcl.Cluster.request",
return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]),
@@ -128,9 +102,10 @@ async def test_action(hass, config_entry, zha_gateway, calls):
)
await hass.async_block_till_done()
+ calls = async_mock_service(hass, DOMAIN, "warning_device_warn")
- on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF]
- on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, [])
+ channel = {ch.name: ch for ch in zha_device.all_channels}[CHANNEL_EVENT_RELAY]
+ channel.zha_send_event(channel.cluster, COMMAND_SINGLE, [])
await hass.async_block_till_done()
assert len(calls) == 1
diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py
index bac338ae5e0..3782cdc09a7 100644
--- a/tests/components/zha/test_device_tracker.py
+++ b/tests/components/zha/test_device_tracker.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import time
+import pytest
import zigpy.zcl.clusters.general as general
import zigpy.zcl.foundation as zcl_f
@@ -14,8 +15,7 @@ import homeassistant.util.dt as dt_util
from .common import (
async_enable_traffic,
- async_init_zigpy_device,
- async_test_device_join,
+ async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
@@ -24,43 +24,43 @@ from .common import (
from tests.common import async_fire_time_changed
-async def test_device_tracker(hass, config_entry, zha_gateway):
+@pytest.fixture
+def zigpy_device_dt(zigpy_device_mock):
+ """Device tracker zigpy device."""
+ endpoints = {
+ 1: {
+ "in_clusters": [
+ general.Basic.cluster_id,
+ general.PowerConfiguration.cluster_id,
+ general.Identify.cluster_id,
+ general.PollControl.cluster_id,
+ general.BinaryInput.cluster_id,
+ ],
+ "out_clusters": [general.Identify.cluster_id, general.Ota.cluster_id],
+ "device_type": SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE,
+ }
+ }
+ return zigpy_device_mock(endpoints)
+
+
+async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt):
"""Test zha device tracker platform."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass,
- [
- general.Basic.cluster_id,
- general.PowerConfiguration.cluster_id,
- general.Identify.cluster_id,
- general.PollControl.cluster_id,
- general.BinaryInput.cluster_id,
- ],
- [general.Identify.cluster_id, general.Ota.cluster_id],
- SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE,
- zha_gateway,
- )
-
- # load up device tracker domain
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
-
- cluster = zigpy_device.endpoints.get(1).power
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
+ zha_device = await zha_device_joined_restored(zigpy_device_dt)
+ cluster = zigpy_device_dt.endpoints.get(1).power
entity_id = await find_entity_id(DOMAIN, zha_device, hass)
assert entity_id is not None
# test that the device tracker was created and that it is unavailable
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
- zigpy_device.last_seen = time.time() - 120
+ zigpy_device_dt.last_seen = time.time() - 120
next_update = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
# allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
+ await async_enable_traffic(hass, [zha_device])
# test that the state has changed from unavailable to not home
assert hass.states.get(entity_id).state == STATE_NOT_HOME
@@ -73,7 +73,7 @@ async def test_device_tracker(hass, config_entry, zha_gateway):
attr = make_attribute(0x0021, 200)
cluster.handle_message(hdr, [[attr]])
- zigpy_device.last_seen = time.time() + 10
+ zigpy_device_dt.last_seen = time.time() + 10
next_update = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
@@ -87,10 +87,5 @@ async def test_device_tracker(hass, config_entry, zha_gateway):
assert entity.battery_level == 100
# test adding device tracker to the network and HA
- await async_test_device_join(
- hass,
- zha_gateway,
- general.PowerConfiguration.cluster_id,
- entity_id,
- SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE,
- )
+ await async_test_rejoin(hass, zigpy_device_dt, [cluster], (2,))
+ assert hass.states.get(entity_id).state == STATE_HOME
diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py
index 973b6673671..4bb7567d1e6 100644
--- a/tests/components/zha/test_device_trigger.py
+++ b/tests/components/zha/test_device_trigger.py
@@ -3,13 +3,10 @@ import pytest
import zigpy.zcl.clusters.general as general
import homeassistant.components.automation as automation
-from homeassistant.components.switch import DOMAIN
-from homeassistant.components.zha.core.const import CHANNEL_ON_OFF
+from homeassistant.components.zha.core.const import CHANNEL_EVENT_RELAY
from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.setup import async_setup_component
-from .common import async_enable_traffic, async_init_zigpy_device
-
from tests.common import async_get_device_automations, async_mock_service
ON = 1
@@ -42,13 +39,30 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
-async def test_triggers(hass, config_entry, zha_gateway):
+@pytest.fixture
+async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored):
+ """IAS device fixture."""
+
+ zigpy_device = zigpy_device_mock(
+ {
+ 1: {
+ "in_clusters": [general.Basic.cluster_id],
+ "out_clusters": [general.OnOff.cluster_id],
+ "device_type": 0,
+ }
+ },
+ )
+
+ zha_device = await zha_device_joined_restored(zigpy_device)
+ zha_device.update_available(True)
+ await hass.async_block_till_done()
+ return zigpy_device, zha_device
+
+
+async def test_triggers(hass, mock_devices):
"""Test zha device triggers."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
- )
+ zigpy_device, zha_device = mock_devices
zigpy_device.device_automation_triggers = {
(SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE},
@@ -58,11 +72,6 @@ async def test_triggers(hass, config_entry, zha_gateway):
(LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD},
}
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
- hass.config_entries._entries.append(config_entry)
-
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
ieee_address = str(zha_device.ieee)
ha_device_registry = await async_get_registry(hass)
@@ -110,19 +119,10 @@ async def test_triggers(hass, config_entry, zha_gateway):
assert _same_lists(triggers, expected_triggers)
-async def test_no_triggers(hass, config_entry, zha_gateway):
+async def test_no_triggers(hass, mock_devices):
"""Test zha device with no triggers."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
- )
-
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
- hass.config_entries._entries.append(config_entry)
-
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
+ _, zha_device = mock_devices
ieee_address = str(zha_device.ieee)
ha_device_registry = await async_get_registry(hass)
@@ -132,13 +132,10 @@ async def test_no_triggers(hass, config_entry, zha_gateway):
assert triggers == []
-async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls):
+async def test_if_fires_on_event(hass, mock_devices, calls):
"""Test for remote triggers firing."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
- )
+ zigpy_device, zha_device = mock_devices
zigpy_device.device_automation_triggers = {
(SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE},
@@ -148,15 +145,6 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls):
(LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD},
}
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
- hass.config_entries._entries.append(config_entry)
-
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
-
- # allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
-
ieee_address = str(zha_device.ieee)
ha_device_registry = await async_get_registry(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set())
@@ -185,30 +173,18 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls):
await hass.async_block_till_done()
- on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF]
- on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, [])
+ channel = {ch.name: ch for ch in zha_device.all_channels}[CHANNEL_EVENT_RELAY]
+ channel.zha_send_event(channel.cluster, COMMAND_SINGLE, [])
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data["message"] == "service called"
-async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, caplog):
+async def test_exception_no_triggers(hass, mock_devices, calls, caplog):
"""Test for exception on event triggers firing."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
- )
-
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
- hass.config_entries._entries.append(config_entry)
-
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
-
- # allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
+ _, zha_device = mock_devices
ieee_address = str(zha_device.ieee)
ha_device_registry = await async_get_registry(hass)
@@ -239,13 +215,10 @@ async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, cap
assert "Invalid config for [automation]" in caplog.text
-async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, caplog):
+async def test_exception_bad_trigger(hass, mock_devices, calls, caplog):
"""Test for exception on event triggers firing."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway
- )
+ zigpy_device, zha_device = mock_devices
zigpy_device.device_automation_triggers = {
(SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE},
@@ -255,15 +228,6 @@ async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, cap
(LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD},
}
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
- hass.config_entries._entries.append(config_entry)
-
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
-
- # allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
-
ieee_address = str(zha_device.ieee)
ha_device_registry = await async_get_registry(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set())
diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py
index 9ed88c86e51..a194453bd65 100644
--- a/tests/components/zha/test_discover.py
+++ b/tests/components/zha/test_discover.py
@@ -1,6 +1,5 @@
"""Test zha device discovery."""
-import asyncio
import re
from unittest import mock
@@ -11,6 +10,7 @@ import homeassistant.components.zha.core.discovery as disc
import homeassistant.components.zha.core.gateway as core_zha_gw
import homeassistant.helpers.entity_registry
+from .common import get_zha_gateway
from .zha_devices_list import DEVICES
NO_TAIL_ID = re.compile("_\\d$")
@@ -18,12 +18,7 @@ NO_TAIL_ID = re.compile("_\\d$")
@pytest.mark.parametrize("device", DEVICES)
async def test_devices(
- device,
- zha_gateway: core_zha_gw.ZHAGateway,
- hass,
- config_entry,
- zigpy_device_mock,
- monkeypatch,
+ device, hass, zigpy_device_mock, monkeypatch, zha_device_joined_restored
):
"""Test device discovery."""
@@ -32,7 +27,7 @@ async def test_devices(
"00:11:22:33:44:55:66:77",
device["manufacturer"],
device["model"],
- node_desc=device["node_descriptor"],
+ node_descriptor=device["node_descriptor"],
)
_dispatch = mock.MagicMock(wraps=disc.async_dispatch_discovery_info)
@@ -45,14 +40,7 @@ async def test_devices(
"homeassistant.components.zha.core.discovery._async_create_cluster_channel",
wraps=disc._async_create_cluster_channel,
):
- await zha_gateway.async_device_restored(zigpy_device)
- await hass.async_block_till_done()
- tasks = [
- hass.config_entries.async_forward_entry_setup(config_entry, component)
- for component in zha_const.COMPONENTS
- ]
- await asyncio.gather(*tasks)
-
+ await zha_device_joined_restored(zigpy_device)
await hass.async_block_till_done()
entity_ids = hass.states.async_entity_ids()
@@ -61,6 +49,7 @@ async def test_devices(
ent for ent in entity_ids if ent.split(".")[0] in zha_const.COMPONENTS
}
+ zha_gateway = get_zha_gateway(hass)
zha_dev = zha_gateway.get_device(zigpy_device.ieee)
event_channels = { # pylint: disable=protected-access
ch.id for ch in zha_dev._relay_channels.values()
diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py
index 660bff2abac..0cf3e3e954d 100644
--- a/tests/components/zha/test_fan.py
+++ b/tests/components/zha/test_fan.py
@@ -1,7 +1,7 @@
"""Test zha fan."""
-from unittest.mock import call, patch
+from unittest.mock import call
-import zigpy.zcl.clusters.general as general
+import pytest
import zigpy.zcl.clusters.hvac as hvac
import zigpy.zcl.foundation as zcl_f
@@ -18,30 +18,27 @@ from homeassistant.const import (
from .common import (
async_enable_traffic,
- async_init_zigpy_device,
- async_test_device_join,
+ async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
)
-from tests.common import mock_coro
+
+@pytest.fixture
+def zigpy_device(zigpy_device_mock):
+ """Device tracker zigpy device."""
+ endpoints = {
+ 1: {"in_clusters": [hvac.Fan.cluster_id], "out_clusters": [], "device_type": 0}
+ }
+ return zigpy_device_mock(endpoints)
-async def test_fan(hass, config_entry, zha_gateway):
+async def test_fan(hass, zha_device_joined_restored, zigpy_device):
"""Test zha fan platform."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass, [hvac.Fan.cluster_id, general.Basic.cluster_id], [], None, zha_gateway
- )
-
- # load up fan domain
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
-
+ zha_device = await zha_device_joined_restored(zigpy_device)
cluster = zigpy_device.endpoints.get(1).fan
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
entity_id = await find_entity_id(DOMAIN, zha_device, hass)
assert entity_id is not None
@@ -49,7 +46,7 @@ async def test_fan(hass, config_entry, zha_gateway):
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
+ await async_enable_traffic(hass, [zha_device])
# test that the state has changed from unavailable to off
assert hass.states.get(entity_id).state == STATE_OFF
@@ -68,37 +65,25 @@ async def test_fan(hass, config_entry, zha_gateway):
assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA
- with patch(
- "zigpy.zcl.Cluster.write_attributes",
- return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
- ):
- # turn on via UI
- await async_turn_on(hass, entity_id)
- assert len(cluster.write_attributes.mock_calls) == 1
- assert cluster.write_attributes.call_args == call({"fan_mode": 2})
+ cluster.write_attributes.reset_mock()
+ await async_turn_on(hass, entity_id)
+ assert len(cluster.write_attributes.mock_calls) == 1
+ assert cluster.write_attributes.call_args == call({"fan_mode": 2})
# turn off from HA
- with patch(
- "zigpy.zcl.Cluster.write_attributes",
- return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]),
- ):
- # turn off via UI
- await async_turn_off(hass, entity_id)
- assert len(cluster.write_attributes.mock_calls) == 1
- assert cluster.write_attributes.call_args == call({"fan_mode": 0})
+ cluster.write_attributes.reset_mock()
+ await async_turn_off(hass, entity_id)
+ assert len(cluster.write_attributes.mock_calls) == 1
+ assert cluster.write_attributes.call_args == call({"fan_mode": 0})
# change speed from HA
- with patch(
- "zigpy.zcl.Cluster.write_attributes",
- 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)
- assert len(cluster.write_attributes.mock_calls) == 1
- assert cluster.write_attributes.call_args == call({"fan_mode": 3})
+ cluster.write_attributes.reset_mock()
+ await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH)
+ assert len(cluster.write_attributes.mock_calls) == 1
+ 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, hvac.Fan.cluster_id, entity_id)
+ await async_test_rejoin(hass, zigpy_device, [cluster], (1,))
async def async_turn_on(hass, entity_id, speed=None):
diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py
index 5c6e8ecfe7a..74aed6f5872 100644
--- a/tests/components/zha/test_gateway.py
+++ b/tests/components/zha/test_gateway.py
@@ -1,29 +1,39 @@
"""Test ZHA Gateway."""
+import pytest
import zigpy.zcl.clusters.general as general
-import homeassistant.components.zha.core.const as zha_const
-
-from .common import async_enable_traffic, async_init_zigpy_device
+from .common import async_enable_traffic, get_zha_gateway
-async def test_device_left(hass, config_entry, zha_gateway):
- """Test zha fan platform."""
-
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass, [general.Basic.cluster_id], [], None, zha_gateway
+@pytest.fixture
+def zigpy_dev_basic(zigpy_device_mock):
+ """Zigpy device with just a basic cluster."""
+ return zigpy_device_mock(
+ {
+ 1: {
+ "in_clusters": [general.Basic.cluster_id],
+ "out_clusters": [],
+ "device_type": 0,
+ }
+ },
)
- # load up fan domain
- await hass.config_entries.async_forward_entry_setup(config_entry, zha_const.SENSOR)
- await hass.async_block_till_done()
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
+@pytest.fixture
+async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic):
+ """ZHA device with just a basic cluster."""
- assert zha_device.available is False
+ zha_device = await zha_device_restored(zigpy_dev_basic)
+ return zha_device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
- assert zha_device.available is True
- zha_gateway.device_left(zigpy_device)
- assert zha_device.available is False
+async def test_device_left(hass, zigpy_dev_basic, zha_dev_basic):
+ """Device leaving the network should become unavailable."""
+
+ assert zha_dev_basic.available is False
+
+ await async_enable_traffic(hass, [zha_dev_basic])
+ assert zha_dev_basic.available is True
+
+ get_zha_gateway(hass).device_left(zigpy_dev_basic)
+ assert zha_dev_basic.available is False
diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py
index 53be188ae80..e21c22d30cf 100644
--- a/tests/components/zha/test_light.py
+++ b/tests/components/zha/test_light.py
@@ -1,10 +1,12 @@
"""Test zha light."""
-import asyncio
-from unittest.mock import MagicMock, call, patch, sentinel
+from unittest.mock import call, sentinel
+import asynctest
+import pytest
import zigpy.profiles.zha
import zigpy.types
import zigpy.zcl.clusters.general as general
+import zigpy.zcl.clusters.lighting as lighting
import zigpy.zcl.foundation as zcl_f
from homeassistant.components.light import DOMAIN
@@ -12,121 +14,115 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from .common import (
async_enable_traffic,
- async_init_zigpy_device,
- async_test_device_join,
+ async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
)
-from tests.common import mock_coro
-
ON = 1
OFF = 0
+LIGHT_ON_OFF = {
+ 1: {
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT,
+ "in_clusters": [general.Basic.cluster_id, general.OnOff.cluster_id],
+ "out_clusters": [general.Ota.cluster_id],
+ }
+}
-async def test_light(hass, config_entry, zha_gateway, monkeypatch):
+LIGHT_LEVEL = {
+ 1: {
+ "device_type": zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT,
+ "in_clusters": [
+ general.Basic.cluster_id,
+ general.LevelControl.cluster_id,
+ general.OnOff.cluster_id,
+ ],
+ "out_clusters": [general.Ota.cluster_id],
+ }
+}
+
+LIGHT_COLOR = {
+ 1: {
+ "device_type": zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT,
+ "in_clusters": [
+ general.Basic.cluster_id,
+ general.LevelControl.cluster_id,
+ general.OnOff.cluster_id,
+ lighting.Color.cluster_id,
+ ],
+ "out_clusters": [general.Ota.cluster_id],
+ }
+}
+
+
+@asynctest.patch(
+ "zigpy.zcl.clusters.lighting.Color.request",
+ new=asynctest.CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
+)
+@asynctest.patch(
+ "zigpy.zcl.clusters.general.LevelControl.request",
+ new=asynctest.CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
+)
+@asynctest.patch(
+ "zigpy.zcl.clusters.general.OnOff.request",
+ new=asynctest.CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
+)
+@pytest.mark.parametrize(
+ "device, reporting",
+ [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 3))],
+)
+async def test_light(
+ hass, zigpy_device_mock, zha_device_joined_restored, device, reporting,
+):
"""Test zha light platform."""
# create zigpy devices
- zigpy_device_on_off = await async_init_zigpy_device(
- hass,
- [general.OnOff.cluster_id, general.Basic.cluster_id],
- [],
- zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT,
- zha_gateway,
- ieee="00:0d:6f:11:0a:90:69:e6",
- )
+ zigpy_device = zigpy_device_mock(device)
+ zha_device = await zha_device_joined_restored(zigpy_device)
+ entity_id = await find_entity_id(DOMAIN, zha_device, hass)
- zigpy_device_level = await async_init_zigpy_device(
- hass,
- [
- general.OnOff.cluster_id,
- general.LevelControl.cluster_id,
- general.Basic.cluster_id,
- ],
- [],
- zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT,
- zha_gateway,
- ieee="00:0d:6f:11:0a:90:69:e7",
- manufacturer="FakeLevelManufacturer",
- model="FakeLevelModel",
- )
+ assert entity_id is not None
- # load up light domain
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
-
- # on off light
- on_off_device_on_off_cluster = zigpy_device_on_off.endpoints.get(1).on_off
- on_off_zha_device = zha_gateway.get_device(zigpy_device_on_off.ieee)
- on_off_entity_id = await find_entity_id(DOMAIN, on_off_zha_device, hass)
- assert on_off_entity_id is not None
-
- # dimmable light
- level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off
- level_device_level_cluster = zigpy_device_level.endpoints.get(1).level
- on_off_mock = MagicMock(
- side_effect=asyncio.coroutine(
- MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS])
- )
- )
- level_mock = MagicMock(
- side_effect=asyncio.coroutine(
- MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS])
- )
- )
- monkeypatch.setattr(level_device_on_off_cluster, "request", on_off_mock)
- monkeypatch.setattr(level_device_level_cluster, "request", level_mock)
- level_zha_device = zha_gateway.get_device(zigpy_device_level.ieee)
- level_entity_id = await find_entity_id(DOMAIN, level_zha_device, hass)
- assert level_entity_id is not None
+ cluster_on_off = zigpy_device.endpoints[1].on_off
+ cluster_level = getattr(zigpy_device.endpoints[1], "level", None)
+ cluster_color = getattr(zigpy_device.endpoints[1], "light_color", None)
# test that the lights were created and that they are unavailable
- assert hass.states.get(on_off_entity_id).state == STATE_UNAVAILABLE
- assert hass.states.get(level_entity_id).state == STATE_UNAVAILABLE
+ assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [on_off_zha_device, level_zha_device])
+ await async_enable_traffic(hass, [zha_device])
# test that the lights were created and are off
- assert hass.states.get(on_off_entity_id).state == STATE_OFF
- assert hass.states.get(level_entity_id).state == STATE_OFF
+ assert hass.states.get(entity_id).state == STATE_OFF
# test turning the lights on and off from the light
- await async_test_on_off_from_light(
- hass, on_off_device_on_off_cluster, on_off_entity_id
- )
-
- await async_test_on_off_from_light(
- hass, level_device_on_off_cluster, level_entity_id
- )
+ await async_test_on_off_from_light(hass, cluster_on_off, entity_id)
# test turning the lights on and off from the HA
- await async_test_on_off_from_hass(
- hass, on_off_device_on_off_cluster, on_off_entity_id
- )
+ await async_test_on_off_from_hass(hass, cluster_on_off, entity_id)
- await async_test_level_on_off_from_hass(
- hass, level_device_on_off_cluster, level_device_level_cluster, level_entity_id
- )
+ if cluster_level:
+ await async_test_level_on_off_from_hass(
+ hass, cluster_on_off, cluster_level, entity_id
+ )
- # test turning the lights on and off from the light
- await async_test_on_from_light(hass, level_device_on_off_cluster, level_entity_id)
+ # test getting a brightness change from the network
+ await async_test_on_from_light(hass, cluster_on_off, entity_id)
+ await async_test_dimmer_from_light(
+ hass, cluster_level, entity_id, 150, STATE_ON
+ )
- # test getting a brightness change from the network
- await async_test_dimmer_from_light(
- hass, level_device_level_cluster, level_entity_id, 150, STATE_ON
- )
-
- # test adding a new light to the network and HA
- await async_test_device_join(
- hass,
- zha_gateway,
- general.OnOff.cluster_id,
- on_off_entity_id,
- device_type=zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT,
- )
+ # test rejoin
+ await async_test_off_from_hass(hass, cluster_on_off, entity_id)
+ clusters = [cluster_on_off]
+ if cluster_level:
+ clusters.append(cluster_level)
+ if cluster_color:
+ clusters.append(cluster_color)
+ await async_test_rejoin(hass, zigpy_device, clusters, reporting)
async def async_test_on_off_from_light(hass, cluster, entity_id):
@@ -157,36 +153,33 @@ async def async_test_on_from_light(hass, cluster, entity_id):
async def async_test_on_off_from_hass(hass, cluster, entity_id):
"""Test on off functionality from hass."""
- with patch(
- "zigpy.zcl.Cluster.request",
- return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]),
- ):
- # turn on via UI
- await hass.services.async_call(
- DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True
- )
- assert cluster.request.call_count == 1
- assert cluster.request.call_args == call(
- False, ON, (), expect_reply=True, manufacturer=None
- )
+ # turn on via UI
+ cluster.request.reset_mock()
+ await hass.services.async_call(
+ DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True
+ )
+ assert cluster.request.call_count == 1
+ assert cluster.request.await_count == 1
+ assert cluster.request.call_args == call(
+ False, ON, (), expect_reply=True, manufacturer=None
+ )
await async_test_off_from_hass(hass, cluster, entity_id)
async def async_test_off_from_hass(hass, cluster, entity_id):
"""Test turning off the light from Home Assistant."""
- with patch(
- "zigpy.zcl.Cluster.request",
- return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]),
- ):
- # turn off via UI
- await hass.services.async_call(
- DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True
- )
- assert cluster.request.call_count == 1
- assert cluster.request.call_args == call(
- False, OFF, (), expect_reply=True, manufacturer=None
- )
+
+ # turn off via UI
+ cluster.request.reset_mock()
+ await hass.services.async_call(
+ DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True
+ )
+ assert cluster.request.call_count == 1
+ assert cluster.request.await_count == 1
+ assert cluster.request.call_args == call(
+ False, OFF, (), expect_reply=True, manufacturer=None
+ )
async def async_test_level_on_off_from_hass(
@@ -194,12 +187,15 @@ async def async_test_level_on_off_from_hass(
):
"""Test on off functionality from hass."""
+ on_off_cluster.request.reset_mock()
# turn on via UI
await hass.services.async_call(
DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True
)
assert on_off_cluster.request.call_count == 1
+ assert on_off_cluster.request.await_count == 1
assert level_cluster.request.call_count == 0
+ assert level_cluster.request.await_count == 0
assert on_off_cluster.request.call_args == call(
False, 1, (), expect_reply=True, manufacturer=None
)
@@ -210,7 +206,9 @@ async def async_test_level_on_off_from_hass(
DOMAIN, "turn_on", {"entity_id": entity_id, "transition": 10}, blocking=True
)
assert on_off_cluster.request.call_count == 1
+ assert on_off_cluster.request.await_count == 1
assert level_cluster.request.call_count == 1
+ assert level_cluster.request.await_count == 1
assert on_off_cluster.request.call_args == call(
False, 1, (), expect_reply=True, manufacturer=None
)
@@ -230,7 +228,9 @@ async def async_test_level_on_off_from_hass(
DOMAIN, "turn_on", {"entity_id": entity_id, "brightness": 10}, blocking=True
)
assert on_off_cluster.request.call_count == 1
+ assert on_off_cluster.request.await_count == 1
assert level_cluster.request.call_count == 1
+ assert level_cluster.request.await_count == 1
assert on_off_cluster.request.call_args == call(
False, 1, (), expect_reply=True, manufacturer=None
)
diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py
index 1daef317fed..0442ea497d7 100644
--- a/tests/components/zha/test_lock.py
+++ b/tests/components/zha/test_lock.py
@@ -1,6 +1,8 @@
"""Test zha lock."""
from unittest.mock import patch
+import pytest
+import zigpy.profiles.zha
import zigpy.zcl.clusters.closures as closures
import zigpy.zcl.clusters.general as general
import zigpy.zcl.foundation as zcl_f
@@ -10,7 +12,6 @@ from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED
from .common import (
async_enable_traffic,
- async_init_zigpy_device,
find_entity_id,
make_attribute,
make_zcl_header,
@@ -22,24 +23,28 @@ LOCK_DOOR = 0
UNLOCK_DOOR = 1
-async def test_lock(hass, config_entry, zha_gateway):
- """Test zha lock platform."""
+@pytest.fixture
+async def lock(hass, zigpy_device_mock, zha_device_joined_restored):
+ """Lock cluster fixture."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass,
- [closures.DoorLock.cluster_id, general.Basic.cluster_id],
- [],
- None,
- zha_gateway,
+ zigpy_device = zigpy_device_mock(
+ {
+ 1: {
+ "in_clusters": [closures.DoorLock.cluster_id, general.Basic.cluster_id],
+ "out_clusters": [],
+ "device_type": zigpy.profiles.zha.DeviceType.DOOR_LOCK,
+ }
+ },
)
- # load up lock domain
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
+ zha_device = await zha_device_joined_restored(zigpy_device)
+ return zha_device, zigpy_device.endpoints[1].door_lock
- cluster = zigpy_device.endpoints.get(1).door_lock
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
+
+async def test_lock(hass, lock):
+ """Test zha lock platform."""
+
+ zha_device, cluster = lock
entity_id = await find_entity_id(DOMAIN, zha_device, hass)
assert entity_id is not None
@@ -47,7 +52,7 @@ async def test_lock(hass, config_entry, zha_gateway):
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
+ await async_enable_traffic(hass, [zha_device])
# test that the state has changed from unavailable to unlocked
assert hass.states.get(entity_id).state == STATE_UNLOCKED
diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py
index 4c913e10034..b81e8f02c12 100644
--- a/tests/components/zha/test_sensor.py
+++ b/tests/components/zha/test_sensor.py
@@ -11,7 +11,6 @@ import zigpy.zcl.foundation as zcl_f
from homeassistant.components.sensor import DOMAIN
import homeassistant.config as config_util
from homeassistant.const import (
- ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
CONF_UNIT_SYSTEM,
CONF_UNIT_SYSTEM_IMPERIAL,
@@ -26,160 +25,44 @@ from homeassistant.util import dt as dt_util
from .common import (
async_enable_traffic,
- async_init_zigpy_device,
- async_test_device_join,
+ async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
)
-async def test_sensor(hass, config_entry, zha_gateway):
- """Test zha sensor platform."""
-
- # list of cluster ids to create devices and sensor entities for
- cluster_ids = [
- 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
- zigpy_device_infos = await async_build_devices(
- hass, zha_gateway, config_entry, cluster_ids
- )
-
- # ensure the sensor entity was created for each id in cluster_ids
- for cluster_id in cluster_ids:
- zigpy_device_info = zigpy_device_infos[cluster_id]
- entity_id = zigpy_device_info[ATTR_ENTITY_ID]
- assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
-
- # allow traffic to flow through the gateway and devices
- await async_enable_traffic(
- hass,
- zha_gateway,
- [
- zigpy_device_info["zha_device"]
- for zigpy_device_info in zigpy_device_infos.values()
- ],
- )
-
- # test that the sensors now have a state of unknown
- for cluster_id in cluster_ids:
- zigpy_device_info = zigpy_device_infos[cluster_id]
- entity_id = zigpy_device_info[ATTR_ENTITY_ID]
- 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[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[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[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[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[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[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, measurement.TemperatureMeasurement.cluster_id, entity_id
- )
-
-
-async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids):
- """Build a zigpy device for each cluster id.
-
- This will build devices for all cluster ids that exist in cluster_ids.
- They get added to the network and then the sensor component is loaded
- which will cause sensor entites to get created for each device.
- A dict containing relevant device info for testing is returned. It contains
- the entity id, zigpy device, and the zigbee cluster for the sensor.
- """
-
- device_infos = {}
- counter = 0
- for cluster_id in cluster_ids:
- # create zigpy device
- device_infos[cluster_id] = {"zigpy_device": None}
- device_infos[cluster_id]["zigpy_device"] = await async_init_zigpy_device(
- hass,
- [cluster_id, general.Basic.cluster_id],
- [],
- None,
- zha_gateway,
- ieee=f"00:15:8d:00:02:32:4f:0{counter}",
- manufacturer=f"Fake{cluster_id}",
- model=f"FakeModel{cluster_id}",
- )
-
- counter += 1
-
- # load up sensor domain
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
-
- # put the other relevant info in the device info dict
- for cluster_id in cluster_ids:
- device_info = device_infos[cluster_id]
- zigpy_device = device_info["zigpy_device"]
- device_info["cluster"] = zigpy_device.endpoints.get(1).in_clusters[cluster_id]
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
- device_info["zha_device"] = zha_device
- device_info[ATTR_ENTITY_ID] = await find_entity_id(DOMAIN, zha_device, hass)
- await hass.async_block_till_done()
- return device_infos
-
-
-async def async_test_humidity(hass, device_info):
+async def async_test_humidity(hass, cluster, entity_id):
"""Test humidity sensor."""
- await send_attribute_report(hass, device_info["cluster"], 0, 1000)
- assert_state(hass, device_info, "10.0", "%")
+ await send_attribute_report(hass, cluster, 0, 1000)
+ assert_state(hass, entity_id, "10.0", "%")
-async def async_test_temperature(hass, device_info):
+async def async_test_temperature(hass, cluster, entity_id):
"""Test temperature sensor."""
- await send_attribute_report(hass, device_info["cluster"], 0, 2900)
- assert_state(hass, device_info, "29.0", "°C")
+ await send_attribute_report(hass, cluster, 0, 2900)
+ assert_state(hass, entity_id, "29.0", "°C")
-async def async_test_pressure(hass, device_info):
+async def async_test_pressure(hass, cluster, entity_id):
"""Test pressure sensor."""
- await send_attribute_report(hass, device_info["cluster"], 0, 1000)
- assert_state(hass, device_info, "1000", "hPa")
+ await send_attribute_report(hass, cluster, 0, 1000)
+ assert_state(hass, entity_id, "1000", "hPa")
-async def async_test_illuminance(hass, device_info):
+async def async_test_illuminance(hass, cluster, entity_id):
"""Test illuminance sensor."""
- await send_attribute_report(hass, device_info["cluster"], 0, 10)
- assert_state(hass, device_info, "1.0", "lx")
+ await send_attribute_report(hass, cluster, 0, 10)
+ assert_state(hass, entity_id, "1.0", "lx")
-async def async_test_metering(hass, device_info):
+async def async_test_metering(hass, cluster, entity_id):
"""Test metering sensor."""
- await send_attribute_report(hass, device_info["cluster"], 1024, 12345)
- assert_state(hass, device_info, "12345.0", "unknown")
+ await send_attribute_report(hass, cluster, 1024, 12345)
+ assert_state(hass, entity_id, "12345.0", "unknown")
-async def async_test_electrical_measurement(hass, device_info):
+async def async_test_electrical_measurement(hass, cluster, entity_id):
"""Test electrical measurement sensor."""
with mock.patch(
(
@@ -189,18 +72,72 @@ async def async_test_electrical_measurement(hass, device_info):
new_callable=mock.PropertyMock,
) as divisor_mock:
divisor_mock.return_value = 1
- await send_attribute_report(hass, device_info["cluster"], 1291, 100)
- assert_state(hass, device_info, "100", "W")
+ await send_attribute_report(hass, cluster, 1291, 100)
+ assert_state(hass, entity_id, "100", "W")
- await send_attribute_report(hass, device_info["cluster"], 1291, 99)
- assert_state(hass, device_info, "99", "W")
+ await send_attribute_report(hass, cluster, 1291, 99)
+ assert_state(hass, entity_id, "99", "W")
divisor_mock.return_value = 10
- await send_attribute_report(hass, device_info["cluster"], 1291, 1000)
- assert_state(hass, device_info, "100", "W")
+ await send_attribute_report(hass, cluster, 1291, 1000)
+ assert_state(hass, entity_id, "100", "W")
- await send_attribute_report(hass, device_info["cluster"], 1291, 99)
- assert_state(hass, device_info, "9.9", "W")
+ await send_attribute_report(hass, cluster, 1291, 99)
+ assert_state(hass, entity_id, "9.9", "W")
+
+
+@pytest.mark.parametrize(
+ "cluster_id, test_func, report_count",
+ (
+ (measurement.RelativeHumidity.cluster_id, async_test_humidity, 1),
+ (measurement.TemperatureMeasurement.cluster_id, async_test_temperature, 1),
+ (measurement.PressureMeasurement.cluster_id, async_test_pressure, 1),
+ (measurement.IlluminanceMeasurement.cluster_id, async_test_illuminance, 1),
+ (smartenergy.Metering.cluster_id, async_test_metering, 1),
+ (
+ homeautomation.ElectricalMeasurement.cluster_id,
+ async_test_electrical_measurement,
+ 1,
+ ),
+ ),
+)
+async def test_sensor(
+ hass,
+ zigpy_device_mock,
+ zha_device_joined_restored,
+ cluster_id,
+ test_func,
+ report_count,
+):
+ """Test zha sensor platform."""
+
+ zigpy_device = zigpy_device_mock(
+ {
+ 1: {
+ "in_clusters": [cluster_id, general.Basic.cluster_id],
+ "out_cluster": [],
+ "device_type": 0x0000,
+ }
+ }
+ )
+ cluster = zigpy_device.endpoints[1].in_clusters[cluster_id]
+ zha_device = await zha_device_joined_restored(zigpy_device)
+ entity_id = await find_entity_id(DOMAIN, zha_device, hass)
+
+ # ensure the sensor entity was created
+ assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
+
+ # allow traffic to flow through the gateway and devices
+ await async_enable_traffic(hass, [zha_device])
+
+ # test that the sensor now have a state of unknown
+ assert hass.states.get(entity_id).state == STATE_UNKNOWN
+
+ # test sensor associated logic
+ await test_func(hass, cluster, entity_id)
+
+ # test rejoin
+ await async_test_rejoin(hass, zigpy_device, [cluster], (report_count,))
async def send_attribute_report(hass, cluster, attrid, value):
@@ -215,13 +152,13 @@ async def send_attribute_report(hass, cluster, attrid, value):
await hass.async_block_till_done()
-def assert_state(hass, device_info, state, unit_of_measurement):
+def assert_state(hass, entity_id, state, unit_of_measurement):
"""Check that the state is what is expected.
This is used to ensure that the logic in each sensor class handled the
attribute report it received correctly.
"""
- hass_state = hass.states.get(device_info[ATTR_ENTITY_ID])
+ hass_state = hass.states.get(entity_id)
assert hass_state.state == state
assert hass_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement
@@ -282,7 +219,14 @@ def core_rs(hass_storage):
],
)
async def test_temp_uom(
- uom, raw_temp, expected, restore, hass_ms, config_entry, zha_gateway, core_rs
+ uom,
+ raw_temp,
+ expected,
+ restore,
+ hass_ms,
+ core_rs,
+ zigpy_device_mock,
+ zha_device_restored,
):
"""Test zha temperature sensor unit of measurement."""
@@ -294,28 +238,33 @@ async def test_temp_uom(
CONF_UNIT_SYSTEM_METRIC if uom == TEMP_CELSIUS else CONF_UNIT_SYSTEM_IMPERIAL
)
- # list of cluster ids to create devices and sensor entities for
- temp_cluster = measurement.TemperatureMeasurement
- cluster_ids = [temp_cluster.cluster_id]
-
- # devices that were created from cluster_ids list above
- zigpy_device_infos = await async_build_devices(
- hass, zha_gateway, config_entry, cluster_ids
+ zigpy_device = zigpy_device_mock(
+ {
+ 1: {
+ "in_clusters": [
+ measurement.TemperatureMeasurement.cluster_id,
+ general.Basic.cluster_id,
+ ],
+ "out_cluster": [],
+ "device_type": 0x0000,
+ }
+ }
)
+ cluster = zigpy_device.endpoints[1].temperature
+ zha_device = await zha_device_restored(zigpy_device)
+ entity_id = await find_entity_id(DOMAIN, zha_device, hass)
- zigpy_device_info = zigpy_device_infos[temp_cluster.cluster_id]
- zha_device = zigpy_device_info["zha_device"]
if not restore:
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and devices
- await async_enable_traffic(hass, zha_gateway, [zha_device])
+ await async_enable_traffic(hass, [zha_device])
# test that the sensors now have a state of unknown
if not restore:
assert hass.states.get(entity_id).state == STATE_UNKNOWN
- await send_attribute_report(hass, zigpy_device_info["cluster"], 0, raw_temp)
+ await send_attribute_report(hass, cluster, 0, raw_temp)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py
index 11a0b8f3481..a088283834b 100644
--- a/tests/components/zha/test_switch.py
+++ b/tests/components/zha/test_switch.py
@@ -1,6 +1,7 @@
"""Test zha switch."""
from unittest.mock import call, patch
+import pytest
import zigpy.zcl.clusters.general as general
import zigpy.zcl.foundation as zcl_f
@@ -9,8 +10,7 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from .common import (
async_enable_traffic,
- async_init_zigpy_device,
- async_test_device_join,
+ async_test_rejoin,
find_entity_id,
make_attribute,
make_zcl_header,
@@ -22,24 +22,24 @@ ON = 1
OFF = 0
-async def test_switch(hass, config_entry, zha_gateway):
+@pytest.fixture
+def zigpy_device(zigpy_device_mock):
+ """Device tracker zigpy device."""
+ endpoints = {
+ 1: {
+ "in_clusters": [general.Basic.cluster_id, general.OnOff.cluster_id],
+ "out_clusters": [],
+ "device_type": 0,
+ }
+ }
+ return zigpy_device_mock(endpoints)
+
+
+async def test_switch(hass, zha_device_joined_restored, zigpy_device):
"""Test zha switch platform."""
- # create zigpy device
- zigpy_device = await async_init_zigpy_device(
- hass,
- [general.OnOff.cluster_id, general.Basic.cluster_id],
- [],
- None,
- zha_gateway,
- )
-
- # load up switch domain
- await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
- await hass.async_block_till_done()
-
+ zha_device = await zha_device_joined_restored(zigpy_device)
cluster = zigpy_device.endpoints.get(1).on_off
- zha_device = zha_gateway.get_device(zigpy_device.ieee)
entity_id = await find_entity_id(DOMAIN, zha_device, hass)
assert entity_id is not None
@@ -47,7 +47,7 @@ async def test_switch(hass, config_entry, zha_gateway):
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
- await async_enable_traffic(hass, zha_gateway, [zha_device])
+ await async_enable_traffic(hass, [zha_device])
# test that the state has changed from unavailable to off
assert hass.states.get(entity_id).state == STATE_OFF
@@ -94,4 +94,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, general.OnOff.cluster_id, entity_id)
+ await async_test_rejoin(hass, zigpy_device, [cluster], (1,))
diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py
index 631bf0a0ce8..3586c992c08 100644
--- a/tests/components/zwave/test_climate.py
+++ b/tests/components/zwave/test_climate.py
@@ -342,7 +342,7 @@ def test_get_device_detects_single_setpoint_device(device_single_setpoint):
def test_default_hvac_modes():
- """Test wether all hvac modes are included in default_hvac_modes."""
+ """Test whether all hvac modes are included in default_hvac_modes."""
for hvac_mode in HVAC_MODES:
assert hvac_mode in DEFAULT_HVAC_MODES
diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py
index 540a3f96604..a8f72d2105c 100644
--- a/tests/components/zwave/test_init.py
+++ b/tests/components/zwave/test_init.py
@@ -100,7 +100,7 @@ async def test_network_key_validation(hass, mock_openzwave):
async def test_erronous_network_key_fails_validation(hass, mock_openzwave):
- """Test failing erronous network key validation."""
+ """Test failing erroneous network key validation."""
test_values = [
(
"0x 01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, "
diff --git a/tests/conftest.py b/tests/conftest.py
index 5a3b2494158..04e584cb158 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -39,7 +39,7 @@ def check_real(func):
"""Force a function to require a keyword _test_real to be passed in."""
@functools.wraps(func)
- def guard_func(*args, **kwargs):
+ async def guard_func(*args, **kwargs):
real = kwargs.pop("_test_real", None)
if not real:
@@ -47,7 +47,7 @@ def check_real(func):
'Forgot to mock or pass "_test_real=True" to %s', func.__name__
)
- return func(*args, **kwargs)
+ return await func(*args, **kwargs)
return guard_func
diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json
index 5ca5f2810d3..a97b1247e2c 100644
--- a/tests/fixtures/homematicip_cloud.json
+++ b/tests/fixtures/homematicip_cloud.json
@@ -14,6 +14,203 @@
}
},
"devices": {
+ "3014F711000BBBB000000000": {
+ "availableFirmwareVersion": "2.0.2",
+ "firmwareVersion": "2.0.2",
+ "firmwareVersionInteger": 131074,
+ "functionalChannels": {
+ "0": {
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "coProUpdateFailure": false,
+ "configPending": false,
+ "deviceId": "3014F711000BBBB000000000",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "deviceUndervoltage": false,
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -45,
+ "rssiPeerValue": -54,
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceCoProError": false,
+ "IFeatureDeviceCoProRestart": false,
+ "IFeatureDeviceCoProUpdate": false,
+ "IFeatureDeviceOverheated": false,
+ "IFeatureDeviceOverloaded": false,
+ "IFeatureDeviceTemperatureOutOfRange": false,
+ "IFeatureDeviceUndervoltage": false
+ },
+ "temperatureOutOfRange": false,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 23.0,
+ "deviceId": "3014F711000BBBB000000000",
+ "display": "ACTUAL",
+ "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "humidity": 52,
+ "index": 1,
+ "label": "",
+ "setPointTemperature": 20.0,
+ "temperatureOffset": 0.0,
+ "vaporAmount": 10.662700840292974
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711000BBBB000000000",
+ "label": "Raumbedienger\u00e4t",
+ "lastStatusUpdate": 1579383507353,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 282,
+ "modelType": "ALPHA-IP-RBG",
+ "oem": "M\u00f6hlenhoff",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711000BBBB000000000",
+ "type": "ROOM_CONTROL_DEVICE",
+ "updateState": "UP_TO_DATE"
+ },
+ "3014F711000000BBBB000005": {
+ "availableFirmwareVersion": "1.0.16",
+ "firmwareVersion": "1.0.12",
+ "firmwareVersionInteger": 65548,
+ "functionalChannels": {
+ "0": {
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "coProUpdateFailure": false,
+ "configPending": false,
+ "deviceId": "3014F711000000BBBB000005",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "deviceUndervoltage": false,
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": [],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -41,
+ "rssiPeerValue": -29,
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceCoProError": false,
+ "IFeatureDeviceCoProRestart": false,
+ "IFeatureDeviceCoProUpdate": false,
+ "IFeatureDeviceOverheated": false,
+ "IFeatureDeviceOverloaded": false,
+ "IFeatureDeviceTemperatureOutOfRange": false,
+ "IFeatureDeviceUndervoltage": false
+ },
+ "temperatureOutOfRange": false,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 23.3,
+ "deviceId": "3014F711000000BBBB000005",
+ "functionalChannelType": "ANALOG_ROOM_CONTROL_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "index": 1,
+ "label": "",
+ "setPointTemperature": 23.0,
+ "temperatureOffset": 0.0
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711000000BBBB000005",
+ "label": "Raumbedienger\u00e4t Analog",
+ "lastStatusUpdate": 1579384126279,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 281,
+ "modelType": "ALPHA-IP-RBGa",
+ "oem": "M\u00f6hlenhoff",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711000000BBBB000005",
+ "type": "ROOM_CONTROL_DEVICE_ANALOG",
+ "updateState": "TRANSFERING_UPDATE"
+ },
+ "3014F711000000000000AAA5": {
+ "availableFirmwareVersion": "0.0.0",
+ "firmwareVersion": "1.0.12",
+ "firmwareVersionInteger": 65548,
+ "functionalChannels": {
+ "0": {
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "coProUpdateFailure": false,
+ "configPending": false,
+ "deviceId": "3014F711000000000000AAA5",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "deviceUndervoltage": false,
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_OPERATIONLOCK",
+ "groupIndex": 0,
+ "groups": [],
+ "index": 0,
+ "label": "",
+ "lowBat": false,
+ "operationLockActive": false,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -58,
+ "rssiPeerValue": -59,
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceCoProError": false,
+ "IFeatureDeviceCoProRestart": false,
+ "IFeatureDeviceCoProUpdate": false,
+ "IFeatureDeviceOverheated": false,
+ "IFeatureDeviceOverloaded": false,
+ "IFeatureDeviceTemperatureOutOfRange": false,
+ "IFeatureDeviceUndervoltage": false
+ },
+ "temperatureOutOfRange": false,
+ "unreach": false
+ },
+ "1": {
+ "actualTemperature": 16.0,
+ "deviceId": "3014F711000000000000AAA5",
+ "display": "ACTUAL",
+ "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "humidity": 42,
+ "index": 1,
+ "label": "",
+ "setPointTemperature": 12.0,
+ "temperatureOffset": 0.0,
+ "vaporAmount": 5.710127947243264
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711000000000000AAA5",
+ "label": "Thermostat Schlafen Tal",
+ "lastStatusUpdate": 1578954498192,
+ "liveUpdateState": "UP_TO_DATE",
+ "manufacturerCode": 1,
+ "modelId": 408,
+ "modelType": "HmIP-WTH-B",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711000000000000AAA5",
+ "type": "WALL_MOUNTED_THERMOSTAT_BASIC_HUMIDITY",
+ "updateState": "BACKGROUND_UPDATE_NOT_SUPPORTED"
+ },
"3014F7110000000000ABCD50": {
"availableFirmwareVersion": "1.0.12",
"firmwareVersion": "1.0.12",
@@ -66,7 +263,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000ABCD50",
- "label": "Netzausfall",
+ "label": "Netzausfall\u00fcberwachung",
"lastStatusUpdate": 1577487207542,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3020,7 +3217,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000013",
- "label": "Heizkörperthermostat",
+ "label": "Heizkörperthermostat2",
"lastStatusUpdate": 1524514007132,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3188,7 +3385,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000016",
- "label": "Heizkörperthermostat",
+ "label": "Heizkörperthermostat3",
"lastStatusUpdate": 1524514626157,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3348,7 +3545,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000019",
- "label": "Rauchwarnmelder",
+ "label": "Rauchwarnmelder2",
"lastStatusUpdate": 1524480981494,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3400,7 +3597,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000020",
- "label": "Rauchwarnmelder",
+ "label": "Rauchwarnmelder3",
"lastStatusUpdate": 1524456324824,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3452,7 +3649,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000021",
- "label": "Rauchwarnmelder",
+ "label": "Rauchwarnmelder4",
"lastStatusUpdate": 1524443129876,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3566,7 +3763,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000023",
- "label": "Wandthermostat",
+ "label": "Wandthermostat4",
"lastStatusUpdate": 1524516454116,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3623,7 +3820,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000024",
- "label": "Wandthermostat",
+ "label": "Wandthermostat2",
"lastStatusUpdate": 1524516436601,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3680,7 +3877,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000025",
- "label": "Wandthermostat",
+ "label": "Wandthermostat3",
"lastStatusUpdate": 1524516556479,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index 57554d37bb1..9b6aa6b812d 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -472,6 +472,30 @@ def test_datetime():
schema("2016-11-23T18:59:08")
+def test_multi_select():
+ """Test multi select validation.
+
+ Expected behavior:
+ - Will not accept any input but a list
+ - Will not accept selections outside of configured scope
+ """
+ schema = vol.Schema(cv.multi_select({"paulus": "Paulus", "robban": "Robban"}))
+
+ with pytest.raises(vol.Invalid):
+ schema("robban")
+ schema(["paulus", "martinhj"])
+
+ schema(["robban", "paulus"])
+
+
+def test_multi_select_in_serializer():
+ """Test multi_select with custom_serializer."""
+ assert cv.custom_serializer(cv.multi_select({"paulus": "Paulus"})) == {
+ "type": "multi_select",
+ "options": {"paulus": "Paulus"},
+ }
+
+
@pytest.fixture
def schema():
"""Create a schema used for testing deprecation."""
diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py
index d7629a393a9..4972fbbc018 100644
--- a/tests/helpers/test_debounce.py
+++ b/tests/helpers/test_debounce.py
@@ -8,7 +8,11 @@ async def test_immediate_works(hass):
"""Test immediate works."""
calls = []
debouncer = debounce.Debouncer(
- hass, None, 0.01, True, CoroutineMock(side_effect=lambda: calls.append(None))
+ hass,
+ None,
+ cooldown=0.01,
+ immediate=True,
+ function=CoroutineMock(side_effect=lambda: calls.append(None)),
)
await debouncer.async_call()
@@ -37,7 +41,11 @@ async def test_not_immediate_works(hass):
"""Test immediate works."""
calls = []
debouncer = debounce.Debouncer(
- hass, None, 0.01, False, CoroutineMock(side_effect=lambda: calls.append(None))
+ hass,
+ None,
+ cooldown=0.01,
+ immediate=False,
+ function=CoroutineMock(side_effect=lambda: calls.append(None)),
)
await debouncer.async_call()
diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py
index 749c11ff1a5..6dc194c09d4 100644
--- a/tests/helpers/test_entity.py
+++ b/tests/helpers/test_entity.py
@@ -661,3 +661,43 @@ async def test_capability_attrs(hass):
assert state is not None
assert state.state == STATE_UNAVAILABLE
assert state.attributes["always"] == "there"
+
+
+async def test_warn_slow_write_state(hass, caplog):
+ """Check that we log a warning if reading properties takes too long."""
+ mock_entity = entity.Entity()
+ mock_entity.hass = hass
+ mock_entity.entity_id = "comp_test.test_entity"
+ mock_entity.platform = MagicMock(platform_name="hue")
+
+ with patch("homeassistant.helpers.entity.timer", side_effect=[0, 10]):
+ mock_entity.async_write_ha_state()
+
+ assert (
+ "Updating state for comp_test.test_entity "
+ "() "
+ "took 10.000 seconds. Please create a bug report at "
+ "https://github.com/home-assistant/home-assistant/issues?"
+ "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22"
+ ) in caplog.text
+
+
+async def test_warn_slow_write_state_custom_component(hass, caplog):
+ """Check that we log a warning if reading properties takes too long."""
+
+ class CustomComponentEntity(entity.Entity):
+ __module__ = "custom_components.bla.sensor"
+
+ mock_entity = CustomComponentEntity()
+ mock_entity.hass = hass
+ mock_entity.entity_id = "comp_test.test_entity"
+ mock_entity.platform = MagicMock(platform_name="hue")
+
+ with patch("homeassistant.helpers.entity.timer", side_effect=[0, 10]):
+ mock_entity.async_write_ha_state()
+
+ assert (
+ "Updating state for comp_test.test_entity "
+ "(.CustomComponentEntity'>) "
+ "took 10.000 seconds. Please report it to the custom component author."
+ ) in caplog.text
diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py
index 07cea74c05f..306402cd2b9 100644
--- a/tests/helpers/test_entity_component.py
+++ b/tests/helpers/test_entity_component.py
@@ -7,8 +7,9 @@ from unittest.mock import Mock, patch
import asynctest
import pytest
+import voluptuous as vol
-from homeassistant.const import ENTITY_MATCH_ALL
+from homeassistant.const import ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
import homeassistant.core as ha
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import discovery
@@ -223,10 +224,21 @@ async def test_extract_from_service_fails_if_no_entity_id(hass):
[MockEntity(name="test_1"), MockEntity(name="test_2")]
)
- call = ha.ServiceCall("test", "service")
-
- assert [] == sorted(
- ent.entity_id for ent in (await component.async_extract_from_service(call))
+ assert (
+ await component.async_extract_from_service(ha.ServiceCall("test", "service"))
+ == []
+ )
+ assert (
+ await component.async_extract_from_service(
+ ha.ServiceCall("test", "service", {"entity_id": ENTITY_MATCH_NONE})
+ )
+ == []
+ )
+ assert (
+ await component.async_extract_from_service(
+ ha.ServiceCall("test", "service", {"area_id": ENTITY_MATCH_NONE})
+ )
+ == []
)
@@ -263,7 +275,7 @@ async def test_extract_from_service_no_group_expand(hass):
async def test_setup_dependencies_platform(hass):
"""Test we setup the dependencies of a platform.
- We're explictely testing that we process dependencies even if a component
+ We're explicitly testing that we process dependencies even if a component
with the same name has already been loaded.
"""
mock_integration(
@@ -305,7 +317,7 @@ async def test_setup_entry(hass):
async def test_setup_entry_platform_not_exist(hass):
- """Test setup entry fails if platform doesnt exist."""
+ """Test setup entry fails if platform does not exist."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
entry = MockConfigEntry(domain="non_existing")
@@ -429,3 +441,53 @@ async def test_extract_all_use_match_all(hass, caplog):
assert (
"Not passing an entity ID to a service to target all entities is deprecated"
) not in caplog.text
+
+
+async def test_register_entity_service(hass):
+ """Test not expanding a group."""
+ entity = MockEntity(entity_id=f"{DOMAIN}.entity")
+ calls = []
+
+ @ha.callback
+ def appender(**kwargs):
+ calls.append(kwargs)
+
+ entity.async_called_by_service = appender
+
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ await component.async_add_entities([entity])
+
+ component.async_register_entity_service(
+ "hello", {"some": str}, "async_called_by_service"
+ )
+
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN,
+ "hello",
+ {"entity_id": entity.entity_id, "invalid": "data"},
+ blocking=True,
+ )
+ assert len(calls) == 0
+
+ await hass.services.async_call(
+ DOMAIN, "hello", {"entity_id": entity.entity_id, "some": "data"}, blocking=True
+ )
+ assert len(calls) == 1
+ assert calls[0] == {"some": "data"}
+
+ await hass.services.async_call(
+ DOMAIN, "hello", {"entity_id": ENTITY_MATCH_ALL, "some": "data"}, blocking=True
+ )
+ assert len(calls) == 2
+ assert calls[1] == {"some": "data"}
+
+ await hass.services.async_call(
+ DOMAIN, "hello", {"entity_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True
+ )
+ assert len(calls) == 2
+
+ await hass.services.async_call(
+ DOMAIN, "hello", {"area_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True
+ )
+ assert len(calls) == 2
diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py
index 8eea8ad004f..ee43f5d4f1d 100644
--- a/tests/helpers/test_entity_platform.py
+++ b/tests/helpers/test_entity_platform.py
@@ -270,8 +270,6 @@ async def test_parallel_updates_async_platform_with_constant(hass):
handle = list(component._platforms.values())[-1]
- assert handle.parallel_updates == 2
-
class AsyncEntity(MockEntity):
"""Mock entity that has async_update."""
@@ -296,7 +294,6 @@ async def test_parallel_updates_sync_platform(hass):
await component.async_setup({DOMAIN: {"platform": "platform"}})
handle = list(component._platforms.values())[-1]
- assert handle.parallel_updates is None
class SyncEntity(MockEntity):
"""Mock entity that has update."""
@@ -323,7 +320,6 @@ async def test_parallel_updates_sync_platform_with_constant(hass):
await component.async_setup({DOMAIN: {"platform": "platform"}})
handle = list(component._platforms.values())[-1]
- assert handle.parallel_updates == 2
class SyncEntity(MockEntity):
"""Mock entity that has update."""
@@ -394,7 +390,7 @@ async def test_using_prescribed_entity_id(hass):
async def test_using_prescribed_entity_id_with_unique_id(hass):
- """Test for ammending predefined entity ID because currently exists."""
+ """Test for amending predefined entity ID because currently exists."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
await component.async_add_entities([MockEntity(entity_id="test_domain.world")])
@@ -839,7 +835,7 @@ async def test_override_restored_entities(hass):
async def test_platform_with_no_setup(hass, caplog):
- """Test setting up a platform that doesnt' support setup."""
+ """Test setting up a platform that does not support setup."""
entity_platform = MockEntityPlatform(
hass, domain="mock-integration", platform_name="mock-platform", platform=None
)
diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py
index e532d99f333..e7a7b856da2 100644
--- a/tests/helpers/test_entity_registry.py
+++ b/tests/helpers/test_entity_registry.py
@@ -72,6 +72,9 @@ def test_get_or_create_updates_data(registry):
supported_features=5,
device_class="mock-device-class",
disabled_by=entity_registry.DISABLED_HASS,
+ unit_of_measurement="initial-unit_of_measurement",
+ original_name="initial-original_name",
+ original_icon="initial-original_icon",
)
assert orig_entry.config_entry_id == orig_config_entry.entry_id
@@ -80,6 +83,9 @@ def test_get_or_create_updates_data(registry):
assert orig_entry.supported_features == 5
assert orig_entry.device_class == "mock-device-class"
assert orig_entry.disabled_by == entity_registry.DISABLED_HASS
+ assert orig_entry.unit_of_measurement == "initial-unit_of_measurement"
+ assert orig_entry.original_name == "initial-original_name"
+ assert orig_entry.original_icon == "initial-original_icon"
new_config_entry = MockConfigEntry(domain="light")
@@ -93,6 +99,9 @@ def test_get_or_create_updates_data(registry):
supported_features=10,
device_class="new-mock-device-class",
disabled_by=entity_registry.DISABLED_USER,
+ unit_of_measurement="updated-unit_of_measurement",
+ original_name="updated-original_name",
+ original_icon="updated-original_icon",
)
assert new_entry.config_entry_id == new_config_entry.entry_id
@@ -100,6 +109,9 @@ def test_get_or_create_updates_data(registry):
assert new_entry.capabilities == {"new-max": 100}
assert new_entry.supported_features == 10
assert new_entry.device_class == "new-mock-device-class"
+ assert new_entry.unit_of_measurement == "updated-unit_of_measurement"
+ assert new_entry.original_name == "updated-original_name"
+ assert new_entry.original_icon == "updated-original_icon"
# Should not be updated
assert new_entry.disabled_by == entity_registry.DISABLED_HASS
@@ -147,6 +159,11 @@ async def test_loading_saving_data(hass, registry):
supported_features=5,
device_class="mock-device-class",
disabled_by=entity_registry.DISABLED_HASS,
+ original_name="Original Name",
+ original_icon="hass:original-icon",
+ )
+ orig_entry2 = registry.async_update_entity(
+ orig_entry2.entity_id, name="User Name", icon="hass:user-icon"
)
assert len(registry.entities) == 2
@@ -169,6 +186,10 @@ async def test_loading_saving_data(hass, registry):
assert new_entry2.capabilities == {"max": 100}
assert new_entry2.supported_features == 5
assert new_entry2.device_class == "mock-device-class"
+ assert new_entry2.name == "User Name"
+ assert new_entry2.icon == "hass:user-icon"
+ assert new_entry2.original_name == "Original Name"
+ assert new_entry2.original_icon == "hass:original-icon"
def test_generate_entity_considers_registered_entities(registry):
@@ -434,6 +455,7 @@ async def test_update_entity(registry):
for attr_name, new_value in (
("name", "new name"),
+ ("icon", "new icon"),
("disabled_by", entity_registry.DISABLED_USER),
):
changes = {attr_name: new_value}
@@ -503,6 +525,8 @@ async def test_restore_states(hass):
capabilities={"max": 100},
supported_features=5,
device_class="mock-device-class",
+ original_name="Mock Original Name",
+ original_icon="hass:original-icon",
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
@@ -524,6 +548,8 @@ async def test_restore_states(hass):
"supported_features": 5,
"device_class": "mock-device-class",
"restored": True,
+ "friendly_name": "Mock Original Name",
+ "icon": "hass:original-icon",
}
registry.async_remove("light.disabled")
diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py
index cef8baec70e..313710d03b7 100644
--- a/tests/helpers/test_event.py
+++ b/tests/helpers/test_event.py
@@ -475,7 +475,7 @@ async def test_track_sunrise_update_location(hass):
with patch("homeassistant.util.dt.utcnow", return_value=utc_now):
async_track_sunrise(hass, lambda: runs.append(1))
- # Mimick sunrise
+ # Mimic sunrise
_send_time_changed(hass, next_rising)
await hass.async_block_till_done()
assert len(runs) == 1
@@ -485,7 +485,7 @@ async def test_track_sunrise_update_location(hass):
await hass.config.async_update(latitude=40.755931, longitude=-73.984606)
await hass.async_block_till_done()
- # Mimick sunrise
+ # Mimic sunrise
_send_time_changed(hass, next_rising)
await hass.async_block_till_done()
# Did not increase
@@ -501,7 +501,7 @@ async def test_track_sunrise_update_location(hass):
break
mod += 1
- # Mimick sunrise at new location
+ # Mimic sunrise at new location
_send_time_changed(hass, next_rising)
await hass.async_block_till_done()
assert len(runs) == 2
diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py
index d90842d1b71..cc4098a613a 100644
--- a/tests/helpers/test_service.py
+++ b/tests/helpers/test_service.py
@@ -12,7 +12,13 @@ import voluptuous as vol
from homeassistant import core as ha, exceptions
from homeassistant.auth.permissions import PolicyPermissions
import homeassistant.components # noqa: F401
-from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, STATE_OFF, STATE_ON
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ENTITY_MATCH_ALL,
+ ENTITY_MATCH_NONE,
+ STATE_OFF,
+ STATE_ON,
+)
from homeassistant.helpers import (
device_registry as dev_reg,
entity_registry as ent_reg,
@@ -33,31 +39,29 @@ from tests.common import (
@pytest.fixture
-def mock_service_platform_call():
+def mock_handle_entity_call():
"""Mock service platform call."""
with patch(
- "homeassistant.helpers.service._handle_service_platform_call",
+ "homeassistant.helpers.service._handle_entity_call",
side_effect=lambda *args: mock_coro(),
) as mock_call:
yield mock_call
@pytest.fixture
-def mock_entities():
+def mock_entities(hass):
"""Return mock entities in an ordered dict."""
- kitchen = Mock(
+ kitchen = MockEntity(
entity_id="light.kitchen",
available=True,
should_poll=False,
supported_features=1,
- platform="test_domain",
)
- living_room = Mock(
+ living_room = MockEntity(
entity_id="light.living_room",
available=True,
should_poll=False,
supported_features=0,
- platform="test_domain",
)
entities = OrderedDict()
entities[kitchen.entity_id] = kitchen
@@ -252,6 +256,14 @@ async def test_extract_entity_ids(hass):
hass, call, expand_group=False
)
+ assert (
+ await service.async_extract_entity_ids(
+ hass,
+ ha.ServiceCall("light", "turn_on", {ATTR_ENTITY_ID: ENTITY_MATCH_NONE}),
+ )
+ == set()
+ )
+
async def test_extract_entity_ids_from_area(hass, area_mock):
"""Test extract_entity_ids method with areas."""
@@ -266,6 +278,13 @@ async def test_extract_entity_ids_from_area(hass, area_mock):
"light.diff_area",
} == await service.async_extract_entity_ids(hass, call)
+ assert (
+ await service.async_extract_entity_ids(
+ hass, ha.ServiceCall("light", "turn_on", {"area_id": ENTITY_MATCH_NONE})
+ )
+ == set()
+ )
+
@asyncio.coroutine
def test_async_get_all_descriptions(hass):
@@ -353,8 +372,8 @@ async def test_call_context_user_not_exist(hass):
assert err.value.context.user_id == "non-existing"
-async def test_call_context_target_all(hass, mock_service_platform_call, mock_entities):
- """Check we only target allowed entities if targetting all."""
+async def test_call_context_target_all(hass, mock_handle_entity_call, mock_entities):
+ """Check we only target allowed entities if targeting all."""
with patch(
"homeassistant.auth.AuthManager.async_get_user",
return_value=mock_coro(
@@ -377,13 +396,12 @@ async def test_call_context_target_all(hass, mock_service_platform_call, mock_en
),
)
- assert len(mock_service_platform_call.mock_calls) == 1
- entities = mock_service_platform_call.mock_calls[0][1][3]
- assert entities == [mock_entities["light.kitchen"]]
+ assert len(mock_handle_entity_call.mock_calls) == 1
+ assert mock_handle_entity_call.mock_calls[0][1][1].entity_id == "light.kitchen"
async def test_call_context_target_specific(
- hass, mock_service_platform_call, mock_entities
+ hass, mock_handle_entity_call, mock_entities
):
"""Check targeting specific entities."""
with patch(
@@ -408,13 +426,12 @@ async def test_call_context_target_specific(
),
)
- assert len(mock_service_platform_call.mock_calls) == 1
- entities = mock_service_platform_call.mock_calls[0][1][3]
- assert entities == [mock_entities["light.kitchen"]]
+ assert len(mock_handle_entity_call.mock_calls) == 1
+ assert mock_handle_entity_call.mock_calls[0][1][1].entity_id == "light.kitchen"
async def test_call_context_target_specific_no_auth(
- hass, mock_service_platform_call, mock_entities
+ hass, mock_handle_entity_call, mock_entities
):
"""Check targeting specific entities without auth."""
with pytest.raises(exceptions.Unauthorized) as err:
@@ -438,9 +455,7 @@ async def test_call_context_target_specific_no_auth(
assert err.value.entity_id == "light.kitchen"
-async def test_call_no_context_target_all(
- hass, mock_service_platform_call, mock_entities
-):
+async def test_call_no_context_target_all(hass, mock_handle_entity_call, mock_entities):
"""Check we target all if no user context given."""
await service.entity_service_call(
hass,
@@ -451,13 +466,14 @@ async def test_call_no_context_target_all(
),
)
- assert len(mock_service_platform_call.mock_calls) == 1
- entities = mock_service_platform_call.mock_calls[0][1][3]
- assert entities == list(mock_entities.values())
+ assert len(mock_handle_entity_call.mock_calls) == 2
+ assert [call[1][1] for call in mock_handle_entity_call.mock_calls] == list(
+ mock_entities.values()
+ )
async def test_call_no_context_target_specific(
- hass, mock_service_platform_call, mock_entities
+ hass, mock_handle_entity_call, mock_entities
):
"""Check we can target specified entities."""
await service.entity_service_call(
@@ -471,15 +487,14 @@ async def test_call_no_context_target_specific(
),
)
- assert len(mock_service_platform_call.mock_calls) == 1
- entities = mock_service_platform_call.mock_calls[0][1][3]
- assert entities == [mock_entities["light.kitchen"]]
+ assert len(mock_handle_entity_call.mock_calls) == 1
+ assert mock_handle_entity_call.mock_calls[0][1][1].entity_id == "light.kitchen"
async def test_call_with_match_all(
- hass, mock_service_platform_call, mock_entities, caplog
+ hass, mock_handle_entity_call, mock_entities, caplog
):
- """Check we only target allowed entities if targetting all."""
+ """Check we only target allowed entities if targeting all."""
await service.entity_service_call(
hass,
[Mock(entities=mock_entities)],
@@ -487,20 +502,13 @@ async def test_call_with_match_all(
ha.ServiceCall("test_domain", "test_service", {"entity_id": "all"}),
)
- assert len(mock_service_platform_call.mock_calls) == 1
- entities = mock_service_platform_call.mock_calls[0][1][3]
- assert entities == [
- mock_entities["light.kitchen"],
- mock_entities["light.living_room"],
- ]
- assert (
- "Not passing an entity ID to a service to target all entities is deprecated"
- ) not in caplog.text
+ assert len(mock_handle_entity_call.mock_calls) == 2
+ assert [call[1][1] for call in mock_handle_entity_call.mock_calls] == list(
+ mock_entities.values()
+ )
-async def test_call_with_omit_entity_id(
- hass, mock_service_platform_call, mock_entities
-):
+async def test_call_with_omit_entity_id(hass, mock_handle_entity_call, mock_entities):
"""Check service call if we do not pass an entity ID."""
await service.entity_service_call(
hass,
@@ -509,9 +517,7 @@ async def test_call_with_omit_entity_id(
ha.ServiceCall("test_domain", "test_service"),
)
- assert len(mock_service_platform_call.mock_calls) == 1
- entities = mock_service_platform_call.mock_calls[0][1][3]
- assert entities == []
+ assert len(mock_handle_entity_call.mock_calls) == 0
async def test_register_admin_service(hass, hass_read_only_user, hass_admin_user):
@@ -623,96 +629,113 @@ async def test_domain_control_unknown(hass, mock_entities):
assert len(calls) == 0
-async def test_domain_control_unauthorized(hass, hass_read_only_user, mock_entities):
+async def test_domain_control_unauthorized(hass, hass_read_only_user):
"""Test domain verification in a service call with an unauthorized user."""
- calls = []
-
- async def mock_service_log(call):
- """Define a protected service."""
- calls.append(call)
-
- with patch(
- "homeassistant.helpers.entity_registry.async_get_registry",
- return_value=mock_coro(Mock(entities=mock_entities)),
- ):
- protected_mock_service = hass.helpers.service.verify_domain_control(
- "test_domain"
- )(mock_service_log)
-
- hass.services.async_register(
- "test_domain", "test_service", protected_mock_service, schema=None
- )
-
- with pytest.raises(exceptions.Unauthorized):
- await hass.services.async_call(
- "test_domain",
- "test_service",
- {},
- blocking=True,
- context=ha.Context(user_id=hass_read_only_user.id),
+ mock_registry(
+ hass,
+ {
+ "light.kitchen": ent_reg.RegistryEntry(
+ entity_id="light.kitchen", unique_id="kitchen", platform="test_domain",
)
+ },
+ )
+
+ calls = []
+
+ async def mock_service_log(call):
+ """Define a protected service."""
+ calls.append(call)
+
+ protected_mock_service = hass.helpers.service.verify_domain_control("test_domain")(
+ mock_service_log
+ )
+
+ hass.services.async_register(
+ "test_domain", "test_service", protected_mock_service, schema=None
+ )
+
+ with pytest.raises(exceptions.Unauthorized):
+ await hass.services.async_call(
+ "test_domain",
+ "test_service",
+ {},
+ blocking=True,
+ context=ha.Context(user_id=hass_read_only_user.id),
+ )
+
+ assert len(calls) == 0
-async def test_domain_control_admin(hass, hass_admin_user, mock_entities):
+async def test_domain_control_admin(hass, hass_admin_user):
"""Test domain verification in a service call with an admin user."""
+ mock_registry(
+ hass,
+ {
+ "light.kitchen": ent_reg.RegistryEntry(
+ entity_id="light.kitchen", unique_id="kitchen", platform="test_domain",
+ )
+ },
+ )
+
calls = []
async def mock_service_log(call):
"""Define a protected service."""
calls.append(call)
- with patch(
- "homeassistant.helpers.entity_registry.async_get_registry",
- return_value=mock_coro(Mock(entities=mock_entities)),
- ):
- protected_mock_service = hass.helpers.service.verify_domain_control(
- "test_domain"
- )(mock_service_log)
+ protected_mock_service = hass.helpers.service.verify_domain_control("test_domain")(
+ mock_service_log
+ )
- hass.services.async_register(
- "test_domain", "test_service", protected_mock_service, schema=None
- )
+ hass.services.async_register(
+ "test_domain", "test_service", protected_mock_service, schema=None
+ )
- await hass.services.async_call(
- "test_domain",
- "test_service",
- {},
- blocking=True,
- context=ha.Context(user_id=hass_admin_user.id),
- )
+ await hass.services.async_call(
+ "test_domain",
+ "test_service",
+ {},
+ blocking=True,
+ context=ha.Context(user_id=hass_admin_user.id),
+ )
- assert len(calls) == 1
+ assert len(calls) == 1
-async def test_domain_control_no_user(hass, mock_entities):
+async def test_domain_control_no_user(hass):
"""Test domain verification in a service call with no user."""
+ mock_registry(
+ hass,
+ {
+ "light.kitchen": ent_reg.RegistryEntry(
+ entity_id="light.kitchen", unique_id="kitchen", platform="test_domain",
+ )
+ },
+ )
+
calls = []
async def mock_service_log(call):
"""Define a protected service."""
calls.append(call)
- with patch(
- "homeassistant.helpers.entity_registry.async_get_registry",
- return_value=mock_coro(Mock(entities=mock_entities)),
- ):
- protected_mock_service = hass.helpers.service.verify_domain_control(
- "test_domain"
- )(mock_service_log)
+ protected_mock_service = hass.helpers.service.verify_domain_control("test_domain")(
+ mock_service_log
+ )
- hass.services.async_register(
- "test_domain", "test_service", protected_mock_service, schema=None
- )
+ hass.services.async_register(
+ "test_domain", "test_service", protected_mock_service, schema=None
+ )
- await hass.services.async_call(
- "test_domain",
- "test_service",
- {},
- blocking=True,
- context=ha.Context(user_id=None),
- )
+ await hass.services.async_call(
+ "test_domain",
+ "test_service",
+ {},
+ blocking=True,
+ context=ha.Context(user_id=None),
+ )
- assert len(calls) == 1
+ assert len(calls) == 1
async def test_extract_from_service_available_device(hass):
@@ -742,6 +765,15 @@ async def test_extract_from_service_available_device(hass):
for ent in (await service.async_extract_entities(hass, entities, call_2))
]
+ assert (
+ await service.async_extract_entities(
+ hass,
+ entities,
+ ha.ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_NONE},),
+ )
+ == []
+ )
+
async def test_extract_from_service_empty_if_no_entity_id(hass):
"""Test the extraction from service without specifying entity."""
diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py
new file mode 100644
index 00000000000..04fd180b60d
--- /dev/null
+++ b/tests/helpers/test_update_coordinator.py
@@ -0,0 +1,127 @@
+"""Tests for the update coordinator."""
+from datetime import timedelta
+import logging
+
+from asynctest import CoroutineMock, Mock
+import pytest
+
+from homeassistant.helpers import update_coordinator
+from homeassistant.util.dt import utcnow
+
+from tests.common import async_fire_time_changed
+
+LOGGER = logging.getLogger(__name__)
+
+
+@pytest.fixture
+def crd(hass):
+ """Coordinator mock."""
+ calls = []
+
+ async def refresh():
+ calls.append(None)
+ return len(calls)
+
+ crd = update_coordinator.DataUpdateCoordinator(
+ hass,
+ LOGGER,
+ name="test",
+ update_method=refresh,
+ update_interval=timedelta(seconds=10),
+ )
+ return crd
+
+
+async def test_async_refresh(crd):
+ """Test async_refresh for update coordinator."""
+ assert crd.data is None
+ await crd.async_refresh()
+ assert crd.data == 1
+ assert crd.last_update_success is True
+
+ updates = []
+
+ def update_callback():
+ updates.append(crd.data)
+
+ crd.async_add_listener(update_callback)
+
+ await crd.async_refresh()
+
+ assert updates == [2]
+
+ crd.async_remove_listener(update_callback)
+
+ await crd.async_refresh()
+
+ assert updates == [2]
+
+
+async def test_request_refresh(crd):
+ """Test request refresh for update coordinator."""
+ assert crd.data is None
+ await crd.async_request_refresh()
+ assert crd.data == 1
+ assert crd.last_update_success is True
+
+ # Second time we hit the debonuce
+ await crd.async_request_refresh()
+ assert crd.data == 1
+ assert crd.last_update_success is True
+
+
+async def test_refresh_fail(crd, caplog):
+ """Test a failing update function."""
+ crd.update_method = CoroutineMock(side_effect=update_coordinator.UpdateFailed)
+
+ await crd.async_refresh()
+
+ assert crd.data is None
+ assert crd.last_update_success is False
+ assert "Error fetching test data" in caplog.text
+
+ crd.update_method = CoroutineMock(return_value=1)
+
+ await crd.async_refresh()
+
+ assert crd.data == 1
+ assert crd.last_update_success is True
+
+ crd.update_method = CoroutineMock(side_effect=ValueError)
+ caplog.clear()
+
+ await crd.async_refresh()
+
+ assert crd.data == 1 # value from previous fetch
+ assert crd.last_update_success is False
+ assert "Unexpected error fetching test data" in caplog.text
+
+
+async def test_update_interval(hass, crd):
+ """Test update interval works."""
+ # Test we don't update without subscriber
+ async_fire_time_changed(hass, utcnow() + crd.update_interval)
+ await hass.async_block_till_done()
+ assert crd.data is None
+
+ # Add subscriber
+ update_callback = Mock()
+ crd.async_add_listener(update_callback)
+
+ # Test twice we update with subscriber
+ async_fire_time_changed(hass, utcnow() + crd.update_interval)
+ await hass.async_block_till_done()
+ assert crd.data == 1
+
+ async_fire_time_changed(hass, utcnow() + crd.update_interval)
+ await hass.async_block_till_done()
+ assert crd.data == 2
+
+ # Test removing listener
+ crd.async_remove_listener(update_callback)
+
+ async_fire_time_changed(hass, utcnow() + crd.update_interval)
+ await hass.async_block_till_done()
+
+ # Test we stop updating after we lose last subscriber
+ assert crd.data == 2
diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py
index 48c5360d888..34704ddfb74 100644
--- a/tests/test_bootstrap.py
+++ b/tests/test_bootstrap.py
@@ -50,18 +50,18 @@ async def test_load_hassio(hass):
async def test_empty_setup(hass):
"""Test an empty set up loads the core."""
- await bootstrap._async_set_up_integrations(hass, {})
+ await bootstrap.async_from_config_dict({}, hass)
for domain in bootstrap.CORE_INTEGRATIONS:
assert domain in hass.config.components, domain
-async def test_core_failure_aborts(hass, caplog):
+async def test_core_failure_loads_safe_mode(hass, caplog):
"""Test failing core setup aborts further setup."""
with patch(
"homeassistant.components.homeassistant.async_setup",
return_value=mock_coro(False),
):
- await bootstrap._async_set_up_integrations(hass, {"group": {}})
+ await bootstrap.async_from_config_dict({"group": {}}, hass)
assert "core failed to initialize" in caplog.text
# We aborted early, group not set up
@@ -250,7 +250,8 @@ async def test_setup_hass(
log_no_color = Mock()
with patch(
- "homeassistant.config.async_hass_config_yaml", return_value={"browser": {}}
+ "homeassistant.config.async_hass_config_yaml",
+ return_value={"browser": {}, "frontend": {}},
):
hass = await bootstrap.async_setup_hass(
config_dir=get_test_config_dir(),
@@ -263,6 +264,7 @@ async def test_setup_hass(
)
assert "browser" in hass.config.components
+ assert "safe_mode" not in hass.config.components
assert len(mock_enable_logging.mock_calls) == 1
assert mock_enable_logging.mock_calls[0][1] == (
@@ -357,3 +359,58 @@ async def test_setup_hass_safe_mode(
# Validate we didn't try to set up config entry.
assert "browser" not in hass.config.components
assert len(browser_setup.mock_calls) == 0
+
+
+async def test_setup_hass_invalid_core_config(
+ mock_enable_logging,
+ mock_is_virtual_env,
+ mock_mount_local_lib_path,
+ mock_ensure_config_exists,
+ mock_process_ha_config_upgrade,
+):
+ """Test it works."""
+ with patch(
+ "homeassistant.config.async_hass_config_yaml",
+ return_value={"homeassistant": {"non-existing": 1}},
+ ):
+ hass = await bootstrap.async_setup_hass(
+ config_dir=get_test_config_dir(),
+ verbose=False,
+ log_rotate_days=10,
+ log_file="",
+ log_no_color=False,
+ skip_pip=True,
+ safe_mode=False,
+ )
+
+ assert "safe_mode" in hass.config.components
+
+
+async def test_setup_safe_mode_if_no_frontend(
+ mock_enable_logging,
+ mock_is_virtual_env,
+ mock_mount_local_lib_path,
+ mock_ensure_config_exists,
+ mock_process_ha_config_upgrade,
+):
+ """Test we setup safe mode if frontend didn't load."""
+ verbose = Mock()
+ log_rotate_days = Mock()
+ log_file = Mock()
+ log_no_color = Mock()
+
+ with patch(
+ "homeassistant.config.async_hass_config_yaml",
+ return_value={"map": {}, "person": {"invalid": True}},
+ ):
+ hass = await bootstrap.async_setup_hass(
+ config_dir=get_test_config_dir(),
+ verbose=verbose,
+ log_rotate_days=log_rotate_days,
+ log_file=log_file,
+ log_no_color=log_no_color,
+ skip_pip=True,
+ safe_mode=False,
+ )
+
+ assert "safe_mode" in hass.config.components
diff --git a/tests/test_config.py b/tests/test_config.py
index 1fc92ee954b..fc5ec43093b 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -4,9 +4,11 @@ import asyncio
from collections import OrderedDict
import copy
import os
-import unittest.mock as mock
+from unittest import mock
+from unittest.mock import Mock
import asynctest
+from asynctest import CoroutineMock, patch
import pytest
from voluptuous import Invalid, MultipleInvalid
import yaml
@@ -576,7 +578,7 @@ async def test_merge(merge_log_err, hass):
async def test_merge_try_falsy(merge_log_err, hass):
- """Ensure we dont add falsy items like empty OrderedDict() to list."""
+ """Ensure we don't add falsy items like empty OrderedDict() to list."""
packages = {
"pack_falsy_to_lst": {"automation": OrderedDict()},
"pack_list2": {"light": OrderedDict()},
@@ -893,3 +895,97 @@ async def test_merge_split_component_definition(hass):
assert len(config["light one"]) == 1
assert len(config["light two"]) == 1
assert len(config["light three"]) == 1
+
+
+async def test_component_config_exceptions(hass, caplog):
+ """Test unexpected exceptions validating component config."""
+ # Config validator
+ assert (
+ await config_util.async_process_component_config(
+ hass,
+ {},
+ integration=Mock(
+ domain="test_domain",
+ get_platform=Mock(
+ return_value=Mock(
+ async_validate_config=CoroutineMock(
+ side_effect=ValueError("broken")
+ )
+ )
+ ),
+ ),
+ )
+ is None
+ )
+ assert "ValueError: broken" in caplog.text
+ assert "Unknown error calling test_domain config validator" in caplog.text
+
+ # component.CONFIG_SCHEMA
+ caplog.clear()
+ assert (
+ await config_util.async_process_component_config(
+ hass,
+ {},
+ integration=Mock(
+ domain="test_domain",
+ get_platform=Mock(return_value=None),
+ get_component=Mock(
+ return_value=Mock(
+ CONFIG_SCHEMA=Mock(side_effect=ValueError("broken"))
+ )
+ ),
+ ),
+ )
+ is None
+ )
+ assert "ValueError: broken" in caplog.text
+ assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text
+
+ # component.PLATFORM_SCHEMA
+ caplog.clear()
+ assert await config_util.async_process_component_config(
+ hass,
+ {"test_domain": {"platform": "test_platform"}},
+ integration=Mock(
+ domain="test_domain",
+ get_platform=Mock(return_value=None),
+ get_component=Mock(
+ return_value=Mock(
+ spec=["PLATFORM_SCHEMA_BASE"],
+ PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")),
+ )
+ ),
+ ),
+ ) == {"test_domain": []}
+ assert "ValueError: broken" in caplog.text
+ assert (
+ "Unknown error validating test_platform platform config with test_domain component platform schema"
+ in caplog.text
+ )
+
+ # platform.PLATFORM_SCHEMA
+ caplog.clear()
+ with patch(
+ "homeassistant.config.async_get_integration_with_requirements",
+ return_value=Mock( # integration that owns platform
+ get_platform=Mock(
+ return_value=Mock( # platform
+ PLATFORM_SCHEMA=Mock(side_effect=ValueError("broken"))
+ )
+ )
+ ),
+ ):
+ assert await config_util.async_process_component_config(
+ hass,
+ {"test_domain": {"platform": "test_platform"}},
+ integration=Mock(
+ domain="test_domain",
+ get_platform=Mock(return_value=None),
+ get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
+ ),
+ ) == {"test_domain": []}
+ assert "ValueError: broken" in caplog.text
+ assert (
+ "Unknown error validating config for test_platform platform for test_domain component with PLATFORM_SCHEMA"
+ in caplog.text
+ )
diff --git a/tests/test_core.py b/tests/test_core.py
index aa0c615ec04..0c7acfbba0e 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -904,6 +904,7 @@ class TestConfig(unittest.TestCase):
"whitelist_external_dirs": set(),
"version": __version__,
"config_source": "default",
+ "safe_mode": False,
}
assert expected == self.config.as_dict()
@@ -1180,3 +1181,28 @@ def test_context():
assert c.user_id == 23
assert c.parent_id == 100
assert c.id is not None
+
+
+async def test_async_functions_with_callback(hass):
+ """Test we deal with async functions accidentally marked as callback."""
+ runs = []
+
+ @ha.callback
+ async def test():
+ runs.append(True)
+
+ await hass.async_add_job(test)
+ assert len(runs) == 1
+
+ hass.async_run_job(test)
+ await hass.async_block_till_done()
+ assert len(runs) == 2
+
+ @ha.callback
+ async def service_handler(call):
+ runs.append(True)
+
+ hass.services.async_register("test_domain", "test_service", service_handler)
+
+ await hass.services.async_call("test_domain", "test_service", blocking=True)
+ assert len(runs) == 3
diff --git a/tests/test_loader.py b/tests/test_loader.py
index 47d9e4e23fa..745bb9c8c2c 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -236,3 +236,9 @@ async def test_get_config_flows(hass):
flows = await loader.async_get_config_flows(hass)
assert "test_2" in flows
assert "test_1" not in flows
+
+
+async def test_get_custom_components_safe_mode(hass):
+ """Test that we get empty custom components in safe mode."""
+ hass.config.safe_mode = True
+ assert await loader.async_get_custom_components(hass) == {}
diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py
index 4b018adb5cb..d3f96c367d8 100644
--- a/tests/testing_config/custom_components/test/light.py
+++ b/tests/testing_config/custom_components/test/light.py
@@ -3,6 +3,7 @@ Provide a mock light platform.
Call init before using it in your tests to ensure clean test data.
"""
+from homeassistant.components.light import Light
from homeassistant.const import STATE_OFF, STATE_ON
from tests.common import MockToggleEntity
@@ -18,9 +19,9 @@ def init(empty=False):
[]
if empty
else [
- MockToggleEntity("Ceiling", STATE_ON),
- MockToggleEntity("Ceiling", STATE_OFF),
- MockToggleEntity(None, STATE_OFF),
+ MockLight("Ceiling", STATE_ON),
+ MockLight("Ceiling", STATE_OFF),
+ MockLight(None, STATE_OFF),
]
)
@@ -30,3 +31,10 @@ async def async_setup_platform(
):
"""Return mock entities."""
async_add_entities_callback(ENTITIES)
+
+
+class MockLight(MockToggleEntity, Light):
+ """Mock light class."""
+
+ brightness = None
+ supported_features = 0
diff --git a/tox.ini b/tox.ini
index 17253e1d1e1..5527db738a6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -36,6 +36,7 @@ deps =
commands =
python -m script.gen_requirements_all validate
python -m script.hassfest validate
+ pre-commit run codespell {posargs: --all-files}
pre-commit run flake8 {posargs: --all-files}
pre-commit run bandit {posargs: --all-files}
@@ -44,4 +45,4 @@ deps =
-r{toxinidir}/requirements_test.txt
-c{toxinidir}/homeassistant/package_constraints.txt
commands =
- pre-commit run --config .pre-commit-config-all.yaml mypy {posargs: --all-files}
+ pre-commit run mypy {posargs: --all-files}