diff --git a/.coveragerc b/.coveragerc
index ca36f4a8dbb..a8d7d89544d 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -42,6 +42,7 @@ omit =
homeassistant/components/asterisk_mbox.py
homeassistant/components/*/asterisk_mbox.py
+ homeassistant/components/*/asterisk_cdr.py
homeassistant/components/august.py
homeassistant/components/*/august.py
@@ -92,6 +93,9 @@ omit =
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py
+ homeassistant/components/edp_redy.py
+ homeassistant/components/*/edp_redy.py
+
homeassistant/components/egardia.py
homeassistant/components/*/egardia.py
@@ -123,6 +127,7 @@ omit =
homeassistant/components/hangouts/const.py
homeassistant/components/hangouts/hangouts_bot.py
homeassistant/components/hangouts/hangups_utils.py
+ homeassistant/components/hangouts/intents.py
homeassistant/components/*/hangouts.py
homeassistant/components/hdmi_cec.py
@@ -140,6 +145,9 @@ omit =
homeassistant/components/homematicip_cloud.py
homeassistant/components/*/homematicip_cloud.py
+ homeassistant/components/huawei_lte.py
+ homeassistant/components/*/huawei_lte.py
+
homeassistant/components/hydrawise.py
homeassistant/components/*/hydrawise.py
@@ -183,6 +191,9 @@ omit =
homeassistant/components/linode.py
homeassistant/components/*/linode.py
+ homeassistant/components/logi_circle.py
+ homeassistant/components/*/logi_circle.py
+
homeassistant/components/lutron.py
homeassistant/components/*/lutron.py
@@ -684,6 +695,7 @@ omit =
homeassistant/components/sensor/kwb.py
homeassistant/components/sensor/lacrosse.py
homeassistant/components/sensor/lastfm.py
+ homeassistant/components/sensor/linky.py
homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/luftdaten.py
@@ -740,6 +752,7 @@ omit =
homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/spotcrime.py
+ homeassistant/components/sensor/starlingbank.py
homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py
@@ -813,6 +826,7 @@ omit =
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py
+ homeassistant/components/weather/met.py
homeassistant/components/weather/metoffice.py
homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/zamg.py
diff --git a/CODEOWNERS b/CODEOWNERS
index b86e09a6b72..b6ce8c04909 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -72,6 +72,7 @@ homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
+homeassistant/components/sensor/jewish_calendar.py @tsvi
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
homeassistant/components/sensor/pollen.py @bachya
@@ -97,6 +98,8 @@ homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p
+homeassistant/components/huawei_lte.py @scop
+homeassistant/components/*/huawei_lte.py @scop
homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342
homeassistant/components/konnected.py @heythisisnate
@@ -117,9 +120,13 @@ homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen
+homeassistant/components/upcloud.py @scop
+homeassistant/components/*/upcloud.py @scop
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
+homeassistant/components/zoneminder.py @rohankapoorcom
+homeassistant/components/*/zoneminder.py @rohankapoorcom
homeassistant/scripts/check_config.py @kellerza
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 86e212bb11d..fbe77c7756f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,5 +10,5 @@ The process is straight-forward.
- Ensure tests work.
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
-Still interested? Then you should take a peek at the [developer documentation](https://home-assistant.io/developers/) to get more details.
+Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details.
diff --git a/README.rst b/README.rst
index 6cf19d89c3c..4f459162a7e 100644
--- a/README.rst
+++ b/README.rst
@@ -21,8 +21,8 @@ Featured integrations
|screenshot-components|
-The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture `__ and the `section on creating your own
-components `__.
+The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture `__ and the `section on creating your own
+components `__.
If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help section `__ of our website for further help and information.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index a6157dc7aac..c592f66c070 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -19,4 +19,4 @@ Indices and tables
* :ref:`modindex`
* :ref:`search`
-.. _Home Assistant developers: https://home-assistant.io/developers/
+.. _Home Assistant developers: https://developers.home-assistant.io/
diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py
index 65b1cd2ae1a..91b7a7f8466 100644
--- a/homeassistant/__main__.py
+++ b/homeassistant/__main__.py
@@ -7,7 +7,6 @@ import platform
import subprocess
import sys
import threading
-
from typing import List, Dict, Any # noqa pylint: disable=unused-import
@@ -20,15 +19,19 @@ from homeassistant.const import (
)
-def attempt_use_uvloop() -> None:
+def set_loop() -> None:
"""Attempt to use uvloop."""
import asyncio
- try:
- import uvloop
- asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
- except ImportError:
- pass
+ if sys.platform == 'win32':
+ asyncio.set_event_loop(asyncio.ProactorEventLoop())
+ else:
+ try:
+ import uvloop
+ except ImportError:
+ pass
+ else:
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
def validate_python() -> None:
@@ -240,51 +243,39 @@ def cmdline() -> List[str]:
return [arg for arg in sys.argv if arg != '--daemon']
-def setup_and_run_hass(config_dir: str,
- args: argparse.Namespace) -> int:
+async def setup_and_run_hass(config_dir: str,
+ args: argparse.Namespace) -> int:
"""Set up HASS and run."""
- from homeassistant import bootstrap
+ from homeassistant import bootstrap, core
- # Run a simple daemon runner process on Windows to handle restarts
- if os.name == 'nt' and '--runner' not in sys.argv:
- nt_args = cmdline() + ['--runner']
- while True:
- try:
- subprocess.check_call(nt_args)
- sys.exit(0)
- except subprocess.CalledProcessError as exc:
- if exc.returncode != RESTART_EXIT_CODE:
- sys.exit(exc.returncode)
+ hass = core.HomeAssistant()
if args.demo_mode:
config = {
'frontend': {},
'demo': {}
} # type: Dict[str, Any]
- hass = bootstrap.from_config_dict(
- config, config_dir=config_dir, verbose=args.verbose,
+ bootstrap.async_from_config_dict(
+ config, hass, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
log_file=args.log_file, log_no_color=args.log_no_color)
else:
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
- hass = bootstrap.from_config_file(
- config_file, verbose=args.verbose, skip_pip=args.skip_pip,
+ await bootstrap.async_from_config_file(
+ config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
log_no_color=args.log_no_color)
- if hass is None:
- return -1
-
if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch
from homeassistant.util.async_ import run_callback_threadsafe
def open_browser(_: Any) -> None:
"""Open the web interface in a browser."""
- if hass.config.api is not None: # type: ignore
+ if hass.config.api is not None:
import webbrowser
- webbrowser.open(hass.config.api.base_url) # type: ignore
+ webbrowser.open(hass.config.api.base_url)
run_callback_threadsafe(
hass.loop,
@@ -292,7 +283,7 @@ def setup_and_run_hass(config_dir: str,
EVENT_HOMEASSISTANT_START, open_browser
)
- return hass.start()
+ return await hass.async_run()
def try_to_restart() -> None:
@@ -347,7 +338,20 @@ def main() -> int:
monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks()
- attempt_use_uvloop()
+ set_loop()
+
+ # Run a simple daemon runner process on Windows to handle restarts
+ if os.name == 'nt' and '--runner' not in sys.argv:
+ nt_args = cmdline() + ['--runner']
+ while True:
+ try:
+ subprocess.check_call(nt_args)
+ sys.exit(0)
+ except KeyboardInterrupt:
+ sys.exit(0)
+ except subprocess.CalledProcessError as exc:
+ if exc.returncode != RESTART_EXIT_CODE:
+ sys.exit(exc.returncode)
args = get_arguments()
@@ -366,11 +370,12 @@ def main() -> int:
if args.pid_file:
write_pid(args.pid_file)
- exit_code = setup_and_run_hass(config_dir, args)
+ from homeassistant.util.async_ import asyncio_run
+ exit_code = asyncio_run(setup_and_run_hass(config_dir, args))
if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart()
- return exit_code
+ return exit_code # type: ignore # mypy cannot yet infer it
if __name__ == "__main__":
diff --git a/homeassistant/auth/const.py b/homeassistant/auth/const.py
index 082d8966275..2e57986958c 100644
--- a/homeassistant/auth/const.py
+++ b/homeassistant/auth/const.py
@@ -2,3 +2,4 @@
from datetime import timedelta
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
+MFA_SESSION_EXPIRATION = timedelta(minutes=5)
diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py
index 603ca6ff3b1..1746ef38f95 100644
--- a/homeassistant/auth/mfa_modules/__init__.py
+++ b/homeassistant/auth/mfa_modules/__init__.py
@@ -1,5 +1,4 @@
"""Plugable auth modules for Home Assistant."""
-from datetime import timedelta
import importlib
import logging
import types
@@ -23,8 +22,6 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
-SESSION_EXPIRATION = timedelta(minutes=5)
-
DATA_REQS = 'mfa_auth_module_reqs_processed'
_LOGGER = logging.getLogger(__name__)
@@ -34,6 +31,7 @@ class MultiFactorAuthModule:
"""Multi-factor Auth Module of validation function."""
DEFAULT_TITLE = 'Unnamed auth module'
+ MAX_RETRY_TIME = 3
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize an auth module."""
@@ -84,7 +82,7 @@ class MultiFactorAuthModule:
"""Return whether user is setup."""
raise NotImplementedError
- async def async_validation(
+ async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
raise NotImplementedError
diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py
index 9c72111ef96..9804cbcf635 100644
--- a/homeassistant/auth/mfa_modules/insecure_example.py
+++ b/homeassistant/auth/mfa_modules/insecure_example.py
@@ -77,7 +77,7 @@ class InsecureExampleModule(MultiFactorAuthModule):
return True
return False
- async def async_validation(
+ async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
for data in self._data:
diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py
new file mode 100644
index 00000000000..84f9de614c1
--- /dev/null
+++ b/homeassistant/auth/mfa_modules/notify.py
@@ -0,0 +1,325 @@
+"""HMAC-based One-time Password auth module.
+
+Sending HOTP through notify service
+"""
+import logging
+from collections import OrderedDict
+from typing import Any, Dict, Optional, Tuple, List # noqa: F401
+
+import attr
+import voluptuous as vol
+
+from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import config_validation as cv
+
+from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
+ MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
+
+REQUIREMENTS = ['pyotp==2.2.6']
+
+CONF_MESSAGE = 'message'
+
+CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
+ vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_MESSAGE,
+ default='{} is your Home Assistant login code'): str
+}, extra=vol.PREVENT_EXTRA)
+
+STORAGE_VERSION = 1
+STORAGE_KEY = 'auth_module.notify'
+STORAGE_USERS = 'users'
+STORAGE_USER_ID = 'user_id'
+
+INPUT_FIELD_CODE = 'code'
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def _generate_secret() -> str:
+ """Generate a secret."""
+ import pyotp
+ return str(pyotp.random_base32())
+
+
+def _generate_random() -> int:
+ """Generate a 8 digit number."""
+ import pyotp
+ return int(pyotp.random_base32(length=8, chars=list('1234567890')))
+
+
+def _generate_otp(secret: str, count: int) -> str:
+ """Generate one time password."""
+ import pyotp
+ return str(pyotp.HOTP(secret).at(count))
+
+
+def _verify_otp(secret: str, otp: str, count: int) -> bool:
+ """Verify one time password."""
+ import pyotp
+ return bool(pyotp.HOTP(secret).verify(otp, count))
+
+
+@attr.s(slots=True)
+class NotifySetting:
+ """Store notify setting for one user."""
+
+ secret = attr.ib(type=str, factory=_generate_secret) # not persistent
+ counter = attr.ib(type=int, factory=_generate_random) # not persistent
+ notify_service = attr.ib(type=Optional[str], default=None)
+ target = attr.ib(type=Optional[str], default=None)
+
+
+_UsersDict = Dict[str, NotifySetting]
+
+
+@MULTI_FACTOR_AUTH_MODULES.register('notify')
+class NotifyAuthModule(MultiFactorAuthModule):
+ """Auth module send hmac-based one time password by notify service."""
+
+ DEFAULT_TITLE = 'Notify One-Time Password'
+
+ def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
+ """Initialize the user data store."""
+ super().__init__(hass, config)
+ self._user_settings = None # type: Optional[_UsersDict]
+ self._user_store = hass.helpers.storage.Store(
+ STORAGE_VERSION, STORAGE_KEY)
+ self._include = config.get(CONF_INCLUDE, [])
+ self._exclude = config.get(CONF_EXCLUDE, [])
+ self._message_template = config[CONF_MESSAGE]
+
+ @property
+ def input_schema(self) -> vol.Schema:
+ """Validate login flow input data."""
+ return vol.Schema({INPUT_FIELD_CODE: str})
+
+ async def _async_load(self) -> None:
+ """Load stored data."""
+ data = await self._user_store.async_load()
+
+ if data is None:
+ data = {STORAGE_USERS: {}}
+
+ self._user_settings = {
+ user_id: NotifySetting(**setting)
+ for user_id, setting in data.get(STORAGE_USERS, {}).items()
+ }
+
+ async def _async_save(self) -> None:
+ """Save data."""
+ if self._user_settings is None:
+ return
+
+ await self._user_store.async_save({STORAGE_USERS: {
+ user_id: attr.asdict(
+ notify_setting, filter=attr.filters.exclude(
+ attr.fields(NotifySetting).secret,
+ attr.fields(NotifySetting).counter,
+ ))
+ for user_id, notify_setting
+ in self._user_settings.items()
+ }})
+
+ @callback
+ def aync_get_available_notify_services(self) -> List[str]:
+ """Return list of notify services."""
+ unordered_services = set()
+
+ for service in self.hass.services.async_services().get('notify', {}):
+ if service not in self._exclude:
+ unordered_services.add(service)
+
+ if self._include:
+ unordered_services &= set(self._include)
+
+ return sorted(unordered_services)
+
+ async def async_setup_flow(self, user_id: str) -> SetupFlow:
+ """Return a data entry flow handler for setup module.
+
+ Mfa module should extend SetupFlow
+ """
+ return NotifySetupFlow(
+ self, self.input_schema, user_id,
+ self.aync_get_available_notify_services())
+
+ async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
+ """Set up auth module for user."""
+ if self._user_settings is None:
+ await self._async_load()
+ assert self._user_settings is not None
+
+ self._user_settings[user_id] = NotifySetting(
+ notify_service=setup_data.get('notify_service'),
+ target=setup_data.get('target'),
+ )
+
+ await self._async_save()
+
+ async def async_depose_user(self, user_id: str) -> None:
+ """Depose auth module for user."""
+ if self._user_settings is None:
+ await self._async_load()
+ assert self._user_settings is not None
+
+ if self._user_settings.pop(user_id, None):
+ await self._async_save()
+
+ async def async_is_user_setup(self, user_id: str) -> bool:
+ """Return whether user is setup."""
+ if self._user_settings is None:
+ await self._async_load()
+ assert self._user_settings is not None
+
+ return user_id in self._user_settings
+
+ async def async_validate(
+ self, user_id: str, user_input: Dict[str, Any]) -> bool:
+ """Return True if validation passed."""
+ if self._user_settings is None:
+ await self._async_load()
+ assert self._user_settings is not None
+
+ notify_setting = self._user_settings.get(user_id, None)
+ if notify_setting is None:
+ return False
+
+ # user_input has been validate in caller
+ return await self.hass.async_add_executor_job(
+ _verify_otp, notify_setting.secret,
+ user_input.get(INPUT_FIELD_CODE, ''),
+ notify_setting.counter)
+
+ async def async_initialize_login_mfa_step(self, user_id: str) -> None:
+ """Generate code and notify user."""
+ if self._user_settings is None:
+ await self._async_load()
+ assert self._user_settings is not None
+
+ notify_setting = self._user_settings.get(user_id, None)
+ if notify_setting is None:
+ raise ValueError('Cannot find user_id')
+
+ def generate_secret_and_one_time_password() -> str:
+ """Generate and send one time password."""
+ assert notify_setting
+ # secret and counter are not persistent
+ notify_setting.secret = _generate_secret()
+ notify_setting.counter = _generate_random()
+ return _generate_otp(
+ notify_setting.secret, notify_setting.counter)
+
+ code = await self.hass.async_add_executor_job(
+ generate_secret_and_one_time_password)
+
+ await self.async_notify_user(user_id, code)
+
+ async def async_notify_user(self, user_id: str, code: str) -> None:
+ """Send code by user's notify service."""
+ if self._user_settings is None:
+ await self._async_load()
+ assert self._user_settings is not None
+
+ notify_setting = self._user_settings.get(user_id, None)
+ if notify_setting is None:
+ _LOGGER.error('Cannot find user %s', user_id)
+ return
+
+ await self.async_notify( # type: ignore
+ code, notify_setting.notify_service, notify_setting.target)
+
+ async def async_notify(self, code: str, notify_service: str,
+ target: Optional[str] = None) -> None:
+ """Send code by notify service."""
+ data = {'message': self._message_template.format(code)}
+ if target:
+ data['target'] = [target]
+
+ await self.hass.services.async_call('notify', notify_service, data)
+
+
+class NotifySetupFlow(SetupFlow):
+ """Handler for the setup flow."""
+
+ def __init__(self, auth_module: NotifyAuthModule,
+ setup_schema: vol.Schema,
+ user_id: str,
+ available_notify_services: List[str]) -> None:
+ """Initialize the setup flow."""
+ super().__init__(auth_module, setup_schema, user_id)
+ # to fix typing complaint
+ self._auth_module = auth_module # type: NotifyAuthModule
+ self._available_notify_services = available_notify_services
+ self._secret = None # type: Optional[str]
+ self._count = None # type: Optional[int]
+ self._notify_service = None # type: Optional[str]
+ self._target = None # type: Optional[str]
+
+ async def async_step_init(
+ self, user_input: Optional[Dict[str, str]] = None) \
+ -> Dict[str, Any]:
+ """Let user select available notify services."""
+ errors = {} # type: Dict[str, str]
+
+ hass = self._auth_module.hass
+ if user_input:
+ self._notify_service = user_input['notify_service']
+ self._target = user_input.get('target')
+ self._secret = await hass.async_add_executor_job(_generate_secret)
+ self._count = await hass.async_add_executor_job(_generate_random)
+
+ return await self.async_step_setup()
+
+ if not self._available_notify_services:
+ return self.async_abort(reason='no_available_service')
+
+ schema = OrderedDict() # type: Dict[str, Any]
+ schema['notify_service'] = vol.In(self._available_notify_services)
+ schema['target'] = vol.Optional(str)
+
+ return self.async_show_form(
+ step_id='init',
+ data_schema=vol.Schema(schema),
+ errors=errors
+ )
+
+ async def async_step_setup(
+ self, user_input: Optional[Dict[str, str]] = None) \
+ -> Dict[str, Any]:
+ """Verify user can recevie one-time password."""
+ errors = {} # type: Dict[str, str]
+
+ hass = self._auth_module.hass
+ if user_input:
+ verified = await hass.async_add_executor_job(
+ _verify_otp, self._secret, user_input['code'], self._count)
+ if verified:
+ await self._auth_module.async_setup_user(
+ self._user_id, {
+ 'notify_service': self._notify_service,
+ 'target': self._target,
+ })
+ return self.async_create_entry(
+ title=self._auth_module.name,
+ data={}
+ )
+
+ errors['base'] = 'invalid_code'
+
+ # generate code every time, no retry logic
+ assert self._secret and self._count
+ code = await hass.async_add_executor_job(
+ _generate_otp, self._secret, self._count)
+
+ assert self._notify_service
+ await self._auth_module.async_notify(
+ code, self._notify_service, self._target)
+
+ return self.async_show_form(
+ step_id='setup',
+ data_schema=self._setup_schema,
+ description_placeholders={'notify_service': self._notify_service},
+ errors=errors,
+ )
diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py
index 0914658a655..625cc0302e1 100644
--- a/homeassistant/auth/mfa_modules/totp.py
+++ b/homeassistant/auth/mfa_modules/totp.py
@@ -60,6 +60,7 @@ class TotpAuthModule(MultiFactorAuthModule):
"""Auth module validate time-based one time password."""
DEFAULT_TITLE = 'Time-based One Time Password'
+ MAX_RETRY_TIME = 5
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store."""
@@ -130,7 +131,7 @@ class TotpAuthModule(MultiFactorAuthModule):
return user_id in self._users # type: ignore
- async def async_validation(
+ async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._users is None:
@@ -149,10 +150,10 @@ class TotpAuthModule(MultiFactorAuthModule):
if ota_secret is None:
# even we cannot find user, we still do verify
# to make timing the same as if user was found.
- pyotp.TOTP(DUMMY_SECRET).verify(code)
+ pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)
return False
- return bool(pyotp.TOTP(ota_secret).verify(code))
+ return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1))
class TotpSetupFlow(SetupFlow):
diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py
index 3cb1c6b121e..e96f6d7ebba 100644
--- a/homeassistant/auth/providers/__init__.py
+++ b/homeassistant/auth/providers/__init__.py
@@ -15,8 +15,8 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry
from ..auth_store import AuthStore
+from ..const import MFA_SESSION_EXPIRATION
from ..models import Credentials, User, UserMeta # noqa: F401
-from ..mfa_modules import SESSION_EXPIRATION
_LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed'
@@ -171,6 +171,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
self._auth_manager = auth_provider.hass.auth # type: ignore
self.available_mfa_modules = {} # type: Dict[str, str]
self.created_at = dt_util.utcnow()
+ self.invalid_mfa_times = 0
self.user = None # type: Optional[User]
async def async_step_init(
@@ -212,6 +213,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of mfa validation."""
+ assert self.user
+
errors = {}
auth_module = self._auth_manager.get_auth_mfa_module(
@@ -221,25 +224,34 @@ class LoginFlow(data_entry_flow.FlowHandler):
# will show invalid_auth_module error
return await self.async_step_select_mfa_module(user_input={})
+ if user_input is None and hasattr(auth_module,
+ 'async_initialize_login_mfa_step'):
+ await auth_module.async_initialize_login_mfa_step(self.user.id)
+
if user_input is not None:
- expires = self.created_at + SESSION_EXPIRATION
+ expires = self.created_at + MFA_SESSION_EXPIRATION
if dt_util.utcnow() > expires:
return self.async_abort(
reason='login_expired'
)
- result = await auth_module.async_validation(
- self.user.id, user_input) # type: ignore
+ result = await auth_module.async_validate(
+ self.user.id, user_input)
if not result:
errors['base'] = 'invalid_code'
+ self.invalid_mfa_times += 1
+ if self.invalid_mfa_times >= auth_module.MAX_RETRY_TIME > 0:
+ return self.async_abort(
+ reason='too_many_retry'
+ )
if not errors:
return await self.async_finish(self.user)
description_placeholders = {
'mfa_module_name': auth_module.name,
- 'mfa_module_id': auth_module.id
- } # type: Dict[str, str]
+ 'mfa_module_id': auth_module.id,
+ } # type: Dict[str, Optional[str]]
return self.async_show_form(
step_id='mfa',
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 2051359c0ba..0676cec7fad 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -5,7 +5,6 @@ import os
import sys
from time import time
from collections import OrderedDict
-
from typing import Any, Optional, Dict
import voluptuous as vol
@@ -19,7 +18,6 @@ from homeassistant.util.logging import AsyncHandler
from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers.signal import async_register_signal_handling
_LOGGER = logging.getLogger(__name__)
@@ -160,7 +158,6 @@ async def async_from_config_dict(config: Dict[str, Any],
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
- async_register_signal_handling(hass)
return hass
diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py
index 2aa157a5cad..9150518022f 100644
--- a/homeassistant/components/alarm_control_panel/spc.py
+++ b/homeassistant/components/alarm_control_panel/spc.py
@@ -4,71 +4,65 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.spc/
"""
-import asyncio
import logging
import homeassistant.components.alarm_control_panel as alarm
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.core import callback
from homeassistant.components.spc import (
- ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway)
+ ATTR_DISCOVER_AREAS, DATA_API, SIGNAL_UPDATE_ALARM)
from homeassistant.const import (
- STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
- STATE_UNKNOWN)
+ STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__)
-SPC_AREA_MODE_TO_STATE = {
- '0': STATE_ALARM_DISARMED,
- '1': STATE_ALARM_ARMED_HOME,
- '3': STATE_ALARM_ARMED_AWAY,
-}
-
-def _get_alarm_state(spc_mode):
+def _get_alarm_state(area):
"""Get the alarm state."""
- return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN)
+ from pyspcwebgw.const import AreaMode
+
+ if area.verified_alarm:
+ return STATE_ALARM_TRIGGERED
+
+ mode_to_state = {
+ AreaMode.UNSET: STATE_ALARM_DISARMED,
+ AreaMode.PART_SET_A: STATE_ALARM_ARMED_HOME,
+ AreaMode.PART_SET_B: STATE_ALARM_ARMED_NIGHT,
+ AreaMode.FULL_SET: STATE_ALARM_ARMED_AWAY,
+ }
+ return mode_to_state.get(area.mode)
-@asyncio.coroutine
-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):
"""Set up the SPC alarm control panel platform."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_AREAS] is None):
return
- api = hass.data[DATA_API]
- devices = [SpcAlarm(api, area)
- for area in discovery_info[ATTR_DISCOVER_AREAS]]
-
- async_add_entities(devices)
+ async_add_entities([SpcAlarm(area=area, api=hass.data[DATA_API])
+ for area in discovery_info[ATTR_DISCOVER_AREAS]])
class SpcAlarm(alarm.AlarmControlPanel):
"""Representation of the SPC alarm panel."""
- def __init__(self, api, area):
+ def __init__(self, area, api):
"""Initialize the SPC alarm panel."""
- self._area_id = area['id']
- self._name = area['name']
- self._state = _get_alarm_state(area['mode'])
- if self._state == STATE_ALARM_DISARMED:
- self._changed_by = area.get('last_unset_user_name', 'unknown')
- else:
- self._changed_by = area.get('last_set_user_name', 'unknown')
+ self._area = area
self._api = api
- @asyncio.coroutine
- def async_added_to_hass(self):
+ async def async_added_to_hass(self):
"""Call for adding new entities."""
- self.hass.data[DATA_REGISTRY].register_alarm_device(
- self._area_id, self)
+ async_dispatcher_connect(self.hass,
+ SIGNAL_UPDATE_ALARM.format(self._area.id),
+ self._update_callback)
- @asyncio.coroutine
- def async_update_from_spc(self, state, extra):
- """Update the alarm panel with a new state."""
- self._state = state
- self._changed_by = extra.get('changed_by', 'unknown')
- self.async_schedule_update_ha_state()
+ @callback
+ def _update_callback(self):
+ """Call update method."""
+ self.async_schedule_update_ha_state(True)
@property
def should_poll(self):
@@ -78,32 +72,34 @@ class SpcAlarm(alarm.AlarmControlPanel):
@property
def name(self):
"""Return the name of the device."""
- return self._name
+ return self._area.name
@property
def changed_by(self):
"""Return the user the last change was triggered by."""
- return self._changed_by
+ return self._area.last_changed_by
@property
def state(self):
"""Return the state of the device."""
- return self._state
+ return _get_alarm_state(self._area)
- @asyncio.coroutine
- def async_alarm_disarm(self, code=None):
+ async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
- yield from self._api.send_area_command(
- self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
+ from pyspcwebgw.const import AreaMode
+ self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET)
- @asyncio.coroutine
- def async_alarm_arm_home(self, code=None):
+ async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
- yield from self._api.send_area_command(
- self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
+ from pyspcwebgw.const import AreaMode
+ self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A)
- @asyncio.coroutine
- def async_alarm_arm_away(self, code=None):
+ async def async_alarm_arm_night(self, code=None):
+ """Send arm home command."""
+ from pyspcwebgw.const import AreaMode
+ self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_B)
+
+ async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
- yield from self._api.send_area_command(
- self._area_id, SpcWebGateway.AREA_COMMAND_SET)
+ from pyspcwebgw.const import AreaMode
+ self._api.change_mode(area=self._area, new_mode=AreaMode.FULL_SET)
diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py
index eab725c4653..176c286ebc3 100644
--- a/homeassistant/components/alexa/smart_home.py
+++ b/homeassistant/components/alexa/smart_home.py
@@ -1529,3 +1529,8 @@ async def async_api_reportstate(hass, config, request, context, entity):
name='StateReport',
context={'properties': properties}
)
+
+
+def turned_off_response(message):
+ """Return a device turned off response."""
+ return api_error(message[API_DIRECTIVE], error_type='BRIDGE_UNREACHABLE')
diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py
index 97fb2363024..21ff0e3286d 100644
--- a/homeassistant/components/apple_tv.py
+++ b/homeassistant/components/apple_tv.py
@@ -6,7 +6,6 @@ https://home-assistant.io/components/apple_tv/
"""
import asyncio
import logging
-
from typing import Sequence, TypeVar, Union
import voluptuous as vol
diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox.py
index 0d6d811db70..0907e48b256 100644
--- a/homeassistant/components/asterisk_mbox.py
+++ b/homeassistant/components/asterisk_mbox.py
@@ -13,7 +13,7 @@ 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, async_dispatcher_send)
+ async_dispatcher_send, dispatcher_connect)
REQUIREMENTS = ['asterisk_mbox==0.5.0']
@@ -21,8 +21,11 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'asterisk_mbox'
+SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform"
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
+SIGNAL_CDR_UPDATE = 'asterisk_mbox.message_updated'
+SIGNAL_CDR_REQUEST = 'asterisk_mbox.message_request'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -41,9 +44,7 @@ def setup(hass, config):
port = conf.get(CONF_PORT)
password = conf.get(CONF_PASSWORD)
- hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
-
- discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
+ hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config)
return True
@@ -51,31 +52,71 @@ def setup(hass, config):
class AsteriskData:
"""Store Asterisk mailbox data."""
- def __init__(self, hass, host, port, password):
+ def __init__(self, hass, host, port, password, config):
"""Init the Asterisk data object."""
from asterisk_mbox import Client as asteriskClient
-
self.hass = hass
- self.client = asteriskClient(host, port, password, self.handle_data)
- self.messages = []
+ self.config = config
+ self.messages = None
+ self.cdr = None
- async_dispatcher_connect(
+ dispatcher_connect(
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
+ dispatcher_connect(
+ self.hass, SIGNAL_CDR_REQUEST, self._request_cdr)
+ dispatcher_connect(
+ self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform)
+ # Only connect after signal connection to ensure we don't miss any
+ self.client = asteriskClient(host, port, password, self.handle_data)
+
+ @callback
+ def _discover_platform(self, component):
+ _LOGGER.debug("Adding mailbox %s", component)
+ self.hass.async_create_task(discovery.async_load_platform(
+ self.hass, "mailbox", component, {}, self.config))
@callback
def handle_data(self, command, msg):
"""Handle changes to the mailbox."""
- from asterisk_mbox.commands import CMD_MESSAGE_LIST
+ from asterisk_mbox.commands import (CMD_MESSAGE_LIST,
+ CMD_MESSAGE_CDR_AVAILABLE,
+ CMD_MESSAGE_CDR)
if command == CMD_MESSAGE_LIST:
- _LOGGER.debug("AsteriskVM sent updated message list")
+ _LOGGER.debug("AsteriskVM sent updated message list: Len %d",
+ len(msg))
+ old_messages = self.messages
self.messages = sorted(
msg, key=lambda item: item['info']['origtime'], reverse=True)
- async_dispatcher_send(
- self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
+ if not isinstance(old_messages, list):
+ async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
+ DOMAIN)
+ async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE,
+ self.messages)
+ elif command == CMD_MESSAGE_CDR:
+ _LOGGER.debug("AsteriskVM sent updated CDR list: Len %d",
+ len(msg.get('entries', [])))
+ self.cdr = msg['entries']
+ async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr)
+ elif command == CMD_MESSAGE_CDR_AVAILABLE:
+ if not isinstance(self.cdr, list):
+ _LOGGER.debug("AsteriskVM adding CDR platform")
+ self.cdr = []
+ async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
+ "asterisk_cdr")
+ async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST)
+ else:
+ _LOGGER.debug("AsteriskVM sent unknown message '%d' len: %d",
+ command, len(msg))
@callback
def _request_messages(self):
"""Handle changes to the mailbox."""
_LOGGER.debug("Requesting message list")
self.client.messages()
+
+ @callback
+ def _request_cdr(self):
+ """Handle changes to the CDR."""
+ _LOGGER.debug("Requesting CDR list")
+ self.client.get_cdr()
diff --git a/homeassistant/components/auth/.translations/ca.json b/homeassistant/components/auth/.translations/ca.json
index 1b3b25dbcff..f4318a0eb21 100644
--- a/homeassistant/components/auth/.translations/ca.json
+++ b/homeassistant/components/auth/.translations/ca.json
@@ -1,8 +1,27 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles."
+ },
+ "error": {
+ "invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho."
+ },
+ "step": {
+ "init": {
+ "description": "Seleccioneu un dels serveis de notificaci\u00f3:",
+ "title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
+ },
+ "setup": {
+ "description": "**notify.{notify_service}** ha enviat una contrasenya d'un sol \u00fas. Introdu\u00efu-la a continuaci\u00f3:",
+ "title": "Verifiqueu la configuraci\u00f3"
+ }
+ },
+ "title": "Contrasenya d'un sol \u00fas del servei de notificacions"
+ },
"totp": {
"error": {
- "invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
+ "invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
},
"step": {
"init": {
diff --git a/homeassistant/components/auth/.translations/de.json b/homeassistant/components/auth/.translations/de.json
index 67f948e8340..21c83290629 100644
--- a/homeassistant/components/auth/.translations/de.json
+++ b/homeassistant/components/auth/.translations/de.json
@@ -1,8 +1,26 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "Keine Benachrichtigungsdienste verf\u00fcgbar."
+ },
+ "error": {
+ "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut."
+ },
+ "step": {
+ "init": {
+ "description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:",
+ "title": "Einmal Passwort f\u00fcr Notify einrichten"
+ },
+ "setup": {
+ "description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:",
+ "title": "\u00dcberpr\u00fcfe das Setup"
+ }
+ }
+ },
"totp": {
"error": {
- "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn Sie diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist."
+ "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn du diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist."
},
"step": {
"init": {
diff --git a/homeassistant/components/auth/.translations/en.json b/homeassistant/components/auth/.translations/en.json
index a0fd20e9d08..66c0e92d9b5 100644
--- a/homeassistant/components/auth/.translations/en.json
+++ b/homeassistant/components/auth/.translations/en.json
@@ -1,5 +1,24 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "No notification services available."
+ },
+ "error": {
+ "invalid_code": "Invalid code, please try again."
+ },
+ "step": {
+ "init": {
+ "description": "Please select one of the notification services:",
+ "title": "Set up one-time password delivered by notify component"
+ },
+ "setup": {
+ "description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:",
+ "title": "Verify setup"
+ }
+ },
+ "title": "Notify One-Time Password"
+ },
"totp": {
"error": {
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."
diff --git a/homeassistant/components/auth/.translations/fr.json b/homeassistant/components/auth/.translations/fr.json
index b8d10dc89d0..85540314af0 100644
--- a/homeassistant/components/auth/.translations/fr.json
+++ b/homeassistant/components/auth/.translations/fr.json
@@ -1,5 +1,12 @@
{
"mfa_setup": {
+ "notify": {
+ "step": {
+ "setup": {
+ "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :"
+ }
+ }
+ },
"totp": {
"error": {
"invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte."
diff --git a/homeassistant/components/auth/.translations/he.json b/homeassistant/components/auth/.translations/he.json
new file mode 100644
index 00000000000..bc1826d4d79
--- /dev/null
+++ b/homeassistant/components/auth/.translations/he.json
@@ -0,0 +1,35 @@
+{
+ "mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 notify \u05d6\u05de\u05d9\u05e0\u05d9\u05dd."
+ },
+ "error": {
+ "invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1."
+ },
+ "step": {
+ "init": {
+ "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d0\u05d7\u05d3 \u05de\u05e9\u05e8\u05d5\u05ea\u05d9 notify",
+ "title": "\u05d4\u05d2\u05d3\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05d4\u05e0\u05e9\u05dc\u05d7\u05ea \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05db\u05d9\u05d1 notify"
+ },
+ "setup": {
+ "description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 **{notify_service}**. \u05d4\u05d6\u05df \u05d0\u05d5\u05ea\u05d4 \u05dc\u05de\u05d8\u05d4:",
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4"
+ }
+ },
+ "title": "\u05dc\u05d4\u05d5\u05d3\u05d9\u05e2 \u200b\u200b\u05e2\u05dc \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea"
+ },
+ "totp": {
+ "error": {
+ "invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e7\u05d1\u05dc \u05d0\u05ea \u05d4\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d4\u05d6\u05d5 \u05d1\u05d0\u05d5\u05e4\u05df \u05e2\u05e7\u05d1\u05d9, \u05d5\u05d3\u05d0 \u05e9\u05d4\u05e9\u05e2\u05d5\u05df \u05e9\u05dc \u05de\u05e2\u05e8\u05db\u05ea \u05d4 - Home Assistant \u05e9\u05dc\u05da \u05de\u05d3\u05d5\u05d9\u05e7."
+ },
+ "step": {
+ "init": {
+ "description": "\u05db\u05d3\u05d9 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d0\u05d5\u05ea \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05d5\u05ea \u05de\u05d1\u05d5\u05e1\u05e1\u05d5\u05ea \u05d6\u05de\u05df, \u05e1\u05e8\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 QR \u05e2\u05dd \u05d9\u05d9\u05e9\u05d5\u05dd \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05da. \u05d0\u05dd \u05d0\u05d9\u05df \u05dc\u05da \u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d6\u05d4, \u05d0\u05e0\u05d5 \u05de\u05de\u05dc\u05d9\u05e6\u05d9\u05dd \u05e2\u05dc [Google Authenticator] (https://support.google.com/accounts/answer/1066447) \u05d0\u05d5 [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u05dc\u05d0\u05d7\u05e8 \u05e1\u05e8\u05d9\u05e7\u05ea \u05d4\u05e7\u05d5\u05d3, \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e7\u05d5\u05d3 \u05d1\u05df \u05e9\u05e9 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05de\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d4. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e1\u05e8\u05d9\u05e7\u05ea \u05e7\u05d5\u05d3 QR, \u05d1\u05e6\u05e2 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d9\u05d3\u05e0\u05d9\u05ea \u05e2\u05dd \u05e7\u05d5\u05d3 **`{code}`**.",
+ "title": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea TOTP"
+ }
+ },
+ "title": "TOTP"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/auth/.translations/id.json b/homeassistant/components/auth/.translations/id.json
new file mode 100644
index 00000000000..f6a22386f99
--- /dev/null
+++ b/homeassistant/components/auth/.translations/id.json
@@ -0,0 +1,16 @@
+{
+ "mfa_setup": {
+ "totp": {
+ "error": {
+ "invalid_code": "Kode salah, coba lagi. Jika Anda mendapatkan kesalahan ini secara konsisten, pastikan jam pada sistem Home Assistant anda akurat."
+ },
+ "step": {
+ "init": {
+ "description": "Untuk mengaktifkan otentikasi dua faktor menggunakan password satu kali berbasis waktu, pindai kode QR dengan aplikasi otentikasi Anda. Jika Anda tidak memilikinya, kami menyarankan [Google Authenticator] (https://support.google.com/accounts/answer/1066447) atau [Authy] (https://authy.com/). \n\n {qr_code} \n \n Setelah memindai kode, masukkan kode enam digit dari aplikasi Anda untuk memverifikasi pengaturan. Jika Anda mengalami masalah saat memindai kode QR, lakukan pengaturan manual dengan kode ** ` {code} ` **.",
+ "title": "Siapkan otentikasi dua faktor menggunakan TOTP"
+ }
+ },
+ "title": "TOTP"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json
index 17fb5c56f57..e1f26e88bc7 100644
--- a/homeassistant/components/auth/.translations/ko.json
+++ b/homeassistant/components/auth/.translations/ko.json
@@ -1,5 +1,24 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc54c\ub9bc \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
+ },
+ "step": {
+ "init": {
+ "description": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:",
+ "title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815"
+ },
+ "setup": {
+ "description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574 \uc8fc\uc138\uc694:",
+ "title": "\uc124\uc815 \ud655\uc778"
+ }
+ },
+ "title": "\uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc54c\ub9bc"
+ },
"totp": {
"error": {
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694."
diff --git a/homeassistant/components/auth/.translations/lb.json b/homeassistant/components/auth/.translations/lb.json
index f55ae4b97ba..12ced930446 100644
--- a/homeassistant/components/auth/.translations/lb.json
+++ b/homeassistant/components/auth/.translations/lb.json
@@ -1,5 +1,24 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "Keen Notifikatioun's D\u00e9ngscht disponibel."
+ },
+ "error": {
+ "invalid_code": "Ong\u00ebltege Code, prob\u00e9iert w.e.g. nach emol."
+ },
+ "step": {
+ "init": {
+ "description": "Wielt w.e.g. een Notifikatioun's D\u00e9ngscht aus:",
+ "title": "Eemolegt Passwuert ariichte wat vun engem Notifikatioun's Komponente versch\u00e9ckt g\u00ebtt"
+ },
+ "setup": {
+ "description": "Een eemolegt Passwuert ass vun **notify.{notify_service}** gesch\u00e9ckt ginn. Gitt et w.e.g hei \u00ebnnen dr\u00ebnner an:",
+ "title": "Astellungen iwwerpr\u00e9iwen"
+ }
+ },
+ "title": "Eemolegt Passwuert Notifikatioun"
+ },
"totp": {
"error": {
"invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass."
diff --git a/homeassistant/components/auth/.translations/nn.json b/homeassistant/components/auth/.translations/nn.json
new file mode 100644
index 00000000000..24d756f938b
--- /dev/null
+++ b/homeassistant/components/auth/.translations/nn.json
@@ -0,0 +1,16 @@
+{
+ "mfa_setup": {
+ "totp": {
+ "error": {
+ "invalid_code": "Ugyldig kode, pr\u00f8v igjen. Dersom du heile tida f\u00e5r denne feilen, m\u00e5 du s\u00f8rge for at klokka p\u00e5 Home Assistant-systemet ditt er n\u00f8yaktig."
+ },
+ "step": {
+ "init": {
+ "description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte eingangspassord, skann QR-koden med autentiseringsappen din. Dersom du ikkje har ein, vil vi r\u00e5de deg til \u00e5 bruke anten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har skanna koda, skriv du inn den sekssifra koda fr\u00e5 appen din for \u00e5 stadfeste oppsettet. Dersom du har problemer med \u00e5 skanne QR-koda, gjer du eit manuelt oppsett med kode ** ` {code} ` **.",
+ "title": "Konfigurer to-faktor-autentisering ved hjelp av TOTP"
+ }
+ },
+ "title": "TOTP"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/auth/.translations/no.json b/homeassistant/components/auth/.translations/no.json
index 43ec497cfb1..48b5db8a3b6 100644
--- a/homeassistant/components/auth/.translations/no.json
+++ b/homeassistant/components/auth/.translations/no.json
@@ -1,5 +1,24 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "Ingen varslingstjenester er tilgjengelig."
+ },
+ "error": {
+ "invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen."
+ },
+ "step": {
+ "init": {
+ "description": "Vennligst velg en av varslingstjenestene:",
+ "title": "Sett opp engangspassord levert av varsel komponent"
+ },
+ "setup": {
+ "description": "Et engangspassord har blitt sendt via **notify.{notify_service}**. Vennligst skriv det inn nedenfor:",
+ "title": "Bekreft oppsettet"
+ }
+ },
+ "title": "Varsle engangspassord"
+ },
"totp": {
"error": {
"invalid_code": "Ugyldig kode, pr\u00f8v igjen. Hvis du f\u00e5r denne feilen konsekvent, m\u00e5 du s\u00f8rge for at klokken p\u00e5 Home Assistant systemet er riktig."
diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json
index 78999c34c22..3e320ba8d62 100644
--- a/homeassistant/components/auth/.translations/pl.json
+++ b/homeassistant/components/auth/.translations/pl.json
@@ -1,5 +1,23 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "Brak dost\u0119pnych us\u0142ug powiadamiania."
+ },
+ "error": {
+ "invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie."
+ },
+ "step": {
+ "init": {
+ "description": "Prosz\u0119 wybra\u0107 jedn\u0105 z us\u0142ugi powiadamiania:",
+ "title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144"
+ },
+ "setup": {
+ "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez ** powiadom. {notify_service} **. Wpisz je poni\u017cej:",
+ "title": "Sprawd\u017a konfiguracj\u0119"
+ }
+ }
+ },
"totp": {
"error": {
"invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie. Je\u015bli b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, upewnij si\u0119, \u017ce czas zegara systemu Home Assistant jest prawid\u0142owy."
diff --git a/homeassistant/components/auth/.translations/pt-BR.json b/homeassistant/components/auth/.translations/pt-BR.json
new file mode 100644
index 00000000000..58c785a5b95
--- /dev/null
+++ b/homeassistant/components/auth/.translations/pt-BR.json
@@ -0,0 +1,15 @@
+{
+ "mfa_setup": {
+ "totp": {
+ "error": {
+ "invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente. Se voc\u00ea obtiver este erro de forma consistente, certifique-se de que o rel\u00f3gio do sistema Home Assistant esteja correto."
+ },
+ "step": {
+ "init": {
+ "title": "Configure a autentica\u00e7\u00e3o de dois fatores usando o TOTP"
+ }
+ },
+ "title": "TOTP"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/auth/.translations/ru.json b/homeassistant/components/auth/.translations/ru.json
index a716425f345..edf136bd7f3 100644
--- a/homeassistant/components/auth/.translations/ru.json
+++ b/homeassistant/components/auth/.translations/ru.json
@@ -1,5 +1,24 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439."
+ },
+ "error": {
+ "invalid_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443."
+ },
+ "step": {
+ "init": {
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u0443 \u0438\u0437 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439:",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439"
+ },
+ "setup": {
+ "description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u0447\u0435\u0440\u0435\u0437 **notify.{notify_service}**. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u043d\u0438\u0436\u0435:",
+ "title": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443"
+ }
+ },
+ "title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439"
+ },
"totp": {
"error": {
"invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f."
diff --git a/homeassistant/components/auth/.translations/sl.json b/homeassistant/components/auth/.translations/sl.json
index 45b57a772f9..2efc23f78f6 100644
--- a/homeassistant/components/auth/.translations/sl.json
+++ b/homeassistant/components/auth/.translations/sl.json
@@ -1,5 +1,24 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "Ni na voljo storitev obve\u0161\u010danja."
+ },
+ "error": {
+ "invalid_code": "Neveljavna koda, poskusite znova."
+ },
+ "step": {
+ "init": {
+ "description": "Prosimo, izberite eno od storitev obve\u0161\u010danja:",
+ "title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento"
+ },
+ "setup": {
+ "description": "Enkratno geslo je poslal **notify.{notify_service} **. Vnesite ga spodaj:",
+ "title": "Preverite nastavitev"
+ }
+ },
+ "title": "Obvesti Enkratno Geslo"
+ },
"totp": {
"error": {
"invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna."
diff --git a/homeassistant/components/auth/.translations/sv.json b/homeassistant/components/auth/.translations/sv.json
index cf8227c09a3..604ae3c4fe5 100644
--- a/homeassistant/components/auth/.translations/sv.json
+++ b/homeassistant/components/auth/.translations/sv.json
@@ -1,5 +1,19 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "Inga tillg\u00e4ngliga meddelande tj\u00e4nster."
+ },
+ "error": {
+ "invalid_code": "Ogiltig kod, var god f\u00f6rs\u00f6k igen."
+ },
+ "step": {
+ "setup": {
+ "description": "Ett eng\u00e5ngsl\u00f6senord har skickats av **notify.{notify_service}**. V\u00e4nligen ange det nedan:",
+ "title": "Verifiera installationen"
+ }
+ }
+ },
"totp": {
"error": {
"invalid_code": "Ogiltig kod, f\u00f6rs\u00f6k igen. Om du flera g\u00e5nger i rad f\u00e5r detta fel, se till att klockan i din Home Assistant \u00e4r korrekt inst\u00e4lld."
diff --git a/homeassistant/components/auth/.translations/zh-Hans.json b/homeassistant/components/auth/.translations/zh-Hans.json
index c5b397a8e12..1cb311f016f 100644
--- a/homeassistant/components/auth/.translations/zh-Hans.json
+++ b/homeassistant/components/auth/.translations/zh-Hans.json
@@ -1,5 +1,24 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "\u6ca1\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52a1\u3002"
+ },
+ "error": {
+ "invalid_code": "\u4ee3\u7801\u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002"
+ },
+ "step": {
+ "init": {
+ "description": "\u8bf7\u4ece\u4e0b\u9762\u9009\u62e9\u4e00\u4e2a\u901a\u77e5\u670d\u52a1\uff1a",
+ "title": "\u8bbe\u7f6e\u7531\u901a\u77e5\u7ec4\u4ef6\u4f20\u9012\u7684\u4e00\u6b21\u6027\u5bc6\u7801"
+ },
+ "setup": {
+ "description": "\u4e00\u6b21\u6027\u5bc6\u7801\u5df2\u7531 **notify.{notify_service}** \u53d1\u9001\u3002\u8bf7\u5728\u4e0b\u9762\u8f93\u5165\uff1a",
+ "title": "\u9a8c\u8bc1\u8bbe\u7f6e"
+ }
+ },
+ "title": "\u4e00\u6b21\u6027\u5bc6\u7801\u901a\u77e5"
+ },
"totp": {
"error": {
"invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002"
diff --git a/homeassistant/components/auth/.translations/zh-Hant.json b/homeassistant/components/auth/.translations/zh-Hant.json
index ef41ea87248..e791f20a738 100644
--- a/homeassistant/components/auth/.translations/zh-Hant.json
+++ b/homeassistant/components/auth/.translations/zh-Hant.json
@@ -1,5 +1,24 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "\u6c92\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52d9\u3002"
+ },
+ "error": {
+ "invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002"
+ },
+ "step": {
+ "init": {
+ "description": "\u8acb\u9078\u64c7\u4e00\u9805\u901a\u77e5\u670d\u52d9\uff1a",
+ "title": "\u8a2d\u5b9a\u4e00\u6b21\u6027\u5bc6\u78bc\u50b3\u9001\u7d44\u4ef6"
+ },
+ "setup": {
+ "description": "\u4e00\u6b21\u6027\u5bc6\u78bc\u5df2\u900f\u904e **notify.{notify_service}** \u50b3\u9001\u3002\u8acb\u65bc\u4e0b\u65b9\u8f38\u5165\uff1a",
+ "title": "\u9a57\u8b49\u8a2d\u5b9a"
+ }
+ },
+ "title": "\u901a\u77e5\u4e00\u6b21\u6027\u5bc6\u78bc"
+ },
"totp": {
"error": {
"invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002"
diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py
index 73a739c2960..3a51cf8066f 100644
--- a/homeassistant/components/auth/login_flow.py
+++ b/homeassistant/components/auth/login_flow.py
@@ -226,8 +226,9 @@ class LoginFlowResourceView(HomeAssistantView):
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
# @log_invalid_auth does not work here since it returns HTTP 200
# need manually log failed login attempts
- if result['errors'] is not None and \
- result['errors'].get('base') == 'invalid_auth':
+ if (result.get('errors') is not None and
+ result['errors'].get('base') in ['invalid_auth',
+ 'invalid_code']):
await process_wrong_login(request)
return self.json(_prepare_result_json(result))
diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json
index b0083ab577b..2b1fc0c94f6 100644
--- a/homeassistant/components/auth/strings.json
+++ b/homeassistant/components/auth/strings.json
@@ -11,6 +11,25 @@
"error": {
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."
}
+ },
+ "notify": {
+ "title": "Notify One-Time Password",
+ "step": {
+ "init": {
+ "title": "Set up one-time password delivered by notify component",
+ "description": "Please select one of notify service:"
+ },
+ "setup": {
+ "title": "Verify setup",
+ "description": "A one-time password have sent by **notify.{notify_service}**. Please input it in below:"
+ }
+ },
+ "abort": {
+ "no_available_service": "No available notify services."
+ },
+ "error": {
+ "invalid_code": "Invalid code, please try again."
+ }
}
}
}
diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py
index d2ca9e7c5e8..b0728ad167c 100644
--- a/homeassistant/components/binary_sensor/deconz.py
+++ b/homeassistant/components/binary_sensor/deconz.py
@@ -127,6 +127,7 @@ class DeconzBinarySensor(BinarySensorDevice):
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
+ bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
@@ -134,4 +135,5 @@ class DeconzBinarySensor(BinarySensorDevice):
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
+ 'via_hub': (DECONZ_DOMAIN, bridgeid),
}
diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py
index dd22a835504..6c8b7ff191e 100644
--- a/homeassistant/components/binary_sensor/homematicip_cloud.py
+++ b/homeassistant/components/binary_sensor/homematicip_cloud.py
@@ -27,17 +27,20 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
from homematicip.aio.device import (
- AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector)
+ AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector,
+ AsyncWaterSensor, AsyncRotaryHandleSensor)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.devices:
- if isinstance(device, AsyncShutterContact):
+ if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)):
devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, AsyncMotionDetectorIndoor):
devices.append(HomematicipMotionDetector(home, device))
elif isinstance(device, AsyncSmokeDetector):
devices.append(HomematicipSmokeDetector(home, device))
+ elif isinstance(device, AsyncWaterSensor):
+ devices.append(HomematicipWaterDetector(home, device))
if devices:
async_add_entities(devices)
@@ -91,3 +94,17 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
def is_on(self):
"""Return true if smoke is detected."""
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF
+
+
+class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice):
+ """Representation of a HomematicIP Cloud water detector."""
+
+ @property
+ def device_class(self):
+ """Return the class of this sensor."""
+ return 'moisture'
+
+ @property
+ def is_on(self):
+ """Return true if moisture or waterlevel is detected."""
+ return self._device.moistureDetected or self._device.waterlevelDetected
diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py
index 412aeb46a3a..ac82ab126fd 100644
--- a/homeassistant/components/binary_sensor/rest.py
+++ b/homeassistant/components/binary_sensor/rest.py
@@ -18,6 +18,7 @@ from homeassistant.const import (
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv
+from homeassistant.exceptions import PlatformNotReady
_LOGGER = logging.getLogger(__name__)
@@ -66,13 +67,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
rest = RestData(method, resource, auth, headers, payload, verify_ssl)
rest.update()
-
if rest.data is None:
- _LOGGER.error("Unable to fetch REST data from %s", resource)
- return False
+ raise PlatformNotReady
+ # No need to update the sensor now because it will determine its state
+ # based in the rest resource that has just been retrieved.
add_entities([RestBinarySensor(
- hass, rest, name, device_class, value_template)], True)
+ hass, rest, name, device_class, value_template)])
class RestBinarySensor(BinarySensorDevice):
diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py
index 9afd4fe4015..c1be72db374 100644
--- a/homeassistant/components/binary_sensor/spc.py
+++ b/homeassistant/components/binary_sensor/spc.py
@@ -4,87 +4,66 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.spc/
"""
-import asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.components.spc import ATTR_DISCOVER_DEVICES, DATA_REGISTRY
-from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.core import callback
+from homeassistant.components.spc import (
+ ATTR_DISCOVER_DEVICES, SIGNAL_UPDATE_SENSOR)
_LOGGER = logging.getLogger(__name__)
-SPC_TYPE_TO_DEVICE_CLASS = {
- '0': 'motion',
- '1': 'opening',
- '3': 'smoke',
-}
-SPC_INPUT_TO_SENSOR_STATE = {
- '0': STATE_OFF,
- '1': STATE_ON,
-}
+def _get_device_class(zone_type):
+ from pyspcwebgw.const import ZoneType
+ return {
+ ZoneType.ALARM: 'motion',
+ ZoneType.ENTRY_EXIT: 'opening',
+ ZoneType.FIRE: 'smoke',
+ }.get(zone_type)
-def _get_device_class(spc_type):
- """Get the device class."""
- return SPC_TYPE_TO_DEVICE_CLASS.get(spc_type, None)
-
-
-def _get_sensor_state(spc_input):
- """Get the sensor state."""
- return SPC_INPUT_TO_SENSOR_STATE.get(spc_input, STATE_UNAVAILABLE)
-
-
-def _create_sensor(hass, zone):
- """Create a SPC sensor."""
- return SpcBinarySensor(
- zone_id=zone['id'], name=zone['zone_name'],
- state=_get_sensor_state(zone['input']),
- device_class=_get_device_class(zone['type']),
- spc_registry=hass.data[DATA_REGISTRY])
-
-
-@asyncio.coroutine
-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):
"""Set up the SPC binary sensor."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None):
return
- async_add_entities(
- _create_sensor(hass, zone)
- for zone in discovery_info[ATTR_DISCOVER_DEVICES]
- if _get_device_class(zone['type']))
+ async_add_entities(SpcBinarySensor(zone)
+ for zone in discovery_info[ATTR_DISCOVER_DEVICES]
+ if _get_device_class(zone.type))
class SpcBinarySensor(BinarySensorDevice):
"""Representation of a sensor based on a SPC zone."""
- def __init__(self, zone_id, name, state, device_class, spc_registry):
+ def __init__(self, zone):
"""Initialize the sensor device."""
- self._zone_id = zone_id
- self._name = name
- self._state = state
- self._device_class = device_class
+ self._zone = zone
- spc_registry.register_sensor_device(zone_id, self)
+ async def async_added_to_hass(self):
+ """Call for adding new entities."""
+ async_dispatcher_connect(self.hass,
+ SIGNAL_UPDATE_SENSOR.format(self._zone.id),
+ self._update_callback)
- @asyncio.coroutine
- def async_update_from_spc(self, state, extra):
- """Update the state of the device."""
- self._state = state
- yield from self.async_update_ha_state()
+ @callback
+ def _update_callback(self):
+ """Call update method."""
+ self.async_schedule_update_ha_state(True)
@property
def name(self):
"""Return the name of the device."""
- return self._name
+ return self._zone.name
@property
def is_on(self):
"""Whether the device is switched on."""
- return self._state == STATE_ON
+ from pyspcwebgw.const import ZoneInput
+ return self._zone.input == ZoneInput.OPEN
@property
def hidden(self) -> bool:
@@ -100,4 +79,4 @@ class SpcBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""Return the device class."""
- return self._device_class
+ return _get_device_class(self._zone.type)
diff --git a/homeassistant/components/binary_sensor/wirelesstag.py b/homeassistant/components/binary_sensor/wirelesstag.py
index 4bec3a824c3..190b408abf3 100644
--- a/homeassistant/components/binary_sensor/wirelesstag.py
+++ b/homeassistant/components/binary_sensor/wirelesstag.py
@@ -14,9 +14,6 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.wirelesstag import (
DOMAIN as WIRELESSTAG_DOMAIN,
- WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
- WIRELESSTAG_TYPE_ALSPRO,
- WIRELESSTAG_TYPE_WEMO_DEVICE,
SIGNAL_BINARY_EVENT_UPDATE,
WirelessTagBaseSensor)
from homeassistant.const import (
@@ -30,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
# On means in range, Off means out of range
SENSOR_PRESENCE = 'presence'
-# On means motion detected, Off means cear
+# On means motion detected, Off means clear
SENSOR_MOTION = 'motion'
# On means open, Off means closed
@@ -55,49 +52,21 @@ SENSOR_LIGHT = 'light'
SENSOR_MOISTURE = 'moisture'
# On means tag battery is low, Off means normal
-SENSOR_BATTERY = 'low_battery'
+SENSOR_BATTERY = 'battery'
# Sensor types: Name, device_class, push notification type representing 'on',
# attr to check
SENSOR_TYPES = {
- SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', {
- "on": "oor",
- "off": "back_in_range"
- }, 2],
- SENSOR_MOTION: ['Motion', 'motion', 'is_moved', {
- "on": "motion_detected",
- }, 5],
- SENSOR_DOOR: ['Door', 'door', 'is_door_open', {
- "on": "door_opened",
- "off": "door_closed"
- }, 5],
- SENSOR_COLD: ['Cold', 'cold', 'is_cold', {
- "on": "temp_toolow",
- "off": "temp_normal"
- }, 4],
- SENSOR_HEAT: ['Heat', 'heat', 'is_heat', {
- "on": "temp_toohigh",
- "off": "temp_normal"
- }, 4],
- SENSOR_DRY: ['Too dry', 'dry', 'is_too_dry', {
- "on": "too_dry",
- "off": "cap_normal"
- }, 2],
- SENSOR_WET: ['Too wet', 'wet', 'is_too_humid', {
- "on": "too_humid",
- "off": "cap_normal"
- }, 2],
- SENSOR_LIGHT: ['Light', 'light', 'is_light_on', {
- "on": "too_bright",
- "off": "light_normal"
- }, 1],
- SENSOR_MOISTURE: ['Leak', 'moisture', 'is_leaking', {
- "on": "water_detected",
- "off": "water_dried",
- }, 1],
- SENSOR_BATTERY: ['Low Battery', 'battery', 'is_battery_low', {
- "on": "low_battery"
- }, 3]
+ SENSOR_PRESENCE: 'Presence',
+ SENSOR_MOTION: 'Motion',
+ SENSOR_DOOR: 'Door',
+ SENSOR_COLD: 'Cold',
+ SENSOR_HEAT: 'Heat',
+ SENSOR_DRY: 'Too dry',
+ SENSOR_WET: 'Too wet',
+ SENSOR_LIGHT: 'Light',
+ SENSOR_MOISTURE: 'Leak',
+ SENSOR_BATTERY: 'Low Battery'
}
@@ -114,7 +83,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sensors = []
tags = platform.tags
for tag in tags.values():
- allowed_sensor_types = WirelessTagBinarySensor.allowed_sensors(tag)
+ allowed_sensor_types = tag.supported_binary_events_types
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
if sensor_type in allowed_sensor_types:
sensors.append(WirelessTagBinarySensor(platform, tag,
@@ -127,59 +96,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
"""A binary sensor implementation for WirelessTags."""
- @classmethod
- def allowed_sensors(cls, tag):
- """Return list of allowed sensor types for specific tag type."""
- sensors_map = {
- # 13-bit tag - allows everything but not light and moisture
- WIRELESSTAG_TYPE_13BIT: [
- SENSOR_PRESENCE, SENSOR_BATTERY,
- SENSOR_MOTION, SENSOR_DOOR,
- SENSOR_COLD, SENSOR_HEAT,
- SENSOR_DRY, SENSOR_WET],
-
- # Moister/water sensor - temperature and moisture only
- WIRELESSTAG_TYPE_WATER: [
- SENSOR_PRESENCE, SENSOR_BATTERY,
- SENSOR_COLD, SENSOR_HEAT,
- SENSOR_MOISTURE],
-
- # ALS Pro: allows everything, but not moisture
- WIRELESSTAG_TYPE_ALSPRO: [
- SENSOR_PRESENCE, SENSOR_BATTERY,
- SENSOR_MOTION, SENSOR_DOOR,
- SENSOR_COLD, SENSOR_HEAT,
- SENSOR_DRY, SENSOR_WET,
- SENSOR_LIGHT],
-
- # Wemo are power switches.
- WIRELESSTAG_TYPE_WEMO_DEVICE: [SENSOR_PRESENCE]
- }
-
- # allow everything if tag type is unknown
- # (i just dont have full catalog of them :))
- tag_type = tag.tag_type
- fullset = SENSOR_TYPES.keys()
- return sensors_map[tag_type] if tag_type in sensors_map else fullset
-
def __init__(self, api, tag, sensor_type):
"""Initialize a binary sensor for a Wireless Sensor Tags."""
super().__init__(api, tag)
self._sensor_type = sensor_type
self._name = '{0} {1}'.format(self._tag.name,
- SENSOR_TYPES[self._sensor_type][0])
- self._device_class = SENSOR_TYPES[self._sensor_type][1]
- self._tag_attr = SENSOR_TYPES[self._sensor_type][2]
- self.binary_spec = SENSOR_TYPES[self._sensor_type][3]
- self.tag_id_index_template = SENSOR_TYPES[self._sensor_type][4]
+ self.event.human_readable_name)
async def async_added_to_hass(self):
"""Register callbacks."""
tag_id = self.tag_id
event_type = self.device_class
+ mac = self.tag_manager_mac
async_dispatcher_connect(
self.hass,
- SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type),
+ SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac),
self._on_binary_event_callback)
@property
@@ -190,7 +121,12 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
@property
def device_class(self):
"""Return the class of the binary sensor."""
- return self._device_class
+ return self._sensor_type
+
+ @property
+ def event(self):
+ """Binary event of tag."""
+ return self._tag.event[self._sensor_type]
@property
def principal_value(self):
@@ -198,9 +134,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
Subclasses need override based on type of sensor.
"""
- return (
- STATE_ON if getattr(self._tag, self._tag_attr, False)
- else STATE_OFF)
+ return STATE_ON if self.event.is_state_on else STATE_OFF
def updated_state_value(self):
"""Use raw princial value."""
@@ -208,7 +142,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
@callback
def _on_binary_event_callback(self, event):
- """Update state from arrive push notification."""
+ """Update state from arrived push notification."""
# state should be 'on' or 'off'
self._state = event.data.get('state')
self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py
index 3fb2d7f7f86..1d85d9c9a47 100644
--- a/homeassistant/components/binary_sensor/workday.py
+++ b/homeassistant/components/binary_sensor/workday.py
@@ -15,35 +15,38 @@ from homeassistant.const import CONF_NAME, WEEKDAYS
from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.helpers.config_validation as cv
-_LOGGER = logging.getLogger(__name__)
+REQUIREMENTS = ['holidays==0.9.7']
-REQUIREMENTS = ['holidays==0.9.6']
+_LOGGER = logging.getLogger(__name__)
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
-ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
- 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
- 'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
- 'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
- 'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland',
- 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
- 'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
- 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
- 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
- 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
- 'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
- 'UnitedStates', 'US', 'Wales']
+ALL_COUNTRIES = [
+ 'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', 'Belarus', 'BY'
+ 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ',
+ 'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
+ 'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
+ 'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
+ 'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ',
+ 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
+ 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK',
+ 'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH',
+ 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales',
+]
+
+ALLOWED_DAYS = WEEKDAYS + ['holiday']
+
CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays'
+CONF_EXCLUDES = 'excludes'
+CONF_OFFSET = 'days_offset'
+
# By default, Monday - Friday are workdays
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
-CONF_EXCLUDES = 'excludes'
# By default, public holidays, Saturdays and Sundays are excluded from workdays
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
DEFAULT_NAME = 'Workday Sensor'
-ALLOWED_DAYS = WEEKDAYS + ['holiday']
-CONF_OFFSET = 'days_offset'
DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -86,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
else:
_LOGGER.error("There is no province/state %s in country %s",
province, country)
- return False
+ return
_LOGGER.debug("Found the following holidays for your configuration:")
for date, name in sorted(obj_holidays.items()):
diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py
index cabbbd704a0..aa07a673c97 100644
--- a/homeassistant/components/binary_sensor/zha.py
+++ b/homeassistant/components/binary_sensor/zha.py
@@ -65,28 +65,25 @@ async def _async_setup_iaszone(hass, config, async_add_entities,
async def _async_setup_remote(hass, config, async_add_entities,
discovery_info):
- async def safe(coro):
- """Run coro, catching ZigBee delivery errors, and ignoring them."""
- import zigpy.exceptions
- try:
- await coro
- except zigpy.exceptions.DeliveryError as exc:
- _LOGGER.warning("Ignoring error during setup: %s", exc)
+ remote = Remote(**discovery_info)
if discovery_info['new_join']:
from zigpy.zcl.clusters.general import OnOff, LevelControl
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
- await safe(cluster.bind())
- await safe(cluster.configure_reporting(0, 0, 600, 1))
+ await zha.configure_reporting(
+ remote.entity_id, cluster, 0, min_report=0, max_report=600,
+ reportable_change=1
+ )
if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id]
- await safe(cluster.bind())
- await safe(cluster.configure_reporting(0, 1, 600, 1))
+ await zha.configure_reporting(
+ remote.entity_id, cluster, 0, min_report=1, max_report=600,
+ reportable_change=1
+ )
- sensor = Switch(**discovery_info)
- async_add_entities([sensor], update_before_add=True)
+ async_add_entities([remote], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice):
@@ -131,17 +128,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
async def async_update(self):
"""Retrieve latest state."""
- from bellows.types.basic import uint16_t
+ from zigpy.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status'],
- allow_cache=False)
+ allow_cache=False,
+ only_cache=(not self._initialized))
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3
-class Switch(zha.Entity, BinarySensorDevice):
+class Remote(zha.Entity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN
diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py
index 784a96d8615..3bb3a3c79c5 100644
--- a/homeassistant/components/binary_sensor/zwave.py
+++ b/homeassistant/components/binary_sensor/zwave.py
@@ -9,8 +9,8 @@ import datetime
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time
from homeassistant.components import zwave
-from homeassistant.components.zwave import workaround
-from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
+from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
+ async_setup_platform, workaround)
from homeassistant.components.binary_sensor import (
DOMAIN,
BinarySensorDevice)
diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py
index b0ad1a867a8..12363627003 100644
--- a/homeassistant/components/bmw_connected_drive/__init__.py
+++ b/homeassistant/components/bmw_connected_drive/__init__.py
@@ -14,7 +14,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['bimmer_connected==0.5.1']
+REQUIREMENTS = ['bimmer_connected==0.5.2']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/camera/axis.py b/homeassistant/components/camera/axis.py
index 5630759a7f8..4227cca7e2f 100644
--- a/homeassistant/components/camera/axis.py
+++ b/homeassistant/components/camera/axis.py
@@ -50,7 +50,7 @@ class AxisCamera(MjpegCamera):
def __init__(self, hass, config, port):
"""Initialize Axis Communications camera component."""
- super().__init__(hass, config)
+ super().__init__(config)
self.port = port
dispatcher_connect(
hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip)
diff --git a/homeassistant/components/camera/logi_circle.py b/homeassistant/components/camera/logi_circle.py
new file mode 100644
index 00000000000..1dae58ad0f7
--- /dev/null
+++ b/homeassistant/components/camera/logi_circle.py
@@ -0,0 +1,210 @@
+"""
+This component provides support to the Logi Circle camera.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/camera.logi_circle/
+"""
+import logging
+import asyncio
+from datetime import timedelta
+
+import voluptuous as vol
+
+from homeassistant.helpers import config_validation as cv
+from homeassistant.components.logi_circle import (
+ DOMAIN as LOGI_CIRCLE_DOMAIN, CONF_ATTRIBUTION)
+from homeassistant.components.camera import (
+ Camera, PLATFORM_SCHEMA, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF,
+ ATTR_ENTITY_ID, ATTR_FILENAME, DOMAIN)
+from homeassistant.const import (
+ ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL,
+ CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF)
+
+DEPENDENCIES = ['logi_circle']
+
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = timedelta(seconds=60)
+
+SERVICE_SET_CONFIG = 'logi_circle_set_config'
+SERVICE_LIVESTREAM_SNAPSHOT = 'logi_circle_livestream_snapshot'
+SERVICE_LIVESTREAM_RECORD = 'logi_circle_livestream_record'
+DATA_KEY = 'camera.logi_circle'
+
+BATTERY_SAVING_MODE_KEY = 'BATTERY_SAVING'
+PRIVACY_MODE_KEY = 'PRIVACY_MODE'
+LED_MODE_KEY = 'LED'
+
+ATTR_MODE = 'mode'
+ATTR_VALUE = 'value'
+ATTR_DURATION = 'duration'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
+ cv.time_period,
+})
+
+LOGI_CIRCLE_SERVICE_SET_CONFIG = CAMERA_SERVICE_SCHEMA.extend({
+ vol.Required(ATTR_MODE): vol.In([BATTERY_SAVING_MODE_KEY, LED_MODE_KEY,
+ PRIVACY_MODE_KEY]),
+ vol.Required(ATTR_VALUE): cv.boolean
+})
+
+LOGI_CIRCLE_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
+ vol.Required(ATTR_FILENAME): cv.template
+})
+
+LOGI_CIRCLE_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend({
+ vol.Required(ATTR_FILENAME): cv.template,
+ vol.Required(ATTR_DURATION): cv.positive_int
+})
+
+
+async def async_setup_platform(
+ hass, config, async_add_entities, discovery_info=None):
+ """Set up a Logi Circle Camera."""
+ devices = hass.data[LOGI_CIRCLE_DOMAIN]
+
+ cameras = []
+ for device in devices:
+ cameras.append(LogiCam(device, config))
+
+ async_add_entities(cameras, True)
+
+ async def service_handler(service):
+ """Dispatch service calls to target entities."""
+ params = {key: value for key, value in service.data.items()
+ if key != ATTR_ENTITY_ID}
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+ if entity_ids:
+ target_devices = [dev for dev in cameras
+ if dev.entity_id in entity_ids]
+ else:
+ target_devices = cameras
+
+ for target_device in target_devices:
+ if service.service == SERVICE_SET_CONFIG:
+ await target_device.set_config(**params)
+ if service.service == SERVICE_LIVESTREAM_SNAPSHOT:
+ await target_device.livestream_snapshot(**params)
+ if service.service == SERVICE_LIVESTREAM_RECORD:
+ await target_device.download_livestream(**params)
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_SET_CONFIG, service_handler,
+ schema=LOGI_CIRCLE_SERVICE_SET_CONFIG)
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_LIVESTREAM_SNAPSHOT, service_handler,
+ schema=LOGI_CIRCLE_SERVICE_SNAPSHOT)
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_LIVESTREAM_RECORD, service_handler,
+ schema=LOGI_CIRCLE_SERVICE_RECORD)
+
+
+class LogiCam(Camera):
+ """An implementation of a Logi Circle camera."""
+
+ def __init__(self, camera, device_info):
+ """Initialize Logi Circle camera."""
+ super().__init__()
+ self._camera = camera
+ self._name = self._camera.name
+ self._id = self._camera.mac_address
+ self._has_battery = self._camera.supports_feature('battery_level')
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._id
+
+ @property
+ def name(self):
+ """Return the name of this camera."""
+ return self._name
+
+ @property
+ def supported_features(self):
+ """Logi Circle camera's support turning on and off ("soft" switch)."""
+ return SUPPORT_ON_OFF
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ state = {
+ ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
+ 'battery_saving_mode': (
+ STATE_ON if self._camera.battery_saving else STATE_OFF),
+ 'ip_address': self._camera.ip_address,
+ 'microphone_gain': self._camera.microphone_gain
+ }
+
+ # Add battery attributes if camera is battery-powered
+ if self._has_battery:
+ state[ATTR_BATTERY_CHARGING] = self._camera.is_charging
+ state[ATTR_BATTERY_LEVEL] = self._camera.battery_level
+
+ return state
+
+ async def async_camera_image(self):
+ """Return a still image from the camera."""
+ return await self._camera.get_snapshot_image()
+
+ async def async_turn_off(self):
+ """Disable streaming mode for this camera."""
+ await self._camera.set_streaming_mode(False)
+
+ async def async_turn_on(self):
+ """Enable streaming mode for this camera."""
+ await self._camera.set_streaming_mode(True)
+
+ @property
+ def should_poll(self):
+ """Update the image periodically."""
+ return True
+
+ async def set_config(self, mode, value):
+ """Set an configuration property for the target camera."""
+ if mode == LED_MODE_KEY:
+ await self._camera.set_led(value)
+ if mode == PRIVACY_MODE_KEY:
+ await self._camera.set_privacy_mode(value)
+ if mode == BATTERY_SAVING_MODE_KEY:
+ await self._camera.set_battery_saving_mode(value)
+
+ async def download_livestream(self, filename, duration):
+ """Download a recording from the camera's livestream."""
+ # Render filename from template.
+ filename.hass = self.hass
+ stream_file = filename.async_render(
+ variables={ATTR_ENTITY_ID: self.entity_id})
+
+ # Respect configured path whitelist.
+ if not self.hass.config.is_allowed_path(stream_file):
+ _LOGGER.error(
+ "Can't write %s, no access to path!", stream_file)
+ return
+
+ asyncio.shield(self._camera.record_livestream(
+ stream_file, timedelta(seconds=duration)), loop=self.hass.loop)
+
+ async def livestream_snapshot(self, filename):
+ """Download a still frame from the camera's livestream."""
+ # Render filename from template.
+ filename.hass = self.hass
+ snapshot_file = filename.async_render(
+ variables={ATTR_ENTITY_ID: self.entity_id})
+
+ # Respect configured path whitelist.
+ if not self.hass.config.is_allowed_path(snapshot_file):
+ _LOGGER.error(
+ "Can't write %s, no access to path!", snapshot_file)
+ return
+
+ asyncio.shield(self._camera.get_livestream_image(
+ snapshot_file), loop=self.hass.loop)
+
+ async def async_update(self):
+ """Update camera entity and refresh attributes."""
+ await self._camera.update()
diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py
index ed7d58658ed..f1917aaf23e 100644
--- a/homeassistant/components/camera/mjpeg.py
+++ b/homeassistant/components/camera/mjpeg.py
@@ -47,7 +47,7 @@ def async_setup_platform(hass, config, async_add_entities,
"""Set up a MJPEG IP Camera."""
if discovery_info:
config = PLATFORM_SCHEMA(discovery_info)
- async_add_entities([MjpegCamera(hass, config)])
+ async_add_entities([MjpegCamera(config)])
def extract_image_from_mjpeg(stream):
@@ -65,7 +65,7 @@ def extract_image_from_mjpeg(stream):
class MjpegCamera(Camera):
"""An implementation of an IP camera that is reachable over a URL."""
- def __init__(self, hass, device_info):
+ def __init__(self, device_info):
"""Initialize a MJPEG camera."""
super().__init__()
self._name = device_info.get(CONF_NAME)
diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py
index cf5c969c650..13c1745615d 100644
--- a/homeassistant/components/camera/mqtt.py
+++ b/homeassistant/components/camera/mqtt.py
@@ -19,12 +19,14 @@ from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_TOPIC = 'topic'
+CONF_UNIQUE_ID = 'unique_id'
DEFAULT_NAME = 'MQTT Camera'
DEPENDENCIES = ['mqtt']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
@@ -38,6 +40,7 @@ def async_setup_platform(hass, config, async_add_entities,
async_add_entities([MqttCamera(
config.get(CONF_NAME),
+ config.get(CONF_UNIQUE_ID),
config.get(CONF_TOPIC)
)])
@@ -45,11 +48,12 @@ def async_setup_platform(hass, config, async_add_entities,
class MqttCamera(Camera):
"""representation of a MQTT camera."""
- def __init__(self, name, topic):
+ def __init__(self, name, unique_id, topic):
"""Initialize the MQTT Camera."""
super().__init__()
self._name = name
+ self._unique_id = unique_id
self._topic = topic
self._qos = 0
self._last_image = None
@@ -64,6 +68,11 @@ class MqttCamera(Camera):
"""Return the name of this camera."""
return self._name
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._unique_id
+
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe MQTT events."""
diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py
index e1d26371984..158123989c0 100644
--- a/homeassistant/components/camera/nest.py
+++ b/homeassistant/components/camera/nest.py
@@ -62,6 +62,23 @@ class NestCamera(Camera):
"""Return the name of the nest, if any."""
return self._name
+ @property
+ def unique_id(self):
+ """Return the serial number."""
+ return self.device.device_id
+
+ @property
+ def device_info(self):
+ """Return information about the device."""
+ return {
+ 'identifiers': {
+ (nest.DOMAIN, self.device.device_id)
+ },
+ 'name': self.device.name_long,
+ 'manufacturer': 'Nest Labs',
+ 'model': "Camera",
+ }
+
@property
def should_poll(self):
"""Nest camera should poll periodically."""
diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py
index f629b501819..d0cb6443fc7 100644
--- a/homeassistant/components/camera/ring.py
+++ b/homeassistant/components/camera/ring.py
@@ -39,9 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_entities,
- discovery_info=None):
+def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Ring Door Bell and StickUp Camera."""
ring = hass.data[DATA_RING]
@@ -67,14 +65,14 @@ def async_setup_platform(hass, config, async_add_entities,
''' following cameras: {}.'''.format(cameras)
_LOGGER.error(err_msg)
- hass.components.persistent_notification.async_create(
+ hass.components.persistent_notification.create(
'Error: {}
'
'You will need to restart hass after fixing.'
''.format(err_msg),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
- async_add_entities(cams, True)
+ add_entities(cams, True)
return True
diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml
index b977fcd5c52..1cae5baf1cf 100644
--- a/homeassistant/components/camera/services.yaml
+++ b/homeassistant/components/camera/services.yaml
@@ -63,3 +63,39 @@ onvif_ptz:
zoom:
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
example: "ZOOM_IN"
+
+logi_circle_set_config:
+ description: Set a configuration property.
+ fields:
+ entity_id:
+ description: Name(s) of entities to apply the operation mode to.
+ example: "camera.living_room_camera"
+ mode:
+ description: "Operation mode. Allowed values: BATTERY_SAVING, LED, PRIVACY_MODE."
+ example: "PRIVACY_MODE"
+ value:
+ description: "Operation value. Allowed values: true, false"
+ example: true
+
+logi_circle_livestream_snapshot:
+ description: Take a snapshot from the camera's livestream. Will wake the camera from sleep if required.
+ fields:
+ entity_id:
+ description: Name(s) of entities to create snapshots from.
+ example: "camera.living_room_camera"
+ filename:
+ description: Template of a Filename. Variable is entity_id.
+ example: "/tmp/snapshot_{{ entity_id }}.jpg"
+
+logi_circle_livestream_record:
+ description: Take a video recording from the camera's livestream.
+ fields:
+ entity_id:
+ description: Name(s) of entities to create recordings from.
+ example: "camera.living_room_camera"
+ filename:
+ description: Template of a Filename. Variable is entity_id.
+ example: "/tmp/snapshot_{{ entity_id }}.mp4"
+ duration:
+ description: Recording duration in seconds.
+ example: 60
diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py
index e48caa42a34..55d8d91d3ee 100644
--- a/homeassistant/components/camera/zoneminder.py
+++ b/homeassistant/components/camera/zoneminder.py
@@ -4,91 +4,47 @@ Support for ZoneMinder camera streaming.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.zoneminder/
"""
-import asyncio
import logging
-from urllib.parse import urljoin, urlencode
from homeassistant.const import CONF_NAME
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
-
-from homeassistant.components import zoneminder
+from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['zoneminder']
-DOMAIN = 'zoneminder'
-
-# From ZoneMinder's web/includes/config.php.in
-ZM_STATE_ALARM = "2"
-def _get_image_url(hass, monitor, mode):
- zm_data = hass.data[DOMAIN]
- query = urlencode({
- 'mode': mode,
- 'buffer': monitor['StreamReplayBuffer'],
- 'monitor': monitor['Id'],
- })
- url = '{zms_url}?{query}'.format(
- zms_url=urljoin(zm_data['server_origin'], zm_data['path_zms']),
- query=query,
- )
- _LOGGER.debug('Monitor %s %s URL (without auth): %s',
- monitor['Id'], mode, url)
-
- if not zm_data['username']:
- return url
-
- url += '&user={:s}'.format(zm_data['username'])
-
- if not zm_data['password']:
- return url
-
- return url + '&pass={:s}'.format(zm_data['password'])
-
-
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_entities,
- discovery_info=None):
+def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder cameras."""
- cameras = []
- monitors = zoneminder.get_state('api/monitors.json')
+ zm_client = hass.data[ZONEMINDER_DOMAIN]
+
+ monitors = zm_client.get_monitors()
if not monitors:
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
return
- for i in monitors['monitors']:
- monitor = i['Monitor']
-
- if monitor['Function'] == 'None':
- _LOGGER.info("Skipping camera %s", monitor['Id'])
- continue
-
- _LOGGER.info("Initializing camera %s", monitor['Id'])
-
- device_info = {
- CONF_NAME: monitor['Name'],
- CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
- CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
- }
- cameras.append(ZoneMinderCamera(hass, device_info, monitor))
-
- if not cameras:
- _LOGGER.warning("No active cameras found")
- return
-
- async_add_entities(cameras)
+ cameras = []
+ for monitor in monitors:
+ _LOGGER.info("Initializing camera %s", monitor.id)
+ cameras.append(ZoneMinderCamera(monitor))
+ add_entities(cameras)
class ZoneMinderCamera(MjpegCamera):
"""Representation of a ZoneMinder Monitor Stream."""
- def __init__(self, hass, device_info, monitor):
+ def __init__(self, monitor):
"""Initialize as a subclass of MjpegCamera."""
- super().__init__(hass, device_info)
- self._monitor_id = int(monitor['Id'])
+ device_info = {
+ CONF_NAME: monitor.name,
+ CONF_MJPEG_URL: monitor.mjpeg_image_url,
+ CONF_STILL_IMAGE_URL: monitor.still_image_url
+ }
+ super().__init__(device_info)
self._is_recording = None
+ self._monitor = monitor
@property
def should_poll(self):
@@ -97,17 +53,8 @@ class ZoneMinderCamera(MjpegCamera):
def update(self):
"""Update our recording state from the ZM API."""
- _LOGGER.debug("Updating camera state for monitor %i", self._monitor_id)
- status_response = zoneminder.get_state(
- 'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
- )
-
- if not status_response:
- _LOGGER.warning("Could not get status for monitor %i",
- self._monitor_id)
- return
-
- self._is_recording = status_response.get('status') == ZM_STATE_ALARM
+ _LOGGER.debug("Updating camera state for monitor %i", self._monitor.id)
+ self._is_recording = self._monitor.is_recording
@property
def is_recording(self):
diff --git a/homeassistant/components/cast/.translations/de.json b/homeassistant/components/cast/.translations/de.json
index a37dbd6f5b7..ac1ebbeb236 100644
--- a/homeassistant/components/cast/.translations/de.json
+++ b/homeassistant/components/cast/.translations/de.json
@@ -6,7 +6,7 @@
},
"step": {
"confirm": {
- "description": "M\u00f6chten Sie Google Cast einrichten?",
+ "description": "M\u00f6chtest du Google Cast einrichten?",
"title": "Google Cast"
}
},
diff --git a/homeassistant/components/cast/.translations/id.json b/homeassistant/components/cast/.translations/id.json
new file mode 100644
index 00000000000..86fb32c0844
--- /dev/null
+++ b/homeassistant/components/cast/.translations/id.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat Google Cast yang ditemukan pada jaringan.",
+ "single_instance_allowed": "Hanya satu konfigurasi Google Cast yang diperlukan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Apakah Anda ingin menyiapkan Google Cast?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/nn.json b/homeassistant/components/cast/.translations/nn.json
new file mode 100644
index 00000000000..7f550155658
--- /dev/null
+++ b/homeassistant/components/cast/.translations/nn.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Klar",
+ "single_instance_allowed": "Du treng berre \u00e5 sette opp \u00e9in Google Cast-konfigurasjon."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vil du sette opp Google Cast?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py
index 6885f24269a..53f5e704019 100644
--- a/homeassistant/components/cast/__init__.py
+++ b/homeassistant/components/cast/__init__.py
@@ -35,4 +35,5 @@ async def _async_has_devices(hass):
config_entry_flow.register_discovery_flow(
- DOMAIN, 'Google Cast', _async_has_devices)
+ DOMAIN, 'Google Cast', _async_has_devices,
+ config_entries.CONN_CLASS_LOCAL_PUSH)
diff --git a/homeassistant/components/climate/econet.py b/homeassistant/components/climate/econet.py
index 9350b8f853d..8be640c37e1 100644
--- a/homeassistant/components/climate/econet.py
+++ b/homeassistant/components/climate/econet.py
@@ -18,7 +18,7 @@ from homeassistant.const import (
TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pyeconet==0.0.5']
+REQUIREMENTS = ['pyeconet==0.0.6']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/climate/opentherm_gw.py b/homeassistant/components/climate/opentherm_gw.py
index c1f7afa61b0..00049d26b7f 100644
--- a/homeassistant/components/climate/opentherm_gw.py
+++ b/homeassistant/components/climate/opentherm_gw.py
@@ -72,8 +72,9 @@ class OpenThermGateway(ClimateDevice):
"""Receive and handle a new report from the Gateway."""
_LOGGER.debug("Received report: %s", status)
ch_active = status.get(self.pyotgw.DATA_SLAVE_CH_ACTIVE)
+ flame_on = status.get(self.pyotgw.DATA_SLAVE_FLAME_ON)
cooling_active = status.get(self.pyotgw.DATA_SLAVE_COOLING_ACTIVE)
- if ch_active:
+ if ch_active and flame_on:
self._current_operation = STATE_HEAT
elif cooling_active:
self._current_operation = STATE_COOL
diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py
index d8e6843bec8..3013a155380 100644
--- a/homeassistant/components/climate/wink.py
+++ b/homeassistant/components/climate/wink.py
@@ -118,7 +118,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
self.hass, self.target_temperature_low, self.temperature_unit,
PRECISION_TENTHS)
- if self.external_temperature:
+ if self.external_temperature is not None:
data[ATTR_EXTERNAL_TEMPERATURE] = show_temp(
self.hass, self.external_temperature, self.temperature_unit,
PRECISION_TENTHS)
@@ -126,16 +126,16 @@ class WinkThermostat(WinkDevice, ClimateDevice):
if self.smart_temperature:
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
- if self.occupied:
+ if self.occupied is not None:
data[ATTR_OCCUPIED] = self.occupied
- if self.eco_target:
+ if self.eco_target is not None:
data[ATTR_ECO_TARGET] = self.eco_target
- if self.heat_on:
+ if self.heat_on is not None:
data[ATTR_HEAT_ON] = self.heat_on
- if self.cool_on:
+ if self.cool_on is not None:
data[ATTR_COOL_ON] = self.cool_on
current_humidity = self.current_humidity
diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py
index f87f2e83f5d..77b5e111686 100644
--- a/homeassistant/components/climate/zwave.py
+++ b/homeassistant/components/climate/zwave.py
@@ -10,8 +10,8 @@ from homeassistant.components.climate import (
DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
-from homeassistant.components.zwave import ZWaveDeviceEntity
-from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
+from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
+ ZWaveDeviceEntity, async_setup_platform)
from homeassistant.const import (
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py
index 8c1a9751c19..33a939bf9d0 100644
--- a/homeassistant/components/cloud/__init__.py
+++ b/homeassistant/components/cloud/__init__.py
@@ -23,12 +23,17 @@ from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c
-from . import http_api, iot
+from . import http_api, iot, auth_api
from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.6.1']
+STORAGE_KEY = DOMAIN
+STORAGE_VERSION = 1
+STORAGE_ENABLE_ALEXA = 'alexa_enabled'
+STORAGE_ENABLE_GOOGLE = 'google_enabled'
_LOGGER = logging.getLogger(__name__)
+_UNDEF = object()
CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases'
@@ -39,6 +44,7 @@ CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
+CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
@@ -79,6 +85,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
+ vol.Optional(CONF_SUBSCRIPTION_INFO_URL): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}),
@@ -114,18 +121,21 @@ class Cloud:
def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None,
- relayer=None, google_actions_sync_url=None):
+ relayer=None, google_actions_sync_url=None,
+ subscription_info_url=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
self.alexa_config = alexa
self._google_actions = google_actions
self._gactions_config = None
+ self._prefs = None
self.jwt_keyset = None
self.id_token = None
self.access_token = None
self.refresh_token = None
self.iot = iot.CloudIoT(self)
+ self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id
@@ -133,6 +143,7 @@ class Cloud:
self.region = region
self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url
+ self.subscription_info_url = subscription_info_url
else:
info = SERVERS[mode]
@@ -142,6 +153,7 @@ class Cloud:
self.region = info['region']
self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url']
+ self.subscription_info_url = info['subscription_info_url']
@property
def is_logged_in(self):
@@ -188,6 +200,16 @@ class Cloud:
return self._gactions_config
+ @property
+ def alexa_enabled(self):
+ """Return if Alexa is enabled."""
+ return self._prefs[STORAGE_ENABLE_ALEXA]
+
+ @property
+ def google_enabled(self):
+ """Return if Google is enabled."""
+ return self._prefs[STORAGE_ENABLE_GOOGLE]
+
def path(self, *parts):
"""Get config path inside cloud dir.
@@ -195,6 +217,15 @@ class Cloud:
"""
return self.hass.config.path(CONFIG_DIR, *parts)
+ async def fetch_subscription_info(self):
+ """Fetch subscription info."""
+ await self.hass.async_add_executor_job(auth_api.check_token, self)
+ websession = self.hass.helpers.aiohttp_client.async_get_clientsession()
+ return await websession.get(
+ self.subscription_info_url, headers={
+ 'authorization': self.id_token
+ })
+
@asyncio.coroutine
def logout(self):
"""Close connection and remove all credentials."""
@@ -217,10 +248,23 @@ class Cloud:
'refresh_token': self.refresh_token,
}, indent=4))
- @asyncio.coroutine
- def async_start(self, _):
+ async def async_start(self, _):
"""Start the cloud component."""
- success = yield from self._fetch_jwt_keyset()
+ prefs = await self._store.async_load()
+ if prefs is None:
+ prefs = {}
+ if self.mode not in prefs:
+ # Default to True if already logged in to make this not a
+ # breaking change.
+ enabled = await self.hass.async_add_executor_job(
+ os.path.isfile, self.user_info_path)
+ prefs = {
+ STORAGE_ENABLE_ALEXA: enabled,
+ STORAGE_ENABLE_GOOGLE: enabled,
+ }
+ self._prefs = prefs
+
+ success = await self._fetch_jwt_keyset()
# Fetching keyset can fail if internet is not up yet.
if not success:
@@ -241,7 +285,7 @@ class Cloud:
with open(user_info, 'rt') as file:
return json.loads(file.read())
- info = yield from self.hass.async_add_job(load_config)
+ info = await self.hass.async_add_job(load_config)
if info is None:
return
@@ -260,6 +304,15 @@ class Cloud:
self.hass.add_job(self.iot.connect())
+ async def update_preferences(self, *, google_enabled=_UNDEF,
+ alexa_enabled=_UNDEF):
+ """Update user preferences."""
+ if google_enabled is not _UNDEF:
+ self._prefs[STORAGE_ENABLE_GOOGLE] = google_enabled
+ if alexa_enabled is not _UNDEF:
+ self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled
+ await self._store.async_save(self._prefs)
+
@asyncio.coroutine
def _fetch_jwt_keyset(self):
"""Fetch the JWT keyset for the Cognito instance."""
diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py
index 82128206d47..88fb88474a1 100644
--- a/homeassistant/components/cloud/const.py
+++ b/homeassistant/components/cloud/const.py
@@ -11,6 +11,8 @@ SERVERS = {
'relayer': 'wss://cloud.hass.io:8000/websocket',
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
'amazonaws.com/prod/smart_home_sync'),
+ 'subscription_info_url': ('https://stripe-api.nabucasa.com/payments/'
+ 'subscription_info')
}
}
diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index a4b3b59f333..c81ec38bace 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -6,22 +6,56 @@ import logging
import async_timeout
import voluptuous as vol
+from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import (
RequestDataValidator)
+from homeassistant.components import websocket_api
from . import auth_api
from .const import DOMAIN, REQUEST_TIMEOUT
+from .iot import STATE_DISCONNECTED
_LOGGER = logging.getLogger(__name__)
+WS_TYPE_STATUS = 'cloud/status'
+SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_STATUS,
+})
+
+
+WS_TYPE_UPDATE_PREFS = 'cloud/update_prefs'
+SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_UPDATE_PREFS,
+ vol.Optional('google_enabled'): bool,
+ vol.Optional('alexa_enabled'): bool,
+})
+
+
+WS_TYPE_SUBSCRIPTION = 'cloud/subscription'
+SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_SUBSCRIPTION,
+})
+
+
async def async_setup(hass):
"""Initialize the HTTP API."""
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_STATUS, websocket_cloud_status,
+ SCHEMA_WS_STATUS
+ )
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_SUBSCRIPTION, websocket_subscription,
+ SCHEMA_WS_SUBSCRIPTION
+ )
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_UPDATE_PREFS, websocket_update_prefs,
+ SCHEMA_WS_UPDATE_PREFS
+ )
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
- hass.http.register_view(CloudAccountView)
hass.http.register_view(CloudRegisterView)
hass.http.register_view(CloudResendConfirmView)
hass.http.register_view(CloudForgotPasswordView)
@@ -102,9 +136,7 @@ class CloudLoginView(HomeAssistantView):
data['password'])
hass.async_add_job(cloud.iot.connect)
- # Allow cloud to start connecting.
- await asyncio.sleep(0, loop=hass.loop)
- return self.json(_account_data(cloud))
+ return self.json({'success': True})
class CloudLogoutView(HomeAssistantView):
@@ -125,23 +157,6 @@ class CloudLogoutView(HomeAssistantView):
return self.json_message('ok')
-class CloudAccountView(HomeAssistantView):
- """View to retrieve account info."""
-
- url = '/api/cloud/account'
- name = 'api:cloud:account'
-
- async def get(self, request):
- """Get account info."""
- hass = request.app['hass']
- cloud = hass.data[DOMAIN]
-
- if not cloud.is_logged_in:
- return self.json_message('Not logged in', 400)
-
- return self.json(_account_data(cloud))
-
-
class CloudRegisterView(HomeAssistantView):
"""Register on the Home Assistant cloud."""
@@ -209,12 +224,73 @@ class CloudForgotPasswordView(HomeAssistantView):
return self.json_message('ok')
+@callback
+def websocket_cloud_status(hass, connection, msg):
+ """Handle request for account info.
+
+ Async friendly.
+ """
+ cloud = hass.data[DOMAIN]
+ connection.to_write.put_nowait(
+ websocket_api.result_message(msg['id'], _account_data(cloud)))
+
+
+@websocket_api.async_response
+async def websocket_subscription(hass, connection, msg):
+ """Handle request for account info."""
+ cloud = hass.data[DOMAIN]
+
+ if not cloud.is_logged_in:
+ connection.to_write.put_nowait(websocket_api.error_message(
+ msg['id'], 'not_logged_in',
+ 'You need to be logged in to the cloud.'))
+ return
+
+ with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
+ response = await cloud.fetch_subscription_info()
+
+ if response.status == 200:
+ connection.send_message_outside(websocket_api.result_message(
+ msg['id'], await response.json()))
+ else:
+ connection.send_message_outside(websocket_api.error_message(
+ msg['id'], 'request_failed', 'Failed to request subscription'))
+
+
+@websocket_api.async_response
+async def websocket_update_prefs(hass, connection, msg):
+ """Handle request for account info."""
+ cloud = hass.data[DOMAIN]
+
+ if not cloud.is_logged_in:
+ connection.to_write.put_nowait(websocket_api.error_message(
+ msg['id'], 'not_logged_in',
+ 'You need to be logged in to the cloud.'))
+ return
+
+ changes = dict(msg)
+ changes.pop('id')
+ changes.pop('type')
+ await cloud.update_preferences(**changes)
+
+ connection.send_message_outside(websocket_api.result_message(
+ msg['id'], {'success': True}))
+
+
def _account_data(cloud):
"""Generate the auth data JSON response."""
+ if not cloud.is_logged_in:
+ return {
+ 'logged_in': False,
+ 'cloud': STATE_DISCONNECTED,
+ }
+
claims = cloud.claims
return {
+ 'logged_in': True,
'email': claims['email'],
- 'sub_exp': claims['custom:sub-exp'],
'cloud': cloud.iot.state,
+ 'google_enabled': cloud.google_enabled,
+ 'alexa_enabled': cloud.alexa_enabled,
}
diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py
index f4ce7bb3d1a..fd525ed33a8 100644
--- a/homeassistant/components/cloud/iot.py
+++ b/homeassistant/components/cloud/iot.py
@@ -227,6 +227,9 @@ def async_handle_message(hass, cloud, handler_name, payload):
@asyncio.coroutine
def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa."""
+ if not cloud.alexa_enabled:
+ return alexa.turned_off_response(payload)
+
result = yield from alexa.async_handle_message(
hass, cloud.alexa_config, payload)
return result
@@ -236,6 +239,9 @@ def async_handle_alexa(hass, cloud, payload):
@asyncio.coroutine
def async_handle_google_actions(hass, cloud, payload):
"""Handle an incoming IoT message for Google Actions."""
+ if not cloud.google_enabled:
+ return ga.turned_off_response(payload)
+
result = yield from ga.async_handle_message(
hass, cloud.gactions_config, payload)
return result
diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py
index 581d8fc3f7b..df0e2f13ac1 100644
--- a/homeassistant/components/config/__init__.py
+++ b/homeassistant/components/config/__init__.py
@@ -13,8 +13,17 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
-SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
- 'entity_registry', 'config_entries')
+SECTIONS = (
+ 'automation',
+ 'config_entries',
+ 'core',
+ 'customize',
+ 'device_registry',
+ 'entity_registry',
+ 'group',
+ 'hassbian',
+ 'script',
+)
ON_DEMAND = ('zwave',)
diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
index e0c0e7daaf4..73b2767be4b 100644
--- a/homeassistant/components/config/config_entries.py
+++ b/homeassistant/components/config/config_entries.py
@@ -54,6 +54,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
'title': entry.title,
'source': entry.source,
'state': entry.state,
+ 'connection_class': entry.connection_class,
} for entry in hass.config_entries.async_entries()])
diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py
new file mode 100644
index 00000000000..88aa5727a97
--- /dev/null
+++ b/homeassistant/components/config/device_registry.py
@@ -0,0 +1,47 @@
+"""HTTP views to interact with the device registry."""
+import voluptuous as vol
+
+from homeassistant.core import callback
+from homeassistant.helpers.device_registry import async_get_registry
+from homeassistant.components import websocket_api
+
+DEPENDENCIES = ['websocket_api']
+
+WS_TYPE_LIST = 'config/device_registry/list'
+SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_LIST,
+})
+
+
+async def async_setup(hass):
+ """Enable the Entity Registry views."""
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_LIST, websocket_list_devices,
+ SCHEMA_WS_LIST
+ )
+ return True
+
+
+@callback
+def websocket_list_devices(hass, connection, msg):
+ """Handle list devices command.
+
+ Async friendly.
+ """
+ async def retrieve_entities():
+ """Get devices from registry."""
+ registry = await async_get_registry(hass)
+ connection.send_message_outside(websocket_api.result_message(
+ msg['id'], [{
+ 'config_entries': list(entry.config_entries),
+ 'connections': list(entry.connections),
+ 'manufacturer': entry.manufacturer,
+ 'model': entry.model,
+ 'name': entry.name,
+ 'sw_version': entry.sw_version,
+ 'id': entry.id,
+ 'hub_device_id': entry.hub_device_id,
+ } for entry in registry.devices.values()]
+ ))
+
+ hass.async_add_job(retrieve_entities())
diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py
index 2fac420c39c..0f9abf167e5 100644
--- a/homeassistant/components/config/entity_registry.py
+++ b/homeassistant/components/config/entity_registry.py
@@ -8,6 +8,11 @@ from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['websocket_api']
+WS_TYPE_LIST = 'config/entity_registry/list'
+SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_LIST,
+})
+
WS_TYPE_GET = 'config/entity_registry/get'
SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET,
@@ -26,6 +31,10 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
async def async_setup(hass):
"""Enable the Entity Registry views."""
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_LIST, websocket_list_entities,
+ SCHEMA_WS_LIST
+ )
hass.components.websocket_api.async_register_command(
WS_TYPE_GET, websocket_get_entity,
SCHEMA_WS_GET
@@ -37,6 +46,29 @@ async def async_setup(hass):
return True
+@callback
+def websocket_list_entities(hass, connection, msg):
+ """Handle list registry entries command.
+
+ Async friendly.
+ """
+ async def retrieve_entities():
+ """Get entities from registry."""
+ registry = await async_get_registry(hass)
+ connection.send_message_outside(websocket_api.result_message(
+ msg['id'], [{
+ 'config_entry_id': entry.config_entry_id,
+ 'device_id': entry.device_id,
+ 'disabled_by': entry.disabled_by,
+ 'entity_id': entry.entity_id,
+ 'name': entry.name,
+ 'platform': entry.platform,
+ } for entry in registry.entities.values()]
+ ))
+
+ hass.async_add_job(retrieve_entities())
+
+
@callback
def websocket_get_entity(hass, connection, msg):
"""Handle get entity registry entry command.
diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py
index 05c5e46e44e..e9a33c27d34 100644
--- a/homeassistant/components/cover/__init__.py
+++ b/homeassistant/components/cover/__init__.py
@@ -35,8 +35,9 @@ ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEVICE_CLASSES = [
- 'window', # Window control
+ 'damper',
'garage', # Garage door control
+ 'window', # Window control
]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
@@ -140,7 +141,7 @@ def stop_cover_tilt(hass, entity_id=None):
async def async_setup(hass, config):
"""Track states and offer events for covers."""
- component = EntityComponent(
+ component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
await component.async_setup(config)
@@ -195,6 +196,16 @@ async def async_setup(hass, config):
return True
+async def async_setup_entry(hass, entry):
+ """Set up a config entry."""
+ return await hass.data[DOMAIN].async_setup_entry(entry)
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ return await hass.data[DOMAIN].async_unload_entry(entry)
+
+
class CoverDevice(Entity):
"""Representation a cover."""
diff --git a/homeassistant/components/cover/deconz.py b/homeassistant/components/cover/deconz.py
new file mode 100644
index 00000000000..9fe65596336
--- /dev/null
+++ b/homeassistant/components/cover/deconz.py
@@ -0,0 +1,146 @@
+"""
+Support for deCONZ covers.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/cover.deconz/
+"""
+from homeassistant.components.deconz.const import (
+ COVER_TYPES, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB,
+ DECONZ_DOMAIN)
+from homeassistant.components.cover import (
+ ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN,
+ SUPPORT_SET_POSITION)
+from homeassistant.core import callback
+from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+DEPENDENCIES = ['deconz']
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Unsupported way of setting up deCONZ covers."""
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up covers for deCONZ component.
+
+ Covers are based on same device class as lights in deCONZ.
+ """
+ @callback
+ def async_add_cover(lights):
+ """Add cover from deCONZ."""
+ entities = []
+ for light in lights:
+ if light.type in COVER_TYPES:
+ entities.append(DeconzCover(light))
+ async_add_entities(entities, True)
+
+ hass.data[DATA_DECONZ_UNSUB].append(
+ async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover))
+
+ async_add_cover(hass.data[DATA_DECONZ].lights.values())
+
+
+class DeconzCover(CoverDevice):
+ """Representation of a deCONZ cover."""
+
+ def __init__(self, cover):
+ """Set up cover and add update callback to get data from websocket."""
+ self._cover = cover
+ self._features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
+
+ async def async_added_to_hass(self):
+ """Subscribe to covers events."""
+ self._cover.register_async_callback(self.async_update_callback)
+ self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._cover.deconz_id
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Disconnect cover object when removed."""
+ self._cover.remove_callback(self.async_update_callback)
+ self._cover = None
+
+ @callback
+ def async_update_callback(self, reason):
+ """Update the cover's state."""
+ self.async_schedule_update_ha_state()
+
+ @property
+ def current_cover_position(self):
+ """Return the current position of the cover."""
+ if self.is_closed:
+ return 0
+ return int(self._cover.brightness / 255 * 100)
+
+ @property
+ def is_closed(self):
+ """Return if the cover is closed."""
+ return not self._cover.state
+
+ @property
+ def name(self):
+ """Return the name of the cover."""
+ return self._cover.name
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this cover."""
+ return self._cover.uniqueid
+
+ @property
+ def device_class(self):
+ """Return the class of the cover."""
+ return 'damper'
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return self._features
+
+ @property
+ def available(self):
+ """Return True if light is available."""
+ return self._cover.reachable
+
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
+
+ async def async_set_cover_position(self, **kwargs):
+ """Move the cover to a specific position."""
+ position = kwargs[ATTR_POSITION]
+ data = {'on': False}
+ if position > 0:
+ data['on'] = True
+ data['bri'] = int(position / 100 * 255)
+ await self._cover.async_set_state(data)
+
+ async def async_open_cover(self, **kwargs):
+ """Open cover."""
+ data = {ATTR_POSITION: 100}
+ await self.async_set_cover_position(**data)
+
+ async def async_close_cover(self, **kwargs):
+ """Close cover."""
+ data = {ATTR_POSITION: 0}
+ await self.async_set_cover_position(**data)
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ if (self._cover.uniqueid is None or
+ self._cover.uniqueid.count(':') != 7):
+ return None
+ serial = self._cover.uniqueid.split('-', 1)[0]
+ bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
+ return {
+ 'connections': {(CONNECTION_ZIGBEE, serial)},
+ 'identifiers': {(DECONZ_DOMAIN, serial)},
+ 'manufacturer': self._cover.manufacturer,
+ 'model': self._cover.modelid,
+ 'name': self._cover.name,
+ 'sw_version': self._cover.swversion,
+ 'via_hub': (DECONZ_DOMAIN, bridgeid),
+ }
diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py
index 428c1f326e4..4ead61e6b7a 100644
--- a/homeassistant/components/cover/isy994.py
+++ b/homeassistant/components/cover/isy994.py
@@ -44,6 +44,8 @@ class ISYCoverDevice(ISYDevice, CoverDevice):
@property
def current_cover_position(self) -> int:
"""Return the current cover position."""
+ if self.is_unknown() or self.value is None:
+ return None
return sorted((0, self.value, 100))[1]
@property
diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py
index 413794505db..78b6f891f11 100644
--- a/homeassistant/components/cover/myq.py
+++ b/homeassistant/components/cover/myq.py
@@ -11,8 +11,8 @@ import voluptuous as vol
from homeassistant.components.cover import (
CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN)
from homeassistant.const import (
- CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING,
- STATE_OPENING)
+ CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_OPEN,
+ STATE_CLOSING, STATE_OPENING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pymyq==0.0.15']
@@ -23,6 +23,7 @@ DEFAULT_NAME = 'myq'
MYQ_TO_HASS = {
'closed': STATE_CLOSED,
+ 'open': STATE_OPEN,
'closing': STATE_CLOSING,
'opening': STATE_OPENING
}
diff --git a/homeassistant/components/cover/tuya.py b/homeassistant/components/cover/tuya.py
index 6ab8581602f..a3a3db972e9 100644
--- a/homeassistant/components/cover/tuya.py
+++ b/homeassistant/components/cover/tuya.py
@@ -45,6 +45,11 @@ class TuyaCover(TuyaDevice, CoverDevice):
@property
def is_closed(self):
"""Return if the cover is closed or not."""
+ state = self.tuya.state()
+ if state == 1:
+ return False
+ if state == 2:
+ return True
return None
def open_cover(self, **kwargs):
diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py
index 8c8c88ecb87..258087702e0 100644
--- a/homeassistant/components/cover/zwave.py
+++ b/homeassistant/components/cover/zwave.py
@@ -9,10 +9,9 @@ https://home-assistant.io/components/cover.zwave/
import logging
from homeassistant.components.cover import (
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION)
-from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
-from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
-from homeassistant.components.zwave import workaround
+from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
+ ZWaveDeviceEntity, async_setup_platform, workaround)
from homeassistant.components.cover import CoverDevice
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json
index 0a9e6fdee3f..10eb9f5bc73 100644
--- a/homeassistant/components/deconz/.translations/ca.json
+++ b/homeassistant/components/deconz/.translations/ca.json
@@ -28,6 +28,6 @@
"title": "Opcions de configuraci\u00f3 addicionals per deCONZ"
}
},
- "title": "deCONZ"
+ "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json
index 51b496906a2..645daa56f6b 100644
--- a/homeassistant/components/deconz/.translations/de.json
+++ b/homeassistant/components/deconz/.translations/de.json
@@ -14,10 +14,10 @@
"host": "Host",
"port": "Port (Standartwert : '80')"
},
- "title": "Definieren Sie den deCONZ-Gateway"
+ "title": "Definiere das deCONZ-Gateway"
},
"link": {
- "description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"",
+ "description": "Entsperre dein deCONZ-Gateway, um dich bei Home Assistant zu registrieren. \n\n 1. Gehe zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"",
"title": "Mit deCONZ verbinden"
},
"options": {
diff --git a/homeassistant/components/deconz/.translations/he.json b/homeassistant/components/deconz/.translations/he.json
index b4b3d54e075..89a2d69950e 100644
--- a/homeassistant/components/deconz/.translations/he.json
+++ b/homeassistant/components/deconz/.translations/he.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
- "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ"
+ "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ",
+ "one_instance_only": "\u05d4\u05e8\u05db\u05d9\u05d1 \u05ea\u05d5\u05de\u05da \u05e8\u05e7 \u05d0\u05d7\u05d3 deCONZ \u05dc\u05de\u05e9\u05dc"
},
"error": {
"no_key": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05e7\u05d1\u05dc \u05de\u05e4\u05ea\u05d7 API"
diff --git a/homeassistant/components/deconz/.translations/id.json b/homeassistant/components/deconz/.translations/id.json
new file mode 100644
index 00000000000..7d0b3163a40
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/id.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge sudah dikonfigurasi",
+ "no_bridges": "deCONZ bridges tidak ditemukan",
+ "one_instance_only": "Komponen hanya mendukung satu instance deCONZ"
+ },
+ "error": {
+ "no_key": "Tidak bisa mendapatkan kunci API"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Host",
+ "port": "Port (nilai default: '80')"
+ },
+ "title": "Tentukan deCONZ gateway"
+ },
+ "link": {
+ "description": "Buka gerbang deCONZ Anda untuk mendaftar dengan Home Assistant. \n\n 1. Pergi ke pengaturan sistem deCONZ \n 2. Tekan tombol \"Buka Kunci Gateway\"",
+ "title": "Tautan dengan deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Izinkan mengimpor sensor virtual",
+ "allow_deconz_groups": "Izinkan mengimpor grup deCONZ"
+ },
+ "title": "Opsi konfigurasi tambahan untuk deCONZ"
+ }
+ },
+ "title": "deCONZ Zigbee gateway"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json
index a584a1db9b5..a501951540b 100644
--- a/homeassistant/components/deconz/.translations/ko.json
+++ b/homeassistant/components/deconz/.translations/ko.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
- "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4 \ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4"
+ "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4"
},
"error": {
"no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
diff --git a/homeassistant/components/deconz/.translations/nn.json b/homeassistant/components/deconz/.translations/nn.json
new file mode 100644
index 00000000000..4bdc4b4c1be
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/nn.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Brua er allereie konfigurert",
+ "no_bridges": "Oppdaga ingen deCONZ-bruer",
+ "one_instance_only": "Komponenten st\u00f8ttar berre \u00e9in deCONZ-instans"
+ },
+ "error": {
+ "no_key": "Kunne ikkje f\u00e5 ein API-n\u00f8kkel"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Vert",
+ "port": "Port (standardverdi: '80')"
+ },
+ "title": "Definer deCONZ-gateway"
+ },
+ "link": {
+ "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere den med Home Assistant.\n\n1. G\u00e5 til systeminnstillingane til deCONZ\n2. Trykk p\u00e5 \"L\u00e5s opp gateway\"-knappen",
+ "title": "Link med deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Tillat importering av virtuelle sensorar",
+ "allow_deconz_groups": "Tillat importering av deCONZ-grupper"
+ },
+ "title": "Ekstra konfigurasjonsalternativ for deCONZ"
+ }
+ },
+ "title": "deCONZ Zigbee gateway"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py
index 6ed0a6e2c11..56b03c89a37 100644
--- a/homeassistant/components/deconz/__init__.py
+++ b/homeassistant/components/deconz/__init__.py
@@ -26,6 +26,9 @@ from .const import (
REQUIREMENTS = ['pydeconz==47']
+SUPPORTED_PLATFORMS = ['binary_sensor', 'cover',
+ 'light', 'scene', 'sensor', 'switch']
+
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_API_KEY): cv.string,
@@ -104,7 +107,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DATA_DECONZ_EVENT] = []
hass.data[DATA_DECONZ_UNSUB] = []
- for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
+ for component in SUPPORTED_PLATFORMS:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
config_entry, component))
@@ -127,7 +130,7 @@ async def async_setup_entry(hass, config_entry):
device_registry = await \
hass.helpers.device_registry.async_get_registry()
device_registry.async_get_or_create(
- config_entry=config_entry.entry_id,
+ config_entry_id=config_entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, deconz.config.mac)},
identifiers={(DOMAIN, deconz.config.bridgeid)},
manufacturer='Dresden Elektronik', model=deconz.config.modelid,
@@ -228,7 +231,7 @@ async def async_unload_entry(hass, config_entry):
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
deconz.close()
- for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
+ for component in SUPPORTED_PLATFORMS:
await hass.config_entries.async_forward_entry_unload(
config_entry, component)
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index fb2eb54232a..65fcf51b930 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -2,7 +2,7 @@
import voluptuous as vol
-from homeassistant import config_entries, data_entry_flow
+from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.helpers import aiohttp_client
@@ -23,10 +23,11 @@ def configured_hosts(hass):
@config_entries.HANDLERS.register(DOMAIN)
-class DeconzFlowHandler(data_entry_flow.FlowHandler):
+class DeconzFlowHandler(config_entries.ConfigFlow):
"""Handle a deCONZ config flow."""
VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize the deCONZ config flow."""
diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py
index e629d57f201..617d231f92e 100644
--- a/homeassistant/components/deconz/const.py
+++ b/homeassistant/components/deconz/const.py
@@ -16,6 +16,8 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
ATTR_DARK = 'dark'
ATTR_ON = 'on'
+COVER_TYPES = ["Level controllable output"]
+
POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"]
SIRENS = ["Warning device"]
SWITCH_TYPES = POWER_PLUGS + SIRENS
diff --git a/homeassistant/components/device_tracker/bbox.py b/homeassistant/components/device_tracker/bbox.py
index 6d870364dcb..297e98e548a 100644
--- a/homeassistant/components/device_tracker/bbox.py
+++ b/homeassistant/components/device_tracker/bbox.py
@@ -5,19 +5,30 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.bbox/
"""
from collections import namedtuple
-import logging
from datetime import timedelta
+import logging
-import homeassistant.util.dt as dt_util
-from homeassistant.components.device_tracker import DOMAIN, DeviceScanner
+import voluptuous as vol
+
+from homeassistant.components.device_tracker import (
+ DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
+from homeassistant.const import CONF_HOST
+import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
+import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pybbox==0.0.5-alpha']
_LOGGER = logging.getLogger(__name__)
+DEFAULT_HOST = '192.168.1.254'
+
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
+})
+
def get_scanner(hass, config):
"""Validate the configuration and return a Bbox scanner."""
@@ -33,6 +44,9 @@ class BboxDeviceScanner(DeviceScanner):
"""This class scans for devices connected to the bbox."""
def __init__(self, config):
+ """Get host from config."""
+ self.host = config[CONF_HOST]
+
"""Initialize the scanner."""
self.last_results = [] # type: List[Device]
@@ -64,7 +78,7 @@ class BboxDeviceScanner(DeviceScanner):
import pybbox
- box = pybbox.Bbox()
+ box = pybbox.Bbox(ip=self.host)
result = box.get_all_connected_devices()
now = dt_util.now()
diff --git a/homeassistant/components/device_tracker/huawei_lte.py b/homeassistant/components/device_tracker/huawei_lte.py
new file mode 100644
index 00000000000..4b4eb3f001a
--- /dev/null
+++ b/homeassistant/components/device_tracker/huawei_lte.py
@@ -0,0 +1,65 @@
+"""
+Support for Huawei LTE routers.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/device_tracker.huawei_lte/
+"""
+from typing import Any, Dict, List, Optional
+
+import attr
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.device_tracker import (
+ PLATFORM_SCHEMA, DeviceScanner,
+)
+from homeassistant.const import CONF_URL
+from ..huawei_lte import DATA_KEY, RouterData
+
+
+DEPENDENCIES = ['huawei_lte']
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_URL): cv.url,
+})
+
+
+def get_scanner(hass, config):
+ """Get a Huawei LTE router scanner."""
+ data = hass.data[DATA_KEY].get_data(config)
+ return HuaweiLteScanner(data)
+
+
+@attr.s
+class HuaweiLteScanner(DeviceScanner):
+ """Huawei LTE router scanner."""
+
+ data = attr.ib(type=RouterData)
+
+ _hosts = attr.ib(init=False, factory=dict)
+
+ def scan_devices(self) -> List[str]:
+ """Scan for devices."""
+ self.data.update()
+ self._hosts = {
+ x["MacAddress"]: x
+ for x in self.data["wlan_host_list.Hosts.Host"]
+ if x.get("MacAddress")
+ }
+ return list(self._hosts)
+
+ def get_device_name(self, device: str) -> Optional[str]:
+ """Get name for a device."""
+ host = self._hosts.get(device)
+ return host.get("HostName") or None if host else None
+
+ def get_extra_attributes(self, device: str) -> Dict[str, Any]:
+ """
+ Get extra attributes of a device.
+
+ Some known extra attributes that may be returned in the dict
+ include MacAddress (MAC address), ID (client ID), IpAddress
+ (IP address), AssociatedSsid (associated SSID), AssociatedTime
+ (associated time in seconds), and HostName (host name).
+ """
+ return self._hosts.get(device) or {}
diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py
index dfc66a412c3..320468159e0 100644
--- a/homeassistant/components/device_tracker/mikrotik.py
+++ b/homeassistant/components/device_tracker/mikrotik.py
@@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
-REQUIREMENTS = ['librouteros==2.1.0']
+REQUIREMENTS = ['librouteros==2.1.1']
MTK_DEFAULT_API_PORT = '8728'
diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py
index 7784f3771de..91f9dea704b 100644
--- a/homeassistant/components/discovery.py
+++ b/homeassistant/components/discovery.py
@@ -48,6 +48,7 @@ CONFIG_ENTRY_HANDLERS = {
SERVICE_DECONZ: 'deconz',
'google_cast': 'cast',
SERVICE_HUE: 'hue',
+ SERVICE_IKEA_TRADFRI: 'tradfri',
'sonos': 'sonos',
}
@@ -55,7 +56,6 @@ SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None),
SERVICE_WEMO: ('wemo', None),
- SERVICE_IKEA_TRADFRI: ('tradfri', None),
SERVICE_HASSIO: ('hassio', None),
SERVICE_AXIS: ('axis', None),
SERVICE_APPLE_TV: ('apple_tv', None),
diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py
index 2e51b048d15..8cbe95ee685 100644
--- a/homeassistant/components/ecovacs.py
+++ b/homeassistant/components/ecovacs.py
@@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \
EVENT_HOMEASSISTANT_STOP
-REQUIREMENTS = ['sucks==0.9.1']
+REQUIREMENTS = ['sucks==0.9.3']
_LOGGER = logging.getLogger(__name__)
@@ -59,8 +59,9 @@ def setup(hass, config):
_LOGGER.debug("Ecobot devices: %s", devices)
for device in devices:
- _LOGGER.info("Discovered Ecovacs device on account: %s",
- device['nick'])
+ _LOGGER.info(
+ "Discovered Ecovacs device on account: %s with nickname %s",
+ device['did'], device['nick'])
vacbot = VacBot(ecovacs_api.uid,
ecovacs_api.REALM,
ecovacs_api.resource,
@@ -74,7 +75,7 @@ def setup(hass, config):
"""Shut down open connections to Ecovacs XMPP server."""
for device in hass.data[ECOVACS_DEVICES]:
_LOGGER.info("Shutting down connection to Ecovacs device %s",
- device.vacuum['nick'])
+ device.vacuum['did'])
device.disconnect()
# Listen for HA stop to disconnect.
diff --git a/homeassistant/components/edp_redy.py b/homeassistant/components/edp_redy.py
new file mode 100644
index 00000000000..caf4ad41d99
--- /dev/null
+++ b/homeassistant/components/edp_redy.py
@@ -0,0 +1,135 @@
+"""
+Support for EDP re:dy.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/edp_redy/
+"""
+
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+
+from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
+ EVENT_HOMEASSISTANT_START)
+from homeassistant.core import callback
+from homeassistant.helpers import discovery, dispatcher, aiohttp_client
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import async_track_point_in_time
+from homeassistant.util import dt as dt_util
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'edp_redy'
+EDP_REDY = 'edp_redy'
+DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN)
+UPDATE_INTERVAL = 30
+
+REQUIREMENTS = ['edp_redy==0.0.2']
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+
+async def async_setup(hass, config):
+ """Set up the EDP re:dy component."""
+ from edp_redy import EdpRedySession
+
+ session = EdpRedySession(config[DOMAIN][CONF_USERNAME],
+ config[DOMAIN][CONF_PASSWORD],
+ aiohttp_client.async_get_clientsession(hass),
+ hass.loop)
+ hass.data[EDP_REDY] = session
+ platform_loaded = False
+
+ async def async_update_and_sched(time):
+ update_success = await session.async_update()
+
+ if update_success:
+ nonlocal platform_loaded
+ # pylint: disable=used-before-assignment
+ if not platform_loaded:
+ for component in ['sensor', 'switch']:
+ await discovery.async_load_platform(hass, component,
+ DOMAIN, {}, config)
+ platform_loaded = True
+
+ dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC)
+
+ # schedule next update
+ async_track_point_in_time(hass, async_update_and_sched,
+ time + timedelta(seconds=UPDATE_INTERVAL))
+
+ async def start_component(event):
+ _LOGGER.debug("Starting updates")
+ await async_update_and_sched(dt_util.utcnow())
+
+ # only start fetching data after HA boots to prevent delaying the boot
+ # process
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component)
+
+ return True
+
+
+class EdpRedyDevice(Entity):
+ """Representation a base re:dy device."""
+
+ def __init__(self, session, device_id, name):
+ """Initialize the device."""
+ self._session = session
+ self._state = None
+ self._is_available = True
+ self._device_state_attributes = {}
+ self._id = device_id
+ self._unique_id = device_id
+ self._name = name if name else device_id
+
+ async def async_added_to_hass(self):
+ """Subscribe to the data updates topic."""
+ dispatcher.async_dispatcher_connect(
+ self.hass, DATA_UPDATE_TOPIC, self._data_updated)
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ return self._unique_id
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._is_available
+
+ @property
+ def should_poll(self):
+ """Return the polling state. No polling needed."""
+ return False
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._device_state_attributes
+
+ @callback
+ def _data_updated(self):
+ """Update state, trigger updates."""
+ self.async_schedule_update_ha_state(True)
+
+ def _parse_data(self, data):
+ """Parse data received from the server."""
+ if "OutOfOrder" in data:
+ try:
+ self._is_available = not data['OutOfOrder']
+ except ValueError:
+ _LOGGER.error(
+ "Could not parse OutOfOrder for %s", self._id)
+ self._is_available = False
diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/fan/zha.py
index 2612c065393..b5615f18d73 100644
--- a/homeassistant/components/fan/zha.py
+++ b/homeassistant/components/fan/zha.py
@@ -4,7 +4,6 @@ Fans on Zigbee Home Automation networks.
For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/fan.zha/
"""
-import asyncio
import logging
from homeassistant.components import zha
from homeassistant.components.fan import (
@@ -38,9 +37,8 @@ VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)}
SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
-@asyncio.coroutine
-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):
"""Set up the Zigbee Home Automation fans."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None:
@@ -76,32 +74,36 @@ class ZhaFan(zha.Entity, FanEntity):
return False
return self._state != SPEED_OFF
- @asyncio.coroutine
- def async_turn_on(self, speed: str = None, **kwargs) -> None:
+ async def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn the entity on."""
if speed is None:
speed = SPEED_MEDIUM
- yield from self.async_set_speed(speed)
+ await self.async_set_speed(speed)
- @asyncio.coroutine
- def async_turn_off(self, **kwargs) -> None:
+ async def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
- yield from self.async_set_speed(SPEED_OFF)
+ await self.async_set_speed(SPEED_OFF)
- @asyncio.coroutine
- def async_set_speed(self, speed: str) -> None:
+ async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
- yield from self._endpoint.fan.write_attributes({
- 'fan_mode': SPEED_TO_VALUE[speed]})
+ from zigpy.exceptions import DeliveryError
+ try:
+ await self._endpoint.fan.write_attributes(
+ {'fan_mode': SPEED_TO_VALUE[speed]}
+ )
+ except DeliveryError as ex:
+ _LOGGER.error("%s: Could not set speed: %s", self.entity_id, ex)
+ return
self._state = speed
self.async_schedule_update_ha_state()
- @asyncio.coroutine
- def async_update(self):
+ async def async_update(self):
"""Retrieve latest state."""
- result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode'])
+ result = await zha.safe_read(self._endpoint.fan, ['fan_mode'],
+ allow_cache=False,
+ only_cache=(not self._initialized))
new_value = result.get('fan_mode', None)
self._state = VALUE_TO_SPEED.get(new_value, None)
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 6dd4be7ecec..9bd13f316b6 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
from homeassistant.util.yaml import load_yaml
-REQUIREMENTS = ['home-assistant-frontend==20180916.0']
+REQUIREMENTS = ['home-assistant-frontend==20180927.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py
index 67ed9520fa4..66753aad221 100644
--- a/homeassistant/components/geo_location/__init__.py
+++ b/homeassistant/components/geo_location/__init__.py
@@ -14,6 +14,7 @@ from typing import Optional
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py
index 8e8d8211086..ddec369e696 100644
--- a/homeassistant/components/geo_location/demo.py
+++ b/homeassistant/components/geo_location/demo.py
@@ -8,7 +8,6 @@ import logging
import random
from datetime import timedelta
from math import pi, cos, sin, radians
-
from typing import Optional
from homeassistant.components.geo_location import GeoLocationEvent
diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py
new file mode 100644
index 00000000000..bb17fb2450e
--- /dev/null
+++ b/homeassistant/components/geo_location/geo_json_events.py
@@ -0,0 +1,196 @@
+"""
+Generic GeoJSON events platform.
+
+Retrieves current events (typically incidents or alerts) in GeoJSON format, and
+displays information on events filtered by distance to the HA instance's
+location.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/geo_location/geo_json_events/
+"""
+import logging
+from datetime import timedelta
+from typing import Optional
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.geo_location import GeoLocationEvent
+from homeassistant.const import CONF_RADIUS, CONF_URL, CONF_SCAN_INTERVAL, \
+ EVENT_HOMEASSISTANT_START
+from homeassistant.components.geo_location import PLATFORM_SCHEMA
+from homeassistant.helpers.event import track_time_interval
+
+REQUIREMENTS = ['geojson_client==0.1']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_EXTERNAL_ID = 'external_id'
+
+DEFAULT_RADIUS_IN_KM = 20.0
+DEFAULT_UNIT_OF_MEASUREMENT = "km"
+
+SCAN_INTERVAL = timedelta(minutes=5)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_URL): cv.string,
+ vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM):
+ vol.Coerce(float),
+})
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the GeoJSON Events platform."""
+ url = config[CONF_URL]
+ scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
+ radius_in_km = config[CONF_RADIUS]
+ # Initialize the entity manager.
+ GeoJsonFeedManager(hass, add_entities, scan_interval, url, radius_in_km)
+
+
+class GeoJsonFeedManager:
+ """Feed Manager for GeoJSON feeds."""
+
+ def __init__(self, hass, add_entities, scan_interval, url, radius_in_km):
+ """Initialize the GeoJSON Feed Manager."""
+ from geojson_client.generic_feed import GenericFeed
+ self._hass = hass
+ self._feed = GenericFeed((hass.config.latitude, hass.config.longitude),
+ filter_radius=radius_in_km, url=url)
+ self._add_entities = add_entities
+ self._scan_interval = scan_interval
+ self._feed_entries = []
+ self._managed_entities = []
+ hass.bus.listen_once(
+ EVENT_HOMEASSISTANT_START, lambda _: self._update())
+ self._init_regular_updates()
+
+ def _init_regular_updates(self):
+ """Schedule regular updates at the specified interval."""
+ track_time_interval(self._hass, lambda now: self._update(),
+ self._scan_interval)
+
+ def _update(self):
+ """Update the feed and then update connected entities."""
+ import geojson_client
+ status, feed_entries = self._feed.update()
+ if status == geojson_client.UPDATE_OK:
+ _LOGGER.debug("Data retrieved %s", feed_entries)
+ # Keep a copy of all feed entries for future lookups by entities.
+ self._feed_entries = feed_entries.copy()
+ keep_entries = self._update_or_remove_entities(feed_entries)
+ self._generate_new_entities(keep_entries)
+ elif status == geojson_client.UPDATE_OK_NO_DATA:
+ _LOGGER.debug("Update successful, but no data received from %s",
+ self._feed)
+ else:
+ _LOGGER.warning("Update not successful, no data received from %s",
+ self._feed)
+ # Remove all entities.
+ self._update_or_remove_entities([])
+
+ def _update_or_remove_entities(self, feed_entries):
+ """Update existing entries and remove obsolete entities."""
+ _LOGGER.debug("Entries for updating: %s", feed_entries)
+ remove_entry = None
+ # Remove obsolete entities for events that have disappeared
+ managed_entities = self._managed_entities.copy()
+ for entity in managed_entities:
+ # Remove entry from previous iteration - if applicable.
+ if remove_entry:
+ feed_entries.remove(remove_entry)
+ remove_entry = None
+ for entry in feed_entries:
+ if entity.external_id == entry.external_id:
+ # Existing entity - update details.
+ _LOGGER.debug("Existing entity found %s", entity)
+ remove_entry = entry
+ entity.schedule_update_ha_state(True)
+ break
+ else:
+ # Remove obsolete entity.
+ _LOGGER.debug("Entity not current anymore %s", entity)
+ self._managed_entities.remove(entity)
+ self._hass.add_job(entity.async_remove())
+ # Remove entry from very last iteration - if applicable.
+ if remove_entry:
+ feed_entries.remove(remove_entry)
+ # Return the remaining entries that new entities must be created for.
+ return feed_entries
+
+ def _generate_new_entities(self, entries):
+ """Generate new entities for events."""
+ new_entities = []
+ for entry in entries:
+ new_entity = GeoJsonLocationEvent(self, entry)
+ _LOGGER.debug("New entity added %s", new_entity)
+ new_entities.append(new_entity)
+ # Add new entities to HA and keep track of them in this manager.
+ self._add_entities(new_entities, True)
+ self._managed_entities.extend(new_entities)
+
+ def get_feed_entry(self, external_id):
+ """Return a feed entry identified by external id."""
+ return next((entry for entry in self._feed_entries
+ if entry.external_id == external_id), None)
+
+
+class GeoJsonLocationEvent(GeoLocationEvent):
+ """This represents an external event with GeoJSON data."""
+
+ def __init__(self, feed_manager, feed_entry):
+ """Initialize entity with data from feed entry."""
+ self._feed_manager = feed_manager
+ self._update_from_feed(feed_entry)
+
+ @property
+ def should_poll(self):
+ """No polling needed for GeoJSON location events."""
+ return False
+
+ async def async_update(self):
+ """Update this entity from the data held in the feed manager."""
+ feed_entry = self._feed_manager.get_feed_entry(self.external_id)
+ if feed_entry:
+ self._update_from_feed(feed_entry)
+
+ def _update_from_feed(self, feed_entry):
+ """Update the internal state from the provided feed entry."""
+ self._name = feed_entry.title
+ self._distance = feed_entry.distance_to_home
+ self._latitude = feed_entry.coordinates[0]
+ self._longitude = feed_entry.coordinates[1]
+ self.external_id = feed_entry.external_id
+
+ @property
+ def name(self) -> Optional[str]:
+ """Return the name of the entity."""
+ return self._name
+
+ @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."""
+ return DEFAULT_UNIT_OF_MEASUREMENT
+
+ @property
+ def device_state_attributes(self):
+ """Return the device state attributes."""
+ attributes = {}
+ if self.external_id:
+ attributes[ATTR_EXTERNAL_ID] = self.external_id
+ return attributes
diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py
index 567a6d84233..22569af1f86 100644
--- a/homeassistant/components/google_assistant/__init__.py
+++ b/homeassistant/components/google_assistant/__init__.py
@@ -6,6 +6,7 @@ https://home-assistant.io/components/google_assistant/
"""
import asyncio
import logging
+from typing import Dict, Any
import aiohttp
import async_timeout
@@ -14,7 +15,6 @@ import voluptuous as vol
# Typing imports
from homeassistant.core import HomeAssistant
-from typing import Dict, Any
from homeassistant.const import CONF_NAME
from homeassistant.helpers import config_validation as cv
diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py
index e80b2282066..5b98e25014d 100644
--- a/homeassistant/components/google_assistant/auth.py
+++ b/homeassistant/components/google_assistant/auth.py
@@ -1,11 +1,11 @@
"""Google Assistant OAuth View."""
import logging
+from typing import Dict, Any
# Typing imports
# if False:
from aiohttp.web import Request, Response
-from typing import Dict, Any
from homeassistant.core import HomeAssistant
from homeassistant.components.http import HomeAssistantView
diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py
index 675e86f9d39..1cb4bf4cb32 100644
--- a/homeassistant/components/google_assistant/smart_home.py
+++ b/homeassistant/components/google_assistant/smart_home.py
@@ -324,3 +324,11 @@ async def handle_devices_execute(hass, config, payload):
})
return {'commands': final_results}
+
+
+def turned_off_response(message):
+ """Return a device turned off response."""
+ return {
+ 'requestId': message.get('requestId'),
+ 'payload': {'errorCode': 'deviceTurnedOff'}
+ }
diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py
index 26e80e6f03b..1ee9d4e2364 100644
--- a/homeassistant/components/google_assistant/trait.py
+++ b/homeassistant/components/google_assistant/trait.py
@@ -1,4 +1,6 @@
"""Implement the Smart Home traits."""
+import logging
+
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components import (
climate,
@@ -25,6 +27,8 @@ from homeassistant.util import color as color_util, temperature as temp_util
from .const import ERR_VALUE_OUT_OF_RANGE
from .helpers import SmartHomeError
+_LOGGER = logging.getLogger(__name__)
+
PREFIX_TRAITS = 'action.devices.traits.'
TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff'
TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness'
@@ -317,7 +321,11 @@ class ColorTemperatureTrait(_Trait):
response = {}
temp = self.state.attributes.get(light.ATTR_COLOR_TEMP)
- if temp is not None:
+ # Some faulty integrations might put 0 in here, raising exception.
+ if temp == 0:
+ _LOGGER.warning('Entity %s has incorrect color temperature %s',
+ self.state.entity_id, temp)
+ elif temp is not None:
response['color'] = {
'temperature':
color_util.color_temperature_mired_to_kelvin(temp)
diff --git a/homeassistant/components/hangouts/.translations/he.json b/homeassistant/components/hangouts/.translations/he.json
new file mode 100644
index 00000000000..28326d97142
--- /dev/null
+++ b/homeassistant/components/hangouts/.translations/he.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Google Hangouts \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
+ "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4."
+ },
+ "error": {
+ "invalid_2fa": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d1\u05d1\u05e7\u05e9\u05d4 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.",
+ "invalid_2fa_method": "\u05d3\u05e8\u05da \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea (\u05d0\u05de\u05ea \u05d1\u05d8\u05dc\u05e4\u05d5\u05df).",
+ "invalid_login": "\u05db\u05e0\u05d9\u05e1\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1."
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9"
+ },
+ "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9"
+ },
+ "user": {
+ "data": {
+ "email": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d3\u05d5\u05d0\"\u05dc",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ },
+ "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc- Google Hangouts"
+ }
+ },
+ "title": "Google Hangouts"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/.translations/id.json b/homeassistant/components/hangouts/.translations/id.json
new file mode 100644
index 00000000000..46a574bdf8a
--- /dev/null
+++ b/homeassistant/components/hangouts/.translations/id.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Google Hangouts sudah dikonfigurasikan",
+ "unknown": "Kesalahan tidak dikenal terjadi."
+ },
+ "error": {
+ "invalid_2fa": "Autentikasi 2 Faktor Tidak Valid, silakan coba lagi.",
+ "invalid_2fa_method": "Metode 2FA Tidak Sah (Verifikasi di Ponsel).",
+ "invalid_login": "Login tidak valid, silahkan coba lagi."
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Pin 2FA"
+ },
+ "description": "Kosong",
+ "title": "2-Faktor-Otentikasi"
+ },
+ "user": {
+ "data": {
+ "email": "Alamat email",
+ "password": "Kata sandi"
+ },
+ "description": "Kosong",
+ "title": "Google Hangouts Login"
+ }
+ },
+ "title": "Google Hangouts"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/.translations/nn.json b/homeassistant/components/hangouts/.translations/nn.json
new file mode 100644
index 00000000000..58e5f4f45fd
--- /dev/null
+++ b/homeassistant/components/hangouts/.translations/nn.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Google Hangouts er allereie konfigurert",
+ "unknown": "Det hende ein ukjent feil"
+ },
+ "error": {
+ "invalid_2fa": "Ugyldig to-faktor-autentisering. Ver vennleg og pr\u00f8v igjen.",
+ "invalid_2fa_method": "Ugyldig 2FA-metode (godkjenn p\u00e5 telefonen).",
+ "invalid_login": "Ugyldig innlogging. Pr\u00f8v igjen."
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "2FA PIN"
+ },
+ "title": "To-faktor-autentiserin"
+ },
+ "user": {
+ "data": {
+ "email": "Epostadresse",
+ "password": "Passord"
+ },
+ "title": "Google Hangouts Login"
+ }
+ },
+ "title": "Google Hangouts"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/.translations/pl.json b/homeassistant/components/hangouts/.translations/pl.json
index a8314761f8d..5e0ecfa2900 100644
--- a/homeassistant/components/hangouts/.translations/pl.json
+++ b/homeassistant/components/hangouts/.translations/pl.json
@@ -14,6 +14,7 @@
"data": {
"2fa": "PIN"
},
+ "description": "Pusty",
"title": "Uwierzytelnianie dwusk\u0142adnikowe"
},
"user": {
@@ -21,6 +22,7 @@
"email": "Adres e-mail",
"password": "Has\u0142o"
},
+ "description": "Pusty",
"title": "Logowanie do Google Hangouts"
}
},
diff --git a/homeassistant/components/hangouts/.translations/pt-BR.json b/homeassistant/components/hangouts/.translations/pt-BR.json
index 516229c3871..00c533311fc 100644
--- a/homeassistant/components/hangouts/.translations/pt-BR.json
+++ b/homeassistant/components/hangouts/.translations/pt-BR.json
@@ -5,16 +5,21 @@
"unknown": "Ocorreu um erro desconhecido."
},
"error": {
- "invalid_2fa": "Autentica\u00e7\u00e3o de 2 fatores inv\u00e1lida, por favor, tente novamente."
+ "invalid_2fa": "Autentica\u00e7\u00e3o de 2 fatores inv\u00e1lida, por favor, tente novamente.",
+ "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).",
+ "invalid_login": "Login inv\u00e1lido, por favor, tente novamente."
},
"step": {
"2fa": {
- "title": ""
+ "description": "Vazio",
+ "title": "Autentica\u00e7\u00e3o de 2 Fatores"
},
"user": {
"data": {
+ "email": "Endere\u00e7o de e-mail",
"password": "Senha"
},
+ "description": "Vazio",
"title": "Login do Hangouts do Google"
}
},
diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py
index ebadff57be3..8480ae09549 100644
--- a/homeassistant/components/hangouts/__init__.py
+++ b/homeassistant/components/hangouts/__init__.py
@@ -9,7 +9,9 @@ import logging
import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.components.hangouts.intents import HelpIntent
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.helpers import intent
from homeassistant.helpers import dispatcher
import homeassistant.helpers.config_validation as cv
@@ -18,12 +20,13 @@ from .const import (
EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
MESSAGE_SCHEMA, SERVICE_SEND_MESSAGE,
SERVICE_UPDATE, CONF_SENTENCES, CONF_MATCHERS,
- CONF_ERROR_SUPPRESSED_CONVERSATIONS, INTENT_SCHEMA, TARGETS_SCHEMA)
+ CONF_ERROR_SUPPRESSED_CONVERSATIONS, INTENT_SCHEMA, TARGETS_SCHEMA,
+ CONF_DEFAULT_CONVERSATIONS, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED,
+ INTENT_HELP)
# We need an import from .config_flow, without it .config_flow is never loaded.
from .config_flow import HangoutsFlowHandler # noqa: F401
-
REQUIREMENTS = ['hangups==0.4.5']
_LOGGER = logging.getLogger(__name__)
@@ -33,6 +36,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_INTENTS, default={}): vol.Schema({
cv.string: INTENT_SCHEMA
}),
+ vol.Optional(CONF_DEFAULT_CONVERSATIONS, default=[]):
+ [TARGETS_SCHEMA],
vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]):
[TARGETS_SCHEMA]
})
@@ -47,16 +52,23 @@ async def async_setup(hass, config):
if config is None:
hass.data[DOMAIN] = {
CONF_INTENTS: {},
+ CONF_DEFAULT_CONVERSATIONS: [],
CONF_ERROR_SUPPRESSED_CONVERSATIONS: [],
}
return True
hass.data[DOMAIN] = {
CONF_INTENTS: config[CONF_INTENTS],
+ CONF_DEFAULT_CONVERSATIONS: config[CONF_DEFAULT_CONVERSATIONS],
CONF_ERROR_SUPPRESSED_CONVERSATIONS:
config[CONF_ERROR_SUPPRESSED_CONVERSATIONS],
}
+ if (hass.data[DOMAIN][CONF_INTENTS] and
+ INTENT_HELP not in hass.data[DOMAIN][CONF_INTENTS]):
+ hass.data[DOMAIN][CONF_INTENTS][INTENT_HELP] = {
+ CONF_SENTENCES: ['HELP']}
+
for data in hass.data[DOMAIN][CONF_INTENTS].values():
matchers = []
for sentence in data[CONF_SENTENCES]:
@@ -82,6 +94,7 @@ async def async_setup_entry(hass, config):
hass,
config.data.get(CONF_REFRESH_TOKEN),
hass.data[DOMAIN][CONF_INTENTS],
+ hass.data[DOMAIN][CONF_DEFAULT_CONVERSATIONS],
hass.data[DOMAIN][CONF_ERROR_SUPPRESSED_CONVERSATIONS])
hass.data[DOMAIN][CONF_BOT] = bot
except GoogleAuthError as exception:
@@ -96,11 +109,12 @@ async def async_setup_entry(hass, config):
dispatcher.async_dispatcher_connect(
hass,
EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
- bot.async_update_conversation_commands)
+ bot.async_resolve_conversations)
+
dispatcher.async_dispatcher_connect(
hass,
- EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
- bot.async_handle_update_error_suppressed_conversations)
+ EVENT_HANGOUTS_CONVERSATIONS_RESOLVED,
+ bot.async_update_conversation_commands)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
bot.async_handle_hass_stop)
@@ -116,6 +130,8 @@ async def async_setup_entry(hass, config):
async_handle_update_users_and_conversations,
schema=vol.Schema({}))
+ intent.async_register(hass, HelpIntent(hass))
+
return True
diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py
index 74eb14b050d..9d66338dff0 100644
--- a/homeassistant/components/hangouts/config_flow.py
+++ b/homeassistant/components/hangouts/config_flow.py
@@ -1,7 +1,7 @@
"""Config flow to configure Google Hangouts."""
import voluptuous as vol
-from homeassistant import config_entries, data_entry_flow
+from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import callback
@@ -19,10 +19,11 @@ def configured_hangouts(hass):
@config_entries.HANDLERS.register(HANGOUTS_DOMAIN)
-class HangoutsFlowHandler(data_entry_flow.FlowHandler):
+class HangoutsFlowHandler(config_entries.ConfigFlow):
"""Config flow Google Hangouts."""
VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
def __init__(self):
"""Initialize Google Hangouts config flow."""
diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py
index 3b96edf93a2..caae0de169b 100644
--- a/homeassistant/components/hangouts/const.py
+++ b/homeassistant/components/hangouts/const.py
@@ -24,10 +24,13 @@ CONF_INTENT_TYPE = 'intent_type'
CONF_SENTENCES = 'sentences'
CONF_MATCHERS = 'matchers'
+INTENT_HELP = 'HangoutsHelp'
+
EVENT_HANGOUTS_CONNECTED = 'hangouts_connected'
EVENT_HANGOUTS_DISCONNECTED = 'hangouts_disconnected'
EVENT_HANGOUTS_USERS_CHANGED = 'hangouts_users_changed'
EVENT_HANGOUTS_CONVERSATIONS_CHANGED = 'hangouts_conversations_changed'
+EVENT_HANGOUTS_CONVERSATIONS_RESOLVED = 'hangouts_conversations_resolved'
EVENT_HANGOUTS_MESSAGE_RECEIVED = 'hangouts_message_received'
CONF_CONVERSATION_ID = 'id'
diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py
index 15f4156d374..7edc8898c8c 100644
--- a/homeassistant/components/hangouts/hangouts_bot.py
+++ b/homeassistant/components/hangouts/hangouts_bot.py
@@ -8,7 +8,7 @@ from .const import (
EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED,
CONF_MATCHERS, CONF_CONVERSATION_ID,
- CONF_CONVERSATION_NAME)
+ CONF_CONVERSATION_NAME, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, INTENT_HELP)
_LOGGER = logging.getLogger(__name__)
@@ -16,7 +16,8 @@ _LOGGER = logging.getLogger(__name__)
class HangoutsBot:
"""The Hangouts Bot."""
- def __init__(self, hass, refresh_token, intents, error_suppressed_convs):
+ def __init__(self, hass, refresh_token, intents,
+ default_convs, error_suppressed_convs):
"""Set up the client."""
self.hass = hass
self._connected = False
@@ -29,6 +30,8 @@ class HangoutsBot:
self._client = None
self._user_list = None
self._conversation_list = None
+ self._default_convs = default_convs
+ self._default_conv_ids = None
self._error_suppressed_convs = error_suppressed_convs
self._error_suppressed_conv_ids = None
@@ -51,7 +54,7 @@ class HangoutsBot:
return conv
return None
- def async_update_conversation_commands(self, _):
+ def async_update_conversation_commands(self):
"""Refresh the commands for every conversation."""
self._conversation_intents = {}
@@ -63,6 +66,8 @@ class HangoutsBot:
if conv_id is not None:
conversations.append(conv_id)
data['_' + CONF_CONVERSATIONS] = conversations
+ elif self._default_conv_ids:
+ data['_' + CONF_CONVERSATIONS] = self._default_conv_ids
else:
data['_' + CONF_CONVERSATIONS] = \
[conv.id_ for conv in self._conversation_list.get_all()]
@@ -81,13 +86,22 @@ class HangoutsBot:
self._conversation_list.on_event.add_observer(
self._async_handle_conversation_event)
- def async_handle_update_error_suppressed_conversations(self, _):
- """Resolve the list of error suppressed conversations."""
+ def async_resolve_conversations(self, _):
+ """Resolve the list of default and error suppressed conversations."""
+ self._default_conv_ids = []
self._error_suppressed_conv_ids = []
+
+ for conversation in self._default_convs:
+ conv_id = self._resolve_conversation_id(conversation)
+ if conv_id is not None:
+ self._default_conv_ids.append(conv_id)
+
for conversation in self._error_suppressed_convs:
conv_id = self._resolve_conversation_id(conversation)
if conv_id is not None:
self._error_suppressed_conv_ids.append(conv_id)
+ dispatcher.async_dispatcher_send(self.hass,
+ EVENT_HANGOUTS_CONVERSATIONS_RESOLVED)
async def _async_handle_conversation_event(self, event):
from hangups import ChatMessageEvent
@@ -112,7 +126,8 @@ class HangoutsBot:
if intents is not None:
is_error = False
try:
- intent_result = await self._async_process(intents, message)
+ intent_result = await self._async_process(intents, message,
+ conv_id)
except (intent.UnknownIntent, intent.IntentHandleError) as err:
is_error = True
intent_result = intent.IntentResponse()
@@ -133,7 +148,7 @@ class HangoutsBot:
[{'text': message, 'parse_str': True}],
[{CONF_CONVERSATION_ID: conv_id}])
- async def _async_process(self, intents, text):
+ async def _async_process(self, intents, text, conv_id):
"""Detect a matching intent."""
for intent_type, data in intents.items():
for matcher in data.get(CONF_MATCHERS, []):
@@ -141,12 +156,15 @@ class HangoutsBot:
if not match:
continue
+ if intent_type == INTENT_HELP:
+ return await self.hass.helpers.intent.async_handle(
+ DOMAIN, intent_type,
+ {'conv_id': {'value': conv_id}}, text)
- response = await self.hass.helpers.intent.async_handle(
+ return await self.hass.helpers.intent.async_handle(
DOMAIN, intent_type,
- {key: {'value': value} for key, value
- in match.groupdict().items()}, text)
- return response
+ {key: {'value': value}
+ for key, value in match.groupdict().items()}, text)
async def async_connect(self):
"""Login to the Google Hangouts."""
@@ -204,15 +222,16 @@ class HangoutsBot:
from hangups import ChatMessageSegment, hangouts_pb2
messages = []
for segment in message:
+ if messages:
+ messages.append(ChatMessageSegment('',
+ segment_type=hangouts_pb2.
+ SEGMENT_TYPE_LINE_BREAK))
if 'parse_str' in segment and segment['parse_str']:
messages.extend(ChatMessageSegment.from_str(segment['text']))
else:
if 'parse_str' in segment:
del segment['parse_str']
messages.append(ChatMessageSegment(**segment))
- messages.append(ChatMessageSegment('',
- segment_type=hangouts_pb2.
- SEGMENT_TYPE_LINE_BREAK))
if not messages:
return False
@@ -247,3 +266,7 @@ class HangoutsBot:
async def async_handle_update_users_and_conversations(self, _=None):
"""Handle the update_users_and_conversations service."""
await self._async_list_conversations()
+
+ def get_intents(self, conv_id):
+ """Return the intents for a specific conversation."""
+ return self._conversation_intents.get(conv_id)
diff --git a/homeassistant/components/hangouts/intents.py b/homeassistant/components/hangouts/intents.py
new file mode 100644
index 00000000000..be52f059139
--- /dev/null
+++ b/homeassistant/components/hangouts/intents.py
@@ -0,0 +1,33 @@
+"""Intents for the hangouts component."""
+from homeassistant.helpers import intent
+import homeassistant.helpers.config_validation as cv
+
+from .const import INTENT_HELP, DOMAIN, CONF_BOT
+
+
+class HelpIntent(intent.IntentHandler):
+ """Handle Help intents."""
+
+ intent_type = INTENT_HELP
+ slot_schema = {
+ 'conv_id': cv.string
+ }
+
+ def __init__(self, hass):
+ """Set up the intent."""
+ self.hass = hass
+
+ async def async_handle(self, intent_obj):
+ """Handle the intent."""
+ slots = self.async_validate_slots(intent_obj.slots)
+ conv_id = slots['conv_id']['value']
+
+ intents = self.hass.data[DOMAIN][CONF_BOT].get_intents(conv_id)
+ response = intent_obj.create_response()
+ help_text = "I understand the following sentences:"
+ for intent_data in intents.values():
+ for sentence in intent_data['sentences']:
+ help_text += "\n'{}'".format(sentence)
+ response.async_set_speech(help_text)
+
+ return response
diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json
index dd421fee57a..c83a0ae0876 100644
--- a/homeassistant/components/hangouts/strings.json
+++ b/homeassistant/components/hangouts/strings.json
@@ -15,14 +15,12 @@
"email": "E-Mail Address",
"password": "Password"
},
- "description": "",
"title": "Google Hangouts Login"
},
"2fa": {
"data": {
"2fa": "2FA Pin"
},
- "description": "",
"title": "2-Factor-Authentication"
}
},
diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py
index c51d45cc339..55cc7f54787 100644
--- a/homeassistant/components/hassio/http.py
+++ b/homeassistant/components/hassio/http.py
@@ -22,23 +22,24 @@ _LOGGER = logging.getLogger(__name__)
X_HASSIO = 'X-HASSIO-KEY'
-NO_TIMEOUT = {
- re.compile(r'^homeassistant/update$'),
- re.compile(r'^host/update$'),
- re.compile(r'^supervisor/update$'),
- re.compile(r'^addons/[^/]*/update$'),
- re.compile(r'^addons/[^/]*/install$'),
- re.compile(r'^addons/[^/]*/rebuild$'),
- re.compile(r'^snapshots/.*/full$'),
- re.compile(r'^snapshots/.*/partial$'),
- re.compile(r'^snapshots/[^/]*/upload$'),
- re.compile(r'^snapshots/[^/]*/download$'),
-}
+NO_TIMEOUT = re.compile(
+ r'^(?:'
+ r'|homeassistant/update'
+ r'|host/update'
+ r'|supervisor/update'
+ r'|addons/[^/]+/(?:update|install|rebuild)'
+ r'|snapshots/.+/full'
+ r'|snapshots/.+/partial'
+ r'|snapshots/[^/]+/(?:upload|download)'
+ r')$'
+)
-NO_AUTH = {
- re.compile(r'^app/.*$'),
- re.compile(r'^addons/[^/]*/logo$')
-}
+NO_AUTH = re.compile(
+ r'^(?:'
+ r'|app/.*'
+ r'|addons/[^/]+/logo'
+ r')$'
+)
class HassIOView(HomeAssistantView):
@@ -128,15 +129,13 @@ def _create_response_log(client, data):
def _get_timeout(path):
"""Return timeout for a URL path."""
- for re_path in NO_TIMEOUT:
- if re_path.match(path):
- return 0
+ if NO_TIMEOUT.match(path):
+ return 0
return 300
def _need_auth(path):
"""Return if a path need authentication."""
- for re_path in NO_AUTH:
- if re_path.match(path):
- return False
+ if NO_AUTH.match(path):
+ return False
return True
diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py
index eac02855b0b..8c12243ee8f 100644
--- a/homeassistant/components/homekit/__init__.py
+++ b/homeassistant/components/homekit/__init__.py
@@ -22,9 +22,9 @@ from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry
from .const import (
BRIDGE_NAME, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST,
- CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2,
- DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START,
- TYPE_OUTLET, TYPE_SWITCH)
+ CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO,
+ DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE,
+ SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH)
from .util import (
show_setup_message, validate_entity_config, validate_media_player_features)
@@ -150,6 +150,8 @@ def get_accessory(hass, driver, state, aid, config):
elif device_class == DEVICE_CLASS_PM25 \
or DEVICE_CLASS_PM25 in state.entity_id:
a_type = 'AirQualitySensor'
+ elif device_class == DEVICE_CLASS_CO:
+ a_type = 'CarbonMonoxideSensor'
elif device_class == DEVICE_CLASS_CO2 \
or DEVICE_CLASS_CO2 in state.entity_id:
a_type = 'CarbonDioxideSensor'
diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py
index 33d2c0bfb85..df488d4a73a 100644
--- a/homeassistant/components/homekit/const.py
+++ b/homeassistant/components/homekit/const.py
@@ -69,6 +69,8 @@ CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected'
+CHAR_CARBON_MONOXIDE_LEVEL = 'CarbonMonoxideLevel'
+CHAR_CARBON_MONOXIDE_PEAK_LEVEL = 'CarbonMonoxidePeakLevel'
CHAR_CHARGING_STATE = 'ChargingState'
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
@@ -114,6 +116,7 @@ PROP_MIN_VALUE = 'minValue'
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
# #### Device Classes ####
+DEVICE_CLASS_CO = 'co'
DEVICE_CLASS_CO2 = 'co2'
DEVICE_CLASS_DOOR = 'door'
DEVICE_CLASS_GARAGE_DOOR = 'garage_door'
@@ -125,3 +128,7 @@ DEVICE_CLASS_OPENING = 'opening'
DEVICE_CLASS_PM25 = 'pm25'
DEVICE_CLASS_SMOKE = 'smoke'
DEVICE_CLASS_WINDOW = 'window'
+
+# #### Thresholds ####
+THRESHOLD_CO = 25
+THRESHOLD_CO2 = 1000
diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py
index d4c2cb58209..d2101b1e6f9 100644
--- a/homeassistant/components/homekit/type_sensors.py
+++ b/homeassistant/components/homekit/type_sensors.py
@@ -13,6 +13,7 @@ from .const import (
CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY,
CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL,
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED,
+ CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL,
CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED,
CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED,
@@ -23,7 +24,7 @@ from .const import (
SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR,
SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR,
SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR,
- SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR)
+ SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR, THRESHOLD_CO, THRESHOLD_CO2)
from .util import (
convert_to_float, temperature_to_homekit, density_to_air_quality)
@@ -114,6 +115,34 @@ class AirQualitySensor(HomeAccessory):
_LOGGER.debug('%s: Set to %d', self.entity_id, density)
+@TYPES.register('CarbonMonoxideSensor')
+class CarbonMonoxideSensor(HomeAccessory):
+ """Generate a CarbonMonoxidSensor accessory as CO sensor."""
+
+ def __init__(self, *args):
+ """Initialize a CarbonMonoxideSensor accessory object."""
+ super().__init__(*args, category=CATEGORY_SENSOR)
+
+ serv_co = self.add_preload_service(SERV_CARBON_MONOXIDE_SENSOR, [
+ CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL])
+ self.char_level = serv_co.configure_char(
+ CHAR_CARBON_MONOXIDE_LEVEL, value=0)
+ self.char_peak = serv_co.configure_char(
+ CHAR_CARBON_MONOXIDE_PEAK_LEVEL, value=0)
+ self.char_detected = serv_co.configure_char(
+ CHAR_CARBON_MONOXIDE_DETECTED, value=0)
+
+ def update_state(self, new_state):
+ """Update accessory after state change."""
+ value = convert_to_float(new_state.state)
+ if value:
+ self.char_level.set_value(value)
+ if value > self.char_peak.value:
+ self.char_peak.set_value(value)
+ self.char_detected.set_value(value > THRESHOLD_CO)
+ _LOGGER.debug('%s: Set to %d', self.entity_id, value)
+
+
@TYPES.register('CarbonDioxideSensor')
class CarbonDioxideSensor(HomeAccessory):
"""Generate a CarbonDioxideSensor accessory as CO2 sensor."""
@@ -124,7 +153,7 @@ class CarbonDioxideSensor(HomeAccessory):
serv_co2 = self.add_preload_service(SERV_CARBON_DIOXIDE_SENSOR, [
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL])
- self.char_co2 = serv_co2.configure_char(
+ self.char_level = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_LEVEL, value=0)
self.char_peak = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0)
@@ -133,13 +162,13 @@ class CarbonDioxideSensor(HomeAccessory):
def update_state(self, new_state):
"""Update accessory after state change."""
- co2 = convert_to_float(new_state.state)
- if co2:
- self.char_co2.set_value(co2)
- if co2 > self.char_peak.value:
- self.char_peak.set_value(co2)
- self.char_detected.set_value(co2 > 1000)
- _LOGGER.debug('%s: Set to %d', self.entity_id, co2)
+ value = convert_to_float(new_state.state)
+ if value:
+ self.char_level.set_value(value)
+ if value > self.char_peak.value:
+ self.char_peak.set_value(value)
+ self.char_detected.set_value(value > THRESHOLD_CO2)
+ _LOGGER.debug('%s: Set to %d', self.entity_id, value)
@TYPES.register('LightSensor')
diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py
index 2b517652ad7..4e6b3f04ee1 100644
--- a/homeassistant/components/homematic/__init__.py
+++ b/homeassistant/components/homematic/__init__.py
@@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.loader import bind_hass
-REQUIREMENTS = ['pyhomematic==0.1.47']
+REQUIREMENTS = ['pyhomematic==0.1.49']
_LOGGER = logging.getLogger(__name__)
@@ -77,7 +77,8 @@ HM_DEVICE_TYPES = {
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor',
- 'IPKeySwitchPowermeter', 'IPThermostatWall230V'],
+ 'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus',
+ 'IPWeatherSensorBasic'],
DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@@ -87,7 +88,7 @@ HM_DEVICE_TYPES = {
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor',
- 'SmartwareMotion'],
+ 'SmartwareMotion', 'IPWeatherSensorPlus'],
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
DISCOVER_LOCKS: ['KeyMatic']
}
@@ -107,6 +108,7 @@ HM_ATTRIBUTE_SUPPORT = {
'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}],
'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}],
'RSSI_PEER': ['rssi', {}],
+ 'RSSI_DEVICE': ['rssi', {}],
'VALVE_STATE': ['valve', {}],
'BATTERY_STATE': ['battery', {}],
'CONTROL_MODE': ['mode', {
diff --git a/homeassistant/components/homematicip_cloud/.translations/ca.json b/homeassistant/components/homematicip_cloud/.translations/ca.json
index aab974ba137..7cc5943b830 100644
--- a/homeassistant/components/homematicip_cloud/.translations/ca.json
+++ b/homeassistant/components/homematicip_cloud/.translations/ca.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "El punt d'acc\u00e9s ja est\u00e0 configurat",
- "conection_aborted": "No s'ha pogut connectar al servidor HMIP",
"connection_aborted": "No s'ha pogut connectar al servidor HMIP",
"unknown": "S'ha produ\u00eft un error desconegut."
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/cs.json b/homeassistant/components/homematicip_cloud/.translations/cs.json
index 4030450e51c..fa98029f6b0 100644
--- a/homeassistant/components/homematicip_cloud/.translations/cs.json
+++ b/homeassistant/components/homematicip_cloud/.translations/cs.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "P\u0159\u00edstupov\u00fd bod je ji\u017e nakonfigurov\u00e1n",
- "conection_aborted": "Nelze se p\u0159ipojit k serveru HMIP",
"connection_aborted": "Nelze se p\u0159ipojit k HMIP serveru",
"unknown": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b"
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/de.json b/homeassistant/components/homematicip_cloud/.translations/de.json
index fdccac0d229..bd600f7d2ef 100644
--- a/homeassistant/components/homematicip_cloud/.translations/de.json
+++ b/homeassistant/components/homematicip_cloud/.translations/de.json
@@ -2,15 +2,14 @@
"config": {
"abort": {
"already_configured": "Der Accesspoint ist bereits konfiguriert",
- "conection_aborted": "Keine Verbindung zum HMIP-Server m\u00f6glich",
"connection_aborted": "Konnte nicht mit HMIP Server verbinden",
"unknown": "Ein unbekannter Fehler ist aufgetreten."
},
"error": {
- "invalid_pin": "Ung\u00fcltige PIN, bitte versuchen Sie es erneut.",
- "press_the_button": "Bitte dr\u00fccken Sie die blaue Taste.",
- "register_failed": "Registrierung fehlgeschlagen, bitte versuchen Sie es erneut.",
- "timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuchen Sie es erneut."
+ "invalid_pin": "Ung\u00fcltige PIN, bitte versuche es erneut.",
+ "press_the_button": "Bitte dr\u00fccke die blaue Taste.",
+ "register_failed": "Registrierung fehlgeschlagen, bitte versuche es erneut.",
+ "timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuche es erneut."
},
"step": {
"init": {
@@ -22,7 +21,7 @@
"title": "HometicIP Accesspoint ausw\u00e4hlen"
},
"link": {
- "description": "Dr\u00fccken Sie den blauen Taster auf dem Accesspoint, sowie den Senden Button um HomematicIP mit Home Assistant zu verbinden.\n\n",
+ "description": "Dr\u00fccke den blauen Taster auf dem Accesspoint, sowie den Senden Button um HomematicIP mit Home Assistant zu verbinden.\n\n",
"title": "Verkn\u00fcpfe den Accesspoint"
}
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/en.json b/homeassistant/components/homematicip_cloud/.translations/en.json
index 6fcfcddd75d..605bb0d250b 100644
--- a/homeassistant/components/homematicip_cloud/.translations/en.json
+++ b/homeassistant/components/homematicip_cloud/.translations/en.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Access point is already configured",
- "conection_aborted": "Could not connect to HMIP server",
"connection_aborted": "Could not connect to HMIP server",
"unknown": "Unknown error occurred."
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json
index e15d0dbae64..8675d6e12b1 100644
--- a/homeassistant/components/homematicip_cloud/.translations/es-419.json
+++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Accesspoint ya est\u00e1 configurado",
- "conection_aborted": "No se pudo conectar al servidor HMIP",
"connection_aborted": "No se pudo conectar al servidor HMIP",
"unknown": "Se produjo un error desconocido."
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/fr.json b/homeassistant/components/homematicip_cloud/.translations/fr.json
index 6cab0993c01..0e724d62bbe 100644
--- a/homeassistant/components/homematicip_cloud/.translations/fr.json
+++ b/homeassistant/components/homematicip_cloud/.translations/fr.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Le point d'acc\u00e8s est d\u00e9j\u00e0 configur\u00e9",
- "conection_aborted": "Impossible de se connecter au serveur HMIP",
"connection_aborted": "Impossible de se connecter au serveur HMIP",
"unknown": "Une erreur inconnue s'est produite."
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/he.json b/homeassistant/components/homematicip_cloud/.translations/he.json
index bdf1e436bad..c60294e21d5 100644
--- a/homeassistant/components/homematicip_cloud/.translations/he.json
+++ b/homeassistant/components/homematicip_cloud/.translations/he.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05d2\u05d9\u05e9\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea",
- "conection_aborted": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e8\u05ea HMIP",
+ "connection_aborted": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e8\u05ea HMIP",
"unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4."
},
"error": {
diff --git a/homeassistant/components/homematicip_cloud/.translations/id.json b/homeassistant/components/homematicip_cloud/.translations/id.json
new file mode 100644
index 00000000000..0487434274c
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud/.translations/id.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Jalur akses sudah dikonfigurasi",
+ "connection_aborted": "Tidak dapat terhubung ke server HMIP",
+ "unknown": "Kesalahan tidak dikenal terjadi."
+ },
+ "error": {
+ "invalid_pin": "PIN tidak valid, silakan coba lagi.",
+ "press_the_button": "Silakan tekan tombol biru.",
+ "register_failed": "Gagal mendaftar, silakan coba lagi.",
+ "timeout_button": "Batas waktu tekan tombol biru berakhir, silakan coba lagi."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "hapid": "Titik akses ID (SGTIN)",
+ "name": "Nama (opsional, digunakan sebagai awalan nama untuk semua perangkat)",
+ "pin": "Kode Pin (opsional)"
+ },
+ "title": "Pilih HomematicIP Access point"
+ },
+ "link": {
+ "description": "Tekan tombol biru pada access point dan tombol submit untuk mendaftarkan HomematicIP dengan rumah asisten.\n\n! [Lokasi tombol di bridge] (/ static/images/config_flows/config_homematicip_cloud.png)",
+ "title": "Tautkan jalur akses"
+ }
+ },
+ "title": "HomematicIP Cloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homematicip_cloud/.translations/ja.json b/homeassistant/components/homematicip_cloud/.translations/ja.json
index 105a7415789..6a03f3ec76b 100644
--- a/homeassistant/components/homematicip_cloud/.translations/ja.json
+++ b/homeassistant/components/homematicip_cloud/.translations/ja.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u306f\u65e2\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059",
- "conection_aborted": "HMIP\u30b5\u30fc\u30d0\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f",
"unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002"
},
"error": {
diff --git a/homeassistant/components/homematicip_cloud/.translations/ko.json b/homeassistant/components/homematicip_cloud/.translations/ko.json
index 617b65ff623..7b8dc8b5087 100644
--- a/homeassistant/components/homematicip_cloud/.translations/ko.json
+++ b/homeassistant/components/homematicip_cloud/.translations/ko.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "conection_aborted": "HMIP \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"connection_aborted": "HMIP \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
@@ -22,7 +21,7 @@
"title": "HomematicIP \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc120\ud0dd"
},
"link": {
- "description": "Home Assistant\uc5d0 HomematicIP\ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \uc11c\ubc0b \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n",
+ "description": "Home Assistant \uc5d0 HomematicIP \ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \uc11c\ubc0b \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n",
"title": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc5d0 \uc5f0\uacb0"
}
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/lb.json b/homeassistant/components/homematicip_cloud/.translations/lb.json
index a21767fc7d9..2cad909a7ee 100644
--- a/homeassistant/components/homematicip_cloud/.translations/lb.json
+++ b/homeassistant/components/homematicip_cloud/.translations/lb.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Acesspoint ass schon konfigur\u00e9iert",
- "conection_aborted": "Konnt sech net mam HMIP Server verbannen",
"connection_aborted": "Konnt sech net mam HMIP Server verbannen",
"unknown": "Onbekannten Feeler opgetrueden"
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/nl.json b/homeassistant/components/homematicip_cloud/.translations/nl.json
index 40d1ced5007..ff3e2dea2cd 100644
--- a/homeassistant/components/homematicip_cloud/.translations/nl.json
+++ b/homeassistant/components/homematicip_cloud/.translations/nl.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Accesspoint is al geconfigureerd",
- "conection_aborted": "Kon geen verbinding maken met de HMIP-server",
"connection_aborted": "Kon geen verbinding maken met de HMIP-server",
"unknown": "Er is een onbekende fout opgetreden."
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/nn.json b/homeassistant/components/homematicip_cloud/.translations/nn.json
new file mode 100644
index 00000000000..966c827c89d
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud/.translations/nn.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tilgangspunktet er allereie konfigurert",
+ "connection_aborted": "Kunne ikkje kople til HMIP-serveren",
+ "unknown": "Det hende ein ukjent feil."
+ },
+ "error": {
+ "invalid_pin": "Ugyldig PIN. Pr\u00f8v igjen.",
+ "press_the_button": "Ver vennleg og trykk p\u00e5 den bl\u00e5 knappen.",
+ "register_failed": "Kunne ikkje registrere. Pr\u00f8v igjen.",
+ "timeout_button": "TIda gjekk ut for \u00e5 trykke p\u00e5 den bl\u00e5 knappen. Ver vennleg og pr\u00f8v igjen."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "hapid": "TilgangspunktID (SGTIN)",
+ "name": "Namn (valfrii. Brukt som namnprefiks for alle einingar)",
+ "pin": "Pinkode (valfritt)"
+ },
+ "title": "Vel HomematicIP tilgangspunkt"
+ },
+ "link": {
+ "description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og sendknappen for \u00e5 registrere HomematicIP med Home Assitant.\n\n ! [Plassering av knapp p\u00e5 bro] (/ static / images / config_flows / config_homematicip_cloud.png)",
+ "title": "Link tilgangspunk"
+ }
+ },
+ "title": "HomematicIP Cloud"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homematicip_cloud/.translations/no.json b/homeassistant/components/homematicip_cloud/.translations/no.json
index 730f00ae625..d9e6636c972 100644
--- a/homeassistant/components/homematicip_cloud/.translations/no.json
+++ b/homeassistant/components/homematicip_cloud/.translations/no.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Tilgangspunktet er allerede konfigurert",
- "conection_aborted": "Kunne ikke koble til HMIP serveren",
"connection_aborted": "Kunne ikke koble til HMIP serveren",
"unknown": "Ukjent feil oppstod."
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/pl.json b/homeassistant/components/homematicip_cloud/.translations/pl.json
index 3fcbe7e69d1..7c8714c2c11 100644
--- a/homeassistant/components/homematicip_cloud/.translations/pl.json
+++ b/homeassistant/components/homematicip_cloud/.translations/pl.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany",
- "conection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP",
"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/.translations/pt-BR.json b/homeassistant/components/homematicip_cloud/.translations/pt-BR.json
index d4ecbe50107..82166a1aaaf 100644
--- a/homeassistant/components/homematicip_cloud/.translations/pt-BR.json
+++ b/homeassistant/components/homematicip_cloud/.translations/pt-BR.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "O Accesspoint j\u00e1 est\u00e1 configurado",
- "conection_aborted": "N\u00e3o foi poss\u00edvel conectar ao servidor HMIP",
"connection_aborted": "N\u00e3o foi poss\u00edvel conectar ao servidor HMIP",
"unknown": "Ocorreu um erro desconhecido."
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/pt.json b/homeassistant/components/homematicip_cloud/.translations/pt.json
index 87ee494a875..18377490a5f 100644
--- a/homeassistant/components/homematicip_cloud/.translations/pt.json
+++ b/homeassistant/components/homematicip_cloud/.translations/pt.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "O ponto de acesso j\u00e1 se encontra configurado",
- "conection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP",
"connection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP",
"unknown": "Ocorreu um erro desconhecido."
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json
index ed42daf19cd..ef2b3be4a64 100644
--- a/homeassistant/components/homematicip_cloud/.translations/ru.json
+++ b/homeassistant/components/homematicip_cloud/.translations/ru.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "\u0422\u043e\u0447\u043a\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430",
- "conection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP",
"connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP",
"unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430"
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/sl.json b/homeassistant/components/homematicip_cloud/.translations/sl.json
index 4c4a00e31e0..eabb31ac833 100644
--- a/homeassistant/components/homematicip_cloud/.translations/sl.json
+++ b/homeassistant/components/homematicip_cloud/.translations/sl.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Dostopna to\u010dka je \u017ee konfigurirana",
- "conection_aborted": "Povezave s stre\u017enikom HMIP ni bila mogo\u010da",
"connection_aborted": "Povezava s stre\u017enikom HMIP ni bila mogo\u010da",
"unknown": "Pri\u0161lo je do neznane napake"
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/sv.json b/homeassistant/components/homematicip_cloud/.translations/sv.json
index 4e8aac999de..da6bde77ae3 100644
--- a/homeassistant/components/homematicip_cloud/.translations/sv.json
+++ b/homeassistant/components/homematicip_cloud/.translations/sv.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Accesspunkten \u00e4r redan konfigurerad",
- "conection_aborted": "Kunde inte ansluta till HMIP server",
"connection_aborted": "Det gick inte att ansluta till HMIP-servern",
"unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat"
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json
index 930b649bceb..629ee4347fe 100644
--- a/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json
+++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "\u63a5\u5165\u70b9\u5df2\u7ecf\u914d\u7f6e\u5b8c\u6210",
- "conection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668",
"connection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668",
"unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002"
},
diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json
index 9340070d9a3..d2d33455191 100644
--- a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json
+++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "Accesspoint \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "conection_aborted": "\u7121\u6cd5\u9023\u7dda\u81f3 HMIP \u4f3a\u670d\u5668",
"connection_aborted": "\u7121\u6cd5\u9023\u7dda\u81f3 HMIP \u4f3a\u670d\u5668",
"unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
},
diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py
index d5045cf151b..ea251a3bf87 100644
--- a/homeassistant/components/homematicip_cloud/config_flow.py
+++ b/homeassistant/components/homematicip_cloud/config_flow.py
@@ -1,7 +1,7 @@
"""Config flow to configure the HomematicIP Cloud component."""
import voluptuous as vol
-from homeassistant import config_entries, data_entry_flow
+from homeassistant import config_entries
from homeassistant.core import callback
from .const import DOMAIN as HMIPC_DOMAIN
@@ -18,10 +18,11 @@ def configured_haps(hass):
@config_entries.HANDLERS.register(HMIPC_DOMAIN)
-class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
+class HomematicipCloudFlowHandler(config_entries.ConfigFlow):
"""Config flow for the HomematicIP Cloud component."""
VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
def __init__(self):
"""Initialize HomematicIP Cloud config flow."""
diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py
index 015c386e836..2a25de96edc 100644
--- a/homeassistant/components/http/ban.py
+++ b/homeassistant/components/http/ban.py
@@ -10,7 +10,6 @@ from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
import voluptuous as vol
from homeassistant.core import callback
-from homeassistant.components import persistent_notification
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
@@ -92,9 +91,10 @@ async def process_wrong_login(request):
msg = ('Login attempt or request with invalid authentication '
'from {}'.format(remote_addr))
_LOGGER.warning(msg)
- persistent_notification.async_create(
- request.app['hass'], msg, 'Login attempt failed',
- NOTIFICATION_ID_LOGIN)
+
+ hass = request.app['hass']
+ hass.components.persistent_notification.async_create(
+ msg, 'Login attempt failed', NOTIFICATION_ID_LOGIN)
# Check if ban middleware is loaded
if (KEY_BANNED_IPS not in request.app or
@@ -108,15 +108,13 @@ async def process_wrong_login(request):
new_ban = IpBan(remote_addr)
request.app[KEY_BANNED_IPS].append(new_ban)
- hass = request.app['hass']
await hass.async_add_job(
update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban)
_LOGGER.warning(
"Banned IP %s for too many login attempts", remote_addr)
- persistent_notification.async_create(
- hass,
+ hass.components.persistent_notification.async_create(
'Too many login attempts from {}'.format(remote_addr),
'Banning IP address', NOTIFICATION_ID_BAN)
diff --git a/homeassistant/components/huawei_lte.py b/homeassistant/components/huawei_lte.py
new file mode 100644
index 00000000000..33da6be56db
--- /dev/null
+++ b/homeassistant/components/huawei_lte.py
@@ -0,0 +1,127 @@
+"""
+Support for Huawei LTE routers.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/huawei_lte/
+"""
+from datetime import timedelta
+from functools import reduce
+import logging
+import operator
+
+import voluptuous as vol
+import attr
+
+from homeassistant.const import (
+ CONF_URL, CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP,
+)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.util import Throttle
+
+
+_LOGGER = logging.getLogger(__name__)
+
+REQUIREMENTS = ['huawei-lte-api==1.0.12']
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
+
+DOMAIN = 'huawei_lte'
+DATA_KEY = 'huawei_lte'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
+ vol.Required(CONF_URL): cv.url,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ })])
+}, extra=vol.ALLOW_EXTRA)
+
+
+@attr.s
+class RouterData:
+ """Class for router state."""
+
+ client = attr.ib()
+ device_information = attr.ib(init=False, factory=dict)
+ device_signal = attr.ib(init=False, factory=dict)
+ traffic_statistics = attr.ib(init=False, factory=dict)
+ wlan_host_list = attr.ib(init=False, factory=dict)
+
+ def __getitem__(self, path: str):
+ """
+ Get value corresponding to a dotted path.
+
+ The first path component designates a member of this class
+ such as device_information, device_signal etc, and the remaining
+ path points to a value in the member's data structure.
+ """
+ root, *rest = path.split(".")
+ try:
+ data = getattr(self, root)
+ except AttributeError as err:
+ raise KeyError from err
+ return reduce(operator.getitem, rest, data)
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self) -> None:
+ """Call API to update data."""
+ self.device_information = self.client.device.information()
+ _LOGGER.debug("device_information=%s", self.device_information)
+ self.device_signal = self.client.device.signal()
+ _LOGGER.debug("device_signal=%s", self.device_signal)
+ self.traffic_statistics = self.client.monitoring.traffic_statistics()
+ _LOGGER.debug("traffic_statistics=%s", self.traffic_statistics)
+ self.wlan_host_list = self.client.wlan.host_list()
+ _LOGGER.debug("wlan_host_list=%s", self.wlan_host_list)
+
+
+@attr.s
+class HuaweiLteData:
+ """Shared state."""
+
+ data = attr.ib(init=False, factory=dict)
+
+ def get_data(self, config):
+ """Get the requested or the only data value."""
+ if CONF_URL in config:
+ return self.data.get(config[CONF_URL])
+ if len(self.data) == 1:
+ return next(iter(self.data.values()))
+
+ return None
+
+
+def setup(hass, config) -> bool:
+ """Set up Huawei LTE component."""
+ if DATA_KEY not in hass.data:
+ hass.data[DATA_KEY] = HuaweiLteData()
+ for conf in config.get(DOMAIN, []):
+ _setup_lte(hass, conf)
+ return True
+
+
+def _setup_lte(hass, lte_config) -> None:
+ """Set up Huawei LTE router."""
+ from huawei_lte_api.AuthorizedConnection import AuthorizedConnection
+ from huawei_lte_api.Client import Client
+
+ url = lte_config[CONF_URL]
+ username = lte_config[CONF_USERNAME]
+ password = lte_config[CONF_PASSWORD]
+
+ connection = AuthorizedConnection(
+ url,
+ username=username,
+ password=password,
+ )
+ client = Client(connection)
+
+ data = RouterData(client)
+ data.update()
+ hass.data[DATA_KEY].data[url] = data
+
+ def cleanup(event):
+ """Clean up resources."""
+ client.user.logout()
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
diff --git a/homeassistant/components/hue/.translations/id.json b/homeassistant/components/hue/.translations/id.json
new file mode 100644
index 00000000000..bf5557436ce
--- /dev/null
+++ b/homeassistant/components/hue/.translations/id.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Semua Philips Hue bridges sudah dikonfigurasi",
+ "already_configured": "Bridge sudah dikonfigurasi",
+ "cannot_connect": "Tidak dapat terhubung ke bridge",
+ "discover_timeout": "Tidak dapat menemukan Hue Bridges.",
+ "no_bridges": "Bridge Philips Hue tidak ditemukan",
+ "unknown": "Kesalahan tidak dikenal terjadi."
+ },
+ "error": {
+ "linking": "Terjadi kesalahan tautan tidak dikenal.",
+ "register_failed": "Gagal mendaftar, silakan coba lagi."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "title": "Pilih Hue bridge"
+ },
+ "link": {
+ "description": "Tekan tombol di bridge untuk mendaftar Philips Hue dengan Home Assistant.\n\n![Lokasi tombol di bridge] (/static/images/config_philips_hue.jpg)",
+ "title": "Tautan Hub"
+ }
+ },
+ "title": "Philips Hue"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/.translations/nn.json b/homeassistant/components/hue/.translations/nn.json
new file mode 100644
index 00000000000..45d6bc89d72
--- /dev/null
+++ b/homeassistant/components/hue/.translations/nn.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Alle Philips Hue-bruer er allereie konfiguert",
+ "already_configured": "Brua er allereie konfiguert",
+ "cannot_connect": "Klarte ikkje \u00e5 kople til brua",
+ "discover_timeout": "Klarte ikkje \u00e5 oppdage Hue-bruer",
+ "no_bridges": "Oppdaga ingen Philips Hue-bruer",
+ "unknown": "Ukjent feil oppstod"
+ },
+ "error": {
+ "linking": "Ukjent linkefeil oppstod.",
+ "register_failed": "Kunne ikkje registrere, pr\u00f8v igjen"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Vert"
+ },
+ "title": "Vel Hue bru"
+ },
+ "link": {
+ "description": "Trykk p\u00e5 knappen p\u00e5 brua, for \u00e5 registrere Philips Hue med Home Assistant.\n\n![Lokasjon til knappen p\u00e5 brua]\n(/statisk/bilete/konfiguer_philips_hue.jpg)",
+ "title": "Link Hub"
+ }
+ },
+ "title": "Philips Hue"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py
index 38b521078f4..7a781c99f53 100644
--- a/homeassistant/components/hue/__init__.py
+++ b/homeassistant/components/hue/__init__.py
@@ -140,7 +140,7 @@ async def async_setup_entry(hass, entry):
config = bridge.api.config
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
- config_entry=entry.entry_id,
+ config_entry_id=entry.entry_id,
connections={
(dr.CONNECTION_NETWORK_MAC, config.mac)
},
diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py
index 49ebbdaabf5..24ad65e1feb 100644
--- a/homeassistant/components/hue/config_flow.py
+++ b/homeassistant/components/hue/config_flow.py
@@ -6,7 +6,7 @@ import os
import async_timeout
import voluptuous as vol
-from homeassistant import config_entries, data_entry_flow
+from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
@@ -41,10 +41,11 @@ def _find_username_from_config(hass, filename):
@config_entries.HANDLERS.register(DOMAIN)
-class HueFlowHandler(data_entry_flow.FlowHandler):
+class HueFlowHandler(config_entries.ConfigFlow):
"""Handle a Hue config flow."""
VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self):
"""Initialize the Hue flow."""
diff --git a/homeassistant/components/ios/.translations/de.json b/homeassistant/components/ios/.translations/de.json
new file mode 100644
index 00000000000..e9e592d18c2
--- /dev/null
+++ b/homeassistant/components/ios/.translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Es wird nur eine Konfiguration von Home Assistant iOS ben\u00f6tigt"
+ },
+ "step": {
+ "confirm": {
+ "description": "M\u00f6chtest du die Home Assistant iOS-Komponente einrichten?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/en.json b/homeassistant/components/ios/.translations/en.json
new file mode 100644
index 00000000000..ae2e4e03f74
--- /dev/null
+++ b/homeassistant/components/ios/.translations/en.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of Home Assistant iOS is necessary."
+ },
+ "step": {
+ "confirm": {
+ "description": "Do you want to set up the Home Assistant iOS component?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/he.json b/homeassistant/components/ios/.translations/he.json
new file mode 100644
index 00000000000..e786e5ae843
--- /dev/null
+++ b/homeassistant/components/ios/.translations/he.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Home Assistant iOS \u05e0\u05d7\u05d5\u05e6\u05d4."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Home Assistant iOS?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/hu.json b/homeassistant/components/ios/.translations/hu.json
new file mode 100644
index 00000000000..5ee001db3c5
--- /dev/null
+++ b/homeassistant/components/ios/.translations/hu.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Csak egyetlen Home Assistant iOS konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges."
+ },
+ "step": {
+ "confirm": {
+ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Home Assistant iOS komponenst?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/id.json b/homeassistant/components/ios/.translations/id.json
new file mode 100644
index 00000000000..5813d9488f0
--- /dev/null
+++ b/homeassistant/components/ios/.translations/id.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Hanya satu konfigurasi Home Assistant iOS yang diperlukan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Apakah Anda ingin mengatur komponen iOS Home Assistant?",
+ "title": "Home Asisten iOS"
+ }
+ },
+ "title": "Home Asisten iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/ko.json b/homeassistant/components/ios/.translations/ko.json
index 6d69ea3126c..1496dab0555 100644
--- a/homeassistant/components/ios/.translations/ko.json
+++ b/homeassistant/components/ios/.translations/ko.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\ud558\ub098\uc758 Home Assistant iOS \uad6c\uc131\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
+ "single_instance_allowed": "\ud558\ub098\uc758 Home Assistant iOS \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
- "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc124\uc815\uc744 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "Home Assistant iOS"
}
},
diff --git a/homeassistant/components/ios/.translations/lb.json b/homeassistant/components/ios/.translations/lb.json
new file mode 100644
index 00000000000..731371cada9
--- /dev/null
+++ b/homeassistant/components/ios/.translations/lb.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Home Assistant iOS ass n\u00e9ideg."
+ },
+ "step": {
+ "confirm": {
+ "description": "W\u00ebllt dir d'Home Assistant iOS Komponent ariichten?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/nn.json b/homeassistant/components/ios/.translations/nn.json
new file mode 100644
index 00000000000..9d2cf692006
--- /dev/null
+++ b/homeassistant/components/ios/.translations/nn.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Du treng berre \u00e9in Home Assistant iOS-konfigurasjon."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vil du sette opp Home Assistant iOS-komponenten?",
+ "title": "Home Assistant Ios"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/no.json b/homeassistant/components/ios/.translations/no.json
new file mode 100644
index 00000000000..a125b96a070
--- /dev/null
+++ b/homeassistant/components/ios/.translations/no.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Kun en enkelt konfigurasjon av Home Assistant iOS er n\u00f8dvendig."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u00d8nsker du \u00e5 konfigurere Home Assistant iOS-komponenten?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/pt-BR.json b/homeassistant/components/ios/.translations/pt-BR.json
new file mode 100644
index 00000000000..77efc04b817
--- /dev/null
+++ b/homeassistant/components/ios/.translations/pt-BR.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do Home Assistant iOS \u00e9 necess\u00e1ria."
+ },
+ "step": {
+ "confirm": {
+ "description": "Deseja configurar o componente iOS do Home Assistant?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/pt.json b/homeassistant/components/ios/.translations/pt.json
new file mode 100644
index 00000000000..6752606d9f5
--- /dev/null
+++ b/homeassistant/components/ios/.translations/pt.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do componente iOS do Home Assistante \u00e9 necess\u00e1ria."
+ },
+ "step": {
+ "confirm": {
+ "description": "Deseja configurar o componente iOS do Home Assistant?",
+ "title": ""
+ }
+ },
+ "title": ""
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/ru.json b/homeassistant/components/ios/.translations/ru.json
new file mode 100644
index 00000000000..7030f18b729
--- /dev/null
+++ b/homeassistant/components/ios/.translations/ru.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u0414\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Home Assistant iOS."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Home Assistant iOS?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/sl.json b/homeassistant/components/ios/.translations/sl.json
new file mode 100644
index 00000000000..28e9102aafd
--- /dev/null
+++ b/homeassistant/components/ios/.translations/sl.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Potrebna je samo ena konfiguracija Home Assistant iOS."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ali \u017eelite nastaviti komponento za Home Assistant iOS?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/sv.json b/homeassistant/components/ios/.translations/sv.json
new file mode 100644
index 00000000000..6806f9bab90
--- /dev/null
+++ b/homeassistant/components/ios/.translations/sv.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "confirm": {
+ "description": "Vill du konfigurera Home Assistants iOS komponent?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/zh-Hant.json b/homeassistant/components/ios/.translations/zh-Hant.json
new file mode 100644
index 00000000000..8cfedf31673
--- /dev/null
+++ b/homeassistant/components/ios/.translations/zh-Hant.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Home Assistant iOS \u5373\u53ef\u3002"
+ },
+ "step": {
+ "confirm": {
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant iOS \u5143\u4ef6\uff1f",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios/__init__.py
similarity index 79%
rename from homeassistant/components/ios.py
rename to homeassistant/components/ios/__init__.py
index 7f7377469fd..a67be0a63de 100644
--- a/homeassistant/components/ios.py
+++ b/homeassistant/components/ios/__init__.py
@@ -9,15 +9,15 @@ import logging
import datetime
import voluptuous as vol
-# from voluptuous.humanize import humanize_error
+from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
HTTP_BAD_REQUEST)
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers import discovery
+from homeassistant.helpers import (
+ config_validation as cv, discovery, config_entry_flow)
from homeassistant.util.json import load_json, save_json
@@ -164,62 +164,70 @@ IDENTIFY_SCHEMA = vol.Schema({
CONFIGURATION_FILE = '.ios.conf'
-CONFIG_FILE = {ATTR_DEVICES: {}}
-CONFIG_FILE_PATH = ""
-
-
-def devices_with_push():
+def devices_with_push(hass):
"""Return a dictionary of push enabled targets."""
targets = {}
- for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
+ for device_name, device in hass.data[DOMAIN][ATTR_DEVICES].items():
if device.get(ATTR_PUSH_ID) is not None:
targets[device_name] = device.get(ATTR_PUSH_ID)
return targets
-def enabled_push_ids():
+def enabled_push_ids(hass):
"""Return a list of push enabled target push IDs."""
push_ids = list()
- for device in CONFIG_FILE[ATTR_DEVICES].values():
+ for device in hass.data[DOMAIN][ATTR_DEVICES].values():
if device.get(ATTR_PUSH_ID) is not None:
push_ids.append(device.get(ATTR_PUSH_ID))
return push_ids
-def devices():
+def devices(hass):
"""Return a dictionary of all identified devices."""
- return CONFIG_FILE[ATTR_DEVICES]
+ return hass.data[DOMAIN][ATTR_DEVICES]
-def device_name_for_push_id(push_id):
+def device_name_for_push_id(hass, push_id):
"""Return the device name for the push ID."""
- for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
+ for device_name, device in hass.data[DOMAIN][ATTR_DEVICES].items():
if device.get(ATTR_PUSH_ID) is push_id:
return device_name
return None
-def setup(hass, config):
+async def async_setup(hass, config):
"""Set up the iOS component."""
- global CONFIG_FILE
- global CONFIG_FILE_PATH
+ conf = config.get(DOMAIN)
- CONFIG_FILE_PATH = hass.config.path(CONFIGURATION_FILE)
+ ios_config = await hass.async_add_executor_job(
+ load_json, hass.config.path(CONFIGURATION_FILE))
- CONFIG_FILE = load_json(CONFIG_FILE_PATH)
+ if ios_config == {}:
+ ios_config[ATTR_DEVICES] = {}
- if CONFIG_FILE == {}:
- CONFIG_FILE[ATTR_DEVICES] = {}
+ ios_config[CONF_PUSH] = (conf or {}).get(CONF_PUSH, {})
+ hass.data[DOMAIN] = ios_config
+
+ # No entry support for notify component yet
discovery.load_platform(hass, 'notify', DOMAIN, {}, config)
- discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
+ if conf is not None:
+ hass.async_create_task(hass.config_entries.flow.async_init(
+ DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
- hass.http.register_view(iOSIdentifyDeviceView)
+ return True
- app_config = config.get(DOMAIN, {})
- hass.http.register_view(iOSPushConfigView(app_config.get(CONF_PUSH, {})))
+
+async def async_setup_entry(hass, entry):
+ """Set up an iOS entry."""
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, 'sensor'))
+
+ hass.http.register_view(
+ iOSIdentifyDeviceView(hass.config.path(CONFIGURATION_FILE)))
+ hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_PUSH]))
return True
@@ -247,6 +255,10 @@ class iOSIdentifyDeviceView(HomeAssistantView):
url = '/api/ios/identify'
name = 'api:ios:identify'
+ def __init__(self, config_path):
+ """Initiliaze the view."""
+ self._config_path = config_path
+
@asyncio.coroutine
def post(self, request):
"""Handle the POST request for device identification."""
@@ -255,23 +267,31 @@ class iOSIdentifyDeviceView(HomeAssistantView):
except ValueError:
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
+ hass = request.app['hass']
+
# Commented for now while iOS app is getting frequent updates
# try:
# data = IDENTIFY_SCHEMA(req_data)
# except vol.Invalid as ex:
- # return self.json_message(humanize_error(request.json, ex),
- # HTTP_BAD_REQUEST)
+ # return self.json_message(
+ # vol.humanize.humanize_error(request.json, ex),
+ # HTTP_BAD_REQUEST)
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat()
name = data.get(ATTR_DEVICE_ID)
- CONFIG_FILE[ATTR_DEVICES][name] = data
+ hass.data[DOMAIN][ATTR_DEVICES][name] = data
try:
- save_json(CONFIG_FILE_PATH, CONFIG_FILE)
+ save_json(self._config_path, hass.data[DOMAIN])
except HomeAssistantError:
return self.json_message("Error saving device.",
HTTP_INTERNAL_SERVER_ERROR)
return self.json({"status": "registered"})
+
+
+config_entry_flow.register_discovery_flow(
+ DOMAIN, 'Home Assistant iOS', lambda *_: True,
+ config_entries.CONN_CLASS_CLOUD_PUSH)
diff --git a/homeassistant/components/ios/strings.json b/homeassistant/components/ios/strings.json
new file mode 100644
index 00000000000..cbb63cf8229
--- /dev/null
+++ b/homeassistant/components/ios/strings.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "title": "Home Assistant iOS",
+ "step": {
+ "confirm": {
+ "title": "Home Assistant iOS",
+ "description": "Do you want to set up the Home Assistant iOS component?"
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of Home Assistant iOS is necessary."
+ }
+ }
+}
diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py
index 3df28586313..388fa41f36f 100644
--- a/homeassistant/components/konnected.py
+++ b/homeassistant/components/konnected.py
@@ -19,7 +19,7 @@ from homeassistant.const import (
HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED,
CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT,
CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN,
- ATTR_ENTITY_ID, ATTR_STATE)
+ ATTR_ENTITY_ID, ATTR_STATE, STATE_ON)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers import discovery
from homeassistant.helpers import config_validation as cv
@@ -35,6 +35,7 @@ CONF_API_HOST = 'api_host'
CONF_MOMENTARY = 'momentary'
CONF_PAUSE = 'pause'
CONF_REPEAT = 'repeat'
+CONF_INVERSE = 'inverse'
STATE_LOW = 'low'
STATE_HIGH = 'high'
@@ -48,6 +49,7 @@ _BINARY_SENSOR_SCHEMA = vol.All(
vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN),
vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_INVERSE): cv.boolean,
}), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE)
)
@@ -156,6 +158,7 @@ class ConfiguredDevice:
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 sensor %s (initial state: %s)',
@@ -259,15 +262,19 @@ class DiscoveredDevice:
def update_initial_states(self):
"""Update the initial state of each sensor from status poll."""
- for sensor in self.status.get('sensors'):
- entity_id = self.stored_configuration[CONF_BINARY_SENSORS]. \
- get(sensor.get(CONF_PIN), {}). \
- get(ATTR_ENTITY_ID)
+ 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
async_dispatcher_send(
self.hass,
SIGNAL_SENSOR_UPDATE.format(entity_id),
- bool(sensor.get(ATTR_STATE)))
+ state)
def sync_device_config(self):
"""Sync the new pin configuration to the Konnected device."""
@@ -321,6 +328,43 @@ class KonnectedView(HomeAssistantView):
"""Initialize the view."""
self.auth_token = auth_token
+ @staticmethod
+ def binary_value(state, activation):
+ """Return binary value for GPIO based on state and activation."""
+ if activation == STATE_HIGH:
+ 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."""
+ 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(
+ '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(
+ 'Switch on pin ' + pin_num + ' not configured',
+ 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])})
+
async def put(self, request: Request, device_id,
pin_num=None, state=None) -> Response:
"""Receive a sensor update via PUT request and async set state."""
@@ -341,7 +385,6 @@ class KonnectedView(HomeAssistantView):
return self.json_message(
"unauthorized", status_code=HTTP_UNAUTHORIZED)
pin_num = int(pin_num)
- state = bool(int(state))
device = data[CONF_DEVICES].get(device_id)
if device is None:
return self.json_message('unregistered device',
@@ -356,6 +399,9 @@ class KonnectedView(HomeAssistantView):
if entity_id is None:
return self.json_message('uninitialized sensor/actuator',
status_code=HTTP_NOT_FOUND)
+ state = bool(int(state))
+ if pin_data.get(CONF_INVERSE):
+ state = not state
async_dispatcher_send(
hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state)
diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py
index ff3fe609924..d3bec079a4c 100644
--- a/homeassistant/components/light/deconz.py
+++ b/homeassistant/components/light/deconz.py
@@ -6,7 +6,8 @@ https://home-assistant.io/components/light.deconz/
"""
from homeassistant.components.deconz.const import (
CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ,
- DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN, SWITCH_TYPES)
+ DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN,
+ COVER_TYPES, SWITCH_TYPES)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
@@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add light from deCONZ."""
entities = []
for light in lights:
- if light.type not in SWITCH_TYPES:
+ if light.type not in COVER_TYPES + SWITCH_TYPES:
entities.append(DeconzLight(light))
async_add_entities(entities, True)
@@ -213,6 +214,7 @@ class DeconzLight(Light):
self._light.uniqueid.count(':') != 7):
return None
serial = self._light.uniqueid.split('-', 1)[0]
+ bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
@@ -220,4 +222,5 @@ class DeconzLight(Light):
'model': self._light.modelid,
'name': self._light.name,
'sw_version': self._light.swversion,
+ 'via_hub': (DECONZ_DOMAIN, bridgeid),
}
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index 6f6e0ed617e..958abaca033 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -302,6 +302,7 @@ class HueLight(Light):
'model': self.light.productname or self.light.modelid,
# Not yet exposed as properties in aiohue
'sw_version': self.light.raw['swversion'],
+ 'via_hub': (hue.DOMAIN, self.bridge.api.config.bridgeid),
}
async def async_turn_on(self, **kwargs):
diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py
index 06507eaeca6..4349bfa1467 100644
--- a/homeassistant/components/light/isy994.py
+++ b/homeassistant/components/light/isy994.py
@@ -31,7 +31,7 @@ class ISYLightDevice(ISYDevice, Light):
@property
def is_on(self) -> bool:
"""Get whether the ISY994 light is on."""
- return self.value > 0
+ return self.value != 0
@property
def brightness(self) -> float:
diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py
index 239c924ed2b..ed4d350d96d 100644
--- a/homeassistant/components/light/mqtt_json.py
+++ b/homeassistant/components/light/mqtt_json.py
@@ -53,6 +53,7 @@ CONF_EFFECT_LIST = 'effect_list'
CONF_FLASH_TIME_LONG = 'flash_time_long'
CONF_FLASH_TIME_SHORT = 'flash_time_short'
CONF_HS = 'hs'
+CONF_UNIQUE_ID = 'unique_id'
# Stealing some of these from the base MQTT configs.
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -67,6 +68,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FLASH_TIME_LONG, default=DEFAULT_FLASH_TIME_LONG):
cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
@@ -87,6 +89,7 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
config = PLATFORM_SCHEMA(discovery_info)
async_add_entities([MqttJson(
config.get(CONF_NAME),
+ config.get(CONF_UNIQUE_ID),
config.get(CONF_EFFECT_LIST),
{
key: config.get(key) for key in (
@@ -120,14 +123,15 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
class MqttJson(MqttAvailability, Light):
"""Representation of a MQTT JSON light."""
- def __init__(self, name, effect_list, topic, qos, retain, optimistic,
- brightness, color_temp, effect, rgb, white_value, xy, hs,
- flash_times, availability_topic, payload_available,
+ def __init__(self, name, unique_id, effect_list, topic, qos, retain,
+ optimistic, brightness, color_temp, effect, rgb, white_value,
+ xy, hs, flash_times, availability_topic, payload_available,
payload_not_available, brightness_scale):
"""Initialize MQTT JSON light."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self._name = name
+ self._unique_id = unique_id
self._effect_list = effect_list
self._topic = topic
self._qos = qos
@@ -316,6 +320,11 @@ class MqttJson(MqttAvailability, Light):
"""Return the name of the device if any."""
return self._name
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._unique_id
+
@property
def is_on(self):
"""Return true if device is on."""
diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py
index 5a0e0546b1f..25c72c247ee 100644
--- a/homeassistant/components/light/rpi_gpio_pwm.py
+++ b/homeassistant/components/light/rpi_gpio_pwm.py
@@ -15,7 +15,7 @@ from homeassistant.components.light import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
-REQUIREMENTS = ['pwmled==1.2.1']
+REQUIREMENTS = ['pwmled==1.3.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py
index 0d12d095bb6..b62900b204c 100644
--- a/homeassistant/components/light/tradfri.py
+++ b/homeassistant/components/light/tradfri.py
@@ -13,8 +13,10 @@ from homeassistant.components.light import (
SUPPORT_COLOR, Light)
from homeassistant.components.light import \
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
-from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \
- KEY_API
+from homeassistant.components.tradfri import (
+ KEY_GATEWAY, KEY_API, DOMAIN as TRADFRI_DOMAIN)
+from homeassistant.components.tradfri.const import (
+ CONF_IMPORT_GROUPS, CONF_GATEWAY_ID)
import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
@@ -31,28 +33,21 @@ SUPPORTED_FEATURES = SUPPORT_TRANSITION
SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
-async def async_setup_platform(hass, config,
- async_add_entities, discovery_info=None):
- """Set up the IKEA Tradfri Light platform."""
- if discovery_info is None:
- return
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Load Tradfri lights based on a config entry."""
+ gateway_id = config_entry.data[CONF_GATEWAY_ID]
+ api = hass.data[KEY_API][config_entry.entry_id]
+ gateway = hass.data[KEY_GATEWAY][config_entry.entry_id]
- gateway_id = discovery_info['gateway']
- api = hass.data[KEY_API][gateway_id]
- gateway = hass.data[KEY_GATEWAY][gateway_id]
-
- devices_command = gateway.get_devices()
- devices_commands = await api(devices_command)
+ devices_commands = await api(gateway.get_devices())
devices = await api(devices_commands)
lights = [dev for dev in devices if dev.has_light_control]
if lights:
async_add_entities(
TradfriLight(light, api, gateway_id) for light in lights)
- allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
- if allow_tradfri_groups:
- groups_command = gateway.get_groups()
- groups_commands = await api(groups_command)
+ if config_entry.data[CONF_IMPORT_GROUPS]:
+ groups_commands = await api(gateway.get_groups())
groups = await api(groups_commands)
if groups:
async_add_entities(
@@ -167,6 +162,7 @@ class TradfriLight(Light):
self._hs_color = None
self._features = SUPPORTED_FEATURES
self._available = True
+ self._gateway_id = gateway_id
self._refresh(light)
@@ -175,6 +171,22 @@ class TradfriLight(Light):
"""Return unique ID for light."""
return self._unique_id
+ @property
+ def device_info(self):
+ """Return the device info."""
+ info = self._light.device_info
+
+ return {
+ 'identifiers': {
+ (TRADFRI_DOMAIN, self._light.id)
+ },
+ 'name': self._name,
+ 'manufacturer': info.manufacturer,
+ 'model': info.model_number,
+ 'sw_version': info.firmware_version,
+ 'via_hub': (TRADFRI_DOMAIN, self._gateway_id),
+ }
+
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py
index a08ebe459b4..b14b1f96e69 100644
--- a/homeassistant/components/light/yeelight.py
+++ b/homeassistant/components/light/yeelight.py
@@ -45,7 +45,7 @@ DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int,
vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean,
- vol.Optional(CONF_SAVE_ON_CHANGE, default=True): cv.boolean,
+ vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py
index dc5c4977944..56a1e9e5169 100644
--- a/homeassistant/components/light/zha.py
+++ b/homeassistant/components/light/zha.py
@@ -81,40 +81,65 @@ class Light(zha.Entity, light.Light):
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
+ from zigpy.exceptions import DeliveryError
+
duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION)
duration = duration * 10 # tenths of s
if light.ATTR_COLOR_TEMP in kwargs:
temperature = kwargs[light.ATTR_COLOR_TEMP]
- await self._endpoint.light_color.move_to_color_temp(
- temperature, duration)
+ try:
+ res = await self._endpoint.light_color.move_to_color_temp(
+ temperature, duration)
+ _LOGGER.debug("%s: moved to %i color temp: %s",
+ self.entity_id, temperature, res)
+ except DeliveryError as ex:
+ _LOGGER.error("%s: Couldn't change color temp: %s",
+ self.entity_id, ex)
+ return
self._color_temp = temperature
if light.ATTR_HS_COLOR in kwargs:
self._hs_color = kwargs[light.ATTR_HS_COLOR]
xy_color = color_util.color_hs_to_xy(*self._hs_color)
- await self._endpoint.light_color.move_to_color(
- int(xy_color[0] * 65535),
- int(xy_color[1] * 65535),
- duration,
- )
+ try:
+ res = await self._endpoint.light_color.move_to_color(
+ int(xy_color[0] * 65535),
+ int(xy_color[1] * 65535),
+ duration,
+ )
+ _LOGGER.debug("%s: moved XY color to (%1.2f, %1.2f): %s",
+ self.entity_id, xy_color[0], xy_color[1], res)
+ except DeliveryError as ex:
+ _LOGGER.error("%s: Couldn't change color temp: %s",
+ self.entity_id, ex)
+ return
if self._brightness is not None:
brightness = kwargs.get(
light.ATTR_BRIGHTNESS, self._brightness or 255)
self._brightness = brightness
# Move to level with on/off:
- await self._endpoint.level.move_to_level_with_on_off(
- brightness,
- duration
- )
+ try:
+ res = await self._endpoint.level.move_to_level_with_on_off(
+ brightness,
+ duration
+ )
+ _LOGGER.debug("%s: moved to %i level with on/off: %s",
+ self.entity_id, brightness, res)
+ except DeliveryError as ex:
+ _LOGGER.error("%s: Couldn't change brightness level: %s",
+ self.entity_id, ex)
+ return
self._state = 1
self.async_schedule_update_ha_state()
return
- from zigpy.exceptions import DeliveryError
+
try:
- await self._endpoint.on_off.on()
+ res = await self._endpoint.on_off.on()
+ _LOGGER.debug("%s was turned on: %s", self.entity_id, res)
except DeliveryError as ex:
- _LOGGER.error("Unable to turn the light on: %s", ex)
+ _LOGGER.error("%s: Unable to turn the light on: %s",
+ self.entity_id, ex)
return
self._state = 1
@@ -124,9 +149,11 @@ class Light(zha.Entity, light.Light):
"""Turn the entity off."""
from zigpy.exceptions import DeliveryError
try:
- await self._endpoint.on_off.off()
+ res = await self._endpoint.on_off.off()
+ _LOGGER.debug("%s was turned off: %s", self.entity_id, res)
except DeliveryError as ex:
- _LOGGER.error("Unable to turn the light off: %s", ex)
+ _LOGGER.error("%s: Unable to turn the light off: %s",
+ self.entity_id, ex)
return
self._state = 0
@@ -154,23 +181,31 @@ class Light(zha.Entity, light.Light):
async def async_update(self):
"""Retrieve latest state."""
- result = await zha.safe_read(self._endpoint.on_off, ['on_off'])
+ result = await zha.safe_read(self._endpoint.on_off, ['on_off'],
+ allow_cache=False,
+ only_cache=(not self._initialized))
self._state = result.get('on_off', self._state)
if self._supported_features & light.SUPPORT_BRIGHTNESS:
result = await zha.safe_read(self._endpoint.level,
- ['current_level'])
+ ['current_level'],
+ allow_cache=False,
+ only_cache=(not self._initialized))
self._brightness = result.get('current_level', self._brightness)
if self._supported_features & light.SUPPORT_COLOR_TEMP:
result = await zha.safe_read(self._endpoint.light_color,
- ['color_temperature'])
+ ['color_temperature'],
+ allow_cache=False,
+ only_cache=(not self._initialized))
self._color_temp = result.get('color_temperature',
self._color_temp)
if self._supported_features & light.SUPPORT_COLOR:
result = await zha.safe_read(self._endpoint.light_color,
- ['current_x', 'current_y'])
+ ['current_x', 'current_y'],
+ allow_cache=False,
+ only_cache=(not self._initialized))
if 'current_x' in result and 'current_y' in result:
xy_color = (round(result['current_x']/65535, 3),
round(result['current_y']/65535, 3))
diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py
index 55feef496f8..1e768eb127a 100644
--- a/homeassistant/components/light/zwave.py
+++ b/homeassistant/components/light/zwave.py
@@ -63,6 +63,16 @@ def brightness_state(value):
return 0, STATE_OFF
+def byte_to_zwave_brightness(value):
+ """Convert brightness in 0-255 scale to 0-99 scale.
+
+ `value` -- (int) Brightness byte value from 0-255.
+ """
+ if value > 0:
+ return max(1, int((value / 255) * 99))
+ return 0
+
+
def ct_to_hs(temp):
"""Convert color temperature (mireds) to hs."""
colorlist = list(
@@ -187,7 +197,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
# brightness. Level 255 means to set it to previous value.
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
- brightness = int((self._brightness / 255) * 99)
+ brightness = byte_to_zwave_brightness(self._brightness)
else:
brightness = 255
diff --git a/homeassistant/components/logi_circle.py b/homeassistant/components/logi_circle.py
new file mode 100644
index 00000000000..c0a7f4c2621
--- /dev/null
+++ b/homeassistant/components/logi_circle.py
@@ -0,0 +1,80 @@
+"""
+Support for Logi Circle cameras.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/logi_circle/
+"""
+import logging
+import asyncio
+
+import voluptuous as vol
+import async_timeout
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
+
+REQUIREMENTS = ['logi_circle==0.1.7']
+
+_LOGGER = logging.getLogger(__name__)
+_TIMEOUT = 15 # seconds
+
+CONF_ATTRIBUTION = "Data provided by circle.logi.com"
+
+NOTIFICATION_ID = 'logi_notification'
+NOTIFICATION_TITLE = 'Logi Circle Setup'
+
+DOMAIN = 'logi_circle'
+DEFAULT_CACHEDB = '.logi_cache.pickle'
+DEFAULT_ENTITY_NAMESPACE = 'logi_circle'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+
+async def async_setup(hass, config):
+ """Set up the Logi Circle component."""
+ conf = config[DOMAIN]
+ username = conf[CONF_USERNAME]
+ password = conf[CONF_PASSWORD]
+
+ try:
+ from logi_circle import Logi
+ from logi_circle.exception import BadLogin
+ from aiohttp.client_exceptions import ClientResponseError
+
+ cache = hass.config.path(DEFAULT_CACHEDB)
+ logi = Logi(username=username, password=password, cache_file=cache)
+
+ with async_timeout.timeout(_TIMEOUT, loop=hass.loop):
+ await logi.login()
+ hass.data[DOMAIN] = await logi.cameras
+
+ if not logi.is_connected:
+ return False
+ except (BadLogin, ClientResponseError) as ex:
+ _LOGGER.error('Unable to connect to Logi Circle API: %s', str(ex))
+ hass.components.persistent_notification.create(
+ 'Error: {}
'
+ 'You will need to restart hass after fixing.'
+ ''.format(ex),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ return False
+ except asyncio.TimeoutError:
+ # The TimeoutError exception object returns nothing when casted to a
+ # string, so we'll handle it separately.
+ err = '{}s timeout exceeded when connecting to Logi Circle API'.format(
+ _TIMEOUT)
+ _LOGGER.error(err)
+ hass.components.persistent_notification.create(
+ 'Error: {}
'
+ 'You will need to restart hass after fixing.'
+ ''.format(err),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ return False
+ return True
diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py
index 0c5dabb6eeb..2ed12b23164 100644
--- a/homeassistant/components/mailbox/__init__.py
+++ b/homeassistant/components/mailbox/__init__.py
@@ -23,36 +23,34 @@ from homeassistant.setup import async_prepare_setup_platform
_LOGGER = logging.getLogger(__name__)
-CONTENT_TYPE_MPEG = 'audio/mpeg'
-
DEPENDENCIES = ['http']
DOMAIN = 'mailbox'
EVENT = 'mailbox_updated'
+CONTENT_TYPE_MPEG = 'audio/mpeg'
+CONTENT_TYPE_NONE = 'none'
SCAN_INTERVAL = timedelta(seconds=30)
-@asyncio.coroutine
-def async_setup(hass, config):
+async def async_setup(hass, config):
"""Track states and offer events for mailboxes."""
mailboxes = []
- yield from hass.components.frontend.async_register_built_in_panel(
+ await hass.components.frontend.async_register_built_in_panel(
'mailbox', 'mailbox', 'mdi:mailbox')
hass.http.register_view(MailboxPlatformsView(mailboxes))
hass.http.register_view(MailboxMessageView(mailboxes))
hass.http.register_view(MailboxMediaView(mailboxes))
hass.http.register_view(MailboxDeleteView(mailboxes))
- @asyncio.coroutine
- def async_setup_platform(p_type, p_config=None, discovery_info=None):
+ async def async_setup_platform(p_type, p_config=None, discovery_info=None):
"""Set up a mailbox platform."""
if p_config is None:
p_config = {}
if discovery_info is None:
discovery_info = {}
- platform = yield from async_prepare_setup_platform(
+ platform = await async_prepare_setup_platform(
hass, config, DOMAIN, p_type)
if platform is None:
@@ -63,10 +61,10 @@ def async_setup(hass, config):
mailbox = None
try:
if hasattr(platform, 'async_get_handler'):
- mailbox = yield from \
+ mailbox = await \
platform.async_get_handler(hass, p_config, discovery_info)
elif hasattr(platform, 'get_handler'):
- mailbox = yield from hass.async_add_job(
+ mailbox = await hass.async_add_executor_job(
platform.get_handler, hass, p_config, discovery_info)
else:
raise HomeAssistantError("Invalid mailbox platform.")
@@ -81,21 +79,20 @@ def async_setup(hass, config):
return
mailboxes.append(mailbox)
- mailbox_entity = MailboxEntity(hass, mailbox)
+ mailbox_entity = MailboxEntity(mailbox)
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
- yield from component.async_add_entities([mailbox_entity])
+ await component.async_add_entities([mailbox_entity])
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
in config_per_platform(config, DOMAIN)]
if setup_tasks:
- yield from asyncio.wait(setup_tasks, loop=hass.loop)
+ await asyncio.wait(setup_tasks, loop=hass.loop)
- @asyncio.coroutine
- def async_platform_discovered(platform, info):
+ async def async_platform_discovered(platform, info):
"""Handle for discovered platform."""
- yield from async_setup_platform(platform, discovery_info=info)
+ await async_setup_platform(platform, discovery_info=info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
@@ -103,19 +100,21 @@ def async_setup(hass, config):
class MailboxEntity(Entity):
- """Entity for each mailbox platform."""
+ """Entity for each mailbox platform to provide a badge display."""
- def __init__(self, hass, mailbox):
+ def __init__(self, mailbox):
"""Initialize mailbox entity."""
self.mailbox = mailbox
- self.hass = hass
self.message_count = 0
+ async def async_added_to_hass(self):
+ """Complete entity initialization."""
@callback
def _mailbox_updated(event):
self.async_schedule_update_ha_state(True)
- hass.bus.async_listen(EVENT, _mailbox_updated)
+ self.hass.bus.async_listen(EVENT, _mailbox_updated)
+ self.async_schedule_update_ha_state(True)
@property
def state(self):
@@ -127,10 +126,9 @@ class MailboxEntity(Entity):
"""Return the name of the entity."""
return self.mailbox.name
- @asyncio.coroutine
- def async_update(self):
+ async def async_update(self):
"""Retrieve messages from platform."""
- messages = yield from self.mailbox.async_get_messages()
+ messages = await self.mailbox.async_get_messages()
self.message_count = len(messages)
@@ -151,13 +149,21 @@ class Mailbox:
"""Return the supported media type."""
raise NotImplementedError()
- @asyncio.coroutine
- def async_get_media(self, msgid):
+ @property
+ def can_delete(self):
+ """Return if messages can be deleted."""
+ return False
+
+ @property
+ def has_media(self):
+ """Return if messages have attached media files."""
+ return False
+
+ async def async_get_media(self, msgid):
"""Return the media blob for the msgid."""
raise NotImplementedError()
- @asyncio.coroutine
- def async_get_messages(self):
+ async def async_get_messages(self):
"""Return a list of the current messages."""
raise NotImplementedError()
@@ -193,12 +199,16 @@ class MailboxPlatformsView(MailboxView):
url = "/api/mailbox/platforms"
name = "api:mailbox:platforms"
- @asyncio.coroutine
- def get(self, request):
+ async def get(self, request):
"""Retrieve list of platforms."""
platforms = []
for mailbox in self.mailboxes:
- platforms.append(mailbox.name)
+ platforms.append(
+ {
+ 'name': mailbox.name,
+ 'has_media': mailbox.has_media,
+ 'can_delete': mailbox.can_delete
+ })
return self.json(platforms)
@@ -208,11 +218,10 @@ class MailboxMessageView(MailboxView):
url = "/api/mailbox/messages/{platform}"
name = "api:mailbox:messages"
- @asyncio.coroutine
- def get(self, request, platform):
+ async def get(self, request, platform):
"""Retrieve messages."""
mailbox = self.get_mailbox(platform)
- messages = yield from mailbox.async_get_messages()
+ messages = await mailbox.async_get_messages()
return self.json(messages)
@@ -222,8 +231,7 @@ class MailboxDeleteView(MailboxView):
url = "/api/mailbox/delete/{platform}/{msgid}"
name = "api:mailbox:delete"
- @asyncio.coroutine
- def delete(self, request, platform, msgid):
+ async def delete(self, request, platform, msgid):
"""Delete items."""
mailbox = self.get_mailbox(platform)
mailbox.async_delete(msgid)
@@ -235,8 +243,7 @@ class MailboxMediaView(MailboxView):
url = r"/api/mailbox/media/{platform}/{msgid}"
name = "api:asteriskmbox:media"
- @asyncio.coroutine
- def get(self, request, platform, msgid):
+ async def get(self, request, platform, msgid):
"""Retrieve media."""
mailbox = self.get_mailbox(platform)
@@ -244,7 +251,7 @@ class MailboxMediaView(MailboxView):
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(10, loop=hass.loop):
try:
- stream = yield from mailbox.async_get_media(msgid)
+ stream = await mailbox.async_get_media(msgid)
except StreamError as err:
error_msg = "Error getting media: %s" % (err)
_LOGGER.error(error_msg)
diff --git a/homeassistant/components/mailbox/asterisk_cdr.py b/homeassistant/components/mailbox/asterisk_cdr.py
new file mode 100644
index 00000000000..ae0939c3da5
--- /dev/null
+++ b/homeassistant/components/mailbox/asterisk_cdr.py
@@ -0,0 +1,64 @@
+"""
+Asterisk CDR interface.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/mailbox.asterisk_cdr/
+"""
+import logging
+import hashlib
+import datetime
+
+from homeassistant.core import callback
+from homeassistant.components.asterisk_mbox import SIGNAL_CDR_UPDATE
+from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN
+from homeassistant.components.mailbox import Mailbox
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+DEPENDENCIES = ['asterisk_mbox']
+_LOGGER = logging.getLogger(__name__)
+MAILBOX_NAME = "asterisk_cdr"
+
+
+async def async_get_handler(hass, config, discovery_info=None):
+ """Set up the Asterix CDR platform."""
+ return AsteriskCDR(hass, MAILBOX_NAME)
+
+
+class AsteriskCDR(Mailbox):
+ """Asterisk VM Call Data Record mailbox."""
+
+ def __init__(self, hass, name):
+ """Initialize Asterisk CDR."""
+ super().__init__(hass, name)
+ self.cdr = []
+ async_dispatcher_connect(
+ self.hass, SIGNAL_CDR_UPDATE, self._update_callback)
+
+ @callback
+ def _update_callback(self, msg):
+ """Update the message count in HA, if needed."""
+ self._build_message()
+ self.async_update()
+
+ def _build_message(self):
+ """Build message structure."""
+ cdr = []
+ for entry in self.hass.data[ASTERISK_DOMAIN].cdr:
+ timestamp = datetime.datetime.strptime(
+ entry['time'], "%Y-%m-%d %H:%M:%S").timestamp()
+ info = {
+ 'origtime': timestamp,
+ 'callerid': entry['callerid'],
+ 'duration': entry['duration'],
+ }
+ sha = hashlib.sha256(str(entry).encode('utf-8')).hexdigest()
+ msg = "Destination: {}\nApplication: {}\n Context: {}".format(
+ entry['dest'], entry['application'], entry['context'])
+ cdr.append({'info': info, 'sha': sha, 'text': msg})
+ self.cdr = cdr
+
+ async def async_get_messages(self):
+ """Return a list of the current messages."""
+ if not self.cdr:
+ self._build_message()
+ return self.cdr
diff --git a/homeassistant/components/mailbox/asterisk_mbox.py b/homeassistant/components/mailbox/asterisk_mbox.py
index 29b34f3e512..087018084f2 100644
--- a/homeassistant/components/mailbox/asterisk_mbox.py
+++ b/homeassistant/components/mailbox/asterisk_mbox.py
@@ -4,10 +4,9 @@ Asterisk Voicemail interface.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/mailbox.asteriskvm/
"""
-import asyncio
import logging
-from homeassistant.components.asterisk_mbox import DOMAIN
+from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN
from homeassistant.components.mailbox import (
CONTENT_TYPE_MPEG, Mailbox, StreamError)
from homeassistant.core import callback
@@ -21,10 +20,9 @@ SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
-@asyncio.coroutine
-def async_get_handler(hass, config, async_add_entities, discovery_info=None):
+async def async_get_handler(hass, config, discovery_info=None):
"""Set up the Asterix VM platform."""
- return AsteriskMailbox(hass, DOMAIN)
+ return AsteriskMailbox(hass, ASTERISK_DOMAIN)
class AsteriskMailbox(Mailbox):
@@ -46,24 +44,32 @@ class AsteriskMailbox(Mailbox):
"""Return the supported media type."""
return CONTENT_TYPE_MPEG
- @asyncio.coroutine
- def async_get_media(self, msgid):
+ @property
+ def can_delete(self):
+ """Return if messages can be deleted."""
+ return True
+
+ @property
+ def has_media(self):
+ """Return if messages have attached media files."""
+ return True
+
+ async def async_get_media(self, msgid):
"""Return the media blob for the msgid."""
from asterisk_mbox import ServerError
- client = self.hass.data[DOMAIN].client
+ client = self.hass.data[ASTERISK_DOMAIN].client
try:
return client.mp3(msgid, sync=True)
except ServerError as err:
raise StreamError(err)
- @asyncio.coroutine
- def async_get_messages(self):
+ async def async_get_messages(self):
"""Return a list of the current messages."""
- return self.hass.data[DOMAIN].messages
+ return self.hass.data[ASTERISK_DOMAIN].messages
def async_delete(self, msgid):
"""Delete the specified messages."""
- client = self.hass.data[DOMAIN].client
+ client = self.hass.data[ASTERISK_DOMAIN].client
_LOGGER.info("Deleting: %s", msgid)
client.delete(msgid)
return True
diff --git a/homeassistant/components/mailbox/demo.py b/homeassistant/components/mailbox/demo.py
index e0d2618ac4e..2aabde42b36 100644
--- a/homeassistant/components/mailbox/demo.py
+++ b/homeassistant/components/mailbox/demo.py
@@ -4,7 +4,6 @@ Asterisk Voicemail interface.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/mailbox.asteriskvm/
"""
-import asyncio
from hashlib import sha1
import logging
import os
@@ -15,13 +14,12 @@ from homeassistant.util import dt
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "DemoMailbox"
+MAILBOX_NAME = "DemoMailbox"
-@asyncio.coroutine
-def async_get_handler(hass, config, discovery_info=None):
+async def async_get_handler(hass, config, discovery_info=None):
"""Set up the Demo mailbox."""
- return DemoMailbox(hass, DOMAIN)
+ return DemoMailbox(hass, MAILBOX_NAME)
class DemoMailbox(Mailbox):
@@ -54,8 +52,17 @@ class DemoMailbox(Mailbox):
"""Return the supported media type."""
return CONTENT_TYPE_MPEG
- @asyncio.coroutine
- def async_get_media(self, msgid):
+ @property
+ def can_delete(self):
+ """Return if messages can be deleted."""
+ return True
+
+ @property
+ def has_media(self):
+ """Return if messages have attached media files."""
+ return True
+
+ async def async_get_media(self, msgid):
"""Return the media blob for the msgid."""
if msgid not in self._messages:
raise StreamError("Message not found")
@@ -65,8 +72,7 @@ class DemoMailbox(Mailbox):
with open(audio_path, 'rb') as file:
return file.read()
- @asyncio.coroutine
- def async_get_messages(self):
+ async def async_get_messages(self):
"""Return a list of the current messages."""
return sorted(self._messages.values(),
key=lambda item: item['info']['origtime'],
diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py
index 0d7d76c4447..a7093579805 100644
--- a/homeassistant/components/media_extractor.py
+++ b/homeassistant/components/media_extractor.py
@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv
-REQUIREMENTS = ['youtube_dl==2018.09.10']
+REQUIREMENTS = ['youtube_dl==2018.09.18']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py
index e14244793cf..67d8ea0b419 100644
--- a/homeassistant/components/media_player/cast.py
+++ b/homeassistant/components/media_player/cast.py
@@ -7,7 +7,6 @@ https://home-assistant.io/components/media_player.cast/
import asyncio
import logging
import threading
-
from typing import Optional, Tuple
import attr
@@ -62,10 +61,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(cv.ensure_list, [cv.string]),
})
-CONNECTION_RETRY = 3
-CONNECTION_RETRY_WAIT = 2
-CONNECTION_TIMEOUT = 10
-
@attr.s(slots=True, frozen=True)
class ChromecastInfo:
@@ -217,9 +212,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if not isinstance(config, list):
config = [config]
- await asyncio.wait([
+ # no pending task
+ done, _ = await asyncio.wait([
_async_setup_platform(hass, cfg, async_add_entities, None)
for cfg in config])
+ if any([task.exception() for task in done]):
+ raise PlatformNotReady
async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType,
@@ -251,8 +249,8 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType,
if cast_device is not None:
async_add_entities([cast_device])
- async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED,
- async_cast_discovered)
+ remove_handler = async_dispatcher_connect(
+ hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered)
# Re-play the callback for all past chromecasts, store the objects in
# a list to avoid concurrent modification resulting in exception.
for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]):
@@ -266,8 +264,11 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType,
info = await hass.async_add_job(_fill_out_missing_chromecast_info,
info)
if info.friendly_name is None:
- # HTTP dial failed, so we won't be able to connect.
+ _LOGGER.debug("Cannot retrieve detail information for chromecast"
+ " %s, the device may not be online", info)
+ remove_handler()
raise PlatformNotReady
+
hass.async_add_job(_discover_chromecast, hass, info)
@@ -380,7 +381,7 @@ class CastDevice(MediaPlayerDevice):
pychromecast._get_chromecast_from_host, (
cast_info.host, cast_info.port, cast_info.uuid,
cast_info.model_name, cast_info.friendly_name
- ), CONNECTION_RETRY, CONNECTION_RETRY_WAIT, CONNECTION_TIMEOUT)
+ ))
self._chromecast = chromecast
self._status_listener = CastStatusListener(self, chromecast)
# Initialise connection status as connected because we can only
diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py
index efe04c7005b..d3e56c4dfb1 100644
--- a/homeassistant/components/media_player/panasonic_viera.py
+++ b/homeassistant/components/media_player/panasonic_viera.py
@@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['panasonic_viera==0.3.1', 'wakeonlan==1.0.0']
+REQUIREMENTS = ['panasonic_viera==0.3.1', 'wakeonlan==1.1.6']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py
index cc966c0d263..c0a5d617f19 100644
--- a/homeassistant/components/media_player/samsungtv.py
+++ b/homeassistant/components/media_player/samsungtv.py
@@ -24,7 +24,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt as dt_util
-REQUIREMENTS = ['samsungctl[websocket]==0.7.1', 'wakeonlan==1.0.0']
+REQUIREMENTS = ['samsungctl[websocket]==0.7.1', 'wakeonlan==1.1.6']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py
index 72ac0a046a3..fd735a5b830 100644
--- a/homeassistant/components/media_player/sonos.py
+++ b/homeassistant/components/media_player/sonos.py
@@ -1,5 +1,5 @@
"""
-Support to interface with Sonos players (via SoCo).
+Support to interface with Sonos players.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.sonos/
@@ -31,11 +31,11 @@ DEPENDENCIES = ('sonos',)
_LOGGER = logging.getLogger(__name__)
-# Quiet down soco logging to just actual problems.
-logging.getLogger('soco').setLevel(logging.WARNING)
-logging.getLogger('soco.events').setLevel(logging.ERROR)
-logging.getLogger('soco.data_structures_entry').setLevel(logging.ERROR)
-_SOCO_SERVICES_LOGGER = logging.getLogger('soco.services')
+# Quiet down pysonos logging to just actual problems.
+logging.getLogger('pysonos').setLevel(logging.WARNING)
+logging.getLogger('pysonos.events').setLevel(logging.ERROR)
+logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR)
+_SOCO_SERVICES_LOGGER = logging.getLogger('pysonos.services')
SUPPORT_SONOS = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_SELECT_SOURCE |\
@@ -143,18 +143,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
def _setup_platform(hass, config, add_entities, discovery_info):
"""Set up the Sonos platform."""
- import soco
+ import pysonos
if DATA_SONOS not in hass.data:
hass.data[DATA_SONOS] = SonosData()
advertise_addr = config.get(CONF_ADVERTISE_ADDR)
if advertise_addr:
- soco.config.EVENT_ADVERTISE_IP = advertise_addr
+ pysonos.config.EVENT_ADVERTISE_IP = advertise_addr
players = []
if discovery_info:
- player = soco.SoCo(discovery_info.get('host'))
+ player = pysonos.SoCo(discovery_info.get('host'))
# If device already exists by config
if player.uid in hass.data[DATA_SONOS].uids:
@@ -174,11 +174,11 @@ def _setup_platform(hass, config, add_entities, discovery_info):
hosts = hosts.split(',') if isinstance(hosts, str) else hosts
for host in hosts:
try:
- players.append(soco.SoCo(socket.gethostbyname(host)))
+ players.append(pysonos.SoCo(socket.gethostbyname(host)))
except OSError:
_LOGGER.warning("Failed to initialize '%s'", host)
else:
- players = soco.discover(
+ players = pysonos.discover(
interface_addr=config.get(CONF_INTERFACE_ADDR))
if not players:
@@ -287,7 +287,7 @@ def soco_error(errorcodes=None):
@ft.wraps(funct)
def wrapper(*args, **kwargs):
"""Wrap for all soco UPnP exception."""
- from soco.exceptions import SoCoUPnPException, SoCoException
+ from pysonos.exceptions import SoCoUPnPException, SoCoException
# Temporarily disable SoCo logging because it will log the
# UPnP exception otherwise
@@ -612,9 +612,9 @@ class SonosDevice(MediaPlayerDevice):
current_uri_metadata = media_info["CurrentURIMetaData"]
if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None):
# currently soco does not have an API for this
- import soco
- current_uri_metadata = soco.xml.XML.fromstring(
- soco.utils.really_utf8(current_uri_metadata))
+ import pysonos
+ current_uri_metadata = pysonos.xml.XML.fromstring(
+ pysonos.utils.really_utf8(current_uri_metadata))
md_title = current_uri_metadata.findtext(
'.//{http://purl.org/dc/elements/1.1/}title')
@@ -950,7 +950,7 @@ class SonosDevice(MediaPlayerDevice):
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
"""
if kwargs.get(ATTR_MEDIA_ENQUEUE):
- from soco.exceptions import SoCoUPnPException
+ from pysonos.exceptions import SoCoUPnPException
try:
self.soco.add_uri_to_queue(media_id)
except SoCoUPnPException:
@@ -981,7 +981,7 @@ class SonosDevice(MediaPlayerDevice):
@soco_error()
def snapshot(self, with_group=True):
"""Snapshot the player."""
- from soco.snapshot import Snapshot
+ from pysonos.snapshot import Snapshot
self._soco_snapshot = Snapshot(self.soco)
self._soco_snapshot.snapshot()
@@ -996,7 +996,7 @@ class SonosDevice(MediaPlayerDevice):
@soco_error()
def restore(self, with_group=True):
"""Restore snapshot for the player."""
- from soco.exceptions import SoCoException
+ from pysonos.exceptions import SoCoException
try:
# need catch exception if a coordinator is going to slave.
# this state will recover with group part.
@@ -1060,7 +1060,7 @@ class SonosDevice(MediaPlayerDevice):
@soco_coordinator
def set_alarm(self, **data):
"""Set the alarm clock on the player."""
- from soco import alarms
+ from pysonos import alarms
alarm = None
for one_alarm in alarms.get_alarms(self.soco):
# pylint: disable=protected-access
diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py
index a16658501cb..b8ade374a46 100644
--- a/homeassistant/components/media_player/soundtouch.py
+++ b/homeassistant/components/media_player/soundtouch.py
@@ -297,7 +297,7 @@ class SoundTouchDevice(MediaPlayerDevice):
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
_LOGGER.debug("Starting media with media_id: %s", media_id)
- if re.match(r'http://', str(media_id)):
+ if re.match(r'https?://', str(media_id)):
# URL
_LOGGER.debug("Playing URL %s", str(media_id))
self._device.play_url(str(media_id))
diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py
index d78619a8279..0a5b9fe509b 100644
--- a/homeassistant/components/media_player/webostv.py
+++ b/homeassistant/components/media_player/webostv.py
@@ -8,9 +8,7 @@ import asyncio
from datetime import timedelta
import logging
from urllib.parse import urlparse
-
-# pylint: disable=unused-import
-from typing import Dict # noqa: F401
+from typing import Dict # noqa: F401 pylint: disable=unused-import
import voluptuous as vol
diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json
index 57e9a83d201..b6c73f35f26 100644
--- a/homeassistant/components/mqtt/.translations/ca.json
+++ b/homeassistant/components/mqtt/.translations/ca.json
@@ -10,6 +10,7 @@
"broker": {
"data": {
"broker": "Broker",
+ "discovery": "Activar descobreix automaticament",
"password": "Contrasenya",
"port": "Port",
"username": "Nom d'usuari"
diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json
new file mode 100644
index 00000000000..2a35e95f559
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Nur eine einzige Konfiguration von MQTT ist zul\u00e4ssig."
+ },
+ "error": {
+ "cannot_connect": "Es konnte keine Verbindung zum Broker hergestellt werden."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "discovery": "Suche aktivieren",
+ "password": "Passwort",
+ "username": "Benutzername"
+ },
+ "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/en.json b/homeassistant/components/mqtt/.translations/en.json
new file mode 100644
index 00000000000..c0b83a1323f
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/en.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of MQTT is allowed."
+ },
+ "error": {
+ "cannot_connect": "Unable to connect to the broker."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Broker",
+ "discovery": "Enable discovery",
+ "password": "Password",
+ "port": "Port",
+ "username": "Username"
+ },
+ "description": "Please enter the connection information of your MQTT broker.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/fr.json b/homeassistant/components/mqtt/.translations/fr.json
index 1870c598e3b..916b4fdaf39 100644
--- a/homeassistant/components/mqtt/.translations/fr.json
+++ b/homeassistant/components/mqtt/.translations/fr.json
@@ -10,6 +10,7 @@
"broker": {
"data": {
"broker": "Broker",
+ "discovery": "Activer la d\u00e9couverte automatique",
"password": "Mot de passe",
"port": "Port",
"username": "Nom d'utilisateur"
diff --git a/homeassistant/components/mqtt/.translations/he.json b/homeassistant/components/mqtt/.translations/he.json
new file mode 100644
index 00000000000..e1e2ed49748
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/he.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc MQTT \u05de\u05d5\u05ea\u05e8\u05ea."
+ },
+ "error": {
+ "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d1\u05e8\u05d5\u05e7\u05e8."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "\u05d1\u05e8\u05d5\u05e7\u05e8",
+ "discovery": "\u05d0\u05e4\u05e9\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "port": "\u05e4\u05d5\u05e8\u05d8",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ },
+ "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/hu.json b/homeassistant/components/mqtt/.translations/hu.json
new file mode 100644
index 00000000000..d85814e917c
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/hu.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Csak egyetlen MQTT konfigur\u00e1ci\u00f3 megengedett."
+ },
+ "error": {
+ "cannot_connect": "Nem siker\u00fclt csatlakozni a br\u00f3kerhez."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Br\u00f3ker",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/id.json b/homeassistant/components/mqtt/.translations/id.json
new file mode 100644
index 00000000000..7a9bf8639e2
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/id.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Hanya satu konfigurasi MQTT yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Tidak dapat terhubung ke broker."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Broker",
+ "password": "Kata sandi",
+ "port": "Port",
+ "username": "Nama pengguna"
+ },
+ "description": "Harap masukkan informasi koneksi dari broker MQTT Anda.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json
index a38b00fd68d..f20658d252c 100644
--- a/homeassistant/components/mqtt/.translations/ko.json
+++ b/homeassistant/components/mqtt/.translations/ko.json
@@ -1,20 +1,21 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\ud558\ub098\uc758 MQTT \ube0c\ub85c\ucee4\ub9cc \uad6c\uc131\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4."
+ "single_instance_allowed": "\ud558\ub098\uc758 MQTT \ube0c\ub85c\ucee4\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"error": {
- "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc74c"
+ "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"step": {
"broker": {
"data": {
"broker": "\ube0c\ub85c\ucee4",
+ "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654",
"password": "\ube44\ubc00\ubc88\ud638",
"port": "\ud3ec\ud2b8",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud558\uc138\uc694.",
+ "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
"title": "MQTT"
}
},
diff --git a/homeassistant/components/mqtt/.translations/lb.json b/homeassistant/components/mqtt/.translations/lb.json
new file mode 100644
index 00000000000..166fce9fbfb
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/lb.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vum MQTT ass erlaabt"
+ },
+ "error": {
+ "cannot_connect": "Kann sech net mam Broker verbannen."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Broker",
+ "discovery": "Entdeckung aktiv\u00e9ieren",
+ "password": "Passwuert",
+ "port": "Port",
+ "username": "Benotzernumm"
+ },
+ "description": "Gitt Verbindungs Informatioune vun \u00e4rem MQTT Broker an.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/nl.json b/homeassistant/components/mqtt/.translations/nl.json
new file mode 100644
index 00000000000..b375f353810
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/nl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Broker",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "username": "Gebruikersnaam"
+ },
+ "description": "MQTT",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/nn.json b/homeassistant/components/mqtt/.translations/nn.json
new file mode 100644
index 00000000000..fb650bc7676
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/nn.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Det er berre lov \u00e5 ha \u00e9in MQTT-konfigurasjon"
+ },
+ "error": {
+ "cannot_connect": "Klarte ikkje \u00e5 kople til meglaren."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Meglar",
+ "password": "Passord",
+ "port": "Port",
+ "username": "Brukarnamn"
+ },
+ "description": "Ver vennleg \u00e5 skriv inn tilkoplingsinformasjonen for MQTT-meglaren din",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json
new file mode 100644
index 00000000000..412efd3e107
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/no.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Kun en enkelt konfigurasjon av MQTT er tillatt."
+ },
+ "error": {
+ "cannot_connect": "Kan ikke koble til megleren."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Megler",
+ "discovery": "Aktiver oppdagelse",
+ "password": "Passord",
+ "port": "Port",
+ "username": "Brukernavn"
+ },
+ "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/pl.json b/homeassistant/components/mqtt/.translations/pl.json
new file mode 100644
index 00000000000..e87e550b98d
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/pl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja MQTT."
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z po\u015brednikiem."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Po\u015brednik",
+ "discovery": "W\u0142\u0105cz wykrywanie",
+ "password": "Has\u0142o",
+ "port": "Port",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/pt-BR.json b/homeassistant/components/mqtt/.translations/pt-BR.json
new file mode 100644
index 00000000000..e73e8b155ec
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/pt-BR.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do MQTT \u00e9 permitida."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "password": "Senha",
+ "port": "Porta",
+ "username": "Nome de usu\u00e1rio"
+ },
+ "description": "Por favor, insira as informa\u00e7\u00f5es de conex\u00e3o do seu agente MQTT.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/pt.json b/homeassistant/components/mqtt/.translations/pt.json
new file mode 100644
index 00000000000..42f7c7f5ad2
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/pt.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do MQTT \u00e9 permitida."
+ },
+ "error": {
+ "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel ligar ao broker."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "",
+ "password": "Palavra-passe",
+ "port": "Porto",
+ "username": "Utilizador"
+ },
+ "description": "Por favor, insira os detalhes de liga\u00e7\u00e3o ao seu broker MQTT.",
+ "title": ""
+ }
+ },
+ "title": ""
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json
new file mode 100644
index 00000000000..f1ff498dd72
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/ru.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u0414\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f MQTT."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "\u0411\u0440\u043e\u043a\u0435\u0440",
+ "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "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 \u0432\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/sl.json b/homeassistant/components/mqtt/.translations/sl.json
new file mode 100644
index 00000000000..a12498ac4c2
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/sl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Dovoljena je samo ena konfiguracija MQTT."
+ },
+ "error": {
+ "cannot_connect": "Ne morem se povezati na posrednik."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Posrednik",
+ "discovery": "Omogo\u010di odkrivanje",
+ "password": "Geslo",
+ "port": "port",
+ "username": "Uporabni\u0161ko ime"
+ },
+ "description": "Prosimo vnesite informacije o povezavi va\u0161ega MQTT posrednika.",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/sv.json b/homeassistant/components/mqtt/.translations/sv.json
new file mode 100644
index 00000000000..7cf6d75b9c1
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/sv.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "broker": {
+ "data": {
+ "password": "L\u00f6senord",
+ "port": "Port",
+ "username": "Anv\u00e4ndarnamn"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/zh-Hans.json b/homeassistant/components/mqtt/.translations/zh-Hans.json
new file mode 100644
index 00000000000..98a7d9eb4be
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/zh-Hans.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u53ea\u5141\u8bb8\u4e00\u4e2a MQTT \u914d\u7f6e\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u3002"
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "\u670d\u52a1\u5668",
+ "discovery": "\u542f\u7528\u53d1\u73b0",
+ "password": "\u5bc6\u7801",
+ "port": "\u7aef\u53e3",
+ "username": "\u7528\u6237\u540d"
+ },
+ "description": "\u8bf7\u8f93\u5165\u60a8\u7684 MQTT \u670d\u52a1\u5668\u7684\u8fde\u63a5\u4fe1\u606f\u3002",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/zh-Hant.json b/homeassistant/components/mqtt/.translations/zh-Hant.json
new file mode 100644
index 00000000000..cf87ceb8f98
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/zh-Hant.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 MQTT\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Broker\u3002"
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Broker",
+ "discovery": "\u958b\u555f\u63a2\u7d22",
+ "password": "\u4f7f\u7528\u8005\u5bc6\u78bc",
+ "port": "\u901a\u8a0a\u57e0",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002",
+ "title": "MQTT"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 6bb08d7e8e5..70f20453633 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -6,48 +6,54 @@ https://home-assistant.io/components/mqtt/
"""
import asyncio
from itertools import groupby
-from typing import Optional, Any, Union, Callable, List, cast # noqa: F401
-from operator import attrgetter
import logging
+from operator import attrgetter
import os
import socket
-import time
import ssl
-import requests.certs
-import attr
+import time
+from typing import Any, Callable, List, Optional, Union, cast # noqa: F401
+import attr
+import requests.certs
import voluptuous as vol
-from homeassistant.helpers.typing import HomeAssistantType, ConfigType, \
- ServiceDataType
-from homeassistant.core import callback, Event, ServiceCall
-from homeassistant.setup import async_prepare_setup_platform
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.loader import bind_hass
-from homeassistant.helpers import template, config_validation as cv
-from homeassistant.helpers.entity import Entity
-from homeassistant.util.async_ import (
- run_coroutine_threadsafe, run_callback_threadsafe)
+from homeassistant import config_entries
from homeassistant.const import (
- EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME,
- CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
+ CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, CONF_USERNAME,
+ CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP)
+from homeassistant.core import Event, ServiceCall, callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import template
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import (
+ ConfigType, HomeAssistantType, ServiceDataType)
+from homeassistant.loader import bind_hass
+from homeassistant.setup import async_prepare_setup_platform
+from homeassistant.util.async_ import (
+ run_callback_threadsafe, run_coroutine_threadsafe)
+# Loading the config flow file will register the flow
+from . import config_flow # noqa # pylint: disable=unused-import
+from .const import CONF_BROKER, CONF_DISCOVERY, DEFAULT_DISCOVERY
from .server import HBMQTT_CONFIG_SCHEMA
-REQUIREMENTS = ['paho-mqtt==1.3.1']
+REQUIREMENTS = ['paho-mqtt==1.4.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'mqtt'
DATA_MQTT = 'mqtt'
+DATA_MQTT_CONFIG = 'mqtt_config'
+DATA_MQTT_HASS_CONFIG = 'mqtt_hass_config'
SERVICE_PUBLISH = 'publish'
CONF_EMBEDDED = 'embedded'
-CONF_BROKER = 'broker'
+
CONF_CLIENT_ID = 'client_id'
-CONF_DISCOVERY = 'discovery'
CONF_DISCOVERY_PREFIX = 'discovery_prefix'
CONF_KEEPALIVE = 'keepalive'
CONF_CERTIFICATE = 'certificate'
@@ -75,7 +81,6 @@ DEFAULT_KEEPALIVE = 60
DEFAULT_QOS = 0
DEFAULT_RETAIN = False
DEFAULT_PROTOCOL = PROTOCOL_311
-DEFAULT_DISCOVERY = False
DEFAULT_DISCOVERY_PREFIX = 'homeassistant'
DEFAULT_TLS_PROTOCOL = 'auto'
DEFAULT_PAYLOAD_AVAILABLE = 'online'
@@ -86,6 +91,7 @@ ATTR_PAYLOAD = 'payload'
ATTR_PAYLOAD_TEMPLATE = 'payload_template'
ATTR_QOS = CONF_QOS
ATTR_RETAIN = CONF_RETAIN
+ATTR_DISCOVERY_HASH = 'discovery_hash'
MAX_RECONNECT_WAIT = 300 # seconds
@@ -290,8 +296,7 @@ def subscribe(hass: HomeAssistantType, topic: str,
return remove
-async def _async_setup_server(hass: HomeAssistantType,
- config: ConfigType):
+async def _async_setup_server(hass: HomeAssistantType, config: ConfigType):
"""Try to start embedded MQTT broker.
This method is a coroutine.
@@ -311,26 +316,25 @@ async def _async_setup_server(hass: HomeAssistantType,
if not success:
return None
+
return broker_config
-async def _async_setup_discovery(hass: HomeAssistantType,
- config: ConfigType) -> bool:
+async def _async_setup_discovery(hass: HomeAssistantType, conf: ConfigType,
+ hass_config: ConfigType) -> bool:
"""Try to start the discovery of MQTT devices.
This method is a coroutine.
"""
- conf = config.get(DOMAIN, {}) # type: ConfigType
-
discovery = await async_prepare_setup_platform(
- hass, config, DOMAIN, 'discovery')
+ hass, hass_config, DOMAIN, 'discovery')
if discovery is None:
_LOGGER.error("Unable to load MQTT discovery")
return False
success = await discovery.async_start(
- hass, conf[CONF_DISCOVERY_PREFIX], config) # type: bool
+ hass, conf[CONF_DISCOVERY_PREFIX], hass_config) # type: bool
return success
@@ -339,20 +343,21 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Start the MQTT protocol service."""
conf = config.get(DOMAIN) # type: Optional[ConfigType]
+ # We need this because discovery can cause components to be set up and
+ # otherwise it will not load the users config.
+ # This needs a better solution.
+ hass.data[DATA_MQTT_HASS_CONFIG] = config
+
if conf is None:
- conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
- conf = cast(ConfigType, conf)
+ # If we have a config entry, setup is done by that config entry.
+ # If there is no config entry, this should fail.
+ return bool(hass.config_entries.async_entries(DOMAIN))
- client_id = conf.get(CONF_CLIENT_ID) # type: Optional[str]
- keepalive = conf.get(CONF_KEEPALIVE) # type: int
+ conf = dict(conf)
- # Only setup if embedded config passed in or no broker specified
- if CONF_EMBEDDED not in conf and CONF_BROKER in conf:
- broker_config = None
- else:
+ if CONF_EMBEDDED in conf or CONF_BROKER not in conf:
if (conf.get(CONF_PASSWORD) is None and
- config.get('http') is not None and
- config['http'].get('api_password') is not None):
+ config.get('http', {}).get('api_password') is not None):
_LOGGER.error(
"Starting from release 0.76, the embedded MQTT broker does not"
" use api_password as default password anymore. Please set"
@@ -362,48 +367,91 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
broker_config = await _async_setup_server(hass, config)
- if CONF_BROKER in conf:
- broker = conf[CONF_BROKER] # type: str
- port = conf[CONF_PORT] # type: int
- username = conf.get(CONF_USERNAME) # type: Optional[str]
- password = conf.get(CONF_PASSWORD) # type: Optional[str]
- certificate = conf.get(CONF_CERTIFICATE) # type: Optional[str]
- client_key = conf.get(CONF_CLIENT_KEY) # type: Optional[str]
- client_cert = conf.get(CONF_CLIENT_CERT) # type: Optional[str]
- tls_insecure = conf.get(CONF_TLS_INSECURE) # type: Optional[bool]
- protocol = conf[CONF_PROTOCOL] # type: str
- elif broker_config is not None:
- # If no broker passed in, auto config to internal server
- broker, port, username, password, certificate, protocol = broker_config
- # Embedded broker doesn't have some ssl variables
- client_key, client_cert, tls_insecure = None, None, None
- # hbmqtt requires a client id to be set.
- if client_id is None:
- client_id = 'home-assistant'
- else:
- err = "Unable to start MQTT broker."
- if conf.get(CONF_EMBEDDED) is not None:
- # Explicit embedded config, requires explicit broker config
- err += " (Broker configuration required.)"
- _LOGGER.error(err)
+ if broker_config is None:
+ _LOGGER.error("Unable to start embedded MQTT broker")
+ return False
+
+ conf.update({
+ CONF_BROKER: broker_config[0],
+ CONF_PORT: broker_config[1],
+ CONF_USERNAME: broker_config[2],
+ CONF_PASSWORD: broker_config[3],
+ CONF_CERTIFICATE: broker_config[4],
+ CONF_PROTOCOL: broker_config[5],
+ CONF_CLIENT_KEY: None,
+ CONF_CLIENT_CERT: None,
+ CONF_TLS_INSECURE: None,
+ })
+
+ hass.data[DATA_MQTT_CONFIG] = conf
+
+ # Only import if we haven't before.
+ if not hass.config_entries.async_entries(DOMAIN):
+ hass.async_create_task(hass.config_entries.flow.async_init(
+ DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
+ data={}
+ ))
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Load a config entry."""
+ conf = hass.data.get(DATA_MQTT_CONFIG)
+
+ # Config entry was created because user had configuration.yaml entry
+ # They removed that, so remove entry.
+ if conf is None and entry.source == config_entries.SOURCE_IMPORT:
+ hass.async_create_task(
+ hass.config_entries.async_remove(entry.entry_id))
return False
+ # If user didn't have configuration.yaml config, generate defaults
+ if conf is None:
+ 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 "
+ "configuration.yaml: %s", entry.data)
+
+ conf.update(entry.data)
+
+ broker = conf[CONF_BROKER]
+ port = conf[CONF_PORT]
+ client_id = conf.get(CONF_CLIENT_ID)
+ keepalive = conf[CONF_KEEPALIVE]
+ username = conf.get(CONF_USERNAME)
+ password = conf.get(CONF_PASSWORD)
+ client_key = conf.get(CONF_CLIENT_KEY)
+ client_cert = conf.get(CONF_CLIENT_CERT)
+ tls_insecure = conf.get(CONF_TLS_INSECURE)
+ protocol = conf[CONF_PROTOCOL]
+
# For cloudmqtt.com, secured connection, auto fill in certificate
- if (certificate is None and 19999 < port < 30000 and
- broker.endswith('.cloudmqtt.com')):
- certificate = os.path.join(os.path.dirname(__file__),
- 'addtrustexternalcaroot.crt')
+ if (conf.get(CONF_CERTIFICATE) is None and
+ 19999 < conf[CONF_PORT] < 30000 and
+ conf[CONF_BROKER].endswith('.cloudmqtt.com')):
+ certificate = os.path.join(
+ os.path.dirname(__file__), 'addtrustexternalcaroot.crt')
# When the certificate is set to auto, use bundled certs from requests
- if certificate == 'auto':
+ elif conf.get(CONF_CERTIFICATE) == 'auto':
certificate = requests.certs.where()
- will_message = None # type: Optional[Message]
- if conf.get(CONF_WILL_MESSAGE) is not None:
- will_message = Message(**conf.get(CONF_WILL_MESSAGE))
- birth_message = None # type: Optional[Message]
- if conf.get(CONF_BIRTH_MESSAGE) is not None:
- birth_message = Message(**conf.get(CONF_BIRTH_MESSAGE))
+ else:
+ certificate = None
+
+ if CONF_WILL_MESSAGE in conf:
+ will_message = Message(**conf[CONF_WILL_MESSAGE])
+ else:
+ will_message = None
+
+ if CONF_BIRTH_MESSAGE in conf:
+ birth_message = Message(**conf[CONF_BIRTH_MESSAGE])
+ else:
+ birth_message = None
# Be able to override versions other than TLSv1.0 under Python3.6
conf_tls_version = conf.get(CONF_TLS_VERSION) # type: str
@@ -421,14 +469,27 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
else:
tls_version = ssl.PROTOCOL_TLSv1
- try:
- hass.data[DATA_MQTT] = MQTT(
- hass, broker, port, client_id, keepalive, username, password,
- certificate, client_key, client_cert, tls_insecure, protocol,
- will_message, birth_message, tls_version)
- except socket.error:
- _LOGGER.exception("Can't connect to the broker. "
- "Please check your settings and the broker itself")
+ hass.data[DATA_MQTT] = MQTT(
+ hass,
+ broker=broker,
+ port=port,
+ client_id=client_id,
+ keepalive=keepalive,
+ username=username,
+ password=password,
+ certificate=certificate,
+ client_key=client_key,
+ client_cert=client_cert,
+ tls_insecure=tls_insecure,
+ protocol=protocol,
+ will_message=will_message,
+ birth_message=birth_message,
+ tls_version=tls_version,
+ )
+
+ success = await hass.data[DATA_MQTT].async_connect() # type: bool
+
+ if not success:
return False
async def async_stop_mqtt(event: Event):
@@ -437,10 +498,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt)
- success = await hass.data[DATA_MQTT].async_connect() # type: bool
- if not success:
- return False
-
async def async_publish_service(call: ServiceCall):
"""Handle MQTT publish service calls."""
msg_topic = call.data[ATTR_TOPIC] # type: str
@@ -467,7 +524,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
schema=MQTT_PUBLISH_SCHEMA)
if conf.get(CONF_DISCOVERY):
- await _async_setup_discovery(hass, config)
+ await _async_setup_discovery(
+ hass, conf, hass.data[DATA_MQTT_HASS_CONFIG])
return True
@@ -501,7 +559,8 @@ class MQTT:
certificate: Optional[str], client_key: Optional[str],
client_cert: Optional[str], tls_insecure: Optional[bool],
protocol: Optional[str], will_message: Optional[Message],
- birth_message: Optional[Message], tls_version) -> None:
+ birth_message: Optional[Message],
+ tls_version: Optional[int]) -> None:
"""Initialize Home Assistant MQTT client."""
import paho.mqtt.client as mqtt
@@ -563,12 +622,12 @@ class MQTT:
result = await self.hass.async_add_job(
self._mqttc.connect, self.broker, self.port, self.keepalive)
except OSError as err:
- _LOGGER.error('Failed to connect due to exception: %s', err)
+ _LOGGER.error("Failed to connect due to exception: %s", err)
return False
if result != 0:
import paho.mqtt.client as mqtt
- _LOGGER.error('Failed to connect: %s', mqtt.error_string(result))
+ _LOGGER.error("Failed to connect: %s", mqtt.error_string(result))
return False
self._mqttc.loop_start()
@@ -595,7 +654,7 @@ class MQTT:
This method is a coroutine.
"""
if not isinstance(topic, str):
- raise HomeAssistantError("topic needs to be a string!")
+ raise HomeAssistantError("Topic needs to be a string!")
subscription = Subscription(topic, msg_callback, qos, encoding)
self.subscriptions.append(subscription)
@@ -637,8 +696,8 @@ class MQTT:
self._mqttc.subscribe, topic, qos)
_raise_on_error(result)
- def _mqtt_on_connect(self, _mqttc, _userdata, _flags,
- result_code: int) -> None:
+ def _mqtt_on_connect(
+ self, _mqttc, _userdata, _flags, result_code: int) -> None:
"""On connect callback.
Resubscribe to all topics we were subscribed to and publish birth
@@ -647,7 +706,7 @@ class MQTT:
import paho.mqtt.client as mqtt
if result_code != mqtt.CONNACK_ACCEPTED:
- _LOGGER.error('Unable to connect to the MQTT broker: %s',
+ _LOGGER.error("Unable to connect to the MQTT broker: %s",
mqtt.connack_string(result_code))
self._mqttc.disconnect()
return
@@ -681,14 +740,13 @@ class MQTT:
try:
payload = msg.payload.decode(subscription.encoding)
except (AttributeError, UnicodeDecodeError):
- _LOGGER.warning("Can't decode payload %s on %s "
- "with encoding %s",
- msg.payload, msg.topic,
- subscription.encoding)
+ _LOGGER.warning(
+ "Can't decode payload %s on %s with encoding %s",
+ msg.payload, msg.topic, subscription.encoding)
continue
- self.hass.async_run_job(subscription.callback,
- msg.topic, payload, msg.qos)
+ self.hass.async_run_job(
+ subscription.callback, msg.topic, payload, msg.qos)
def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None:
"""Disconnected callback."""
@@ -750,7 +808,7 @@ class MqttAvailability(Entity):
self._payload_not_available = payload_not_available
async def async_added_to_hass(self) -> None:
- """Subscribe mqtt events.
+ """Subscribe MQTT events.
This method must be run in the event loop and returns a coroutine.
"""
@@ -775,3 +833,36 @@ class MqttAvailability(Entity):
def available(self) -> bool:
"""Return if the device is available."""
return self._available
+
+
+class MqttDiscoveryUpdate(Entity):
+ """Mixin used to handle updated discovery message."""
+
+ def __init__(self, discovery_hash) -> None:
+ """Initialize the discovery update mixin."""
+ self._discovery_hash = discovery_hash
+ self._remove_signal = None
+
+ async def async_added_to_hass(self) -> None:
+ """Subscribe to discovery updates."""
+ from homeassistant.helpers.dispatcher import async_dispatcher_connect
+ from homeassistant.components.mqtt.discovery import (
+ ALREADY_DISCOVERED, MQTT_DISCOVERY_UPDATED)
+
+ @callback
+ def discovery_callback(payload):
+ """Handle discovery update."""
+ _LOGGER.info("Got update for entity with hash: %s '%s'",
+ self._discovery_hash, payload)
+ if not payload:
+ # Empty payload: Remove component
+ _LOGGER.info("Removing component: %s", self.entity_id)
+ self.hass.async_create_task(self.async_remove())
+ del self.hass.data[ALREADY_DISCOVERED][self._discovery_hash]
+ self._remove_signal()
+
+ if self._discovery_hash:
+ self._remove_signal = async_dispatcher_connect(
+ self.hass,
+ MQTT_DISCOVERY_UPDATED.format(self._discovery_hash),
+ discovery_callback)
diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py
new file mode 100644
index 00000000000..22072857b03
--- /dev/null
+++ b/homeassistant/components/mqtt/config_flow.py
@@ -0,0 +1,88 @@
+"""Config flow for MQTT."""
+from collections import OrderedDict
+import queue
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME
+
+from .const import CONF_BROKER, CONF_DISCOVERY, DEFAULT_DISCOVERY
+
+
+@config_entries.HANDLERS.register('mqtt')
+class FlowHandler(config_entries.ConfigFlow):
+ """Handle a config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason='single_instance_allowed')
+
+ return await self.async_step_broker()
+
+ async def async_step_broker(self, user_input=None):
+ """Confirm the setup."""
+ errors = {}
+
+ if user_input is not None:
+ can_connect = await self.hass.async_add_executor_job(
+ try_connection, user_input[CONF_BROKER], user_input[CONF_PORT],
+ user_input.get(CONF_USERNAME), user_input.get(CONF_PASSWORD))
+
+ if can_connect:
+ return self.async_create_entry(
+ title=user_input[CONF_BROKER], data=user_input)
+
+ errors['base'] = 'cannot_connect'
+
+ fields = OrderedDict()
+ fields[vol.Required(CONF_BROKER)] = str
+ fields[vol.Required(CONF_PORT, default=1883)] = vol.Coerce(int)
+ fields[vol.Optional(CONF_USERNAME)] = str
+ fields[vol.Optional(CONF_PASSWORD)] = str
+ fields[vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY)] = bool
+
+ return self.async_show_form(
+ step_id='broker', data_schema=vol.Schema(fields), errors=errors)
+
+ async def async_step_import(self, user_input):
+ """Import a config entry.
+
+ Special type of import, we're not actually going to store any data.
+ Instead, we're going to rely on the values that are in config file.
+ """
+ if self._async_current_entries():
+ return self.async_abort(reason='single_instance_allowed')
+
+ return self.async_create_entry(title='configuration.yaml', data={})
+
+
+def try_connection(broker, port, username, password):
+ """Test if we can connect to an MQTT broker."""
+ import paho.mqtt.client as mqtt
+ client = mqtt.Client()
+ if username and password:
+ client.username_pw_set(username, password)
+
+ result = queue.Queue(maxsize=1)
+
+ def on_connect(client_, userdata, flags, result_code):
+ """Handle connection result."""
+ result.put(result_code == mqtt.CONNACK_ACCEPTED)
+
+ client.on_connect = on_connect
+
+ client.connect_async(broker, port)
+ client.loop_start()
+
+ try:
+ return result.get(timeout=5)
+ except queue.Empty:
+ return False
+ finally:
+ client.disconnect()
+ client.loop_stop()
diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py
new file mode 100644
index 00000000000..3c22001f91c
--- /dev/null
+++ b/homeassistant/components/mqtt/const.py
@@ -0,0 +1,4 @@
+"""Constants used by multiple MQTT modules."""
+CONF_BROKER = 'broker'
+CONF_DISCOVERY = 'discovery'
+DEFAULT_DISCOVERY = False
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index 128c45f1311..f42c1ed58e9 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -9,9 +9,10 @@ import logging
import re
from homeassistant.components import mqtt
-from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.components.mqtt import CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH
from homeassistant.const import CONF_PLATFORM
-from homeassistant.components.mqtt import CONF_STATE_TOPIC
+from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers.dispatcher import async_dispatcher_send
_LOGGER = logging.getLogger(__name__)
@@ -38,6 +39,7 @@ ALLOWED_PLATFORMS = {
}
ALREADY_DISCOVERED = 'mqtt_discovered_components'
+MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}'
async def async_start(hass, discovery_topic, hass_config):
@@ -51,47 +53,53 @@ async def async_start(hass, discovery_topic, hass_config):
_prefix_topic, component, node_id, object_id = match.groups()
- try:
- payload = json.loads(payload)
- except ValueError:
- _LOGGER.warning("Unable to parse JSON %s: %s", object_id, payload)
- return
-
if component not in SUPPORTED_COMPONENTS:
_LOGGER.warning("Component %s is not supported", component)
return
- payload = dict(payload)
- platform = payload.get(CONF_PLATFORM, 'mqtt')
- if platform not in ALLOWED_PLATFORMS.get(component, []):
- _LOGGER.warning("Platform %s (component %s) is not allowed",
- platform, component)
- return
-
- payload[CONF_PLATFORM] = platform
- if CONF_STATE_TOPIC not in payload:
- payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format(
- discovery_topic, component, '%s/' % node_id if node_id else '',
- object_id)
-
- if ALREADY_DISCOVERED not in hass.data:
- hass.data[ALREADY_DISCOVERED] = set()
-
# If present, the node_id will be included in the discovered object id
discovery_id = '_'.join((node_id, object_id)) if node_id else object_id
+ if ALREADY_DISCOVERED not in hass.data:
+ hass.data[ALREADY_DISCOVERED] = {}
+
discovery_hash = (component, discovery_id)
+
if discovery_hash in hass.data[ALREADY_DISCOVERED]:
- _LOGGER.info("Component has already been discovered: %s %s",
- component, discovery_id)
- return
+ _LOGGER.info(
+ "Component has already been discovered: %s %s, sending update",
+ component, discovery_id)
+ async_dispatcher_send(
+ hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), payload)
+ elif payload:
+ # Add component
+ try:
+ payload = json.loads(payload)
+ except ValueError:
+ _LOGGER.warning("Unable to parse JSON %s: '%s'",
+ object_id, payload)
+ return
- hass.data[ALREADY_DISCOVERED].add(discovery_hash)
+ payload = dict(payload)
+ platform = payload.get(CONF_PLATFORM, 'mqtt')
+ if platform not in ALLOWED_PLATFORMS.get(component, []):
+ _LOGGER.warning("Platform %s (component %s) is not allowed",
+ platform, component)
+ return
- _LOGGER.info("Found new component: %s %s", component, discovery_id)
+ payload[CONF_PLATFORM] = platform
+ if CONF_STATE_TOPIC not in payload:
+ payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format(
+ discovery_topic, component,
+ '%s/' % node_id if node_id else '', object_id)
- await async_load_platform(
- hass, component, platform, payload, hass_config)
+ hass.data[ALREADY_DISCOVERED][discovery_hash] = None
+ payload[ATTR_DISCOVERY_HASH] = discovery_hash
+
+ _LOGGER.info("Found new component: %s %s", component, discovery_id)
+
+ await async_load_platform(
+ hass, component, platform, payload, hass_config)
await mqtt.async_subscribe(
hass, discovery_topic + '/#', async_device_message_received, 0)
diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py
index 45529411ed5..dda2214ce46 100644
--- a/homeassistant/components/mqtt/server.py
+++ b/homeassistant/components/mqtt/server.py
@@ -14,6 +14,9 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['hbmqtt==0.9.4']
+
+_LOGGER = logging.getLogger(__name__)
+
DEPENDENCIES = ['http']
# None allows custom config to be created through generate_config
@@ -27,8 +30,6 @@ HBMQTT_CONFIG_SCHEMA = vol.Any(None, vol.Schema({
})
}, extra=vol.ALLOW_EXTRA))
-_LOGGER = logging.getLogger(__name__)
-
@asyncio.coroutine
def async_start(hass, password, server_config):
diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json
new file mode 100644
index 00000000000..0a2cb255cc4
--- /dev/null
+++ b/homeassistant/components/mqtt/strings.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "title": "MQTT",
+ "step": {
+ "broker": {
+ "title": "MQTT",
+ "description": "Please enter the connection information of your MQTT broker.",
+ "data": {
+ "broker": "Broker",
+ "port": "Port",
+ "username": "Username",
+ "password": "Password",
+ "discovery": "Enable discovery"
+ }
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of MQTT is allowed."
+ },
+ "error": {
+ "cannot_connect": "Unable to connect to the broker."
+ }
+ }
+}
diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py
index 25da38e7f75..38af84e3176 100644
--- a/homeassistant/components/neato.py
+++ b/homeassistant/components/neato.py
@@ -17,7 +17,7 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['pybotvac==0.0.9']
+REQUIREMENTS = ['pybotvac==0.0.10']
DOMAIN = 'neato'
NEATO_ROBOTS = 'neato_robots'
diff --git a/homeassistant/components/nest/.translations/ca.json b/homeassistant/components/nest/.translations/ca.json
index 2fb17916aee..e15d0106da8 100644
--- a/homeassistant/components/nest/.translations/ca.json
+++ b/homeassistant/components/nest/.translations/ca.json
@@ -9,7 +9,7 @@
"error": {
"internal_error": "Error intern al validar el codi",
"invalid_code": "Codi inv\u00e0lid",
- "timeout": "Temps d'espera de validaci\u00f3 del codi esgotat",
+ "timeout": "S'ha acabat el temps d'espera durant la validaci\u00f3 del codi.",
"unknown": "Error desconegut al validar el codi"
},
"step": {
diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json
index 86b50ab3c10..500862039a2 100644
--- a/homeassistant/components/nest/.translations/de.json
+++ b/homeassistant/components/nest/.translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_setup": "Sie k\u00f6nnen nur ein einziges Nest-Konto konfigurieren.",
+ "already_setup": "Du kannst nur ein einziges Nest-Konto konfigurieren.",
"authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL",
"authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL",
- "no_flows": "Sie m\u00fcssen Nest konfigurieren, bevor Sie sich authentifizieren k\u00f6nnen. [Bitte lesen Sie die Anweisungen] (https://www.home-assistant.io/components/nest/)."
+ "no_flows": "Du musst Nest konfigurieren, bevor du dich authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/nest/)."
},
"error": {
"internal_error": "Ein interner Fehler ist aufgetreten",
@@ -17,14 +17,14 @@
"data": {
"flow_impl": "Anbieter"
},
- "description": "W\u00e4hlen Sie, \u00fcber welchen Authentifizierungsanbieter Sie sich bei Nest authentifizieren m\u00f6chten.",
+ "description": "W\u00e4hlen, \u00fcber welchen Authentifizierungsanbieter du dich bei Nest authentifizieren m\u00f6chtest.",
"title": "Authentifizierungsanbieter"
},
"link": {
"data": {
"code": "PIN Code"
},
- "description": "[Autorisieren Sie ihr Konto] ( {url} ), um ihren Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcgen Sie anschlie\u00dfend den erhaltenen PIN Code hier ein.",
+ "description": "[Autorisiere dein Konto] ( {url} ), um deinen Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcge anschlie\u00dfend den erhaltenen PIN Code hier ein.",
"title": "Nest-Konto verkn\u00fcpfen"
}
},
diff --git a/homeassistant/components/nest/.translations/id.json b/homeassistant/components/nest/.translations/id.json
new file mode 100644
index 00000000000..58f86f5474e
--- /dev/null
+++ b/homeassistant/components/nest/.translations/id.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Anda hanya dapat mengonfigurasi satu akun Nest.",
+ "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.",
+ "authorize_url_timeout": "Waktu tunggu menghasilkan otorisasi url telah habis.",
+ "no_flows": "Anda harus mengonfigurasi Nest sebelum dapat mengautentikasi dengan Nest. [Silakan baca instruksi] (https://www.home-assistant.io/components/nest/)."
+ },
+ "error": {
+ "internal_error": "Kesalahan Internal memvalidasi kode",
+ "invalid_code": "Kode salah",
+ "timeout": "Waktu tunggu memvalidasi kode telah habis.",
+ "unknown": "Error tidak diketahui saat memvalidasi kode"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "Penyedia"
+ },
+ "description": "Pilih melalui penyedia autentikasi mana yang ingin Anda autentikasi dengan Nest.",
+ "title": "Penyedia Otentikasi"
+ },
+ "link": {
+ "data": {
+ "code": "Kode PIN"
+ },
+ "description": "Untuk menautkan akun Nest Anda, [beri kuasa akun Anda] ( {url} ). \n\n Setelah otorisasi, salin-tempel kode pin yang disediakan di bawah ini.",
+ "title": "Hubungkan Akun Nest"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/nn.json b/homeassistant/components/nest/.translations/nn.json
new file mode 100644
index 00000000000..be3915c464f
--- /dev/null
+++ b/homeassistant/components/nest/.translations/nn.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Du kan berre konfiguere \u00e9in Nest-brukar.",
+ "authorize_url_fail": "Ukjent feil ved generering av autentiserings-URL",
+ "authorize_url_timeout": "Tida gjekk ut for generert autentikasjons-URL",
+ "no_flows": "Du m\u00e5 konfiguere Nest f\u00f8r du kan autentisere den. (Les instruksjonane) (https://www.home-assistant.io/components/nest/)"
+ },
+ "error": {
+ "internal_error": "Intern feil ved validering av kode",
+ "invalid_code": "Ugyldig kode",
+ "timeout": "Tida gjekk ut for validering av kode",
+ "unknown": "Det hende ein ukjent feil ved validering av kode."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "Leverand\u00f8r"
+ },
+ "description": "Vel kva for ein autentiseringsleverand\u00f8r du vil godkjenne med Nest.",
+ "title": "Autentiseringsleverand\u00f8r"
+ },
+ "link": {
+ "data": {
+ "code": "Pinkode"
+ },
+ "description": "For \u00e5 linke Nestkontoen din, [autoriser kontoen din]{url}.\nEtter autentiseringa, kopier-lim inn koda du fekk under her.",
+ "title": "Link Nestkonto"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py
index 57111350396..f609c774b12 100644
--- a/homeassistant/components/nest/__init__.py
+++ b/homeassistant/components/nest/__init__.py
@@ -309,6 +309,37 @@ class NestSensorDevice(Entity):
"""Do not need poll thanks using Nest streaming API."""
return False
+ @property
+ def unique_id(self):
+ """Return unique id based on device serial and variable."""
+ return "{}-{}".format(self.device.serial, self.variable)
+
+ @property
+ def device_info(self):
+ """Return information about the device."""
+ if not hasattr(self.device, 'name_long'):
+ name = self.structure.name
+ model = "Structure"
+ else:
+ name = self.device.name_long
+ if self.device.is_thermostat:
+ model = 'Thermostat'
+ elif self.device.is_camera:
+ model = 'Camera'
+ elif self.device.is_smoke_co_alarm:
+ model = 'Nest Protect'
+ else:
+ model = None
+
+ return {
+ 'identifiers': {
+ (DOMAIN, self.device.serial)
+ },
+ 'name': name,
+ 'manufacturer': 'Nest Labs',
+ 'model': model,
+ }
+
def update(self):
"""Do not use NestSensorDevice directly."""
raise NotImplementedError
diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py
index c9987693b1a..3385fd4f850 100644
--- a/homeassistant/components/nest/config_flow.py
+++ b/homeassistant/components/nest/config_flow.py
@@ -7,7 +7,7 @@ import os
import async_timeout
import voluptuous as vol
-from homeassistant import config_entries, data_entry_flow
+from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.json import load_json
@@ -49,10 +49,11 @@ class CodeInvalid(NestAuthError):
@config_entries.HANDLERS.register(DOMAIN)
-class NestFlowHandler(data_entry_flow.FlowHandler):
+class NestFlowHandler(config_entries.ConfigFlow):
"""Handle a Nest config flow."""
VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
def __init__(self):
"""Initialize the Nest config flow."""
diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py
index 5ab10cc2a5e..393a36e4a9c 100644
--- a/homeassistant/components/nest/local_auth.py
+++ b/homeassistant/components/nest/local_auth.py
@@ -11,7 +11,8 @@ from .const import DOMAIN
def initialize(hass, client_id, client_secret):
"""Initialize a local auth provider."""
config_flow.register_flow_implementation(
- hass, DOMAIN, 'local', partial(generate_auth_url, client_id),
+ hass, DOMAIN, 'configuration.yaml',
+ partial(generate_auth_url, client_id),
partial(resolve_auth_code, hass, client_id, client_secret)
)
diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte.py
index 7f54e6fd6f9..e5e9a0fc2e9 100644
--- a/homeassistant/components/netgear_lte.py
+++ b/homeassistant/components/netgear_lte.py
@@ -6,6 +6,7 @@ https://home-assistant.io/components/netgear_lte/
"""
import asyncio
from datetime import timedelta
+import logging
import voluptuous as vol
import attr
@@ -17,7 +18,9 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.util import Throttle
-REQUIREMENTS = ['eternalegypt==0.0.3']
+REQUIREMENTS = ['eternalegypt==0.0.5']
+
+_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
@@ -37,17 +40,23 @@ class ModemData:
"""Class for modem state."""
modem = attr.ib()
- serial_number = attr.ib(init=False)
- unread_count = attr.ib(init=False)
- usage = attr.ib(init=False)
+
+ serial_number = attr.ib(init=False, default=None)
+ unread_count = attr.ib(init=False, default=None)
+ usage = attr.ib(init=False, default=None)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Call the API to update the data."""
- information = await self.modem.information()
- self.serial_number = information.serial_number
- self.unread_count = sum(1 for x in information.sms if x.unread)
- self.usage = information.usage
+ import eternalegypt
+ try:
+ information = await self.modem.information()
+ self.serial_number = information.serial_number
+ self.unread_count = sum(1 for x in information.sms if x.unread)
+ self.usage = information.usage
+ except eternalegypt.Error:
+ self.unread_count = None
+ self.usage = None
@attr.s
@@ -81,17 +90,27 @@ async def async_setup(hass, config):
return True
-async def _setup_lte(hass, lte_config):
+async def _setup_lte(hass, lte_config, delay=0):
"""Set up a Netgear LTE modem."""
import eternalegypt
+ if delay:
+ await asyncio.sleep(delay)
+
host = lte_config[CONF_HOST]
password = lte_config[CONF_PASSWORD]
websession = hass.data[DATA_KEY].websession
modem = eternalegypt.Modem(hostname=host, websession=websession)
- await modem.login(password=password)
+
+ try:
+ await modem.login(password=password)
+ except eternalegypt.Error:
+ delay = max(15, min(2*delay, 300))
+ _LOGGER.warning("Retrying %s in %d seconds", host, delay)
+ hass.loop.create_task(_setup_lte(hass, lte_config, delay))
+ return
modem_data = ModemData(modem)
await modem_data.async_update()
diff --git a/homeassistant/components/notify/ios.py b/homeassistant/components/notify/ios.py
index 8609e1dabee..e6a37d707ad 100644
--- a/homeassistant/components/notify/ios.py
+++ b/homeassistant/components/notify/ios.py
@@ -24,7 +24,7 @@ DEPENDENCIES = ["ios"]
# pylint: disable=invalid-name
-def log_rate_limits(target, resp, level=20):
+def log_rate_limits(hass, target, resp, level=20):
"""Output rate limit log line at given level."""
rate_limits = resp["rateLimits"]
resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"])
@@ -33,7 +33,7 @@ def log_rate_limits(target, resp, level=20):
"%d sent, %d allowed, %d errors, "
"resets in %s")
_LOGGER.log(level, rate_limit_msg,
- ios.device_name_for_push_id(target),
+ ios.device_name_for_push_id(hass, target),
rate_limits["successful"],
rate_limits["maximum"], rate_limits["errors"],
str(resetsAtTime).split(".")[0])
@@ -45,7 +45,7 @@ def get_service(hass, config, discovery_info=None):
# Need this to enable requirements checking in the app.
hass.config.components.add("notify.ios")
- if not ios.devices_with_push():
+ if not ios.devices_with_push(hass):
_LOGGER.error("The notify.ios platform was loaded but no "
"devices exist! Please check the documentation at "
"https://home-assistant.io/ecosystem/ios/notifications"
@@ -64,7 +64,7 @@ class iOSNotificationService(BaseNotificationService):
@property
def targets(self):
"""Return a dictionary of registered targets."""
- return ios.devices_with_push()
+ return ios.devices_with_push(self.hass)
def send_message(self, message="", **kwargs):
"""Send a message to the Lambda APNS gateway."""
@@ -78,13 +78,13 @@ class iOSNotificationService(BaseNotificationService):
targets = kwargs.get(ATTR_TARGET)
if not targets:
- targets = ios.enabled_push_ids()
+ targets = ios.enabled_push_ids(self.hass)
if kwargs.get(ATTR_DATA) is not None:
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
for target in targets:
- if target not in ios.enabled_push_ids():
+ if target not in ios.enabled_push_ids(self.hass):
_LOGGER.error("The target (%s) does not exist in .ios.conf.",
targets)
return
@@ -102,8 +102,8 @@ class iOSNotificationService(BaseNotificationService):
message = req.json().get("message", fallback_message)
if req.status_code == 429:
_LOGGER.warning(message)
- log_rate_limits(target, req.json(), 30)
+ log_rate_limits(self.hass, target, req.json(), 30)
else:
_LOGGER.error(message)
else:
- log_rate_limits(target, req.json())
+ log_rate_limits(self.hass, target, req.json())
diff --git a/homeassistant/components/notify/netgear_lte.py b/homeassistant/components/notify/netgear_lte.py
index 97dfe504a51..9ba804e193d 100644
--- a/homeassistant/components/notify/netgear_lte.py
+++ b/homeassistant/components/notify/netgear_lte.py
@@ -4,6 +4,8 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.netgear_lte/
"""
+import logging
+
import voluptuous as vol
import attr
@@ -17,6 +19,8 @@ from ..netgear_lte import DATA_KEY
DEPENDENCIES = ['netgear_lte']
+_LOGGER = logging.getLogger(__name__)
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]),
@@ -25,21 +29,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
async def async_get_service(hass, config, discovery_info=None):
"""Get the notification service."""
- modem_data = hass.data[DATA_KEY].get_modem_data(config)
- phone = config.get(ATTR_TARGET)
- return NetgearNotifyService(modem_data, phone)
+ return NetgearNotifyService(hass, config)
@attr.s
class NetgearNotifyService(BaseNotificationService):
"""Implementation of a notification service."""
- modem_data = attr.ib()
- phone = attr.ib()
+ hass = attr.ib()
+ config = attr.ib()
async def async_send_message(self, message="", **kwargs):
"""Send a message to a user."""
- targets = kwargs.get(ATTR_TARGET, self.phone)
+ modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config)
+ if not modem_data:
+ _LOGGER.error("No modem available")
+ return
+
+ phone = self.config.get(ATTR_TARGET)
+ targets = kwargs.get(ATTR_TARGET, phone)
if targets and message:
for target in targets:
- await self.modem_data.modem.sms(target, message)
+ import eternalegypt
+ try:
+ await modem_data.modem.sms(target, message)
+ except eternalegypt.Error:
+ _LOGGER.error("Unable to send to %s", target)
diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py
index 044a037cc29..faf5e90e016 100644
--- a/homeassistant/components/notify/nfandroidtv.py
+++ b/homeassistant/components/notify/nfandroidtv.py
@@ -9,6 +9,8 @@ import io
import base64
import requests
+from requests.auth import HTTPBasicAuth
+from requests.auth import HTTPDigestAuth
import voluptuous as vol
from homeassistant.components.notify import (
@@ -21,12 +23,14 @@ _LOGGER = logging.getLogger(__name__)
CONF_IP = 'host'
CONF_DURATION = 'duration'
+CONF_FONTSIZE = 'fontsize'
CONF_POSITION = 'position'
CONF_TRANSPARENCY = 'transparency'
CONF_COLOR = 'color'
CONF_INTERRUPT = 'interrupt'
DEFAULT_DURATION = 5
+DEFAULT_FONTSIZE = 'medium'
DEFAULT_POSITION = 'bottom-right'
DEFAULT_TRANSPARENCY = 'default'
DEFAULT_COLOR = 'grey'
@@ -37,11 +41,29 @@ DEFAULT_ICON = (
'cMXEAAAAASUVORK5CYII=')
ATTR_DURATION = 'duration'
+ATTR_FONTSIZE = 'fontsize'
ATTR_POSITION = 'position'
ATTR_TRANSPARENCY = 'transparency'
ATTR_COLOR = 'color'
ATTR_BKGCOLOR = 'bkgcolor'
ATTR_INTERRUPT = 'interrupt'
+ATTR_IMAGE = 'filename2'
+ATTR_FILE = 'file'
+# Attributes contained in file
+ATTR_FILE_URL = 'url'
+ATTR_FILE_PATH = 'path'
+ATTR_FILE_USERNAME = 'username'
+ATTR_FILE_PASSWORD = 'password'
+ATTR_FILE_AUTH = 'auth'
+# Any other value or absence of 'auth' lead to basic authentication being used
+ATTR_FILE_AUTH_DIGEST = 'digest'
+
+FONTSIZES = {
+ 'small': 1,
+ 'medium': 0,
+ 'large': 2,
+ 'max': 3
+}
POSITIONS = {
'bottom-right': 0,
@@ -75,6 +97,8 @@ COLORS = {
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP): cv.string,
vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Coerce(int),
+ vol.Optional(CONF_FONTSIZE, default=DEFAULT_FONTSIZE):
+ vol.In(FONTSIZES.keys()),
vol.Optional(CONF_POSITION, default=DEFAULT_POSITION):
vol.In(POSITIONS.keys()),
vol.Optional(CONF_TRANSPARENCY, default=DEFAULT_TRANSPARENCY):
@@ -90,6 +114,7 @@ def get_service(hass, config, discovery_info=None):
"""Get the Notifications for Android TV notification service."""
remoteip = config.get(CONF_IP)
duration = config.get(CONF_DURATION)
+ fontsize = config.get(CONF_FONTSIZE)
position = config.get(CONF_POSITION)
transparency = config.get(CONF_TRANSPARENCY)
color = config.get(CONF_COLOR)
@@ -97,23 +122,26 @@ def get_service(hass, config, discovery_info=None):
timeout = config.get(CONF_TIMEOUT)
return NFAndroidTVNotificationService(
- remoteip, duration, position, transparency, color, interrupt, timeout)
+ remoteip, duration, fontsize, position,
+ transparency, color, interrupt, timeout, hass.config.is_allowed_path)
class NFAndroidTVNotificationService(BaseNotificationService):
"""Notification service for Notifications for Android TV."""
- def __init__(self, remoteip, duration, position, transparency, color,
- interrupt, timeout):
+ def __init__(self, remoteip, duration, fontsize, position, transparency,
+ color, interrupt, timeout, is_allowed_path):
"""Initialize the service."""
self._target = 'http://{}:7676'.format(remoteip)
self._default_duration = duration
+ self._default_fontsize = fontsize
self._default_position = position
self._default_transparency = transparency
self._default_color = color
self._default_interrupt = interrupt
self._timeout = timeout
self._icon_file = io.BytesIO(base64.b64decode(DEFAULT_ICON))
+ self.is_allowed_path = is_allowed_path
def send_message(self, message="", **kwargs):
"""Send a message to a Android TV device."""
@@ -123,7 +151,8 @@ class NFAndroidTVNotificationService(BaseNotificationService):
'application/octet-stream',
{'Expires': '0'}), type='0',
title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
- msg=message, duration="%i" % self._default_duration,
+ msg=message, duration='%i' % self._default_duration,
+ fontsize='%i' % FONTSIZES.get(self._default_fontsize),
position='%i' % POSITIONS.get(self._default_position),
bkgcolor='%s' % COLORS.get(self._default_color),
transparency='%i' % TRANSPARENCIES.get(
@@ -140,6 +169,13 @@ class NFAndroidTVNotificationService(BaseNotificationService):
except ValueError:
_LOGGER.warning("Invalid duration-value: %s",
str(duration))
+ if ATTR_FONTSIZE in data:
+ fontsize = data.get(ATTR_FONTSIZE)
+ if fontsize in FONTSIZES:
+ payload[ATTR_FONTSIZE] = '%i' % FONTSIZES.get(fontsize)
+ else:
+ _LOGGER.warning("Invalid fontsize-value: %s",
+ str(fontsize))
if ATTR_POSITION in data:
position = data.get(ATTR_POSITION)
if position in POSITIONS:
@@ -168,6 +204,19 @@ class NFAndroidTVNotificationService(BaseNotificationService):
except vol.Invalid:
_LOGGER.warning("Invalid interrupt-value: %s",
str(interrupt))
+ filedata = data.get(ATTR_FILE) if data else None
+ if filedata is not None:
+ # Load from file or URL
+ file_as_bytes = self.load_file(
+ url=filedata.get(ATTR_FILE_URL),
+ local_path=filedata.get(ATTR_FILE_PATH),
+ username=filedata.get(ATTR_FILE_USERNAME),
+ password=filedata.get(ATTR_FILE_PASSWORD),
+ auth=filedata.get(ATTR_FILE_AUTH))
+ if file_as_bytes:
+ payload[ATTR_IMAGE] = (
+ 'image', file_as_bytes,
+ 'application/octet-stream', {'Expires': '0'})
try:
_LOGGER.debug("Payload: %s", str(payload))
@@ -178,3 +227,37 @@ class NFAndroidTVNotificationService(BaseNotificationService):
except requests.exceptions.ConnectionError as err:
_LOGGER.error("Error communicating with %s: %s",
self._target, str(err))
+
+ def load_file(self, url=None, local_path=None, username=None,
+ password=None, auth=None):
+ """Load image/document/etc from a local path or URL."""
+ try:
+ if url is not None:
+ # Check whether authentication parameters are provided
+ if username is not None and password is not None:
+ # Use digest or basic authentication
+ if ATTR_FILE_AUTH_DIGEST == auth:
+ auth_ = HTTPDigestAuth(username, password)
+ else:
+ auth_ = HTTPBasicAuth(username, password)
+ # Load file from URL with authentication
+ req = requests.get(
+ url, auth=auth_, timeout=DEFAULT_TIMEOUT)
+ else:
+ # Load file from URL without authentication
+ req = requests.get(url, timeout=DEFAULT_TIMEOUT)
+ return req.content
+
+ elif local_path is not None:
+ # Check whether path is whitelisted in configuration.yaml
+ if self.is_allowed_path(local_path):
+ return open(local_path, "rb")
+ _LOGGER.warning("'%s' is not secure to load data from!",
+ local_path)
+ else:
+ _LOGGER.warning("Neither URL nor local path found in params!")
+
+ except OSError as error:
+ _LOGGER.error("Can't load from url or local path: %s", error)
+
+ return None
diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py
index d4c5a196a3f..d576cdcc95e 100644
--- a/homeassistant/components/notify/slack.py
+++ b/homeassistant/components/notify/slack.py
@@ -136,9 +136,9 @@ class SlackNotificationService(BaseNotificationService):
password=None, auth=None):
"""Load image/document/etc from a local path or URL."""
try:
- if url is not None:
+ if url:
# Check whether authentication parameters are provided
- if username is not None and password is not None:
+ if username:
# Use digest or basic authentication
if ATTR_FILE_AUTH_DIGEST == auth:
auth_ = HTTPDigestAuth(username, password)
@@ -151,7 +151,7 @@ class SlackNotificationService(BaseNotificationService):
req = requests.get(url, timeout=CONF_TIMEOUT)
return req.content
- elif local_path is not None:
+ elif local_path:
# Check whether path is whitelisted in configuration.yaml
if self.is_allowed_path(local_path):
return open(local_path, "rb")
diff --git a/homeassistant/components/openuv/.translations/de.json b/homeassistant/components/openuv/.translations/de.json
index 1f81ac30f53..7f8121dd96b 100644
--- a/homeassistant/components/openuv/.translations/de.json
+++ b/homeassistant/components/openuv/.translations/de.json
@@ -11,7 +11,8 @@
"elevation": "H\u00f6he",
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad"
- }
+ },
+ "title": "Gebe deine Informationen ein"
}
},
"title": "OpenUV"
diff --git a/homeassistant/components/openuv/.translations/he.json b/homeassistant/components/openuv/.translations/he.json
new file mode 100644
index 00000000000..262a3d732a2
--- /dev/null
+++ b/homeassistant/components/openuv/.translations/he.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "\u05d4\u05e7\u05d5\u05d0\u05d5\u05e8\u05d3\u05d9\u05e0\u05d8\u05d5\u05ea \u05db\u05d1\u05e8 \u05e8\u05e9\u05d5\u05de\u05d5\u05ea",
+ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API \u05e9\u05dc OpenUV",
+ "elevation": "\u05d2\u05d5\u05d1\u05d4 \u05de\u05e2\u05dc \u05e4\u05e0\u05d9 \u05d4\u05d9\u05dd",
+ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ },
+ "title": "\u05de\u05dc\u05d0 \u05d0\u05ea \u05d4\u05e4\u05e8\u05d8\u05d9\u05dd \u05e9\u05dc\u05da"
+ }
+ },
+ "title": "OpenUV"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/.translations/id.json b/homeassistant/components/openuv/.translations/id.json
new file mode 100644
index 00000000000..beb7c839eb9
--- /dev/null
+++ b/homeassistant/components/openuv/.translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Koordinat sudah terdaftar",
+ "invalid_api_key": "Kunci API tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API OpenUV",
+ "elevation": "Ketinggian",
+ "latitude": "Lintang",
+ "longitude": "Garis bujur"
+ },
+ "title": "Isi informasi Anda"
+ }
+ },
+ "title": "OpenUV"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/.translations/lb.json b/homeassistant/components/openuv/.translations/lb.json
new file mode 100644
index 00000000000..86e558cc807
--- /dev/null
+++ b/homeassistant/components/openuv/.translations/lb.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Koordinate si scho\u00a0registr\u00e9iert",
+ "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "OpenUV API Schl\u00ebssel",
+ "elevation": "H\u00e9icht",
+ "latitude": "Breedegrad",
+ "longitude": "L\u00e4ngegrad"
+ },
+ "title": "F\u00ebllt \u00e4r Informatiounen aus"
+ }
+ },
+ "title": "OpenUV"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/.translations/nn.json b/homeassistant/components/openuv/.translations/nn.json
new file mode 100644
index 00000000000..135e26cede3
--- /dev/null
+++ b/homeassistant/components/openuv/.translations/nn.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Koordinata er allereie registrerte",
+ "invalid_api_key": "Ugyldig API-n\u00f8kkel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "OpenUV API-n\u00f8kkel",
+ "elevation": "H\u00f8gde",
+ "latitude": "Breiddegrad",
+ "longitude": "Lengdegrad"
+ },
+ "title": "Fyll ut informasjonen din"
+ }
+ },
+ "title": "OpenUV"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/.translations/pt-BR.json b/homeassistant/components/openuv/.translations/pt-BR.json
new file mode 100644
index 00000000000..905fdbacab8
--- /dev/null
+++ b/homeassistant/components/openuv/.translations/pt-BR.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Coordenadas j\u00e1 cadastradas",
+ "invalid_api_key": "Chave de API inv\u00e1lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chave de API do OpenUV",
+ "elevation": "Eleva\u00e7\u00e3o",
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ },
+ "title": "Preencha suas informa\u00e7\u00f5es"
+ }
+ },
+ "title": "OpenUV"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/.translations/pt.json b/homeassistant/components/openuv/.translations/pt.json
new file mode 100644
index 00000000000..36f875efc00
--- /dev/null
+++ b/homeassistant/components/openuv/.translations/pt.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Coordenadas j\u00e1 registadas",
+ "invalid_api_key": "Chave de API inv\u00e1lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chave de API do OpenUV",
+ "elevation": "Eleva\u00e7\u00e3o",
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ },
+ "title": "Preencha com as suas informa\u00e7\u00f5es"
+ }
+ },
+ "title": ""
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py
index bfd90b4a574..35ab16b4d1f 100644
--- a/homeassistant/components/openuv/__init__.py
+++ b/homeassistant/components/openuv/__init__.py
@@ -179,7 +179,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN][DATA_OPENUV_LISTENER][
config_entry.entry_id] = async_track_time_interval(
hass, refresh_sensors,
- hass.data[DOMAIN][CONF_SCAN_INTERVAL])
+ hass.data[DOMAIN].get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
return True
diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py
index 55ee566268e..6d7ae0f65bd 100644
--- a/homeassistant/components/openuv/config_flow.py
+++ b/homeassistant/components/openuv/config_flow.py
@@ -4,7 +4,7 @@ from collections import OrderedDict
import voluptuous as vol
-from homeassistant import config_entries, data_entry_flow
+from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import (
CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE)
@@ -23,10 +23,11 @@ def configured_instances(hass):
@config_entries.HANDLERS.register(DOMAIN)
-class OpenUvFlowHandler(data_entry_flow.FlowHandler):
+class OpenUvFlowHandler(config_entries.ConfigFlow):
"""Handle an OpenUV config flow."""
VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize the config flow."""
diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py
index 2850a5f96cd..6b8fd68bc26 100644
--- a/homeassistant/components/persistent_notification/__init__.py
+++ b/homeassistant/components/persistent_notification/__init__.py
@@ -6,10 +6,12 @@ https://home-assistant.io/components/persistent_notification/
"""
import asyncio
import logging
+from collections import OrderedDict
from typing import Awaitable
import voluptuous as vol
+from homeassistant.components import websocket_api
from homeassistant.core import callback, HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.loader import bind_hass
@@ -20,13 +22,17 @@ from homeassistant.util import slugify
ATTR_MESSAGE = 'message'
ATTR_NOTIFICATION_ID = 'notification_id'
ATTR_TITLE = 'title'
+ATTR_STATUS = 'status'
DOMAIN = 'persistent_notification'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
+EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = 'persistent_notifications_updated'
+
SERVICE_CREATE = 'create'
SERVICE_DISMISS = 'dismiss'
+SERVICE_MARK_READ = 'mark_read'
SCHEMA_SERVICE_CREATE = vol.Schema({
vol.Required(ATTR_MESSAGE): cv.template,
@@ -38,11 +44,21 @@ SCHEMA_SERVICE_DISMISS = vol.Schema({
vol.Required(ATTR_NOTIFICATION_ID): cv.string,
})
+SCHEMA_SERVICE_MARK_READ = vol.Schema({
+ vol.Required(ATTR_NOTIFICATION_ID): cv.string,
+})
DEFAULT_OBJECT_ID = 'notification'
_LOGGER = logging.getLogger(__name__)
STATE = 'notifying'
+STATUS_UNREAD = 'unread'
+STATUS_READ = 'read'
+
+WS_TYPE_GET_NOTIFICATIONS = 'persistent_notification/get'
+SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_GET_NOTIFICATIONS,
+})
@bind_hass
@@ -76,7 +92,7 @@ def async_create(hass: HomeAssistant, message: str, title: str = None,
@callback
@bind_hass
-def async_dismiss(hass, notification_id):
+def async_dismiss(hass: HomeAssistant, notification_id: str) -> None:
"""Remove a notification."""
data = {ATTR_NOTIFICATION_ID: notification_id}
@@ -86,6 +102,9 @@ def async_dismiss(hass, notification_id):
@asyncio.coroutine
def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]:
"""Set up the persistent notification component."""
+ persistent_notifications = OrderedDict()
+ hass.data[DOMAIN] = {'notifications': persistent_notifications}
+
@callback
def create_service(call):
"""Handle a create notification service call."""
@@ -98,6 +117,8 @@ def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]:
else:
entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, hass=hass)
+ notification_id = entity_id.split('.')[1]
+
attr = {}
if title is not None:
try:
@@ -120,18 +141,72 @@ def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]:
hass.states.async_set(entity_id, STATE, attr)
+ # Store notification and fire event
+ # This will eventually replace state machine storage
+ persistent_notifications[entity_id] = {
+ ATTR_MESSAGE: message,
+ ATTR_NOTIFICATION_ID: notification_id,
+ ATTR_STATUS: STATUS_UNREAD,
+ ATTR_TITLE: title,
+ }
+
+ hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)
+
@callback
def dismiss_service(call):
"""Handle the dismiss notification service call."""
notification_id = call.data.get(ATTR_NOTIFICATION_ID)
entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))
+ if entity_id not in persistent_notifications:
+ return
+
hass.states.async_remove(entity_id)
+ del persistent_notifications[entity_id]
+ hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)
+
+ @callback
+ def mark_read_service(call):
+ """Handle the mark_read notification service call."""
+ notification_id = call.data.get(ATTR_NOTIFICATION_ID)
+ entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))
+
+ if entity_id not in persistent_notifications:
+ _LOGGER.error('Marking persistent_notification read failed: '
+ 'Notification ID %s not found.', notification_id)
+ return
+
+ persistent_notifications[entity_id][ATTR_STATUS] = STATUS_READ
+ hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)
+
hass.services.async_register(DOMAIN, SERVICE_CREATE, create_service,
SCHEMA_SERVICE_CREATE)
hass.services.async_register(DOMAIN, SERVICE_DISMISS, dismiss_service,
SCHEMA_SERVICE_DISMISS)
+ hass.services.async_register(DOMAIN, SERVICE_MARK_READ, mark_read_service,
+ SCHEMA_SERVICE_MARK_READ)
+
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_GET_NOTIFICATIONS, websocket_get_notifications,
+ SCHEMA_WS_GET
+ )
+
return True
+
+
+@callback
+def websocket_get_notifications(
+ hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
+ """Return a list of persistent_notifications."""
+ connection.to_write.put_nowait(
+ websocket_api.result_message(msg['id'], [
+ {
+ key: data[key] for key in (ATTR_NOTIFICATION_ID, ATTR_MESSAGE,
+ ATTR_STATUS, ATTR_TITLE)
+ }
+ for data in hass.data[DOMAIN]['notifications'].values()
+ ])
+ )
diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py
index bbc6e07f2b0..5c56caf6470 100644
--- a/homeassistant/components/python_script.py
+++ b/homeassistant/components/python_script.py
@@ -18,7 +18,7 @@ from homeassistant.loader import bind_hass
from homeassistant.util import sanitize_filename
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['restrictedpython==4.0b4']
+REQUIREMENTS = ['restrictedpython==4.0b5']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/rachio.py b/homeassistant/components/rachio.py
index 0e67e15d5c0..cd80b7bec9b 100644
--- a/homeassistant/components/rachio.py
+++ b/homeassistant/components/rachio.py
@@ -6,10 +6,10 @@ https://home-assistant.io/components/rachio/
"""
import asyncio
import logging
+from typing import Optional
from aiohttp import web
import voluptuous as vol
-from typing import Optional
from homeassistant.auth.util import generate_secret
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index 47d6e181c8f..a3cd2eebd8c 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -15,7 +15,6 @@ import logging
import queue
import threading
import time
-
from typing import Any, Dict, Optional # noqa: F401
import voluptuous as vol
diff --git a/homeassistant/components/sensor/.translations/moon.he.json b/homeassistant/components/sensor/.translations/moon.he.json
index 60999f83645..6531d3c8265 100644
--- a/homeassistant/components/sensor/.translations/moon.he.json
+++ b/homeassistant/components/sensor/.translations/moon.he.json
@@ -3,6 +3,10 @@
"first_quarter": "\u05e8\u05d1\u05e2\u05d5\u05df \u05e8\u05d0\u05e9\u05d5\u05df",
"full_moon": "\u05d9\u05e8\u05d7 \u05de\u05dc\u05d0",
"last_quarter": "\u05e8\u05d1\u05e2\u05d5\u05df \u05d0\u05d7\u05e8\u05d5\u05df",
- "new_moon": "\u05e8\u05d0\u05e9 \u05d7\u05d5\u05d3\u05e9"
+ "new_moon": "\u05e8\u05d0\u05e9 \u05d7\u05d5\u05d3\u05e9",
+ "waning_crescent": "Waning crescent",
+ "waning_gibbous": "Waning gibbous",
+ "waxing_crescent": "Waxing crescent",
+ "waxing_gibbous": "Waxing gibbous"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/moon.id.json b/homeassistant/components/sensor/.translations/moon.id.json
new file mode 100644
index 00000000000..3ce14204fb5
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/moon.id.json
@@ -0,0 +1,12 @@
+{
+ "state": {
+ "first_quarter": "Babak pertama",
+ "full_moon": "Bulan purnama",
+ "last_quarter": "Kuartal terakhir",
+ "new_moon": "Bulan baru",
+ "waning_crescent": "Waning crescent",
+ "waning_gibbous": "Waning gibbous",
+ "waxing_crescent": "Waxing crescent",
+ "waxing_gibbous": "Waxing gibbous"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/moon.nn.json b/homeassistant/components/sensor/.translations/moon.nn.json
new file mode 100644
index 00000000000..7c516bcce50
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/moon.nn.json
@@ -0,0 +1,12 @@
+{
+ "state": {
+ "first_quarter": "Fyrste kvartal",
+ "full_moon": "Fullm\u00e5ne",
+ "last_quarter": "Siste kvartal",
+ "new_moon": "Nym\u00e5ne",
+ "waning_crescent": "Minkande halvm\u00e5ne",
+ "waning_gibbous": "Minkande m\u00e5ne",
+ "waxing_crescent": "Veksande halvm\u00e5ne",
+ "waxing_gibbous": "Veksande m\u00e5ne"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/season.id.json b/homeassistant/components/sensor/.translations/season.id.json
new file mode 100644
index 00000000000..ed0666aee36
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/season.id.json
@@ -0,0 +1,8 @@
+{
+ "state": {
+ "autumn": "Musim gugur",
+ "spring": "Musim semi",
+ "summer": "Musim panas",
+ "winter": "Musim dingin"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/season.nn.json b/homeassistant/components/sensor/.translations/season.nn.json
new file mode 100644
index 00000000000..dbcff7ef819
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/season.nn.json
@@ -0,0 +1,8 @@
+{
+ "state": {
+ "autumn": "Haust",
+ "spring": "V\u00e5r",
+ "summer": "Sommar",
+ "winter": "Vinter"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py
index dcd89ccb78a..6837b9e1b2f 100644
--- a/homeassistant/components/sensor/airvisual.py
+++ b/homeassistant/components/sensor/airvisual.py
@@ -281,7 +281,7 @@ class AirVisualData:
_LOGGER.debug("New data retrieved: %s", resp)
self.pollution_info = resp['current']['pollution']
- except AirVisualError as err:
+ except (KeyError, AirVisualError) as err:
if self.city and self.state and self.country:
location = (self.city, self.state, self.country)
else:
diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py
index 37fab727299..c66bda2bc1d 100644
--- a/homeassistant/components/sensor/deconz.py
+++ b/homeassistant/components/sensor/deconz.py
@@ -147,6 +147,7 @@ class DeconzSensor(Entity):
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
+ bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
@@ -154,6 +155,7 @@ class DeconzSensor(Entity):
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
+ 'via_hub': (DECONZ_DOMAIN, bridgeid),
}
@@ -227,6 +229,7 @@ class DeconzBattery(Entity):
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
+ bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
@@ -234,4 +237,5 @@ class DeconzBattery(Entity):
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
+ 'via_hub': (DECONZ_DOMAIN, bridgeid),
}
diff --git a/homeassistant/components/sensor/dyson.py b/homeassistant/components/sensor/dyson.py
index 0619e3f6069..4097bff32bf 100644
--- a/homeassistant/components/sensor/dyson.py
+++ b/homeassistant/components/sensor/dyson.py
@@ -14,12 +14,20 @@ from homeassistant.helpers.entity import Entity
DEPENDENCIES = ['dyson']
SENSOR_UNITS = {
- 'air_quality': 'level',
- 'dust': 'level',
+ 'air_quality': None,
+ 'dust': None,
'filter_life': 'hours',
'humidity': '%',
}
+SENSOR_ICONS = {
+ 'air_quality': 'mdi:fan',
+ 'dust': 'mdi:cloud',
+ 'filter_life': 'mdi:filter-outline',
+ 'humidity': 'mdi:water-percent',
+ 'temperature': 'mdi:thermometer',
+}
+
_LOGGER = logging.getLogger(__name__)
@@ -32,23 +40,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink
for device in [d for d in hass.data[DYSON_DEVICES] if
isinstance(d, DysonPureCoolLink)]:
- devices.append(DysonFilterLifeSensor(hass, device))
- devices.append(DysonDustSensor(hass, device))
- devices.append(DysonHumiditySensor(hass, device))
- devices.append(DysonTemperatureSensor(hass, device, unit))
- devices.append(DysonAirQualitySensor(hass, device))
+ devices.append(DysonFilterLifeSensor(device))
+ devices.append(DysonDustSensor(device))
+ devices.append(DysonHumiditySensor(device))
+ devices.append(DysonTemperatureSensor(device, unit))
+ devices.append(DysonAirQualitySensor(device))
add_entities(devices)
class DysonSensor(Entity):
- """Representation of Dyson sensor."""
+ """Representation of a generic Dyson sensor."""
- def __init__(self, hass, device):
- """Create a new Dyson filter life sensor."""
- self.hass = hass
+ def __init__(self, device, sensor_type):
+ """Create a new generic Dyson sensor."""
self._device = device
self._old_value = None
self._name = None
+ self._sensor_type = sensor_type
@asyncio.coroutine
def async_added_to_hass(self):
@@ -72,17 +80,27 @@ class DysonSensor(Entity):
@property
def name(self):
- """Return the name of the dyson sensor name."""
+ """Return the name of the Dyson sensor name."""
return self._name
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return SENSOR_UNITS[self._sensor_type]
+
+ @property
+ def icon(self):
+ """Return the icon for this sensor."""
+ return SENSOR_ICONS[self._sensor_type]
+
class DysonFilterLifeSensor(DysonSensor):
- """Representation of Dyson filter life sensor (in hours)."""
+ """Representation of Dyson Filter Life sensor (in hours)."""
- def __init__(self, hass, device):
- """Create a new Dyson filter life sensor."""
- DysonSensor.__init__(self, hass, device)
- self._name = "{} filter life".format(self._device.name)
+ def __init__(self, device):
+ """Create a new Dyson Filter Life sensor."""
+ super().__init__(device, 'filter_life')
+ self._name = "{} Filter Life".format(self._device.name)
@property
def state(self):
@@ -91,19 +109,14 @@ class DysonFilterLifeSensor(DysonSensor):
return int(self._device.state.filter_life)
return None
- @property
- def unit_of_measurement(self):
- """Return the unit the value is expressed in."""
- return SENSOR_UNITS['filter_life']
-
class DysonDustSensor(DysonSensor):
"""Representation of Dyson Dust sensor (lower is better)."""
- def __init__(self, hass, device):
+ def __init__(self, device):
"""Create a new Dyson Dust sensor."""
- DysonSensor.__init__(self, hass, device)
- self._name = "{} dust".format(self._device.name)
+ super().__init__(device, 'dust')
+ self._name = "{} Dust".format(self._device.name)
@property
def state(self):
@@ -112,47 +125,37 @@ class DysonDustSensor(DysonSensor):
return self._device.environmental_state.dust
return None
- @property
- def unit_of_measurement(self):
- """Return the unit the value is expressed in."""
- return SENSOR_UNITS['dust']
-
class DysonHumiditySensor(DysonSensor):
"""Representation of Dyson Humidity sensor."""
- def __init__(self, hass, device):
+ def __init__(self, device):
"""Create a new Dyson Humidity sensor."""
- DysonSensor.__init__(self, hass, device)
- self._name = "{} humidity".format(self._device.name)
+ super().__init__(device, 'humidity')
+ self._name = "{} Humidity".format(self._device.name)
@property
def state(self):
- """Return Dust value."""
+ """Return Humidity value."""
if self._device.environmental_state:
if self._device.environmental_state.humidity == 0:
return STATE_OFF
return self._device.environmental_state.humidity
return None
- @property
- def unit_of_measurement(self):
- """Return the unit the value is expressed in."""
- return SENSOR_UNITS['humidity']
-
class DysonTemperatureSensor(DysonSensor):
"""Representation of Dyson Temperature sensor."""
- def __init__(self, hass, device, unit):
+ def __init__(self, device, unit):
"""Create a new Dyson Temperature sensor."""
- DysonSensor.__init__(self, hass, device)
- self._name = "{} temperature".format(self._device.name)
+ super().__init__(device, 'temperature')
+ self._name = "{} Temperature".format(self._device.name)
self._unit = unit
@property
def state(self):
- """Return Dust value."""
+ """Return Temperature value."""
if self._device.environmental_state:
temperature_kelvin = self._device.environmental_state.temperature
if temperature_kelvin == 0:
@@ -171,10 +174,10 @@ class DysonTemperatureSensor(DysonSensor):
class DysonAirQualitySensor(DysonSensor):
"""Representation of Dyson Air Quality sensor (lower is better)."""
- def __init__(self, hass, device):
+ def __init__(self, device):
"""Create a new Dyson Air Quality sensor."""
- DysonSensor.__init__(self, hass, device)
- self._name = "{} air quality".format(self._device.name)
+ super().__init__(device, 'air_quality')
+ self._name = "{} AQI".format(self._device.name)
@property
def state(self):
@@ -182,8 +185,3 @@ class DysonAirQualitySensor(DysonSensor):
if self._device.environmental_state:
return self._device.environmental_state.volatil_organic_compounds
return None
-
- @property
- def unit_of_measurement(self):
- """Return the unit the value is expressed in."""
- return SENSOR_UNITS['air_quality']
diff --git a/homeassistant/components/sensor/edp_redy.py b/homeassistant/components/sensor/edp_redy.py
new file mode 100644
index 00000000000..0f259ec673a
--- /dev/null
+++ b/homeassistant/components/sensor/edp_redy.py
@@ -0,0 +1,115 @@
+"""Support for EDP re:dy sensors."""
+import logging
+
+from homeassistant.helpers.entity import Entity
+
+from homeassistant.components.edp_redy import EdpRedyDevice, EDP_REDY
+
+_LOGGER = logging.getLogger(__name__)
+
+DEPENDENCIES = ['edp_redy']
+
+# Load power in watts (W)
+ATTR_ACTIVE_POWER = 'active_power'
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Perform the setup for re:dy devices."""
+ from edp_redy.session import ACTIVE_POWER_ID
+
+ session = hass.data[EDP_REDY]
+ devices = []
+
+ # Create sensors for modules
+ for device_json in session.modules_dict.values():
+ if 'HA_POWER_METER' not in device_json['Capabilities']:
+ continue
+ devices.append(EdpRedyModuleSensor(session, device_json))
+
+ # Create a sensor for global active power
+ devices.append(EdpRedySensor(session, ACTIVE_POWER_ID, "Power Home",
+ 'mdi:flash', 'W'))
+
+ async_add_entities(devices, True)
+
+
+class EdpRedySensor(EdpRedyDevice, Entity):
+ """Representation of a EDP re:dy generic sensor."""
+
+ def __init__(self, session, sensor_id, name, icon, unit):
+ """Initialize the sensor."""
+ super().__init__(session, sensor_id, name)
+
+ self._icon = icon
+ self._unit = unit
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return self._icon
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this sensor."""
+ return self._unit
+
+ async def async_update(self):
+ """Parse the data for this sensor."""
+ if self._id in self._session.values_dict:
+ self._state = self._session.values_dict[self._id]
+ self._is_available = True
+ else:
+ self._is_available = False
+
+
+class EdpRedyModuleSensor(EdpRedyDevice, Entity):
+ """Representation of a EDP re:dy module sensor."""
+
+ def __init__(self, session, device_json):
+ """Initialize the sensor."""
+ super().__init__(session, device_json['PKID'],
+ "Power {0}".format(device_json['Name']))
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return 'mdi:flash'
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this sensor."""
+ return 'W'
+
+ async def async_update(self):
+ """Parse the data for this sensor."""
+ if self._id in self._session.modules_dict:
+ device_json = self._session.modules_dict[self._id]
+ self._parse_data(device_json)
+ else:
+ self._is_available = False
+
+ def _parse_data(self, data):
+ """Parse data received from the server."""
+ super()._parse_data(data)
+
+ _LOGGER.debug("Sensor data: %s", str(data))
+
+ for state_var in data['StateVars']:
+ if state_var['Name'] == 'ActivePower':
+ try:
+ self._state = float(state_var['Value']) * 1000
+ except ValueError:
+ _LOGGER.error("Could not parse power for %s", self._id)
+ self._state = 0
+ self._is_available = False
diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py
index 2b8365b8f64..73fef98fb76 100644
--- a/homeassistant/components/sensor/homematicip_cloud.py
+++ b/homeassistant/components/sensor/homematicip_cloud.py
@@ -32,20 +32,22 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the HomematicIP Cloud sensors from a config entry."""
- from homematicip.device import (
- HeatingThermostat, TemperatureHumiditySensorWithoutDisplay,
- TemperatureHumiditySensorDisplay, MotionDetectorIndoor)
+ from homematicip.aio.device import (
+ AsyncHeatingThermostat, AsyncTemperatureHumiditySensorWithoutDisplay,
+ AsyncTemperatureHumiditySensorDisplay, AsyncMotionDetectorIndoor,
+ AsyncTemperatureHumiditySensorOutdoor)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = [HomematicipAccesspointStatus(home)]
for device in home.devices:
- if isinstance(device, HeatingThermostat):
+ if isinstance(device, AsyncHeatingThermostat):
devices.append(HomematicipHeatingThermostat(home, device))
- if isinstance(device, (TemperatureHumiditySensorDisplay,
- TemperatureHumiditySensorWithoutDisplay)):
+ if isinstance(device, (AsyncTemperatureHumiditySensorDisplay,
+ AsyncTemperatureHumiditySensorWithoutDisplay,
+ AsyncTemperatureHumiditySensorOutdoor)):
devices.append(HomematicipTemperatureSensor(home, device))
devices.append(HomematicipHumiditySensor(home, device))
- if isinstance(device, MotionDetectorIndoor):
+ if isinstance(device, AsyncMotionDetectorIndoor):
devices.append(HomematicipIlluminanceSensor(home, device))
if devices:
diff --git a/homeassistant/components/sensor/huawei_lte.py b/homeassistant/components/sensor/huawei_lte.py
new file mode 100644
index 00000000000..f5a21999ab8
--- /dev/null
+++ b/homeassistant/components/sensor/huawei_lte.py
@@ -0,0 +1,169 @@
+"""Huawei LTE sensors.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.huawei_lte/
+"""
+
+import logging
+import re
+
+import attr
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_URL, CONF_MONITORED_CONDITIONS, STATE_UNKNOWN,
+)
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.helpers.entity import Entity
+import homeassistant.helpers.config_validation as cv
+
+from ..huawei_lte import DATA_KEY, RouterData
+
+
+_LOGGER = logging.getLogger(__name__)
+
+DEPENDENCIES = ['huawei_lte']
+
+DEFAULT_NAME_TEMPLATE = 'Huawei {}: {}'
+
+DEFAULT_SENSORS = [
+ "device_information.WanIPAddress",
+ "device_signal.rssi",
+]
+
+SENSOR_META = {
+ "device_information.SoftwareVersion": dict(
+ name="Software version",
+ ),
+ "device_information.WanIPAddress": dict(
+ name="WAN IP address",
+ icon="mdi:ip",
+ ),
+ "device_information.WanIPv6Address": dict(
+ name="WAN IPv6 address",
+ icon="mdi:ip",
+ ),
+ "device_signal.rsrq": dict(
+ name="RSRQ",
+ # http://www.lte-anbieter.info/technik/rsrq.php
+ icon=lambda x:
+ x >= -5 and "mdi:signal-cellular-3"
+ or x >= -8 and "mdi:signal-cellular-2"
+ or x >= -11 and "mdi:signal-cellular-1"
+ or "mdi:signal-cellular-outline"
+ ),
+ "device_signal.rsrp": dict(
+ name="RSRP",
+ # http://www.lte-anbieter.info/technik/rsrp.php
+ icon=lambda x:
+ x >= -80 and "mdi:signal-cellular-3"
+ or x >= -95 and "mdi:signal-cellular-2"
+ or x >= -110 and "mdi:signal-cellular-1"
+ or "mdi:signal-cellular-outline"
+ ),
+ "device_signal.rssi": dict(
+ name="RSSI",
+ # https://eyesaas.com/wi-fi-signal-strength/
+ icon=lambda x:
+ x >= -60 and "mdi:signal-cellular-3"
+ or x >= -70 and "mdi:signal-cellular-2"
+ or x >= -80 and "mdi:signal-cellular-1"
+ or "mdi:signal-cellular-outline"
+ ),
+ "device_signal.sinr": dict(
+ name="SINR",
+ # http://www.lte-anbieter.info/technik/sinr.php
+ icon=lambda x:
+ x >= 10 and "mdi:signal-cellular-3"
+ or x >= 5 and "mdi:signal-cellular-2"
+ or x >= 0 and "mdi:signal-cellular-1"
+ or "mdi:signal-cellular-outline"
+ ),
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_URL): cv.url,
+ vol.Optional(
+ CONF_MONITORED_CONDITIONS, default=DEFAULT_SENSORS): cv.ensure_list,
+})
+
+
+def setup_platform(
+ hass, config, add_entities, discovery_info):
+ """Set up Huawei LTE sensor devices."""
+ data = hass.data[DATA_KEY].get_data(config)
+ sensors = []
+ for path in config.get(CONF_MONITORED_CONDITIONS):
+ sensors.append(HuaweiLteSensor(
+ data, path, SENSOR_META.get(path, {})))
+ add_entities(sensors, True)
+
+
+@attr.s
+class HuaweiLteSensor(Entity):
+ """Huawei LTE sensor entity."""
+
+ data = attr.ib(type=RouterData)
+ path = attr.ib(type=list)
+ meta = attr.ib(type=dict)
+
+ _state = attr.ib(init=False, default=STATE_UNKNOWN)
+ _unit = attr.ib(init=False, type=str)
+
+ @property
+ def unique_id(self) -> str:
+ """Return unique ID for sensor."""
+ return "{}_{}".format(
+ self.path,
+ self.data["device_information.SerialNumber"],
+ )
+
+ @property
+ def name(self) -> str:
+ """Return sensor name."""
+ dname = self.data["device_information.DeviceName"]
+ vname = self.meta.get("name", self.path)
+ return DEFAULT_NAME_TEMPLATE.format(dname, vname)
+
+ @property
+ def state(self):
+ """Return sensor state."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self):
+ """Return sensor's unit of measurement."""
+ return self.meta.get("unit", self._unit)
+
+ @property
+ def icon(self):
+ """Return icon for sensor."""
+ icon = self.meta.get("icon")
+ if callable(icon):
+ return icon(self.state)
+ return icon
+
+ def update(self):
+ """Update state."""
+ self.data.update()
+
+ unit = None
+ try:
+ value = self.data[self.path]
+ except KeyError:
+ _LOGGER.warning("%s not in data", self.path)
+ value = None
+
+ if value is not None:
+ # Clean up value and infer unit, e.g. -71dBm, 15 dB
+ match = re.match(
+ r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value))
+ if match:
+ try:
+ value = float(match.group("value"))
+ unit = match.group("unit")
+ except ValueError:
+ pass
+
+ self._state = value
+ self._unit = unit
diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py
index f775381c4ec..d206cd1df87 100644
--- a/homeassistant/components/sensor/ios.py
+++ b/homeassistant/components/sensor/ios.py
@@ -21,14 +21,17 @@ DEFAULT_ICON_STATE = 'mdi:power-plug'
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the iOS sensor."""
- if discovery_info is None:
- return
+ # Leave here for if someone accidentally adds platform: ios to config
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up iOS from a config entry."""
dev = list()
- for device_name, device in ios.devices().items():
+ for device_name, device in ios.devices(hass).items():
for sensor_type in ('level', 'state'):
dev.append(IOSSensor(sensor_type, device_name, device))
- add_entities(dev, True)
+ async_add_entities(dev, True)
class IOSSensor(Entity):
@@ -43,6 +46,21 @@ class IOSSensor(Entity):
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
+ @property
+ def device_info(self):
+ """Return information about the device."""
+ return {
+ 'identifiers': {
+ (ios.DOMAIN,
+ self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_PERMANENT_ID]),
+ },
+ 'name': self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_NAME],
+ 'manufacturer': 'Apple',
+ 'model': self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_TYPE],
+ 'sw_version':
+ self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_SYSTEM_VERSION],
+ }
+
@property
def name(self):
"""Return the name of the iOS sensor."""
@@ -102,5 +120,5 @@ class IOSSensor(Entity):
def update(self):
"""Get the latest state of the sensor."""
- self._device = ios.devices().get(self._device_name)
+ self._device = ios.devices(self.hass).get(self._device_name)
self._state = self._device[ios.ATTR_BATTERY][self.type]
diff --git a/homeassistant/components/sensor/jewish_calendar.py b/homeassistant/components/sensor/jewish_calendar.py
new file mode 100644
index 00000000000..e5838fa8543
--- /dev/null
+++ b/homeassistant/components/sensor/jewish_calendar.py
@@ -0,0 +1,133 @@
+"""
+Platform to retrieve Jewish calendar information for Home Assistant.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.jewish_calendar/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+import homeassistant.util.dt as dt_util
+
+REQUIREMENTS = ['hdate==0.6.3']
+
+_LOGGER = logging.getLogger(__name__)
+
+SENSOR_TYPES = {
+ 'date': ['Date', 'mdi:judaism'],
+ 'weekly_portion': ['Parshat Hashavua', 'mdi:book-open-variant'],
+ 'holiday_name': ['Holiday', 'mdi:calendar-star'],
+ 'holyness': ['Holyness', 'mdi:counter'],
+ 'first_light': ['Alot Hashachar', 'mdi:weather-sunset-up'],
+ 'gra_end_shma': ['Latest time for Shm"a GR"A', 'mdi:calendar-clock'],
+ 'mga_end_shma': ['Latest time for Shm"a MG"A', 'mdi:calendar-clock'],
+ 'plag_mincha': ['Plag Hamincha', 'mdi:weather-sunset-down'],
+ 'first_stars': ['T\'set Hakochavim', 'mdi:weather-night'],
+}
+
+CONF_DIASPORA = 'diaspora'
+CONF_LANGUAGE = 'language'
+CONF_SENSORS = 'sensors'
+
+DEFAULT_NAME = 'Jewish Calendar'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_DIASPORA, default=False): cv.boolean,
+ vol.Optional(CONF_LATITUDE): cv.latitude,
+ vol.Optional(CONF_LONGITUDE): cv.longitude,
+ vol.Optional(CONF_LANGUAGE, default='english'):
+ vol.In(['hebrew', 'english']),
+ vol.Optional(CONF_SENSORS, default=['date']):
+ vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]),
+})
+
+
+async def async_setup_platform(
+ hass, config, async_add_entities, discovery_info=None):
+ """Set up the Jewish calendar sensor platform."""
+ language = config.get(CONF_LANGUAGE)
+ name = config.get(CONF_NAME)
+ latitude = config.get(CONF_LATITUDE, hass.config.latitude)
+ longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
+ diaspora = config.get(CONF_DIASPORA)
+
+ if None in (latitude, longitude):
+ _LOGGER.error("Latitude or longitude not set in Home Assistant config")
+ return
+
+ dev = []
+ for sensor_type in config[CONF_SENSORS]:
+ dev.append(JewishCalSensor(
+ name, language, sensor_type, latitude, longitude, diaspora))
+ async_add_entities(dev, True)
+
+
+class JewishCalSensor(Entity):
+ """Representation of an Jewish calendar sensor."""
+
+ def __init__(
+ self, name, language, sensor_type, latitude, longitude, diaspora):
+ """Initialize the Jewish calendar sensor."""
+ self.client_name = name
+ self._name = SENSOR_TYPES[sensor_type][0]
+ self.type = sensor_type
+ self._hebrew = (language == 'hebrew')
+ self._state = None
+ self.latitude = latitude
+ self.longitude = longitude
+ self.diaspora = diaspora
+ _LOGGER.debug("Sensor %s initialized", self.type)
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return '{} {}'.format(self.client_name, self._name)
+
+ @property
+ def icon(self):
+ """Icon to display in the front end."""
+ return SENSOR_TYPES[self.type][1]
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ async def async_update(self):
+ """Update the state of the sensor."""
+ import hdate
+
+ today = dt_util.now().date()
+
+ date = hdate.HDate(
+ today, diaspora=self.diaspora, hebrew=self._hebrew)
+
+ if self.type == 'date':
+ self._state = hdate.date.get_hebrew_date(
+ date.h_day, date.h_month, date.h_year, hebrew=self._hebrew)
+ elif self.type == 'weekly_portion':
+ self._state = hdate.date.get_parashe(
+ date.get_reading(self.diaspora), hebrew=self._hebrew)
+ elif self.type == 'holiday_name':
+ try:
+ self._state = next(
+ x.description[self._hebrew].long
+ for x in hdate.htables.HOLIDAYS
+ if x.index == date.get_holyday())
+ except StopIteration:
+ self._state = None
+ elif self.type == 'holyness':
+ self._state = hdate.date.get_holyday_type(date.get_holyday())
+ else:
+ times = hdate.Zmanim(
+ date=today, latitude=self.latitude, longitude=self.longitude,
+ hebrew=self._hebrew).zmanim
+ self._state = times[self.type].time()
+
+ _LOGGER.debug("New value: %s", self._state)
diff --git a/homeassistant/components/sensor/linky.py b/homeassistant/components/sensor/linky.py
new file mode 100644
index 00000000000..83a6d793085
--- /dev/null
+++ b/homeassistant/components/sensor/linky.py
@@ -0,0 +1,91 @@
+"""
+Support for Linky.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/sensor.linky/
+"""
+import logging
+import json
+from datetime import timedelta
+
+import voluptuous as vol
+
+from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['pylinky==0.1.6']
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = timedelta(minutes=10)
+DEFAULT_TIMEOUT = 10
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
+})
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Configure the platform and add the Linky sensor."""
+ username = config[CONF_USERNAME]
+ password = config[CONF_PASSWORD]
+ timeout = config[CONF_TIMEOUT]
+
+ from pylinky.client import LinkyClient, PyLinkyError
+ client = LinkyClient(username, password, None, timeout)
+
+ try:
+ client.fetch_data()
+ except PyLinkyError as exp:
+ _LOGGER.error(exp)
+ return
+
+ devices = [LinkySensor('Linky', client)]
+ add_entities(devices, True)
+
+
+class LinkySensor(Entity):
+ """Representation of a sensor entity for Linky."""
+
+ def __init__(self, name, client):
+ """Initialize the sensor."""
+ self._name = name
+ self._client = client
+ self._state = None
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return 'kWh'
+
+ @Throttle(SCAN_INTERVAL)
+ def update(self):
+ """Fetch new state data for the sensor."""
+ from pylinky.client import PyLinkyError
+ try:
+ self._client.fetch_data()
+ except PyLinkyError as exp:
+ _LOGGER.error(exp)
+ return
+
+ _LOGGER.debug(json.dumps(self._client.get_data(), indent=2))
+
+ if self._client.get_data():
+ # get the last past day data
+ self._state = self._client.get_data()['daily'][-2]['conso']
+ else:
+ self._state = None
diff --git a/homeassistant/components/sensor/logi_circle.py b/homeassistant/components/sensor/logi_circle.py
new file mode 100644
index 00000000000..a0a2ca96444
--- /dev/null
+++ b/homeassistant/components/sensor/logi_circle.py
@@ -0,0 +1,156 @@
+"""
+This component provides HA sensor support for Logi Circle cameras.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.logi_circle/
+"""
+import logging
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.logi_circle import (
+ CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DOMAIN as LOGI_CIRCLE_DOMAIN)
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING,
+ CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS,
+ STATE_ON, STATE_OFF)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.icon import icon_for_battery_level
+from homeassistant.util.dt import as_local
+
+DEPENDENCIES = ['logi_circle']
+
+_LOGGER = logging.getLogger(__name__)
+
+# Sensor types: Name, unit of measure, icon per sensor key.
+SENSOR_TYPES = {
+ 'battery_level': [
+ 'Battery', '%', 'battery-50'],
+
+ 'last_activity_time': [
+ 'Last Activity', None, 'history'],
+
+ 'privacy_mode': [
+ 'Privacy Mode', None, 'eye'],
+
+ 'signal_strength_category': [
+ 'WiFi Signal Category', None, 'wifi'],
+
+ 'signal_strength_percentage': [
+ 'WiFi Signal Strength', '%', 'wifi'],
+
+ 'speaker_volume': [
+ 'Volume', '%', 'volume-high'],
+
+ 'streaming_mode': [
+ 'Streaming Mode', None, 'camera'],
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE):
+ cv.string,
+ vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
+ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+})
+
+
+async def async_setup_platform(
+ hass, config, async_add_entities, discovery_info=None):
+ """Set up a sensor for a Logi Circle device."""
+ devices = hass.data[LOGI_CIRCLE_DOMAIN]
+ time_zone = str(hass.config.time_zone)
+
+ sensors = []
+ for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
+ for device in devices:
+ if device.supports_feature(sensor_type):
+ sensors.append(LogiSensor(device, time_zone, sensor_type))
+
+ async_add_entities(sensors, True)
+
+
+class LogiSensor(Entity):
+ """A sensor implementation for a Logi Circle camera."""
+
+ def __init__(self, camera, time_zone, sensor_type):
+ """Initialize a sensor for Logi Circle camera."""
+ self._sensor_type = sensor_type
+ self._camera = camera
+ self._id = '{}-{}'.format(self._camera.mac_address, self._sensor_type)
+ self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2])
+ self._name = "{0} {1}".format(
+ self._camera.name, SENSOR_TYPES.get(self._sensor_type)[0])
+ self._state = None
+ self._tz = time_zone
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._id
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ state = {
+ ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
+ 'battery_saving_mode': (
+ STATE_ON if self._camera.battery_saving else STATE_OFF),
+ 'ip_address': self._camera.ip_address,
+ 'microphone_gain': self._camera.microphone_gain
+ }
+
+ if self._sensor_type == 'battery_level':
+ state[ATTR_BATTERY_CHARGING] = self._camera.is_charging
+
+ return state
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ if (self._sensor_type == 'battery_level' and
+ self._state is not None):
+ return icon_for_battery_level(battery_level=int(self._state),
+ charging=False)
+ if (self._sensor_type == 'privacy_mode' and
+ self._state is not None):
+ return 'mdi:eye-off' if self._state == STATE_ON else 'mdi:eye'
+ if (self._sensor_type == 'streaming_mode' and
+ self._state is not None):
+ return (
+ 'mdi:camera' if self._state == STATE_ON else 'mdi:camera-off')
+ return self._icon
+
+ @property
+ def unit_of_measurement(self):
+ """Return the units of measurement."""
+ return SENSOR_TYPES.get(self._sensor_type)[1]
+
+ async def update(self):
+ """Get the latest data and updates the state."""
+ _LOGGER.debug("Pulling data from %s sensor", self._name)
+ await self._camera.update()
+
+ if self._sensor_type == 'last_activity_time':
+ last_activity = await self._camera.last_activity
+ if last_activity is not None:
+ last_activity_time = as_local(last_activity.end_time_utc)
+ self._state = '{0:0>2}:{1:0>2}'.format(
+ last_activity_time.hour, last_activity_time.minute)
+ else:
+ state = getattr(self._camera, self._sensor_type, None)
+ if isinstance(state, bool):
+ self._state = STATE_ON if state is True else STATE_OFF
+ else:
+ self._state = state
diff --git a/homeassistant/components/sensor/netdata.py b/homeassistant/components/sensor/netdata.py
index 79fb59b4f7b..dc517a0c50d 100644
--- a/homeassistant/components/sensor/netdata.py
+++ b/homeassistant/components/sensor/netdata.py
@@ -24,6 +24,7 @@ _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
+CONF_DATA_GROUP = 'data_group'
CONF_ELEMENT = 'element'
DEFAULT_HOST = 'localhost'
@@ -33,9 +34,9 @@ DEFAULT_PORT = 19999
DEFAULT_ICON = 'mdi:desktop-classic'
RESOURCE_SCHEMA = vol.Any({
+ vol.Required(CONF_DATA_GROUP): cv.string,
vol.Required(CONF_ELEMENT): cv.string,
vol.Optional(CONF_ICON, default=DEFAULT_ICON): cv.icon,
- vol.Optional(CONF_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -65,16 +66,14 @@ async def async_setup_platform(
dev = []
for entry, data in resources.items():
- sensor = entry
+ icon = data[CONF_ICON]
+ sensor = data[CONF_DATA_GROUP]
element = data[CONF_ELEMENT]
- sensor_name = icon = None
+ sensor_name = entry
try:
resource_data = netdata.api.metrics[sensor]
unit = '%' if resource_data['units'] == 'percentage' else \
resource_data['units']
- if data is not None:
- sensor_name = data.get(CONF_NAME)
- icon = data.get(CONF_ICON)
except KeyError:
_LOGGER.error("Sensor is not available: %s", sensor)
continue
diff --git a/homeassistant/components/sensor/netgear_lte.py b/homeassistant/components/sensor/netgear_lte.py
index b13a8e39132..3c17750d6ad 100644
--- a/homeassistant/components/sensor/netgear_lte.py
+++ b/homeassistant/components/sensor/netgear_lte.py
@@ -9,6 +9,7 @@ import attr
from homeassistant.const import CONF_HOST, CONF_SENSORS
from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
@@ -31,6 +32,9 @@ async def async_setup_platform(
"""Set up Netgear LTE sensor devices."""
modem_data = hass.data[DATA_KEY].get_modem_data(config)
+ if not modem_data:
+ raise PlatformNotReady
+
sensors = []
for sensor_type in config[CONF_SENSORS]:
if sensor_type == SENSOR_SMS:
@@ -88,4 +92,7 @@ class UsageSensor(LTESensor):
@property
def state(self):
"""Return the state of the sensor."""
+ if self.modem_data.usage is None:
+ return None
+
return round(self.modem_data.usage / 1024**2, 1)
diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py
index 29eb8cd6749..a6a9c6e30d0 100644
--- a/homeassistant/components/sensor/qnap.py
+++ b/homeassistant/components/sensor/qnap.py
@@ -15,6 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, ATTR_NAME,
CONF_VERIFY_SSL, CONF_TIMEOUT, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS)
from homeassistant.util import Throttle
+from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['qnapstats==0.2.7']
@@ -107,14 +108,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
api = QNAPStatsAPI(config)
api.update()
+ # QNAP is not available
if not api.data:
- hass.components.persistent_notification.create(
- 'Error: Failed to set up QNAP sensor.
'
- 'Check the logs for additional information. '
- 'You will need to restart hass after fixing.',
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID)
- return False
+ raise PlatformNotReady
sensors = []
diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py
index 53aab3f1ff7..f2fbe6cd191 100644
--- a/homeassistant/components/sensor/rest.py
+++ b/homeassistant/components/sensor/rest.py
@@ -18,6 +18,7 @@ from homeassistant.const import (
CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME,
CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, STATE_UNKNOWN)
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
@@ -76,7 +77,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
auth = None
rest = RestData(method, resource, auth, headers, payload, verify_ssl)
rest.update()
+ if rest.data is None:
+ raise PlatformNotReady
+ # Must update the sensor now (including fetching the rest resource) to
+ # ensure it's updating its state.
add_entities([RestSensor(
hass, rest, name, unit, value_template, json_attrs, force_update
)], True)
@@ -170,6 +175,7 @@ class RestData:
def update(self):
"""Get the latest data from REST service with provided method."""
+ _LOGGER.debug("Updating from %s", self._request.url)
try:
with requests.Session() as sess:
response = sess.send(
diff --git a/homeassistant/components/sensor/rmvtransport.py b/homeassistant/components/sensor/rmvtransport.py
index 3d7fd2aa3b7..0916765e12d 100644
--- a/homeassistant/components/sensor/rmvtransport.py
+++ b/homeassistant/components/sensor/rmvtransport.py
@@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION)
-REQUIREMENTS = ['PyRMVtransport==0.0.7']
+REQUIREMENTS = ['PyRMVtransport==0.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py
index e702c52e06a..9a43c3ff295 100644
--- a/homeassistant/components/sensor/scrape.py
+++ b/homeassistant/components/sensor/scrape.py
@@ -114,11 +114,16 @@ class ScrapeSensor(Entity):
raw_data = BeautifulSoup(self.rest.data, 'html.parser')
_LOGGER.debug(raw_data)
- if self._attr is not None:
- value = raw_data.select(self._select)[0][self._attr]
- else:
- value = raw_data.select(self._select)[0].text
- _LOGGER.debug(value)
+
+ try:
+ if self._attr is not None:
+ value = raw_data.select(self._select)[0][self._attr]
+ else:
+ value = raw_data.select(self._select)[0].text
+ _LOGGER.debug(value)
+ except IndexError:
+ _LOGGER.error("Unable to extract data from HTML")
+ return
if self._value_template is not None:
self._state = self._value_template.render_with_possible_json_value(
diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py
index 6f228df3a93..bd74bcaeb2c 100644
--- a/homeassistant/components/sensor/shodan.py
+++ b/homeassistant/components/sensor/shodan.py
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['shodan==1.10.1']
+REQUIREMENTS = ['shodan==1.10.2']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/starlingbank.py b/homeassistant/components/sensor/starlingbank.py
new file mode 100644
index 00000000000..a0c6f23e496
--- /dev/null
+++ b/homeassistant/components/sensor/starlingbank.py
@@ -0,0 +1,103 @@
+"""
+Support for balance data via the Starling Bank API.
+
+For more details about this platform, please refer to the documentation at
+https://www.home-assistant.io/components/sensor.starlingbank/
+"""
+import logging
+
+import requests
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+
+REQUIREMENTS = ['starlingbank==1.2']
+
+_LOGGER = logging.getLogger(__name__)
+
+BALANCE_TYPES = ['cleared_balance', 'effective_balance']
+
+CONF_ACCOUNTS = 'accounts'
+CONF_BALANCE_TYPES = 'balance_types'
+CONF_SANDBOX = 'sandbox'
+
+DEFAULT_SANDBOX = False
+DEFAULT_ACCOUNT_NAME = 'Starling'
+
+ICON = 'mdi:currency-gbp'
+
+ACCOUNT_SCHEMA = vol.Schema({
+ vol.Required(CONF_ACCESS_TOKEN): cv.string,
+ vol.Optional(CONF_BALANCE_TYPES, default=BALANCE_TYPES):
+ vol.All(cv.ensure_list, [vol.In(BALANCE_TYPES)]),
+ vol.Optional(CONF_NAME, default=DEFAULT_ACCOUNT_NAME): cv.string,
+ vol.Optional(CONF_SANDBOX, default=DEFAULT_SANDBOX): cv.boolean,
+})
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_ACCOUNTS): vol.Schema([ACCOUNT_SCHEMA]),
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Sterling Bank sensor platform."""
+ from starlingbank import StarlingAccount
+
+ sensors = []
+ for account in config[CONF_ACCOUNTS]:
+ try:
+ starling_account = StarlingAccount(
+ account[CONF_ACCESS_TOKEN], sandbox=account[CONF_SANDBOX])
+ for balance_type in account[CONF_BALANCE_TYPES]:
+ sensors.append(StarlingBalanceSensor(
+ starling_account, account[CONF_NAME], balance_type))
+ except requests.exceptions.HTTPError as error:
+ _LOGGER.error(
+ "Unable to set up Starling account '%s': %s",
+ account[CONF_NAME], error)
+
+ add_devices(sensors, True)
+
+
+class StarlingBalanceSensor(Entity):
+ """Representation of a Starling balance sensor."""
+
+ def __init__(self, starling_account, account_name, balance_data_type):
+ """Initialize the sensor."""
+ self._starling_account = starling_account
+ self._balance_data_type = balance_data_type
+ self._state = None
+ self._account_name = account_name
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return "{0} {1}".format(
+ self._account_name,
+ self._balance_data_type.replace('_', ' ').capitalize())
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._starling_account.currency
+
+ @property
+ def icon(self):
+ """Return the entity icon."""
+ return ICON
+
+ def update(self):
+ """Fetch new state data for the sensor."""
+ self._starling_account.balance.update()
+ if self._balance_data_type == 'cleared_balance':
+ self._state = self._starling_account.balance.cleared_balance
+ elif self._balance_data_type == 'effective_balance':
+ self._state = self._starling_account.balance.effective_balance
diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py
index 6f44350c5cf..6b34930075a 100644
--- a/homeassistant/components/sensor/swiss_public_transport.py
+++ b/homeassistant/components/sensor/swiss_public_transport.py
@@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['python_opendata_transport==0.1.3']
+REQUIREMENTS = ['python_opendata_transport==0.1.4']
_LOGGER = logging.getLogger(__name__)
@@ -80,6 +80,7 @@ class SwissPublicTransportSensor(Entity):
self._name = name
self._from = start
self._to = destination
+ self._remaining_time = ""
@property
def name(self):
@@ -98,7 +99,7 @@ class SwissPublicTransportSensor(Entity):
if self._opendata is None:
return
- remaining_time = dt_util.parse_datetime(
+ self._remaining_time = dt_util.parse_datetime(
self._opendata.connections[0]['departure']) -\
dt_util.as_local(dt_util.utcnow())
@@ -111,7 +112,7 @@ class SwissPublicTransportSensor(Entity):
ATTR_DEPARTURE_TIME2: self._opendata.connections[2]['departure'],
ATTR_START: self._opendata.from_name,
ATTR_TARGET: self._opendata.to_name,
- ATTR_REMAINING_TIME: '{}'.format(remaining_time),
+ ATTR_REMAINING_TIME: '{}'.format(self._remaining_time),
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
}
return attr
@@ -126,6 +127,7 @@ class SwissPublicTransportSensor(Entity):
from opendata_transport.exceptions import OpendataTransportError
try:
- await self._opendata.async_get_data()
+ if self._remaining_time.total_seconds() < 0:
+ await self._opendata.async_get_data()
except OpendataTransportError:
_LOGGER.error("Unable to retrieve data from transport.opendata.ch")
diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py
index a59eb705498..5918bd7c9f8 100644
--- a/homeassistant/components/sensor/tahoma.py
+++ b/homeassistant/components/sensor/tahoma.py
@@ -56,6 +56,10 @@ class TahomaSensor(TahomaDevice, Entity):
return 'lx'
if self.tahoma_device.type == 'Humidity Sensor':
return '%'
+ if self.tahoma_device.type == 'rtds:RTDSContactSensor':
+ return None
+ if self.tahoma_device.type == 'rtds:RTDSMotionSensor':
+ return None
def update(self):
"""Update the state."""
@@ -63,12 +67,21 @@ class TahomaSensor(TahomaDevice, Entity):
if self.tahoma_device.type == 'io:LightIOSystemSensor':
self.current_value = self.tahoma_device.active_states[
'core:LuminanceState']
+ self._available = bool(self.tahoma_device.active_states.get(
+ 'core:StatusState') == 'available')
if self.tahoma_device.type == 'io:SomfyContactIOSystemSensor':
self.current_value = self.tahoma_device.active_states[
'core:ContactState']
-
- self._available = bool(self.tahoma_device.active_states.get(
- 'core:StatusState') == 'available')
+ self._available = bool(self.tahoma_device.active_states.get(
+ 'core:StatusState') == 'available')
+ if self.tahoma_device.type == 'rtds:RTDSContactSensor':
+ self.current_value = self.tahoma_device.active_states[
+ 'core:ContactState']
+ self._available = True
+ if self.tahoma_device.type == 'rtds:RTDSMotionSensor':
+ self.current_value = self.tahoma_device.active_states[
+ 'core:OccupancyState']
+ self._available = True
_LOGGER.debug("Update %s, value: %d", self._name, self.current_value)
diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py
index ebc38fcb739..dbea54ff353 100644
--- a/homeassistant/components/sensor/tibber.py
+++ b/homeassistant/components/sensor/tibber.py
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import dt as dt_util
from homeassistant.util import Throttle
-REQUIREMENTS = ['pyTibber==0.4.1']
+REQUIREMENTS = ['pyTibber==0.5.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/tradfri.py b/homeassistant/components/sensor/tradfri.py
index 0849169b747..86d0c1abc19 100644
--- a/homeassistant/components/sensor/tradfri.py
+++ b/homeassistant/components/sensor/tradfri.py
@@ -19,20 +19,14 @@ DEPENDENCIES = ['tradfri']
SCAN_INTERVAL = timedelta(minutes=5)
-async def async_setup_platform(hass, config, async_add_entities,
- discovery_info=None):
- """Set up the IKEA Tradfri device platform."""
- if discovery_info is None:
- return
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up a Tradfri config entry."""
+ api = hass.data[KEY_API][config_entry.entry_id]
+ gateway = hass.data[KEY_GATEWAY][config_entry.entry_id]
- gateway_id = discovery_info['gateway']
- api = hass.data[KEY_API][gateway_id]
- gateway = hass.data[KEY_GATEWAY][gateway_id]
-
- devices_command = gateway.get_devices()
- devices_commands = await api(devices_command)
+ devices_commands = await api(gateway.get_devices())
all_devices = await api(devices_commands)
- devices = [dev for dev in all_devices if not dev.has_light_control]
+ devices = (dev for dev in all_devices if not dev.has_light_control)
async_add_entities(TradfriDevice(device, api) for device in devices)
diff --git a/homeassistant/components/sensor/twitch.py b/homeassistant/components/sensor/twitch.py
index 57e98bc273d..3e00f799dcf 100644
--- a/homeassistant/components/sensor/twitch.py
+++ b/homeassistant/components/sensor/twitch.py
@@ -12,7 +12,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['python-twitch-client==0.5.1']
+REQUIREMENTS = ['python-twitch-client==0.6.0']
_LOGGER = logging.getLogger(__name__)
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
client.ingests.get_server_list()
except HTTPError:
_LOGGER.error("Client ID is not valid")
- return False
+ return
users = client.users.translate_usernames_to_ids(channels)
diff --git a/homeassistant/components/sensor/velbus.py b/homeassistant/components/sensor/velbus.py
index ea4af320add..8e9aafd3605 100644
--- a/homeassistant/components/sensor/velbus.py
+++ b/homeassistant/components/sensor/velbus.py
@@ -6,8 +6,6 @@ https://home-assistant.io/components/sensor.velbus/
"""
import logging
-from homeassistant.const import (
- TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE)
from homeassistant.components.velbus import (
DOMAIN as VELBUS_DOMAIN, VelbusEntity)
@@ -25,24 +23,24 @@ async def async_setup_platform(hass, config, async_add_entities,
for sensor in discovery_info:
module = hass.data[VELBUS_DOMAIN].get_module(sensor[0])
channel = sensor[1]
- sensors.append(VelbusTempSensor(module, channel))
+ sensors.append(VelbusSensor(module, channel))
async_add_entities(sensors)
-class VelbusTempSensor(VelbusEntity):
- """Representation of a temperature sensor."""
+class VelbusSensor(VelbusEntity):
+ """Representation of a sensor."""
@property
def device_class(self):
"""Return the device class of the sensor."""
- return DEVICE_CLASS_TEMPERATURE
+ return self._module.get_class(self._channel)
@property
def state(self):
"""Return the state of the sensor."""
- return self._module.getCurTemp()
+ return self._module.get_state(self._channel)
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
- return TEMP_CELSIUS
+ return self._module.get_unit(self._channel)
diff --git a/homeassistant/components/sensor/wirelesstag.py b/homeassistant/components/sensor/wirelesstag.py
index a68fb5d0caf..eb9ce297065 100644
--- a/homeassistant/components/sensor/wirelesstag.py
+++ b/homeassistant/components/sensor/wirelesstag.py
@@ -15,13 +15,9 @@ from homeassistant.const import (
CONF_MONITORED_CONDITIONS)
from homeassistant.components.wirelesstag import (
DOMAIN as WIRELESSTAG_DOMAIN,
- WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
- WIRELESSTAG_TYPE_ALSPRO,
- WIRELESSTAG_TYPE_WEMO_DEVICE,
SIGNAL_TAG_UPDATE,
WirelessTagBaseSensor)
import homeassistant.helpers.config_validation as cv
-from homeassistant.const import TEMP_CELSIUS
DEPENDENCIES = ['wirelesstag']
@@ -32,24 +28,12 @@ SENSOR_HUMIDITY = 'humidity'
SENSOR_MOISTURE = 'moisture'
SENSOR_LIGHT = 'light'
-SENSOR_TYPES = {
- SENSOR_TEMPERATURE: {
- 'unit': TEMP_CELSIUS,
- 'attr': 'temperature'
- },
- SENSOR_HUMIDITY: {
- 'unit': '%',
- 'attr': 'humidity'
- },
- SENSOR_MOISTURE: {
- 'unit': '%',
- 'attr': 'moisture'
- },
- SENSOR_LIGHT: {
- 'unit': 'lux',
- 'attr': 'light'
- }
-}
+SENSOR_TYPES = [
+ SENSOR_TEMPERATURE,
+ SENSOR_HUMIDITY,
+ SENSOR_MOISTURE,
+ SENSOR_LIGHT
+]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
@@ -64,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
tags = platform.tags
for tag in tags.values():
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
- if sensor_type in WirelessTagSensor.allowed_sensors(tag):
+ if sensor_type in tag.allowed_sensor_types:
sensors.append(WirelessTagSensor(
platform, tag, sensor_type, hass.config))
@@ -74,36 +58,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class WirelessTagSensor(WirelessTagBaseSensor):
"""Representation of a Sensor."""
- @classmethod
- def allowed_sensors(cls, tag):
- """Return array of allowed sensor types for tag."""
- all_sensors = SENSOR_TYPES.keys()
- sensors_per_tag_type = {
- WIRELESSTAG_TYPE_13BIT: [
- SENSOR_TEMPERATURE,
- SENSOR_HUMIDITY],
- WIRELESSTAG_TYPE_WATER: [
- SENSOR_TEMPERATURE,
- SENSOR_MOISTURE],
- WIRELESSTAG_TYPE_ALSPRO: [
- SENSOR_TEMPERATURE,
- SENSOR_HUMIDITY,
- SENSOR_LIGHT],
- WIRELESSTAG_TYPE_WEMO_DEVICE: []
- }
-
- tag_type = tag.tag_type
- return (
- sensors_per_tag_type[tag_type] if tag_type in sensors_per_tag_type
- else all_sensors)
-
def __init__(self, api, tag, sensor_type, config):
"""Initialize a WirelessTag sensor."""
super().__init__(api, tag)
self._sensor_type = sensor_type
- self._tag_attr = SENSOR_TYPES[self._sensor_type]['attr']
- self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit']
self._name = self._tag.name
# I want to see entity_id as:
@@ -118,7 +77,7 @@ class WirelessTagSensor(WirelessTagBaseSensor):
"""Register callbacks."""
async_dispatcher_connect(
self.hass,
- SIGNAL_TAG_UPDATE.format(self.tag_id),
+ SIGNAL_TAG_UPDATE.format(self.tag_id, self.tag_manager_mac),
self._update_tag_info_callback)
@property
@@ -144,33 +103,23 @@ class WirelessTagSensor(WirelessTagBaseSensor):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
- return self._unit_of_measurement
+ return self._sensor.unit
@property
def principal_value(self):
"""Return sensor current value."""
- return getattr(self._tag, self._tag_attr, False)
+ return self._sensor.value
+
+ @property
+ def _sensor(self):
+ """Return tag sensor entity."""
+ return self._tag.sensor[self._sensor_type]
@callback
def _update_tag_info_callback(self, event):
"""Handle push notification sent by tag manager."""
- if event.data.get('id') != self.tag_id:
- return
-
_LOGGER.info("Entity to update state: %s event data: %s",
self, event.data)
- new_value = self.principal_value
- try:
- if self._sensor_type == SENSOR_TEMPERATURE:
- new_value = event.data.get('temp')
- elif (self._sensor_type == SENSOR_HUMIDITY or
- self._sensor_type == SENSOR_MOISTURE):
- new_value = event.data.get('cap')
- elif self._sensor_type == SENSOR_LIGHT:
- new_value = event.data.get('lux')
- except Exception as error: # pylint: disable=broad-except
- _LOGGER.info("Unable to update value of entity: \
- %s error: %s event: %s", self, error, event)
-
+ new_value = self._sensor.value_from_update_event(event.data)
self._state = self.decorate_value(new_value)
self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py
index 16ae98f9141..0cb9c3765ec 100644
--- a/homeassistant/components/sensor/yr.py
+++ b/homeassistant/components/sensor/yr.py
@@ -66,9 +66,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-@asyncio.coroutine
-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):
"""Set up the Yr.no sensor."""
elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0)
forecast = config.get(CONF_FORECAST)
@@ -92,8 +91,9 @@ def async_setup_platform(hass, config, async_add_entities,
async_add_entities(dev)
weather = YrData(hass, coordinates, forecast, dev)
- async_track_utc_time_change(hass, weather.updating_devices, minute=31)
- yield from weather.fetching_data()
+ async_track_utc_time_change(hass, weather.updating_devices,
+ minute=31, second=0)
+ await weather.fetching_data()
class YrSensor(Entity):
@@ -156,8 +156,7 @@ class YrData:
self.data = {}
self.hass = hass
- @asyncio.coroutine
- def fetching_data(self, *_):
+ async def fetching_data(self, *_):
"""Get the latest data from yr.no."""
import xmltodict
@@ -169,12 +168,12 @@ class YrData:
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(10, loop=self.hass.loop):
- resp = yield from websession.get(
+ resp = await websession.get(
self._url, params=self._urlparams)
if resp.status != 200:
try_again('{} returned {}'.format(resp.url, resp.status))
return
- text = yield from resp.text()
+ text = await resp.text()
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
try_again(err)
@@ -186,11 +185,10 @@ class YrData:
try_again(err)
return
- yield from self.updating_devices()
+ await self.updating_devices()
async_call_later(self.hass, 60*60, self.fetching_data)
- @asyncio.coroutine
- def updating_devices(self, *_):
+ async def updating_devices(self, *_):
"""Find the current data from self.data."""
if not self.data:
return
@@ -256,4 +254,4 @@ class YrData:
tasks.append(dev.async_update_ha_state())
if tasks:
- yield from asyncio.wait(tasks, loop=self.hass.loop)
+ await asyncio.wait(tasks, loop=self.hass.loop)
diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py
index 6202f8cb7ef..0d5b40d1d98 100644
--- a/homeassistant/components/sensor/zha.py
+++ b/homeassistant/components/sensor/zha.py
@@ -4,7 +4,6 @@ Sensors on Zigbee Home Automation networks.
For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/sensor.zha/
"""
-import asyncio
import logging
from homeassistant.components.sensor import DOMAIN
@@ -17,20 +16,18 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['zha']
-@asyncio.coroutine
-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):
"""Set up Zigbee Home Automation sensors."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
- sensor = yield from make_sensor(discovery_info)
+ sensor = await make_sensor(discovery_info)
async_add_entities([sensor], update_before_add=True)
-@asyncio.coroutine
-def make_sensor(discovery_info):
+async def make_sensor(discovery_info):
"""Create ZHA sensors factory."""
from zigpy.zcl.clusters.measurement import (
RelativeHumidity, TemperatureMeasurement, PressureMeasurement,
@@ -57,9 +54,9 @@ def make_sensor(discovery_info):
if discovery_info['new_join']:
cluster = list(in_clusters.values())[0]
- yield from cluster.bind()
- yield from cluster.configure_reporting(
- sensor.value_attribute, 300, 600, sensor.min_reportable_change,
+ await zha.configure_reporting(
+ sensor.entity_id, cluster, sensor.value_attribute,
+ reportable_change=sensor.min_reportable_change
)
return sensor
@@ -95,7 +92,9 @@ class Sensor(zha.Entity):
"""Retrieve latest state."""
result = await zha.safe_read(
list(self._in_clusters.values())[0],
- [self.value_attribute]
+ [self.value_attribute],
+ allow_cache=False,
+ only_cache=(not self._initialized)
)
self._state = result.get(self.value_attribute, self._state)
@@ -224,7 +223,6 @@ class ElectricalMeasurementSensor(Sensor):
_LOGGER.debug("%s async_update", self.entity_id)
result = await zha.safe_read(
- self._endpoint.electrical_measurement,
- ['active_power'],
- allow_cache=False)
+ self._endpoint.electrical_measurement, ['active_power'],
+ allow_cache=False, only_cache=(not self._initialized))
self._state = result.get('active_power', self._state)
diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py
index 80f8529d847..d4164bbf721 100644
--- a/homeassistant/components/sensor/zoneminder.py
+++ b/homeassistant/components/sensor/zoneminder.py
@@ -8,12 +8,11 @@ import logging
import voluptuous as vol
+import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import STATE_UNKNOWN
+from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.helpers.entity import Entity
-from homeassistant.components import zoneminder
-import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -43,20 +42,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder sensor platform."""
include_archived = config.get(CONF_INCLUDE_ARCHIVED)
- sensors = []
+ zm_client = hass.data[ZONEMINDER_DOMAIN]
+ monitors = zm_client.get_monitors()
+ if not monitors:
+ _LOGGER.warning('Could not fetch any monitors from ZoneMinder')
- monitors = zoneminder.get_state('api/monitors.json')
- for i in monitors['monitors']:
- sensors.append(
- ZMSensorMonitors(int(i['Monitor']['Id']), i['Monitor']['Name'])
- )
+ sensors = []
+ for monitor in monitors:
+ sensors.append(ZMSensorMonitors(monitor))
for sensor in config[CONF_MONITORED_CONDITIONS]:
- sensors.append(
- ZMSensorEvents(int(i['Monitor']['Id']),
- i['Monitor']['Name'],
- include_archived, sensor)
- )
+ sensors.append(ZMSensorEvents(monitor, include_archived, sensor))
add_entities(sensors)
@@ -64,16 +60,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class ZMSensorMonitors(Entity):
"""Get the status of each ZoneMinder monitor."""
- def __init__(self, monitor_id, monitor_name):
+ def __init__(self, monitor):
"""Initialize monitor sensor."""
- self._monitor_id = monitor_id
- self._monitor_name = monitor_name
- self._state = None
+ self._monitor = monitor
+ self._state = monitor.function.value
@property
def name(self):
"""Return the name of the sensor."""
- return '{} Status'.format(self._monitor_name)
+ return '{} Status'.format(self._monitor.name)
@property
def state(self):
@@ -82,32 +77,28 @@ class ZMSensorMonitors(Entity):
def update(self):
"""Update the sensor."""
- monitor = zoneminder.get_state(
- 'api/monitors/{}.json'.format(self._monitor_id)
- )
- if monitor['monitor']['Monitor']['Function'] is None:
- self._state = STATE_UNKNOWN
+ state = self._monitor.function
+ if not state:
+ self._state = None
else:
- self._state = monitor['monitor']['Monitor']['Function']
+ self._state = state.value
class ZMSensorEvents(Entity):
"""Get the number of events for each monitor."""
- def __init__(self, monitor_id, monitor_name, include_archived,
- sensor_type):
+ def __init__(self, monitor, include_archived, sensor_type):
"""Initialize event sensor."""
- self._monitor_id = monitor_id
- self._monitor_name = monitor_name
+ from zoneminder.monitor import TimePeriod
+ self._monitor = monitor
self._include_archived = include_archived
- self._type = sensor_type
- self._name = SENSOR_TYPES[sensor_type][0]
+ self.time_period = TimePeriod.get_time_period(sensor_type)
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
- return '{} {}'.format(self._monitor_name, self._name)
+ return '{} {}'.format(self._monitor.name, self.time_period.title)
@property
def unit_of_measurement(self):
@@ -121,22 +112,5 @@ class ZMSensorEvents(Entity):
def update(self):
"""Update the sensor."""
- date_filter = '1%20{}'.format(self._type)
- if self._type == 'all':
- # The consoleEvents API uses DATE_SUB, so give it
- # something large
- date_filter = '100%20year'
-
- archived_filter = '/Archived=:0'
- if self._include_archived:
- archived_filter = ''
-
- event = zoneminder.get_state(
- 'api/events/consoleEvents/{}{}.json'.format(date_filter,
- archived_filter)
- )
-
- try:
- self._state = event['results'][str(self._monitor_id)]
- except (TypeError, KeyError):
- self._state = '0'
+ self._state = self._monitor.get_events(
+ self.time_period, self._include_archived)
diff --git a/homeassistant/components/shiftr.py b/homeassistant/components/shiftr.py
index 67baa045b18..17a46be4734 100644
--- a/homeassistant/components/shiftr.py
+++ b/homeassistant/components/shiftr.py
@@ -14,7 +14,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import state as state_helper
-REQUIREMENTS = ['paho-mqtt==1.3.1']
+REQUIREMENTS = ['paho-mqtt==1.4.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sonos/.translations/de.json b/homeassistant/components/sonos/.translations/de.json
index dd44fca5888..920d25a3bfa 100644
--- a/homeassistant/components/sonos/.translations/de.json
+++ b/homeassistant/components/sonos/.translations/de.json
@@ -6,7 +6,7 @@
},
"step": {
"confirm": {
- "description": "M\u00f6chten Sie Sonos einrichten?",
+ "description": "M\u00f6chtest du Sonos einrichten?",
"title": "Sonos"
}
},
diff --git a/homeassistant/components/sonos/.translations/id.json b/homeassistant/components/sonos/.translations/id.json
new file mode 100644
index 00000000000..dc810d9773c
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/id.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat Sonos yang ditemukan pada jaringan.",
+ "single_instance_allowed": "Hanya satu konfigurasi Sonos yang diperlukan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Apakah Anda ingin mengatur Sonos?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/nn.json b/homeassistant/components/sonos/.translations/nn.json
new file mode 100644
index 00000000000..f2451efaff4
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/nn.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Det vart ikkje funne noko Sonoseiningar p\u00e5 nettverket.",
+ "single_instance_allowed": "Du treng berre \u00e5 sette opp \u00e9in Sonos-konfigurasjon."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vil du sette opp Sonos?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py
index 6c9280195cc..b4565794844 100644
--- a/homeassistant/components/sonos/__init__.py
+++ b/homeassistant/components/sonos/__init__.py
@@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow
DOMAIN = 'sonos'
-REQUIREMENTS = ['SoCo==0.16']
+REQUIREMENTS = ['pysonos==0.0.2']
async def async_setup(hass, config):
@@ -29,9 +29,10 @@ async def async_setup_entry(hass, entry):
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
- import soco
+ import pysonos
- return await hass.async_add_executor_job(soco.discover)
+ return await hass.async_add_executor_job(pysonos.discover)
-config_entry_flow.register_discovery_flow(DOMAIN, 'Sonos', _async_has_devices)
+config_entry_flow.register_discovery_flow(
+ DOMAIN, 'Sonos', _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH)
diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py
index 5b357efcabd..b00a4aeed2c 100644
--- a/homeassistant/components/spc.py
+++ b/homeassistant/components/spc.py
@@ -4,23 +4,15 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/spc/
"""
-import asyncio
-import json
import logging
-from urllib.parse import urljoin
-import aiohttp
-import async_timeout
import voluptuous as vol
-from homeassistant.const import (
- STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
- STATE_ALARM_TRIGGERED, STATE_OFF, STATE_ON, STATE_UNAVAILABLE,
- STATE_UNKNOWN)
-from homeassistant.helpers import discovery
+from homeassistant.helpers import discovery, aiohttp_client
+from homeassistant.helpers.dispatcher import async_dispatcher_send
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['websockets==6.0']
+REQUIREMENTS = ['pyspcwebgw==0.4.0']
_LOGGER = logging.getLogger(__name__)
@@ -30,9 +22,11 @@ ATTR_DISCOVER_AREAS = 'areas'
CONF_WS_URL = 'ws_url'
CONF_API_URL = 'api_url'
-DATA_REGISTRY = 'spc_registry'
-DATA_API = 'spc_api'
DOMAIN = 'spc'
+DATA_API = 'spc_api'
+
+SIGNAL_UPDATE_ALARM = 'spc_update_alarm_{}'
+SIGNAL_UPDATE_SENSOR = 'spc_update_sensor_{}'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -42,244 +36,45 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
-@asyncio.coroutine
-def async_setup(hass, config):
- """Set up the SPC platform."""
- hass.data[DATA_REGISTRY] = SpcRegistry()
+async def async_setup(hass, config):
+ """Set up the SPC component."""
+ from pyspcwebgw import SpcWebGateway
- api = SpcWebGateway(hass,
- config[DOMAIN].get(CONF_API_URL),
- config[DOMAIN].get(CONF_WS_URL))
+ async def async_upate_callback(spc_object):
+ from pyspcwebgw.area import Area
+ from pyspcwebgw.zone import Zone
- hass.data[DATA_API] = api
+ if isinstance(spc_object, Area):
+ async_dispatcher_send(hass,
+ SIGNAL_UPDATE_ALARM.format(spc_object.id))
+ elif isinstance(spc_object, Zone):
+ async_dispatcher_send(hass,
+ SIGNAL_UPDATE_SENSOR.format(spc_object.id))
+
+ session = aiohttp_client.async_get_clientsession(hass)
+
+ spc = SpcWebGateway(loop=hass.loop, session=session,
+ api_url=config[DOMAIN].get(CONF_API_URL),
+ ws_url=config[DOMAIN].get(CONF_WS_URL),
+ async_callback=async_upate_callback)
+
+ hass.data[DATA_API] = spc
+
+ if not await spc.async_load_parameters():
+ _LOGGER.error('Failed to load area/zone information from SPC.')
+ return False
# add sensor devices for each zone (typically motion/fire/door sensors)
- zones = yield from api.get_zones()
- if zones:
- hass.async_create_task(discovery.async_load_platform(
- hass, 'binary_sensor', DOMAIN,
- {ATTR_DISCOVER_DEVICES: zones}, config))
+ hass.async_create_task(discovery.async_load_platform(
+ hass, 'binary_sensor', DOMAIN,
+ {ATTR_DISCOVER_DEVICES: spc.zones.values()}, config))
# create a separate alarm panel for each area
- areas = yield from api.get_areas()
- if areas:
- hass.async_create_task(discovery.async_load_platform(
- hass, 'alarm_control_panel', DOMAIN,
- {ATTR_DISCOVER_AREAS: areas}, config))
+ hass.async_create_task(discovery.async_load_platform(
+ hass, 'alarm_control_panel', DOMAIN,
+ {ATTR_DISCOVER_AREAS: spc.areas.values()}, config))
# start listening for incoming events over websocket
- api.start_listener(_async_process_message, hass.data[DATA_REGISTRY])
+ spc.start()
return True
-
-
-@asyncio.coroutine
-def _async_process_message(sia_message, spc_registry):
- spc_id = sia_message['sia_address']
- sia_code = sia_message['sia_code']
-
- # BA - Burglary Alarm
- # CG - Close Area
- # NL - Perimeter Armed
- # OG - Open Area
- # ZO - Zone Open
- # ZC - Zone Close
- # ZX - Zone Short
- # ZD - Zone Disconnected
-
- extra = {}
-
- if sia_code in ('BA', 'CG', 'NL', 'OG'):
- # change in area status, notify alarm panel device
- device = spc_registry.get_alarm_device(spc_id)
- data = sia_message['description'].split('¦')
- if len(data) == 3:
- extra['changed_by'] = data[1]
- else:
- # Change in zone status, notify sensor device
- device = spc_registry.get_sensor_device(spc_id)
-
- sia_code_to_state_map = {
- 'BA': STATE_ALARM_TRIGGERED,
- 'CG': STATE_ALARM_ARMED_AWAY,
- 'NL': STATE_ALARM_ARMED_HOME,
- 'OG': STATE_ALARM_DISARMED,
- 'ZO': STATE_ON,
- 'ZC': STATE_OFF,
- 'ZX': STATE_UNKNOWN,
- 'ZD': STATE_UNAVAILABLE,
- }
-
- new_state = sia_code_to_state_map.get(sia_code, None)
-
- if new_state and not device:
- _LOGGER.warning(
- "No device mapping found for SPC area/zone id %s", spc_id)
- elif new_state:
- yield from device.async_update_from_spc(new_state, extra)
-
-
-class SpcRegistry:
- """Maintain mappings between SPC zones/areas and HA entities."""
-
- def __init__(self):
- """Initialize the registry."""
- self._zone_id_to_sensor_map = {}
- self._area_id_to_alarm_map = {}
-
- def register_sensor_device(self, zone_id, device):
- """Add a sensor device to the registry."""
- self._zone_id_to_sensor_map[zone_id] = device
-
- def get_sensor_device(self, zone_id):
- """Retrieve a sensor device for a specific zone."""
- return self._zone_id_to_sensor_map.get(zone_id, None)
-
- def register_alarm_device(self, area_id, device):
- """Add an alarm device to the registry."""
- self._area_id_to_alarm_map[area_id] = device
-
- def get_alarm_device(self, area_id):
- """Retrieve an alarm device for a specific area."""
- return self._area_id_to_alarm_map.get(area_id, None)
-
-
-@asyncio.coroutine
-def _ws_process_message(message, async_callback, *args):
- if message.get('status', '') != 'success':
- _LOGGER.warning(
- "Unsuccessful websocket message delivered, ignoring: %s", message)
- try:
- yield from async_callback(message['data']['sia'], *args)
- except: # noqa: E722 pylint: disable=bare-except
- _LOGGER.exception("Exception in callback, ignoring")
-
-
-class SpcWebGateway:
- """Simple binding for the Lundix SPC Web Gateway REST API."""
-
- AREA_COMMAND_SET = 'set'
- AREA_COMMAND_PART_SET = 'set_a'
- AREA_COMMAND_UNSET = 'unset'
-
- def __init__(self, hass, api_url, ws_url):
- """Initialize the web gateway client."""
- self._hass = hass
- self._api_url = api_url
- self._ws_url = ws_url
- self._ws = None
-
- @asyncio.coroutine
- def get_zones(self):
- """Retrieve all available zones."""
- return (yield from self._get_data('zone'))
-
- @asyncio.coroutine
- def get_areas(self):
- """Retrieve all available areas."""
- return (yield from self._get_data('area'))
-
- @asyncio.coroutine
- def send_area_command(self, area_id, command):
- """Send an area command."""
- _LOGGER.debug(
- "Sending SPC area command '%s' to area %s", command, area_id)
- resource = "area/{}/{}".format(area_id, command)
- return (yield from self._call_web_gateway(resource, use_get=False))
-
- def start_listener(self, async_callback, *args):
- """Start the websocket listener."""
- asyncio.ensure_future(self._ws_listen(async_callback, *args))
-
- def _build_url(self, resource):
- return urljoin(self._api_url, "spc/{}".format(resource))
-
- @asyncio.coroutine
- def _get_data(self, resource):
- """Get the data from the resource."""
- data = yield from self._call_web_gateway(resource)
- if not data:
- return False
- if data['status'] != 'success':
- _LOGGER.error(
- "SPC Web Gateway call unsuccessful for resource: %s", resource)
- return False
- return [item for item in data['data'][resource]]
-
- @asyncio.coroutine
- def _call_web_gateway(self, resource, use_get=True):
- """Call web gateway for data."""
- response = None
- session = None
- url = self._build_url(resource)
- try:
- _LOGGER.debug("Attempting to retrieve SPC data from %s", url)
- session = \
- self._hass.helpers.aiohttp_client.async_get_clientsession()
- with async_timeout.timeout(10, loop=self._hass.loop):
- action = session.get if use_get else session.put
- response = yield from action(url)
- if response.status != 200:
- _LOGGER.error(
- "SPC Web Gateway returned http status %d, response %s",
- response.status, (yield from response.text()))
- return False
- result = yield from response.json()
- except asyncio.TimeoutError:
- _LOGGER.error("Timeout getting SPC data from %s", url)
- return False
- except aiohttp.ClientError:
- _LOGGER.exception("Error getting SPC data from %s", url)
- return False
- finally:
- if session:
- yield from session.close()
- if response:
- yield from response.release()
- _LOGGER.debug("Data from SPC: %s", result)
- return result
-
- @asyncio.coroutine
- def _ws_read(self):
- """Read from websocket."""
- import websockets as wslib
-
- try:
- if not self._ws:
- self._ws = yield from wslib.connect(self._ws_url)
- _LOGGER.info("Connected to websocket at %s", self._ws_url)
- except Exception as ws_exc: # pylint: disable=broad-except
- _LOGGER.error("Failed to connect to websocket: %s", ws_exc)
- return
-
- result = None
-
- try:
- result = yield from self._ws.recv()
- _LOGGER.debug("Data from websocket: %s", result)
- except Exception as ws_exc: # pylint: disable=broad-except
- _LOGGER.error("Failed to read from websocket: %s", ws_exc)
- try:
- yield from self._ws.close()
- finally:
- self._ws = None
-
- return result
-
- @asyncio.coroutine
- def _ws_listen(self, async_callback, *args):
- """Listen on websocket."""
- try:
- while True:
- result = yield from self._ws_read()
-
- if result:
- yield from _ws_process_message(
- json.loads(result), async_callback, *args)
- else:
- _LOGGER.info("Trying again in 30 seconds")
- yield from asyncio.sleep(30)
-
- finally:
- if self._ws:
- yield from self._ws.close()
diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py
index 3dd8eafcf1f..e6115872390 100644
--- a/homeassistant/components/switch/broadlink.py
+++ b/homeassistant/components/switch/broadlink.py
@@ -142,9 +142,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if switch_type in RM_TYPES:
broadlink_device = broadlink.rm((ip_addr, 80), mac_addr, None)
hass.services.register(DOMAIN, SERVICE_LEARN + '_' +
- ip_addr.replace('.', '_'), _learn_command)
+ slugify(ip_addr.replace('.', '_')),
+ _learn_command)
hass.services.register(DOMAIN, SERVICE_SEND + '_' +
- ip_addr.replace('.', '_'), _send_packet,
+ slugify(ip_addr.replace('.', '_')),
+ _send_packet,
vol.Schema({'packet': cv.ensure_list}))
switches = []
for object_id, device_config in devices.items():
diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py
index bd8167d89a0..f8911d65d98 100644
--- a/homeassistant/components/switch/deconz.py
+++ b/homeassistant/components/switch/deconz.py
@@ -92,6 +92,7 @@ class DeconzSwitch(SwitchDevice):
self._switch.uniqueid.count(':') != 7):
return None
serial = self._switch.uniqueid.split('-', 1)[0]
+ bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
@@ -99,6 +100,7 @@ class DeconzSwitch(SwitchDevice):
'model': self._switch.modelid,
'name': self._switch.name,
'sw_version': self._switch.swversion,
+ 'via_hub': (DECONZ_DOMAIN, bridgeid),
}
diff --git a/homeassistant/components/switch/edp_redy.py b/homeassistant/components/switch/edp_redy.py
new file mode 100644
index 00000000000..1576361da33
--- /dev/null
+++ b/homeassistant/components/switch/edp_redy.py
@@ -0,0 +1,94 @@
+"""Support for EDP re:dy plugs/switches."""
+import logging
+
+from homeassistant.components.edp_redy import EdpRedyDevice, EDP_REDY
+from homeassistant.components.switch import SwitchDevice
+
+_LOGGER = logging.getLogger(__name__)
+
+DEPENDENCIES = ['edp_redy']
+
+# Load power in watts (W)
+ATTR_ACTIVE_POWER = 'active_power'
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Perform the setup for re:dy devices."""
+ session = hass.data[EDP_REDY]
+ devices = []
+ for device_json in session.modules_dict.values():
+ if 'HA_SWITCH' not in device_json['Capabilities']:
+ continue
+ devices.append(EdpRedySwitch(session, device_json))
+
+ async_add_entities(devices, True)
+
+
+class EdpRedySwitch(EdpRedyDevice, SwitchDevice):
+ """Representation of a Edp re:dy switch (plugs, switches, etc)."""
+
+ def __init__(self, session, device_json):
+ """Initialize the switch."""
+ super().__init__(session, device_json['PKID'], device_json['Name'])
+
+ self._active_power = None
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return 'mdi:power-plug'
+
+ @property
+ def is_on(self):
+ """Return true if it is on."""
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if self._active_power is not None:
+ attrs = {ATTR_ACTIVE_POWER: self._active_power}
+ else:
+ attrs = {}
+ attrs.update(super().device_state_attributes)
+ return attrs
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the switch on."""
+ if await self._async_send_state_cmd(True):
+ self._state = True
+ self.async_schedule_update_ha_state()
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the switch off."""
+ if await self._async_send_state_cmd(False):
+ self._state = False
+ self.async_schedule_update_ha_state()
+
+ async def _async_send_state_cmd(self, state):
+ state_json = {'devModuleId': self._id, 'key': 'RelayState',
+ 'value': state}
+ return await self._session.async_set_state_var(state_json)
+
+ async def async_update(self):
+ """Parse the data for this switch."""
+ if self._id in self._session.modules_dict:
+ device_json = self._session.modules_dict[self._id]
+ self._parse_data(device_json)
+ else:
+ self._is_available = False
+
+ def _parse_data(self, data):
+ """Parse data received from the server."""
+ super()._parse_data(data)
+
+ for state_var in data['StateVars']:
+ if state_var['Name'] == 'RelayState':
+ self._state = state_var['Value'] == 'true'
+ elif state_var['Name'] == 'ActivePower':
+ try:
+ self._active_power = float(state_var['Value']) * 1000
+ except ValueError:
+ _LOGGER.error("Could not parse power for %s", self._id)
+ self._active_power = None
diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py
index 20774accbd5..84016dac28d 100644
--- a/homeassistant/components/switch/konnected.py
+++ b/homeassistant/components/switch/konnected.py
@@ -83,7 +83,7 @@ class KonnectedSwitch(ToggleEntity):
if self._momentary and resp.get(ATTR_STATE) != -1:
# Immediately set the state back off for momentary switches
- self._set_state(self._boolean_state(False))
+ self._set_state(False)
def turn_off(self, **kwargs):
"""Send a command to turn off the switch."""
diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py
index f6075d5e49f..b79f8f12b87 100644
--- a/homeassistant/components/switch/mqtt.py
+++ b/homeassistant/components/switch/mqtt.py
@@ -11,9 +11,10 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.mqtt import (
- CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC,
- CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
- MqttAvailability)
+ ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
+ CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
+ CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability,
+ MqttDiscoveryUpdate)
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import (
CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF,
@@ -56,7 +57,11 @@ async def async_setup_platform(hass, config, async_add_entities,
if value_template is not None:
value_template.hass = hass
- async_add_entities([MqttSwitch(
+ discovery_hash = None
+ if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info:
+ discovery_hash = discovery_info[ATTR_DISCOVERY_HASH]
+
+ newswitch = MqttSwitch(
config.get(CONF_NAME),
config.get(CONF_ICON),
config.get(CONF_STATE_TOPIC),
@@ -73,10 +78,13 @@ async def async_setup_platform(hass, config, async_add_entities,
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_UNIQUE_ID),
value_template,
- )])
+ discovery_hash,
+ )
+
+ async_add_entities([newswitch])
-class MqttSwitch(MqttAvailability, SwitchDevice):
+class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, SwitchDevice):
"""Representation of a switch that can be toggled using MQTT."""
def __init__(self, name, icon,
@@ -84,10 +92,11 @@ class MqttSwitch(MqttAvailability, SwitchDevice):
qos, retain, payload_on, payload_off, state_on,
state_off, optimistic, payload_available,
payload_not_available, unique_id: Optional[str],
- value_template):
+ value_template, discovery_hash):
"""Initialize the MQTT switch."""
- super().__init__(availability_topic, qos, payload_available,
- payload_not_available)
+ MqttAvailability.__init__(self, availability_topic, qos,
+ payload_available, payload_not_available)
+ MqttDiscoveryUpdate.__init__(self, discovery_hash)
self._state = False
self._name = name
self._icon = icon
@@ -102,10 +111,12 @@ class MqttSwitch(MqttAvailability, SwitchDevice):
self._optimistic = optimistic
self._template = value_template
self._unique_id = unique_id
+ self._discovery_hash = discovery_hash
async def async_added_to_hass(self):
"""Subscribe to MQTT events."""
- await super().async_added_to_hass()
+ await MqttAvailability.async_added_to_hass(self)
+ await MqttDiscoveryUpdate.async_added_to_hass(self)
@callback
def state_message_received(topic, payload, qos):
diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py
index 7ccd3bee4b6..4955d72c5e3 100644
--- a/homeassistant/components/switch/switchmate.py
+++ b/homeassistant/components/switch/switchmate.py
@@ -12,48 +12,40 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_MAC
-from homeassistant.exceptions import PlatformNotReady
-REQUIREMENTS = ['bluepy==1.1.4']
+REQUIREMENTS = ['pySwitchmate==0.4.1']
_LOGGER = logging.getLogger(__name__)
+CONF_FLIP_ON_OFF = 'flip_on_off'
DEFAULT_NAME = 'Switchmate'
-HANDLE = 0x2e
-ON_KEY = b'\x00'
-OFF_KEY = b'\x01'
SCAN_INTERVAL = timedelta(minutes=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MAC): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_FLIP_ON_OFF, default=False): cv.boolean,
})
def setup_platform(hass, config, add_entities, discovery_info=None) -> None:
"""Perform the setup for Switchmate devices."""
name = config.get(CONF_NAME)
- mac_addr = config.get(CONF_MAC)
- add_entities([Switchmate(mac_addr, name)], True)
+ mac_addr = config[CONF_MAC]
+ flip_on_off = config[CONF_FLIP_ON_OFF]
+ add_entities([Switchmate(mac_addr, name, flip_on_off)], True)
class Switchmate(SwitchDevice):
"""Representation of a Switchmate."""
- def __init__(self, mac, name) -> None:
+ def __init__(self, mac, name, flip_on_off) -> None:
"""Initialize the Switchmate."""
- # pylint: disable=import-error
- import bluepy
- self._state = False
- self._name = name
+ import switchmate
self._mac = mac
- try:
- self._device = bluepy.btle.Peripheral(self._mac,
- bluepy.btle.ADDR_TYPE_RANDOM)
- except bluepy.btle.BTLEException:
- _LOGGER.error("Failed to set up switchmate")
- raise PlatformNotReady()
+ self._name = name
+ self._device = switchmate.Switchmate(mac=mac, flip_on_off=flip_on_off)
@property
def unique_id(self) -> str:
@@ -67,17 +59,17 @@ class Switchmate(SwitchDevice):
def update(self) -> None:
"""Synchronize state with switch."""
- self._state = self._device.readCharacteristic(HANDLE) == ON_KEY
+ self._device.update()
@property
def is_on(self) -> bool:
"""Return true if it is on."""
- return self._state
+ return self._device.state
def turn_on(self, **kwargs) -> None:
"""Turn the switch on."""
- self._device.writeCharacteristic(HANDLE, ON_KEY, True)
+ self._device.turn_on()
def turn_off(self, **kwargs) -> None:
"""Turn the switch off."""
- self._device.writeCharacteristic(HANDLE, OFF_KEY, True)
+ self._device.turn_off()
diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py
index 06f86865064..16bd700e1d5 100644
--- a/homeassistant/components/switch/wake_on_lan.py
+++ b/homeassistant/components/switch/wake_on_lan.py
@@ -10,26 +10,26 @@ import subprocess as sp
import voluptuous as vol
-from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
+from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
+from homeassistant.const import CONF_HOST, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.script import Script
-from homeassistant.const import (CONF_HOST, CONF_NAME)
-REQUIREMENTS = ['wakeonlan==1.0.0']
+REQUIREMENTS = ['wakeonlan==1.1.6']
_LOGGER = logging.getLogger(__name__)
+CONF_BROADCAST_ADDRESS = 'broadcast_address'
CONF_MAC_ADDRESS = 'mac_address'
CONF_OFF_ACTION = 'turn_off'
-CONF_BROADCAST_ADDRESS = 'broadcast_address'
DEFAULT_NAME = 'Wake on LAN'
DEFAULT_PING_TIMEOUT = 1
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MAC_ADDRESS): cv.string,
- vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_BROADCAST_ADDRESS): cv.string,
+ vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
})
@@ -37,21 +37,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a wake on lan switch."""
- name = config.get(CONF_NAME)
+ broadcast_address = config.get(CONF_BROADCAST_ADDRESS)
host = config.get(CONF_HOST)
mac_address = config.get(CONF_MAC_ADDRESS)
- broadcast_address = config.get(CONF_BROADCAST_ADDRESS)
+ name = config.get(CONF_NAME)
off_action = config.get(CONF_OFF_ACTION)
- add_entities([WOLSwitch(hass, name, host, mac_address,
- off_action, broadcast_address)], True)
+ add_entities([WOLSwitch(
+ hass, name, host, mac_address, off_action, broadcast_address)], True)
class WOLSwitch(SwitchDevice):
"""Representation of a wake on lan switch."""
- def __init__(self, hass, name, host, mac_address,
- off_action, broadcast_address):
+ def __init__(
+ self, hass, name, host, mac_address, off_action,
+ broadcast_address):
"""Initialize the WOL switch."""
import wakeonlan
self._hass = hass
@@ -63,11 +64,6 @@ class WOLSwitch(SwitchDevice):
self._state = False
self._wol = wakeonlan
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def is_on(self):
"""Return true if switch is on."""
diff --git a/homeassistant/components/switch/wirelesstag.py b/homeassistant/components/switch/wirelesstag.py
index 5796216d50f..cbe62d107da 100644
--- a/homeassistant/components/switch/wirelesstag.py
+++ b/homeassistant/components/switch/wirelesstag.py
@@ -11,9 +11,6 @@ import voluptuous as vol
from homeassistant.components.wirelesstag import (
DOMAIN as WIRELESSTAG_DOMAIN,
- WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
- WIRELESSTAG_TYPE_ALSPRO,
- WIRELESSTAG_TYPE_WEMO_DEVICE,
WirelessTagBaseSensor)
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
from homeassistant.const import (
@@ -53,7 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
tags = platform.load_tags()
for switch_type in config.get(CONF_MONITORED_CONDITIONS):
for _, tag in tags.items():
- if switch_type in WirelessTagSwitch.allowed_switches(tag):
+ if switch_type in tag.allowed_monitoring_types:
switches.append(WirelessTagSwitch(platform, tag, switch_type))
add_entities(switches, True)
@@ -62,30 +59,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class WirelessTagSwitch(WirelessTagBaseSensor, SwitchDevice):
"""A switch implementation for Wireless Sensor Tags."""
- @classmethod
- def allowed_switches(cls, tag):
- """Return allowed switch types for wireless tag."""
- all_sensors = SWITCH_TYPES.keys()
- sensors_per_tag_spec = {
- WIRELESSTAG_TYPE_13BIT: [
- ARM_TEMPERATURE, ARM_HUMIDITY, ARM_MOTION],
- WIRELESSTAG_TYPE_WATER: [
- ARM_TEMPERATURE, ARM_MOISTURE],
- WIRELESSTAG_TYPE_ALSPRO: [
- ARM_TEMPERATURE, ARM_HUMIDITY, ARM_MOTION, ARM_LIGHT],
- WIRELESSTAG_TYPE_WEMO_DEVICE: []
- }
-
- tag_type = tag.tag_type
-
- result = (
- sensors_per_tag_spec[tag_type]
- if tag_type in sensors_per_tag_spec else all_sensors)
- _LOGGER.info("Allowed switches: %s tag_type: %s",
- str(result), tag_type)
-
- return result
-
def __init__(self, api, tag, switch_type):
"""Initialize a switch for Wireless Sensor Tag."""
super().__init__(api, tag)
diff --git a/homeassistant/components/switch/zha.py b/homeassistant/components/switch/zha.py
index 9f780b631b6..68a94cc1ca5 100644
--- a/homeassistant/components/switch/zha.py
+++ b/homeassistant/components/switch/zha.py
@@ -17,17 +17,23 @@ DEPENDENCIES = ['zha']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Zigbee Home Automation switches."""
+ from zigpy.zcl.clusters.general import OnOff
+
discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
- from zigpy.zcl.clusters.general import OnOff
- in_clusters = discovery_info['in_clusters']
- cluster = in_clusters[OnOff.cluster_id]
- await cluster.bind()
- await cluster.configure_reporting(0, 0, 600, 1,)
+ switch = Switch(**discovery_info)
- async_add_entities([Switch(**discovery_info)], update_before_add=True)
+ if discovery_info['new_join']:
+ in_clusters = discovery_info['in_clusters']
+ cluster = in_clusters[OnOff.cluster_id]
+ await zha.configure_reporting(
+ switch.entity_id, cluster, switch.value_attribute,
+ min_report=0, max_report=600, reportable_change=1
+ )
+
+ async_add_entities([switch], update_before_add=True)
class Switch(zha.Entity, SwitchDevice):
@@ -38,7 +44,10 @@ class Switch(zha.Entity, SwitchDevice):
def attribute_updated(self, attribute, value):
"""Handle attribute update from device."""
- _LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value)
+ cluster = self._endpoint.on_off
+ attr_name = cluster.attributes.get(attribute, [attribute])[0]
+ _LOGGER.debug("%s: Attribute '%s' on cluster '%s' updated to %s",
+ self.entity_id, attr_name, cluster.ep_attribute, value)
if attribute == self.value_attribute:
self._state = value
self.async_schedule_update_ha_state()
@@ -59,26 +68,34 @@ class Switch(zha.Entity, SwitchDevice):
"""Turn the entity on."""
from zigpy.exceptions import DeliveryError
try:
- await self._endpoint.on_off.on()
+ res = await self._endpoint.on_off.on()
+ _LOGGER.debug("%s: turned 'on': %s", self.entity_id, res[1])
except DeliveryError as ex:
- _LOGGER.error("Unable to turn the switch on: %s", ex)
+ _LOGGER.error("%s: Unable to turn the switch on: %s",
+ self.entity_id, ex)
return
self._state = 1
+ self.async_schedule_update_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
from zigpy.exceptions import DeliveryError
try:
- await self._endpoint.on_off.off()
+ res = await self._endpoint.on_off.off()
+ _LOGGER.debug("%s: turned 'off': %s", self.entity_id, res[1])
except DeliveryError as ex:
- _LOGGER.error("Unable to turn the switch off: %s", ex)
+ _LOGGER.error("%s: Unable to turn the switch off: %s",
+ self.entity_id, ex)
return
self._state = 0
+ self.async_schedule_update_ha_state()
async def async_update(self):
"""Retrieve latest state."""
result = await zha.safe_read(self._endpoint.on_off,
- ['on_off'])
+ ['on_off'],
+ allow_cache=False,
+ only_cache=(not self._initialized))
self._state = result.get('on_off', self._state)
diff --git a/homeassistant/components/switch/zoneminder.py b/homeassistant/components/switch/zoneminder.py
index 496e7549aaa..265f94fbbb1 100644
--- a/homeassistant/components/switch/zoneminder.py
+++ b/homeassistant/components/switch/zoneminder.py
@@ -9,8 +9,8 @@ import logging
import voluptuous as vol
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
+from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN
from homeassistant.const import (CONF_COMMAND_ON, CONF_COMMAND_OFF)
-from homeassistant.components import zoneminder
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -25,22 +25,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder switch platform."""
- on_state = config.get(CONF_COMMAND_ON)
- off_state = config.get(CONF_COMMAND_OFF)
+ from zoneminder.monitor import MonitorState
+ on_state = MonitorState(config.get(CONF_COMMAND_ON))
+ off_state = MonitorState(config.get(CONF_COMMAND_OFF))
+
+ zm_client = hass.data[ZONEMINDER_DOMAIN]
+
+ monitors = zm_client.get_monitors()
+ if not monitors:
+ _LOGGER.warning('Could not fetch monitors from ZoneMinder')
+ return
switches = []
-
- monitors = zoneminder.get_state('api/monitors.json')
- for i in monitors['monitors']:
- switches.append(
- ZMSwitchMonitors(
- int(i['Monitor']['Id']),
- i['Monitor']['Name'],
- on_state,
- off_state
- )
- )
-
+ for monitor in monitors:
+ switches.append(ZMSwitchMonitors(monitor, on_state, off_state))
add_entities(switches)
@@ -49,10 +47,9 @@ class ZMSwitchMonitors(SwitchDevice):
icon = 'mdi:record-rec'
- def __init__(self, monitor_id, monitor_name, on_state, off_state):
+ def __init__(self, monitor, on_state, off_state):
"""Initialize the switch."""
- self._monitor_id = monitor_id
- self._monitor_name = monitor_name
+ self._monitor = monitor
self._on_state = on_state
self._off_state = off_state
self._state = None
@@ -60,15 +57,11 @@ class ZMSwitchMonitors(SwitchDevice):
@property
def name(self):
"""Return the name of the switch."""
- return "%s State" % self._monitor_name
+ return '{}\'s State'.format(self._monitor.name)
def update(self):
"""Update the switch value."""
- monitor = zoneminder.get_state(
- 'api/monitors/%i.json' % self._monitor_id
- )
- current_state = monitor['monitor']['Monitor']['Function']
- self._state = True if current_state == self._on_state else False
+ self._state = self._monitor.function == self._on_state
@property
def is_on(self):
@@ -77,14 +70,8 @@ class ZMSwitchMonitors(SwitchDevice):
def turn_on(self, **kwargs):
"""Turn the entity on."""
- zoneminder.change_state(
- 'api/monitors/%i.json' % self._monitor_id,
- {'Monitor[Function]': self._on_state}
- )
+ self._monitor.function = self._on_state
def turn_off(self, **kwargs):
"""Turn the entity off."""
- zoneminder.change_state(
- 'api/monitors/%i.json' % self._monitor_id,
- {'Monitor[Function]': self._off_state}
- )
+ self._monitor.function = self._off_state
diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py
index 64071ddb037..366799b872c 100644
--- a/homeassistant/components/tahoma.py
+++ b/homeassistant/components/tahoma.py
@@ -54,6 +54,8 @@ TAHOMA_TYPES = {
'io:HorizontalAwningIOComponent': 'cover',
'io:OnOffLightIOComponent': 'switch',
'rtds:RTDSSmokeSensor': 'smoke',
+ 'rtds:RTDSContactSensor': 'sensor',
+ 'rtds:RTDSMotionSensor': 'sensor'
}
diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py
deleted file mode 100644
index b2e41902552..00000000000
--- a/homeassistant/components/tradfri.py
+++ /dev/null
@@ -1,173 +0,0 @@
-"""
-Support for IKEA Tradfri.
-
-For more details about this component, please refer to the documentation at
-https://home-assistant.io/components/ikea_tradfri/
-"""
-import logging
-from uuid import uuid4
-
-import voluptuous as vol
-
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers import discovery
-from homeassistant.const import CONF_HOST
-from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI
-from homeassistant.util.json import load_json, save_json
-
-REQUIREMENTS = ['pytradfri[async]==5.5.1']
-
-DOMAIN = 'tradfri'
-GATEWAY_IDENTITY = 'homeassistant'
-CONFIG_FILE = '.tradfri_psk.conf'
-KEY_CONFIG = 'tradfri_configuring'
-KEY_GATEWAY = 'tradfri_gateway'
-KEY_API = 'tradfri_api'
-KEY_TRADFRI_GROUPS = 'tradfri_allow_tradfri_groups'
-CONF_ALLOW_TRADFRI_GROUPS = 'allow_tradfri_groups'
-DEFAULT_ALLOW_TRADFRI_GROUPS = True
-
-CONFIG_SCHEMA = vol.Schema({
- DOMAIN: vol.Schema({
- vol.Inclusive(CONF_HOST, 'gateway'): cv.string,
- vol.Optional(CONF_ALLOW_TRADFRI_GROUPS,
- default=DEFAULT_ALLOW_TRADFRI_GROUPS): cv.boolean,
- })
-}, extra=vol.ALLOW_EXTRA)
-
-_LOGGER = logging.getLogger(__name__)
-
-
-def request_configuration(hass, config, host):
- """Request configuration steps from the user."""
- configurator = hass.components.configurator
- hass.data.setdefault(KEY_CONFIG, {})
- instance = hass.data[KEY_CONFIG].get(host)
-
- # Configuration already in progress
- if instance:
- return
-
- async def configuration_callback(callback_data):
- """Handle the submitted configuration."""
- try:
- from pytradfri.api.aiocoap_api import APIFactory
- from pytradfri import RequestError
- except ImportError:
- _LOGGER.exception("Looks like something isn't installed!")
- return
-
- identity = uuid4().hex
- security_code = callback_data.get('security_code')
-
- api_factory = APIFactory(host, psk_id=identity, loop=hass.loop)
- # Need To Fix: currently entering a wrong security code sends
- # pytradfri aiocoap API into an endless loop.
- # Should just raise a requestError or something.
- try:
- key = await api_factory.generate_psk(security_code)
- except RequestError:
- configurator.async_notify_errors(hass, instance,
- "Security Code not accepted.")
- return
-
- res = await _setup_gateway(hass, config, host, identity, key,
- DEFAULT_ALLOW_TRADFRI_GROUPS)
-
- if not res:
- configurator.async_notify_errors(hass, instance,
- "Unable to connect.")
- return
-
- def success():
- """Set up was successful."""
- conf = load_json(hass.config.path(CONFIG_FILE))
- conf[host] = {'identity': identity,
- 'key': key}
- save_json(hass.config.path(CONFIG_FILE), conf)
- configurator.request_done(instance)
-
- hass.async_add_job(success)
-
- instance = configurator.request_config(
- "IKEA Trådfri", configuration_callback,
- description='Please enter the security code written at the bottom of '
- 'your IKEA Trådfri Gateway.',
- submit_caption="Confirm",
- fields=[{'id': 'security_code', 'name': 'Security Code',
- 'type': 'password'}]
- )
-
-
-async def async_setup(hass, config):
- """Set up the Tradfri component."""
- conf = config.get(DOMAIN, {})
- host = conf.get(CONF_HOST)
- allow_tradfri_groups = conf.get(CONF_ALLOW_TRADFRI_GROUPS)
- known_hosts = await hass.async_add_job(load_json,
- hass.config.path(CONFIG_FILE))
-
- async def gateway_discovered(service, info,
- allow_groups=DEFAULT_ALLOW_TRADFRI_GROUPS):
- """Run when a gateway is discovered."""
- host = info['host']
-
- if host in known_hosts:
- # use fallbacks for old config style
- # identity was hard coded as 'homeassistant'
- identity = known_hosts[host].get('identity', 'homeassistant')
- key = known_hosts[host].get('key')
- await _setup_gateway(hass, config, host, identity, key,
- allow_groups)
- else:
- hass.async_add_job(request_configuration, hass, config, host)
-
- discovery.async_listen(hass, SERVICE_IKEA_TRADFRI, gateway_discovered)
-
- if host:
- await gateway_discovered(None,
- {'host': host},
- allow_tradfri_groups)
- return True
-
-
-async def _setup_gateway(hass, hass_config, host, identity, key,
- allow_tradfri_groups):
- """Create a gateway."""
- from pytradfri import Gateway, RequestError # pylint: disable=import-error
- try:
- from pytradfri.api.aiocoap_api import APIFactory
- except ImportError:
- _LOGGER.exception("Looks like something isn't installed!")
- return False
-
- try:
- factory = APIFactory(host, psk_id=identity, psk=key,
- loop=hass.loop)
- api = factory.request
- gateway = Gateway()
- gateway_info_result = await api(gateway.get_gateway_info())
- except RequestError:
- _LOGGER.exception("Tradfri setup failed.")
- return False
-
- gateway_id = gateway_info_result.id
- hass.data.setdefault(KEY_API, {})
- hass.data.setdefault(KEY_GATEWAY, {})
- gateways = hass.data[KEY_GATEWAY]
- hass.data[KEY_API][gateway_id] = api
-
- hass.data.setdefault(KEY_TRADFRI_GROUPS, {})
- tradfri_groups = hass.data[KEY_TRADFRI_GROUPS]
- tradfri_groups[gateway_id] = allow_tradfri_groups
-
- # Check if already set up
- if gateway_id in gateways:
- return True
-
- gateways[gateway_id] = gateway
- hass.async_create_task(discovery.async_load_platform(
- hass, 'light', DOMAIN, {'gateway': gateway_id}, hass_config))
- hass.async_create_task(discovery.async_load_platform(
- hass, 'sensor', DOMAIN, {'gateway': gateway_id}, hass_config))
- return True
diff --git a/homeassistant/components/tradfri/.translations/ca.json b/homeassistant/components/tradfri/.translations/ca.json
new file mode 100644
index 00000000000..acbbb275fc3
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/ca.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "No es pot connectar amb la passarel\u00b7la d'enlla\u00e7",
+ "invalid_key": "Ha fallat el registre amb la clau proporcionada. Si aix\u00f2 continua passant, intenteu reiniciar la passarel\u00b7la d'enlla\u00e7.",
+ "timeout": "S'ha acabat el temps d'espera durant la validaci\u00f3 del codi."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "security_code": "Codi de seguretat"
+ },
+ "description": "Podeu trobar el codi de seguretat a la part posterior de la vostra passarel\u00b7la d'enlla\u00e7.",
+ "title": "Introdu\u00efu el codi de seguretat"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/de.json b/homeassistant/components/tradfri/.translations/de.json
new file mode 100644
index 00000000000..5284ae18b6d
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung zum Gateway nicht m\u00f6glich.",
+ "invalid_key": "Fehler beim Registrieren mit dem angegebenen Schl\u00fcssel. Wenn dies weiterhin geschieht, versuche, das Gateway neu zu starten.",
+ "timeout": "Timeout bei der \u00dcberpr\u00fcfung des Codes."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "security_code": "Sicherheitscode"
+ },
+ "description": "Du findest den Sicherheitscode auf der R\u00fcckseite deines Gateways.",
+ "title": "Sicherheitscode eingeben"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/en.json b/homeassistant/components/tradfri/.translations/en.json
new file mode 100644
index 00000000000..7b0d2005c2a
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/en.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge is already configured"
+ },
+ "error": {
+ "cannot_connect": "Unable to connect to the gateway.",
+ "invalid_key": "Failed to register with provided key. If this keeps happening, try restarting the gateway.",
+ "timeout": "Timeout validating the code."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Host",
+ "security_code": "Security Code"
+ },
+ "description": "You can find the security code on the back of your gateway.",
+ "title": "Enter security code"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/fr.json b/homeassistant/components/tradfri/.translations/fr.json
new file mode 100644
index 00000000000..3c22885fe81
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/fr.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le pont est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "Impossible de se connecter \u00e0 la passerelle.",
+ "invalid_key": "\u00c9chec de l'enregistrement avec la cl\u00e9 fournie. Si cela se reproduit, essayez de red\u00e9marrer la passerelle.",
+ "timeout": "D\u00e9lai d'attente de la validation du code expir\u00e9"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "H\u00f4te",
+ "security_code": "Code de s\u00e9curit\u00e9"
+ },
+ "description": "Vous pouvez trouver le code de s\u00e9curit\u00e9 au dos de votre passerelle.",
+ "title": "Entrer le code de s\u00e9curit\u00e9"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/he.json b/homeassistant/components/tradfri/.translations/he.json
new file mode 100644
index 00000000000..09af3d09bdc
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/he.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05d2\u05e9\u05e8",
+ "invalid_key": "\u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05e0\u05db\u05e9\u05dc \u05e2\u05dd \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05e1\u05d5\u05e4\u05e7. \u05d0\u05dd \u05d6\u05d4 \u05e7\u05d5\u05e8\u05d4 \u05e9\u05d5\u05d1, \u05e0\u05e1\u05d4 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05de\u05d2\u05e9\u05e8.",
+ "timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "security_code": "\u05e7\u05d5\u05d3 \u05d0\u05d1\u05d8\u05d7\u05d4"
+ },
+ "description": "\u05ea\u05d5\u05db\u05dc \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d1\u05d8\u05d7\u05d4 \u05d1\u05d2\u05d1 \u05d4\u05de\u05d2\u05e9\u05e8 \u05e9\u05dc\u05da.",
+ "title": "\u05d4\u05d6\u05df \u05e7\u05d5\u05d3 \u05d0\u05d1\u05d8\u05d7\u05d4"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/hu.json b/homeassistant/components/tradfri/.translations/hu.json
new file mode 100644
index 00000000000..0844e6d7095
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/hu.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Hoszt",
+ "security_code": "Biztons\u00e1gi K\u00f3d"
+ }
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/id.json b/homeassistant/components/tradfri/.translations/id.json
new file mode 100644
index 00000000000..5e1439c8d7d
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/id.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Tidak dapat terhubung ke gateway.",
+ "invalid_key": "Gagal mendaftar dengan kunci yang disediakan. Jika ini terus terjadi, coba mulai ulang gateway.",
+ "timeout": "Waktu tunggu memvalidasi kode telah habis."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Host",
+ "security_code": "Kode keamanan"
+ },
+ "description": "Anda dapat menemukan kode keamanan di belakang gateway Anda.",
+ "title": "Masukkan kode keamanan"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/ko.json b/homeassistant/components/tradfri/.translations/ko.json
new file mode 100644
index 00000000000..b901a1fd508
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/ko.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "invalid_key": "\uc81c\uacf5\ub41c \ud0a4\ub85c \ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc774 \ubb38\uc81c\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\ubcf4\uc138\uc694.",
+ "timeout": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "security_code": "\ubcf4\uc548 \ucf54\ub4dc"
+ },
+ "description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ub4b7\uba74\uc5d0\uc11c \ubcf4\uc548 \ucf54\ub4dc\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "title": "\ubcf4\uc548 \ucf54\ub4dc \uc785\ub825"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/lb.json b/homeassistant/components/tradfri/.translations/lb.json
new file mode 100644
index 00000000000..8a623929d23
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/lb.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge ass schon konfigur\u00e9iert"
+ },
+ "error": {
+ "cannot_connect": "Keng Verbindung mat der Gateway m\u00e9iglech.",
+ "invalid_key": "Konnt sech net mam ugebuedenem Schl\u00ebssel registr\u00e9ieren. Falls d\u00ebst widderhuelt optr\u00ebtt, prob\u00e9iert de Gateway fr\u00ebsch ze starten.",
+ "timeout": "Z\u00e4it Iwwerschreidung\u00a0beim valid\u00e9ieren vum Code"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Apparat",
+ "security_code": "S\u00e9cherheets Code"
+ },
+ "description": "Dir fannt de S\u00e9cherheets Code op der R\u00e9cks\u00e4it vun \u00e4rem Gateway.",
+ "title": "Gitt de S\u00e9cherheets Code an"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/nl.json b/homeassistant/components/tradfri/.translations/nl.json
new file mode 100644
index 00000000000..1a681933b0b
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/nl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken met bridge",
+ "invalid_key": "Mislukt om te registreren met de meegeleverde sleutel. Als dit blijft gebeuren, probeer dan de gateway opnieuw op te starten.",
+ "timeout": "Time-out bij validatie van code"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Host",
+ "security_code": "Beveiligingscode"
+ },
+ "description": "U vindt de beveiligingscode op de achterkant van uw gateway.",
+ "title": "Voer beveiligingscode in"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/nn.json b/homeassistant/components/tradfri/.translations/nn.json
new file mode 100644
index 00000000000..b9c68668dac
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/nn.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Brua er allereie konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Klarte ikkje \u00e5 kople til gatewayen.",
+ "invalid_key": "Kunne ikkje registrere med den brukte n\u00f8kkelen. Dersom dette held fram, pr\u00f8v \u00e5 starte gatewayen p\u00e5 nytt. ",
+ "timeout": "Tida gjekk ut for validering av kode"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Vert",
+ "security_code": "Sikkerheitskode"
+ },
+ "description": "Du finn sikkerheitskoda p\u00e5 baksida av gatewayen din.",
+ "title": "Skriv inn sikkerheitskode"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/no.json b/homeassistant/components/tradfri/.translations/no.json
new file mode 100644
index 00000000000..7244648b4e7
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/no.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Kan ikke koble til gatewayen.",
+ "invalid_key": "Kunne ikke registrere med gitt n\u00f8kkel. Hvis dette fortsetter, pr\u00f8v \u00e5 starte gatewayen p\u00e5 nytt.",
+ "timeout": "Tidsavbrudd ved validering av kode."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Vert",
+ "security_code": "Sikkerhetskode"
+ },
+ "description": "Du finner sikkerhetskoden p\u00e5 baksiden av gatewayen din.",
+ "title": "Skriv inn sikkerhetskode"
+ }
+ },
+ "title": "Ikea Tr\u00e5dfri"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json
new file mode 100644
index 00000000000..ec253447ef4
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Mostek jest ju\u017c skonfigurowany"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z bram\u0105.",
+ "invalid_key": "Rejestracja si\u0119 nie powiod\u0142a z podanym kluczem. Je\u015bli tak si\u0119 stanie, spr\u00f3buj ponownie uruchomi\u0107 bramk\u0119.",
+ "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Host",
+ "security_code": "Kod bezpiecze\u0144stwa"
+ },
+ "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramy.",
+ "title": "Wprowad\u017a kod bezpiecze\u0144stwa"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/pt-BR.json b/homeassistant/components/tradfri/.translations/pt-BR.json
new file mode 100644
index 00000000000..d5ad6b96670
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/pt-BR.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge j\u00e1 est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao gateway.",
+ "timeout": "Excedido tempo limite para validar c\u00f3digo"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Hospedeiro",
+ "security_code": "C\u00f3digo de seguran\u00e7a"
+ },
+ "description": "Voc\u00ea pode encontrar o c\u00f3digo de seguran\u00e7a na parte de tr\u00e1s do seu gateway.",
+ "title": "Digite o c\u00f3digo de seguran\u00e7a"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/pt.json b/homeassistant/components/tradfri/.translations/pt.json
new file mode 100644
index 00000000000..05d3cbb57fe
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/pt.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge j\u00e1 est\u00e1 configurada"
+ },
+ "error": {
+ "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel ligar ao gateway.",
+ "invalid_key": "Falha ao registrar-se com a chave fornecida. Se o problema persistir, tente reiniciar o gateway.",
+ "timeout": "Tempo excedido a validar o c\u00f3digo."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Servidor",
+ "security_code": "C\u00f3digo de Seguran\u00e7a"
+ },
+ "description": "Encontra o c\u00f3digo de seguran\u00e7a na base da gateway.",
+ "title": "Introduzir c\u00f3digo de seguran\u00e7a"
+ }
+ },
+ "title": ""
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json
new file mode 100644
index 00000000000..c7fcfd50b56
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/ru.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443",
+ "invalid_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0448\u043b\u044e\u0437.",
+ "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "security_code": "\u041a\u043e\u0434 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438"
+ },
+ "description": "\u041a\u043e\u0434 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u043d\u0430 \u0437\u0430\u0434\u043d\u0435\u0439 \u043f\u0430\u043d\u0435\u043b\u0438 \u0448\u043b\u044e\u0437\u0430.",
+ "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 "
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/sl.json b/homeassistant/components/tradfri/.translations/sl.json
new file mode 100644
index 00000000000..ee2bf7d3d2b
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/sl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Most je \u017ee konfiguriran"
+ },
+ "error": {
+ "cannot_connect": "Povezava s prehodom ni mogo\u010de.",
+ "invalid_key": "Ni se bilo mogo\u010de registrirati s prilo\u017eenim klju\u010dem. \u010ce se to dogaja, poskusite znova zagnati prehod.",
+ "timeout": "\u010casovna omejitev za potrditev kode je potekla."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Gostitelj",
+ "security_code": "Varnostna koda"
+ },
+ "description": "Varnostno kodo najdete na hrbtni strani va\u0161ega prehoda.",
+ "title": "Vnesite varnostno kodo"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/sv.json b/homeassistant/components/tradfri/.translations/sv.json
new file mode 100644
index 00000000000..ffe8bff22b4
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/sv.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bryggan \u00e4r redan konfigurerad"
+ },
+ "error": {
+ "cannot_connect": "Det gick inte att ansluta till gatewayen."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "security_code": "S\u00e4kerhetskod"
+ },
+ "description": "Du kan hitta s\u00e4kerhetskoden p\u00e5 baksidan av din gateway.",
+ "title": "Ange s\u00e4kerhetskod"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/zh-Hans.json b/homeassistant/components/tradfri/.translations/zh-Hans.json
new file mode 100644
index 00000000000..4791e46062a
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/zh-Hans.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u6865\u63a5\u5668\u5df2\u914d\u7f6e\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u7f51\u5173\u3002",
+ "invalid_key": "\u65e0\u6cd5\u7528\u63d0\u4f9b\u7684\u5bc6\u94a5\u6ce8\u518c\u3002\u5982\u679c\u9519\u8bef\u6301\u7eed\u53d1\u751f\uff0c\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u542f\u52a8\u7f51\u5173\u3002",
+ "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "\u4e3b\u673a",
+ "security_code": "\u5b89\u5168\u7801"
+ },
+ "description": "\u60a8\u53ef\u4ee5\u5728\u7f51\u5173\u80cc\u9762\u627e\u5230\u5b89\u5168\u7801\u3002",
+ "title": "\u8f93\u5165\u5b89\u5168\u7801"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/zh-Hant.json b/homeassistant/components/tradfri/.translations/zh-Hant.json
new file mode 100644
index 00000000000..b295bba0564
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/zh-Hant.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u9598\u9053\u5668\u3002",
+ "invalid_key": "\u63d0\u4f9b\u4e4b\u5b89\u5168\u78bc\u8a3b\u518a\u5931\u6557\u3002\u5047\u5982\u6b64\u60c5\u6cc1\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5617\u8a66\u91cd\u555f\u9598\u9053\u5668\u3002",
+ "timeout": "\u8a8d\u8b49\u78bc\u903e\u6642\u3002"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "security_code": "\u5b89\u5168\u78bc"
+ },
+ "description": "\u60a8\u53ef\u4ee5\u65bc\u9598\u9053\u5668\u80cc\u9762\u627e\u5230\u5b89\u5168\u78bc\u3002",
+ "title": "\u8f38\u5165\u5b89\u5168\u78bc"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py
new file mode 100644
index 00000000000..6e91ab338a3
--- /dev/null
+++ b/homeassistant/components/tradfri/__init__.py
@@ -0,0 +1,123 @@
+"""
+Support for IKEA Tradfri.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/ikea_tradfri/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+import homeassistant.helpers.config_validation as cv
+from homeassistant.util.json import load_json
+
+from .const import (
+ CONF_IMPORT_GROUPS, CONF_IDENTITY, CONF_HOST, CONF_KEY, CONF_GATEWAY_ID)
+
+from . import config_flow # noqa pylint_disable=unused-import
+
+REQUIREMENTS = ['pytradfri[async]==5.5.1']
+
+DOMAIN = 'tradfri'
+CONFIG_FILE = '.tradfri_psk.conf'
+KEY_GATEWAY = 'tradfri_gateway'
+KEY_API = 'tradfri_api'
+CONF_ALLOW_TRADFRI_GROUPS = 'allow_tradfri_groups'
+DEFAULT_ALLOW_TRADFRI_GROUPS = True
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Inclusive(CONF_HOST, 'gateway'): cv.string,
+ vol.Optional(CONF_ALLOW_TRADFRI_GROUPS,
+ default=DEFAULT_ALLOW_TRADFRI_GROUPS): cv.boolean,
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass, config):
+ """Set up the Tradfri component."""
+ conf = config.get(DOMAIN)
+
+ if conf is None:
+ return True
+
+ configured_hosts = [entry.data['host'] for entry in
+ hass.config_entries.async_entries(DOMAIN)]
+
+ legacy_hosts = await hass.async_add_executor_job(
+ load_json, hass.config.path(CONFIG_FILE))
+
+ for host, info in legacy_hosts.items():
+ if host in configured_hosts:
+ continue
+
+ info[CONF_HOST] = host
+ info[CONF_IMPORT_GROUPS] = conf[CONF_ALLOW_TRADFRI_GROUPS]
+
+ hass.async_create_task(hass.config_entries.flow.async_init(
+ DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
+ data=info
+ ))
+
+ host = conf.get(CONF_HOST)
+
+ if host is None or host in configured_hosts or host in legacy_hosts:
+ return True
+
+ hass.async_create_task(hass.config_entries.flow.async_init(
+ DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
+ data={'host': host}
+ ))
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Create a gateway."""
+ # host, identity, key, allow_tradfri_groups
+ from pytradfri import Gateway, RequestError # pylint: disable=import-error
+ from pytradfri.api.aiocoap_api import APIFactory
+
+ factory = APIFactory(
+ entry.data[CONF_HOST],
+ psk_id=entry.data[CONF_IDENTITY],
+ psk=entry.data[CONF_KEY],
+ loop=hass.loop
+ )
+ api = factory.request
+ gateway = Gateway()
+
+ try:
+ gateway_info = await api(gateway.get_gateway_info())
+ except RequestError:
+ _LOGGER.error("Tradfri setup failed.")
+ return False
+
+ hass.data.setdefault(KEY_API, {})[entry.entry_id] = api
+ hass.data.setdefault(KEY_GATEWAY, {})[entry.entry_id] = gateway
+
+ dev_reg = await hass.helpers.device_registry.async_get_registry()
+ dev_reg.async_get_or_create(
+ config_entry_id=entry.entry_id,
+ connections=set(),
+ identifiers={
+ (DOMAIN, entry.data[CONF_GATEWAY_ID])
+ },
+ manufacturer='IKEA',
+ name='Gateway',
+ # They just have 1 gateway model. Type is not exposed yet.
+ model='E1526',
+ sw_version=gateway_info.firmware_version,
+ )
+
+ hass.async_create_task(hass.config_entries.async_forward_entry_setup(
+ entry, 'light'
+ ))
+ hass.async_create_task(hass.config_entries.async_forward_entry_setup(
+ entry, 'sensor'
+ ))
+
+ return True
diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py
new file mode 100644
index 00000000000..29aa768dbb5
--- /dev/null
+++ b/homeassistant/components/tradfri/config_flow.py
@@ -0,0 +1,180 @@
+"""Config flow for Tradfri."""
+import asyncio
+from collections import OrderedDict
+from uuid import uuid4
+
+import async_timeout
+import voluptuous as vol
+
+from homeassistant import config_entries
+
+from .const import (
+ CONF_IMPORT_GROUPS, CONF_IDENTITY, CONF_HOST, CONF_KEY, CONF_GATEWAY_ID)
+
+KEY_HOST = 'host'
+KEY_SECURITY_CODE = 'security_code'
+KEY_IMPORT_GROUPS = 'import_groups'
+
+
+class AuthError(Exception):
+ """Exception if authentication occurs."""
+
+ def __init__(self, code):
+ """Initialize exception."""
+ super().__init__()
+ self.code = code
+
+
+@config_entries.HANDLERS.register('tradfri')
+class FlowHandler(config_entries.ConfigFlow):
+ """Handle a config flow."""
+
+ VERSION = 1
+
+ def __init__(self):
+ """Initialize flow."""
+ self._host = None
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ return await self.async_step_auth()
+
+ async def async_step_auth(self, user_input=None):
+ """Handle the authentication with a gateway."""
+ errors = {}
+
+ if user_input is not None:
+ host = user_input.get(KEY_HOST, self._host)
+ try:
+ auth = await authenticate(
+ self.hass, host,
+ user_input[KEY_SECURITY_CODE])
+
+ # We don't ask for import group anymore as group state
+ # is not reliable, don't want to show that to the user.
+ auth[CONF_IMPORT_GROUPS] = False
+
+ return await self._entry_from_data(auth)
+
+ except AuthError as err:
+ if err.code == 'invalid_security_code':
+ errors[KEY_SECURITY_CODE] = err.code
+ else:
+ errors['base'] = err.code
+
+ fields = OrderedDict()
+
+ if self._host is None:
+ fields[vol.Required(KEY_HOST)] = str
+
+ fields[vol.Required(KEY_SECURITY_CODE)] = str
+
+ return self.async_show_form(
+ step_id='auth',
+ data_schema=vol.Schema(fields),
+ errors=errors,
+ )
+
+ async def async_step_discovery(self, user_input):
+ """Handle discovery."""
+ for entry in self._async_current_entries():
+ if entry.data[CONF_HOST] == user_input['host']:
+ return self.async_abort(
+ reason='already_configured'
+ )
+
+ self._host = user_input['host']
+ return await self.async_step_auth()
+
+ async def async_step_import(self, user_input):
+ """Import a config entry."""
+ for entry in self._async_current_entries():
+ if entry.data[CONF_HOST] == user_input['host']:
+ return self.async_abort(
+ reason='already_configured'
+ )
+
+ # Happens if user has host directly in configuration.yaml
+ if 'key' not in user_input:
+ self._host = user_input['host']
+ return await self.async_step_auth()
+
+ try:
+ data = await get_gateway_info(
+ self.hass, user_input['host'],
+ # Old config format had a fixed identity
+ user_input.get('identity', 'homeassistant'),
+ user_input['key'])
+
+ data[CONF_IMPORT_GROUPS] = user_input[CONF_IMPORT_GROUPS]
+
+ return await self._entry_from_data(data)
+ except AuthError:
+ # If we fail to connect, just pass it on to discovery
+ self._host = user_input['host']
+ return await self.async_step_auth()
+
+ async def _entry_from_data(self, data):
+ """Create an entry from data."""
+ host = data[CONF_HOST]
+ gateway_id = data[CONF_GATEWAY_ID]
+
+ same_hub_entries = [entry.entry_id for entry
+ in self._async_current_entries()
+ if entry.data[CONF_GATEWAY_ID] == gateway_id or
+ entry.data[CONF_HOST] == host]
+
+ if same_hub_entries:
+ await asyncio.wait([self.hass.config_entries.async_remove(entry_id)
+ for entry_id in same_hub_entries])
+
+ return self.async_create_entry(
+ title=host,
+ data=data
+ )
+
+
+async def authenticate(hass, host, security_code):
+ """Authenticate with a Tradfri hub."""
+ from pytradfri.api.aiocoap_api import APIFactory
+ from pytradfri import RequestError
+
+ identity = uuid4().hex
+
+ api_factory = APIFactory(host, psk_id=identity, loop=hass.loop)
+
+ try:
+ with async_timeout.timeout(5):
+ key = await api_factory.generate_psk(security_code)
+ except RequestError:
+ raise AuthError('invalid_security_code')
+ except asyncio.TimeoutError:
+ raise AuthError('timeout')
+
+ return await get_gateway_info(hass, host, identity, key)
+
+
+async def get_gateway_info(hass, host, identity, key):
+ """Return info for the gateway."""
+ from pytradfri.api.aiocoap_api import APIFactory
+ from pytradfri import Gateway, RequestError
+
+ try:
+ factory = APIFactory(
+ host,
+ psk_id=identity,
+ psk=key,
+ loop=hass.loop
+ )
+ api = factory.request
+ gateway = Gateway()
+ gateway_info_result = await api(gateway.get_gateway_info())
+ except RequestError:
+ raise AuthError('cannot_connect')
+
+ return {
+ CONF_HOST: host,
+ CONF_IDENTITY: identity,
+ CONF_KEY: key,
+ CONF_GATEWAY_ID: gateway_info_result.id,
+ }
diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py
new file mode 100644
index 00000000000..15177bc1a20
--- /dev/null
+++ b/homeassistant/components/tradfri/const.py
@@ -0,0 +1,7 @@
+"""Consts used by Tradfri."""
+from homeassistant.const import CONF_HOST # noqa pylint: disable=unused-import
+
+CONF_IMPORT_GROUPS = 'import_groups'
+CONF_IDENTITY = 'identity'
+CONF_KEY = 'key'
+CONF_GATEWAY_ID = 'gateway_id'
diff --git a/homeassistant/components/tradfri/strings.json b/homeassistant/components/tradfri/strings.json
new file mode 100644
index 00000000000..38c58486a6a
--- /dev/null
+++ b/homeassistant/components/tradfri/strings.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "title": "IKEA TRÅDFRI",
+ "step": {
+ "auth": {
+ "title": "Enter security code",
+ "description": "You can find the security code on the back of your gateway.",
+ "data": {
+ "host": "Host",
+ "security_code": "Security Code"
+ }
+ }
+ },
+ "error": {
+ "invalid_key": "Failed to register with provided key. If this keeps happening, try restarting the gateway.",
+ "cannot_connect": "Unable to connect to the gateway.",
+ "timeout": "Timeout validating the code."
+ },
+ "abort": {
+ "already_configured": "Bridge is already configured"
+ }
+ }
+}
diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py
index 0cb22bd98dc..4e64e3be2e6 100644
--- a/homeassistant/components/updater.py
+++ b/homeassistant/components/updater.py
@@ -76,7 +76,7 @@ async def async_setup(hass, config):
"""Set up the updater component."""
if 'dev' in current_version:
# This component only makes sense in release versions
- _LOGGER.warning("Running on 'dev', only analytics will be submitted")
+ _LOGGER.info("Running on 'dev', only analytics will be submitted")
config = config.get(DOMAIN, {})
if config.get(CONF_REPORTING):
diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py
index ac01d8e7a20..7f05554c496 100644
--- a/homeassistant/components/vacuum/ecovacs.py
+++ b/homeassistant/components/vacuum/ecovacs.py
@@ -2,7 +2,7 @@
Support for Ecovacs Ecovacs Vaccums.
For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/vacuum.neato/
+https://home-assistant.io/components/vacuum.ecovacs/
"""
import logging
@@ -43,9 +43,9 @@ class EcovacsVacuum(VacuumDevice):
"""Initialize the Ecovacs Vacuum."""
self.device = device
self.device.connect_and_wait_until_ready()
- try:
+ if self.device.vacuum.get('nick', None) is not None:
self._name = '{}'.format(self.device.vacuum['nick'])
- except KeyError:
+ else:
# In case there is no nickname defined, use the device id
self._name = '{}'.format(self.device.vacuum['did'])
@@ -189,10 +189,6 @@ class EcovacsVacuum(VacuumDevice):
for key, val in self.device.components.items():
attr_name = ATTR_COMPONENT_PREFIX + key
- data[attr_name] = int(val * 100 / 0.2777778)
- # The above calculation includes a fix for a bug in sucks 0.9.1
- # When sucks 0.9.2+ is released, it should be changed to the
- # following:
- # data[attr_name] = int(val * 100)
+ data[attr_name] = int(val * 100)
return data
diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py
index dd27b2a33d2..29db94de762 100644
--- a/homeassistant/components/vacuum/neato.py
+++ b/homeassistant/components/vacuum/neato.py
@@ -75,6 +75,8 @@ class NeatoConnectedVacuum(StateVacuumDevice):
requests.exceptions.HTTPError) as ex:
_LOGGER.warning("Neato connection error: %s", ex)
self._state = None
+ self._clean_state = STATE_ERROR
+ self._status_state = 'Robot Offline'
return
_LOGGER.debug('self._state=%s', self._state)
if self._state['state'] == 1:
@@ -188,6 +190,8 @@ class NeatoConnectedVacuum(StateVacuumDevice):
def return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock."""
+ if self._clean_state == STATE_CLEANING:
+ self.robot.pause_cleaning()
self._clean_state = STATE_RETURNING
self.robot.send_to_base()
diff --git a/homeassistant/components/velbus.py b/homeassistant/components/velbus.py
index d2def6f96bc..2304054c404 100644
--- a/homeassistant/components/velbus.py
+++ b/homeassistant/components/velbus.py
@@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_PORT
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['python-velbus==2.0.19']
+REQUIREMENTS = ['python-velbus==2.0.20']
_LOGGER = logging.getLogger(__name__)
@@ -48,7 +48,7 @@ async def async_setup(hass, config):
discovery_info = {
'switch': [],
'binary_sensor': [],
- 'temp_sensor': []
+ 'sensor': []
}
for module in modules:
for channel in range(1, module.number_of_channels() + 1):
@@ -63,7 +63,7 @@ async def async_setup(hass, config):
load_platform(hass, 'binary_sensor', DOMAIN,
discovery_info['binary_sensor'], config)
load_platform(hass, 'sensor', DOMAIN,
- discovery_info['temp_sensor'], config)
+ discovery_info['sensor'], config)
controller.scan(callback)
diff --git a/homeassistant/components/wake_on_lan.py b/homeassistant/components/wake_on_lan.py
index 4e729c7ccc7..5bcb0d4dd79 100644
--- a/homeassistant/components/wake_on_lan.py
+++ b/homeassistant/components/wake_on_lan.py
@@ -13,11 +13,12 @@ import voluptuous as vol
from homeassistant.const import CONF_MAC
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['wakeonlan==1.0.0']
+REQUIREMENTS = ['wakeonlan==1.1.6']
-DOMAIN = "wake_on_lan"
_LOGGER = logging.getLogger(__name__)
+DOMAIN = 'wake_on_lan'
+
CONF_BROADCAST_ADDRESS = 'broadcast_address'
SERVICE_SEND_MAGIC_PACKET = 'send_magic_packet'
diff --git a/homeassistant/components/weather/met.py b/homeassistant/components/weather/met.py
new file mode 100644
index 00000000000..f888af2e909
--- /dev/null
+++ b/homeassistant/components/weather/met.py
@@ -0,0 +1,225 @@
+"""
+Support for Met.no weather service.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/weather.met/
+"""
+import logging
+from random import randrange
+
+import voluptuous as vol
+
+from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity
+from homeassistant.const import (CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE,
+ CONF_NAME, TEMP_CELSIUS)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.event import (async_track_utc_time_change,
+ async_call_later)
+from homeassistant.util import dt as dt_util
+
+REQUIREMENTS = ['pyMetno==0.2.0']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_ATTRIBUTION = "Weather forecast from met.no, delivered " \
+ "by the Norwegian Meteorological Institute."
+DEFAULT_NAME = "Met.no"
+
+# https://api.met.no/weatherapi/weathericon/_/documentation/#___top
+CONDITIONS = {1: 'sunny',
+ 2: 'partlycloudy',
+ 3: 'partlycloudy',
+ 4: 'cloudy',
+ 5: 'rainy',
+ 6: 'lightning-rainy',
+ 7: 'snowy-rainy',
+ 8: 'snowy',
+ 9: 'rainy',
+ 10: 'rainy',
+ 11: 'lightning-rainy',
+ 12: 'snowy-rainy',
+ 13: 'snowy',
+ 14: 'snowy',
+ 15: 'fog',
+ 20: 'lightning-rainy',
+ 21: 'lightning-rainy',
+ 22: 'lightning-rainy',
+ 23: 'lightning-rainy',
+ 24: 'lightning-rainy',
+ 25: 'lightning-rainy',
+ 26: 'lightning-rainy',
+ 27: 'lightning-rainy',
+ 28: 'lightning-rainy',
+ 29: 'lightning-rainy',
+ 30: 'lightning-rainy',
+ 31: 'lightning-rainy',
+ 32: 'lightning-rainy',
+ 33: 'lightning-rainy',
+ 34: 'lightning-rainy',
+ 40: 'rainy',
+ 41: 'rainy',
+ 42: 'snowy-rainy',
+ 43: 'snowy-rainy',
+ 44: 'snowy',
+ 45: 'snowy',
+ 46: 'rainy',
+ 47: 'snowy-rainy',
+ 48: 'snowy-rainy',
+ 49: 'snowy',
+ 50: 'snowy',
+ }
+URL = 'https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Inclusive(CONF_LATITUDE, 'coordinates',
+ 'Latitude and longitude must exist together'): cv.latitude,
+ vol.Inclusive(CONF_LONGITUDE, 'coordinates',
+ 'Latitude and longitude must exist together'): cv.longitude,
+})
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Set up the Met.no weather platform."""
+ elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0)
+ latitude = config.get(CONF_LATITUDE, hass.config.latitude)
+ longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
+ name = config.get(CONF_NAME)
+
+ if None in (latitude, longitude):
+ _LOGGER.error("Latitude or longitude not set in Home Assistant config")
+ return
+
+ coordinates = {
+ 'lat': str(latitude),
+ 'lon': str(longitude),
+ 'msl': str(elevation),
+ }
+
+ async_add_entities([MetWeather(name, coordinates,
+ async_get_clientsession(hass))])
+
+
+class MetWeather(WeatherEntity):
+ """Implementation of a Met.no weather condition."""
+
+ def __init__(self, name, coordinates, clientsession):
+ """Initialise the platform with a data instance and site."""
+ import metno
+ self._name = name
+ self._weather_data = metno.MetWeatherData(coordinates,
+ clientsession,
+ URL
+ )
+ self._temperature = None
+ self._condition = None
+ self._pressure = None
+ self._humidity = None
+ self._wind_speed = None
+ self._wind_bearing = None
+
+ async def async_added_to_hass(self):
+ """Start fetching data."""
+ await self._fetch_data()
+ async_track_utc_time_change(self.hass, self._update,
+ minute=31, second=0)
+
+ async def _fetch_data(self, *_):
+ """Get the latest data from met.no."""
+ if not await self._weather_data.fetching_data():
+ # Retry in 15 to 20 minutes.
+ minutes = 15 + randrange(6)
+ _LOGGER.error("Retrying in %i minutes", minutes)
+ async_call_later(self.hass, minutes*60, self._fetch_data)
+ return
+
+ async_call_later(self.hass, 60*60, self._fetch_data)
+ await self._update()
+
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
+
+ async def _update(self, *_):
+ """Get the latest data from Met.no."""
+ import metno
+ if self._weather_data is None:
+ return
+
+ now = dt_util.utcnow()
+
+ ordered_entries = []
+ for time_entry in self._weather_data.data['product']['time']:
+ valid_from = dt_util.parse_datetime(time_entry['@from'])
+ valid_to = dt_util.parse_datetime(time_entry['@to'])
+
+ if now >= valid_to:
+ # Has already passed. Never select this.
+ continue
+
+ average_dist = (abs((valid_to - now).total_seconds()) +
+ abs((valid_from - now).total_seconds()))
+
+ ordered_entries.append((average_dist, time_entry))
+
+ if not ordered_entries:
+ return
+ ordered_entries.sort(key=lambda item: item[0])
+
+ self._temperature = metno.get_forecast('temperature', ordered_entries)
+ self._condition = CONDITIONS.get(metno.get_forecast('symbol',
+ ordered_entries))
+ self._pressure = metno.get_forecast('pressure', ordered_entries)
+ self._humidity = metno.get_forecast('humidity', ordered_entries)
+ self._wind_speed = metno.get_forecast('windSpeed', ordered_entries)
+ self._wind_bearing = metno.get_forecast('windDirection',
+ ordered_entries)
+ self.async_schedule_update_ha_state()
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def condition(self):
+ """Return the current condition."""
+ return self._condition
+
+ @property
+ def temperature(self):
+ """Return the temperature."""
+ return self._temperature
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def pressure(self):
+ """Return the pressure."""
+ return self._pressure
+
+ @property
+ def humidity(self):
+ """Return the humidity."""
+ return self._humidity
+
+ @property
+ def wind_speed(self):
+ """Return the wind speed."""
+ return self._wind_speed
+
+ @property
+ def wind_bearing(self):
+ """Return the wind direction."""
+ return self._wind_bearing
+
+ @property
+ def attribution(self):
+ """Return the attribution."""
+ return CONF_ATTRIBUTION
diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py
index e9db666c032..4e7c186facc 100644
--- a/homeassistant/components/websocket_api.py
+++ b/homeassistant/components/websocket_api.py
@@ -2,7 +2,7 @@
Websocket based API for Home Assistant.
For more details about this component, please refer to the documentation at
-https://home-assistant.io/developers/websocket_api/
+https://developers.home-assistant.io/docs/external_api_websocket.html
"""
import asyncio
from concurrent import futures
@@ -40,6 +40,7 @@ ERR_ID_REUSE = 1
ERR_INVALID_FORMAT = 2
ERR_NOT_FOUND = 3
ERR_UNKNOWN_COMMAND = 4
+ERR_UNKNOWN_ERROR = 5
TYPE_AUTH = 'auth'
TYPE_AUTH_INVALID = 'auth_invalid'
@@ -405,7 +406,13 @@ class ActiveConnection:
else:
handler, schema = handlers[msg['type']]
- handler(self.hass, self, schema(msg))
+ try:
+ handler(self.hass, self, schema(msg))
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception('Error handling message: %s', msg)
+ self.to_write.put_nowait(error_message(
+ cur_id, ERR_UNKNOWN_ERROR,
+ 'Unknown error.'))
last_id = cur_id
msg = await wsock.receive_json()
@@ -480,6 +487,26 @@ class ActiveConnection:
return wsock
+def async_response(func):
+ """Decorate an async function to handle WebSocket API messages."""
+ async def handle_msg_response(hass, connection, msg):
+ """Create a response and handle exception."""
+ try:
+ await func(hass, connection, msg)
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ connection.send_message_outside(error_message(
+ msg['id'], 'unknown', 'Unexpected error occurred'))
+
+ @callback
+ @wraps(func)
+ def schedule_handler(hass, connection, msg):
+ """Schedule the handler."""
+ hass.async_create_task(handle_msg_response(hass, connection, msg))
+
+ return schedule_handler
+
+
@callback
def handle_subscribe_events(hass, connection, msg):
"""Handle subscribe events command.
@@ -515,24 +542,20 @@ def handle_unsubscribe_events(hass, connection, msg):
msg['id'], ERR_NOT_FOUND, 'Subscription not found.'))
-@callback
-def handle_call_service(hass, connection, msg):
+@async_response
+async def handle_call_service(hass, connection, msg):
"""Handle call service command.
Async friendly.
"""
- async def call_service_helper(msg):
- """Call a service and fire complete message."""
- blocking = True
- if (msg['domain'] == 'homeassistant' and
- msg['service'] in ['restart', 'stop']):
- blocking = False
- await hass.services.async_call(
- msg['domain'], msg['service'], msg.get('service_data'), blocking,
- connection.context(msg))
- connection.send_message_outside(result_message(msg['id']))
-
- hass.async_add_job(call_service_helper(msg))
+ blocking = True
+ if (msg['domain'] == 'homeassistant' and
+ msg['service'] in ['restart', 'stop']):
+ blocking = False
+ await hass.services.async_call(
+ msg['domain'], msg['service'], msg.get('service_data'), blocking,
+ connection.context(msg))
+ connection.send_message_outside(result_message(msg['id']))
@callback
@@ -545,19 +568,15 @@ def handle_get_states(hass, connection, msg):
msg['id'], hass.states.async_all()))
-@callback
-def handle_get_services(hass, connection, msg):
+@async_response
+async def handle_get_services(hass, connection, msg):
"""Handle get services command.
Async friendly.
"""
- async def get_services_helper(msg):
- """Get available services and fire complete message."""
- descriptions = await async_get_all_descriptions(hass)
- connection.send_message_outside(
- result_message(msg['id'], descriptions))
-
- hass.async_add_job(get_services_helper(msg))
+ descriptions = await async_get_all_descriptions(hass)
+ connection.send_message_outside(
+ result_message(msg['id'], descriptions))
@callback
diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py
index c996572bf51..0399b25b278 100644
--- a/homeassistant/components/wink/__init__.py
+++ b/homeassistant/components/wink/__init__.py
@@ -26,7 +26,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import track_time_interval
from homeassistant.util.json import load_json, save_json
-REQUIREMENTS = ['python-wink==1.9.1', 'pubnubsub-handler==1.0.2']
+REQUIREMENTS = ['python-wink==1.10.1', 'pubnubsub-handler==1.0.2']
_LOGGER = logging.getLogger(__name__)
@@ -73,11 +73,25 @@ SERVICE_SET_AUTO_SHUTOFF = "siren_set_auto_shutoff"
SERVICE_SIREN_STROBE_ENABLED = "set_siren_strobe_enabled"
SERVICE_CHIME_STROBE_ENABLED = "set_chime_strobe_enabled"
SERVICE_ENABLE_SIREN = "enable_siren"
+SERVICE_SET_DIAL_CONFIG = "set_nimbus_dial_configuration"
+SERVICE_SET_DIAL_STATE = "set_nimbus_dial_state"
ATTR_VOLUME = "volume"
ATTR_TONE = "tone"
ATTR_ENABLED = "enabled"
ATTR_AUTO_SHUTOFF = "auto_shutoff"
+ATTR_MIN_VALUE = "min_value"
+ATTR_MAX_VALUE = "max_value"
+ATTR_ROTATION = "rotation"
+ATTR_SCALE = "scale"
+ATTR_TICKS = "ticks"
+ATTR_MIN_POSITION = "min_position"
+ATTR_MAX_POSITION = "max_position"
+ATTR_VALUE = "value"
+ATTR_LABELS = "labels"
+
+SCALES = ["linear", "log"]
+ROTATIONS = ["cw", "ccw"]
VOLUMES = ["low", "medium", "high"]
TONES = ["doorbell", "fur_elise", "doorbell_extended", "alert",
@@ -145,6 +159,23 @@ ENABLED_SIREN_SCHEMA = vol.Schema({
vol.Required(ATTR_ENABLED): cv.boolean
})
+DIAL_CONFIG_SCHEMA = vol.Schema({
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Optional(ATTR_MIN_VALUE): vol.Coerce(int),
+ vol.Optional(ATTR_MAX_VALUE): vol.Coerce(int),
+ vol.Optional(ATTR_MIN_POSITION): cv.positive_int,
+ vol.Optional(ATTR_MAX_POSITION): cv.positive_int,
+ vol.Optional(ATTR_ROTATION): vol.In(ROTATIONS),
+ vol.Optional(ATTR_SCALE): vol.In(SCALES),
+ vol.Optional(ATTR_TICKS): cv.positive_int
+})
+
+DIAL_STATE_SCHEMA = vol.Schema({
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Required(ATTR_VALUE): vol.Coerce(int),
+ vol.Optional(ATTR_LABELS): cv.ensure_list(cv.string)
+})
+
WINK_COMPONENTS = [
'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover', 'climate',
'fan', 'alarm_control_panel', 'scene'
@@ -432,8 +463,23 @@ def setup(hass, config):
DOMAIN, SERVICE_SET_PAIRING_MODE, set_pairing_mode,
schema=SET_PAIRING_MODE_SCHEMA)
- def service_handle(service):
- """Handle services."""
+ def nimbus_service_handle(service):
+ """Handle nimbus services."""
+ entity_id = service.data.get('entity_id')[0]
+ _all_dials = []
+ for sensor in hass.data[DOMAIN]['entities']['sensor']:
+ if isinstance(sensor, WinkNimbusDialDevice):
+ _all_dials.append(sensor)
+ for _dial in _all_dials:
+ if _dial.entity_id == entity_id:
+ if service.service == SERVICE_SET_DIAL_CONFIG:
+ _dial.set_configuration(**service.data)
+ if service.service == SERVICE_SET_DIAL_STATE:
+ _dial.wink.set_state(service.data.get("value"),
+ service.data.get("labels"))
+
+ def siren_service_handle(service):
+ """Handle siren services."""
entity_ids = service.data.get('entity_id')
all_sirens = []
for switch in hass.data[DOMAIN]['entities']['switch']:
@@ -495,41 +541,68 @@ def setup(hass, config):
if sirens:
hass.services.register(DOMAIN, SERVICE_SET_AUTO_SHUTOFF,
- service_handle,
+ siren_service_handle,
schema=SET_AUTO_SHUTOFF_SCHEMA)
hass.services.register(DOMAIN, SERVICE_ENABLE_SIREN,
- service_handle,
+ siren_service_handle,
schema=ENABLED_SIREN_SCHEMA)
if has_dome_or_wink_siren:
hass.services.register(DOMAIN, SERVICE_SET_SIREN_TONE,
- service_handle,
+ siren_service_handle,
schema=SET_SIREN_TONE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_ENABLE_CHIME,
- service_handle,
+ siren_service_handle,
schema=SET_CHIME_MODE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SET_SIREN_VOLUME,
- service_handle,
+ siren_service_handle,
schema=SET_VOLUME_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SET_CHIME_VOLUME,
- service_handle,
+ siren_service_handle,
schema=SET_VOLUME_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SIREN_STROBE_ENABLED,
- service_handle,
+ siren_service_handle,
schema=SET_STROBE_ENABLED_SCHEMA)
hass.services.register(DOMAIN, SERVICE_CHIME_STROBE_ENABLED,
- service_handle,
+ siren_service_handle,
schema=SET_STROBE_ENABLED_SCHEMA)
component.add_entities(sirens)
+ nimbi = []
+ dials = {}
+ all_nimbi = pywink.get_cloud_clocks()
+ all_dials = []
+ for nimbus in all_nimbi:
+ if nimbus.object_type() == "cloud_clock":
+ nimbi.append(nimbus)
+ dials[nimbus.object_id()] = []
+ for nimbus in all_nimbi:
+ if nimbus.object_type() == "dial":
+ dials[nimbus.parent_id()].append(nimbus)
+
+ for nimbus in nimbi:
+ for dial in dials[nimbus.object_id()]:
+ all_dials.append(WinkNimbusDialDevice(nimbus, dial, hass))
+
+ if nimbi:
+ hass.services.register(DOMAIN, SERVICE_SET_DIAL_CONFIG,
+ nimbus_service_handle,
+ schema=DIAL_CONFIG_SCHEMA)
+
+ hass.services.register(DOMAIN, SERVICE_SET_DIAL_STATE,
+ nimbus_service_handle,
+ schema=DIAL_STATE_SCHEMA)
+
+ component.add_entities(all_dials)
+
return True
@@ -596,6 +669,7 @@ class WinkDevice(Entity):
self.wink.name())
def _pubnub_update(self, message):
+ _LOGGER.debug(message)
try:
if message is None:
_LOGGER.error("Error on pubnub update for %s "
@@ -740,3 +814,70 @@ class WinkSirenDevice(WinkDevice):
attributes["chime_mode"] = chime_mode
return attributes
+
+
+class WinkNimbusDialDevice(WinkDevice):
+ """Representation of the Quirky Nimbus device."""
+
+ def __init__(self, nimbus, dial, hass):
+ """Initialize the Nimbus dial."""
+ super().__init__(dial, hass)
+ self.parent = nimbus
+
+ @asyncio.coroutine
+ def async_added_to_hass(self):
+ """Call when entity is added to hass."""
+ self.hass.data[DOMAIN]['entities']['sensor'].append(self)
+
+ @property
+ def state(self):
+ """Return dials current value."""
+ return self.wink.state()
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self.parent.name() + " dial " + str(self.wink.index() + 1)
+
+ @property
+ def device_state_attributes(self):
+ """Return the device state attributes."""
+ attributes = super(WinkNimbusDialDevice, self).device_state_attributes
+ dial_attributes = self.dial_attributes()
+
+ return {**attributes, **dial_attributes}
+
+ def dial_attributes(self):
+ """Return the dial only attributes."""
+ return {
+ "labels": self.wink.labels(),
+ "position": self.wink.position(),
+ "rotation": self.wink.rotation(),
+ "max_value": self.wink.max_value(),
+ "min_value": self.wink.min_value(),
+ "num_ticks": self.wink.ticks(),
+ "scale_type": self.wink.scale(),
+ "max_position": self.wink.max_position(),
+ "min_position": self.wink.min_position()
+ }
+
+ def set_configuration(self, **kwargs):
+ """
+ Set the dial config.
+
+ Anything not sent will default to current setting.
+ """
+ attributes = {**self.dial_attributes(), **kwargs}
+
+ min_value = attributes["min_value"]
+ max_value = attributes["max_value"]
+ rotation = attributes["rotation"]
+ ticks = attributes["num_ticks"]
+ scale = attributes["scale_type"]
+ min_position = attributes["min_position"]
+ max_position = attributes["max_position"]
+
+ self.wink.set_configuration(min_value, max_value, rotation,
+ scale=scale, ticks=ticks,
+ min_position=min_position,
+ max_position=max_position)
diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml
index 1dc4ecf959b..a3b489f9cf5 100644
--- a/homeassistant/components/wink/services.yaml
+++ b/homeassistant/components/wink/services.yaml
@@ -111,3 +111,44 @@ set_chime_volume:
volume:
description: Volume level. One of ["low", "medium", "high"]
example: "low"
+
+set_nimbus_dial_configuration:
+ description: Set the configuration of an individual nimbus dial
+ fields:
+ entity_id:
+ description: Name of the entity to set.
+ example: 'wink.nimbus_dial_3'
+ rotation:
+ description: Direction dial hand should spin ["cw" or "ccw"]
+ example: 'cw'
+ ticks:
+ description: Number of times the hand should move
+ example: 12
+ scale:
+ description: How the dial should move in response to higher values ["log" or "linear"]
+ example: "linear"
+ min_value:
+ description: The minimum value allowed to be set
+ example: 0
+ max_value:
+ description: The maximum value allowd to be set
+ example: 500
+ min_position:
+ description: The minimum position the dial hand can rotate to generally [0-360]
+ example: 0
+ max_position:
+ description: The maximum position the dial hand can rotate to generally [0-360]
+ example: 360
+
+set_nimbus_dial_state:
+ description: Set the value and lables of an individual nimbus dial
+ fields:
+ entity_id:
+ description: Name fo 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)
+ example: 250
+ labels:
+ description: The values shown on the dial labels ["Dial 1", "test"] the first value is what is shown by default the second value is shown when the nimbus is pressed
+ example: ["example", "test"]
\ No newline at end of file
diff --git a/homeassistant/components/wirelesstag.py b/homeassistant/components/wirelesstag.py
index 19fb2d40b5d..f2832100066 100644
--- a/homeassistant/components/wirelesstag.py
+++ b/homeassistant/components/wirelesstag.py
@@ -4,6 +4,7 @@ Wireless Sensor Tags platform support.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/wirelesstag/
"""
+
import logging
from requests.exceptions import HTTPError, ConnectTimeout
@@ -11,17 +12,18 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_USERNAME, CONF_PASSWORD)
import homeassistant.helpers.config_validation as cv
+from homeassistant import util
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.dispatcher import (
dispatcher_send)
-REQUIREMENTS = ['wirelesstagpy==0.3.0']
+REQUIREMENTS = ['wirelesstagpy==0.4.0']
_LOGGER = logging.getLogger(__name__)
-# straight of signal in dBm
-ATTR_TAG_SIGNAL_STRAIGHT = 'signal_straight'
+# strength of signal in dBm
+ATTR_TAG_SIGNAL_STRENGTH = 'signal_strength'
# indicates if tag is out of range or not
ATTR_TAG_OUT_OF_RANGE = 'out_of_range'
# number in percents from max power of tag receiver
@@ -34,13 +36,13 @@ NOTIFICATION_TITLE = "Wireless Sensor Tag Setup"
DOMAIN = 'wirelesstag'
DEFAULT_ENTITY_NAMESPACE = 'wirelesstag'
-WIRELESSTAG_TYPE_13BIT = 13
-WIRELESSTAG_TYPE_ALSPRO = 26
-WIRELESSTAG_TYPE_WATER = 32
-WIRELESSTAG_TYPE_WEMO_DEVICE = 82
+# template for signal - first parameter is tag_id,
+# second, tag manager mac address
+SIGNAL_TAG_UPDATE = 'wirelesstag.tag_info_updated_{}_{}'
-SIGNAL_TAG_UPDATE = 'wirelesstag.tag_info_updated_{}'
-SIGNAL_BINARY_EVENT_UPDATE = 'wirelesstag.binary_event_updated_{}_{}'
+# template for signal - tag_id, sensor type and
+# tag manager mac address
+SIGNAL_BINARY_EVENT_UPDATE = 'wirelesstag.binary_event_updated_{}_{}_{}'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -58,6 +60,12 @@ class WirelessTagPlatform:
self.hass = hass
self.api = api
self.tags = {}
+ self._local_base_url = None
+
+ @property
+ def tag_manager_macs(self):
+ """Return list of tag managers mac addresses in user account."""
+ return self.api.mac_addresses
def load_tags(self):
"""Load tags from remote server."""
@@ -69,72 +77,84 @@ class WirelessTagPlatform:
func_name = 'arm_{}'.format(switch.sensor_type)
arm_func = getattr(self.api, func_name)
if arm_func is not None:
- arm_func(switch.tag_id)
+ arm_func(switch.tag_id, switch.tag_manager_mac)
def disarm(self, switch):
"""Disarm entity sensor monitoring."""
func_name = 'disarm_{}'.format(switch.sensor_type)
disarm_func = getattr(self.api, func_name)
if disarm_func is not None:
- disarm_func(switch.tag_id)
+ disarm_func(switch.tag_id, switch.tag_manager_mac)
- # pylint: disable=no-self-use
- def make_push_notitication(self, name, url, content):
- """Create notification config."""
- from wirelesstagpy import NotificationConfig
- return NotificationConfig(name, {
- 'url': url, 'verb': 'POST',
- 'content': content, 'disabled': False, 'nat': True})
-
- def install_push_notifications(self, binary_sensors):
- """Set up local push notification from tag manager."""
- _LOGGER.info("Registering local push notifications.")
+ def make_notifications(self, binary_sensors, mac):
+ """Create configurations for push notifications."""
+ _LOGGER.info("Creating configurations for push notifications.")
configs = []
- binary_url = self.binary_event_callback_url
- for event in binary_sensors:
- for state, name in event.binary_spec.items():
- content = ('{"type": "' + event.device_class +
- '", "id":{' + str(event.tag_id_index_template) +
- '}, "state": \"' + state + '\"}')
- config = self.make_push_notitication(name, binary_url, content)
- configs.append(config)
+ bi_url = self.binary_event_callback_url
+ for bi_sensor in binary_sensors:
+ configs.extend(bi_sensor.event.build_notifications(bi_url, mac))
- content = ("{\"name\":\"{0}\",\"id\":{1},\"temp\":{2}," +
- "\"cap\":{3},\"lux\":{4}}")
update_url = self.update_callback_url
- update_config = self.make_push_notitication(
- 'update', update_url, content)
- configs.append(update_config)
+ from wirelesstagpy import NotificationConfig as NC
+ update_config = NC.make_config_for_update_event(update_url, mac)
- result = self.api.install_push_notification(0, configs, True)
- if not result:
- self.hass.components.persistent_notification.create(
- "Error: failed to install local push notifications
",
- title="Wireless Sensor Tag Setup Local Push Notifications",
- notification_id="wirelesstag_failed_push_notification")
- else:
- _LOGGER.info("Installed push notifications for all tags.")
+ configs.append(update_config)
+ return configs
+
+ def install_push_notifications(self, binary_sensors):
+ """Register local push notification from tag manager."""
+ _LOGGER.info("Registering local push notifications.")
+ for mac in self.tag_manager_macs:
+ configs = self.make_notifications(binary_sensors, mac)
+ # install notifications for all tags in tag manager
+ # specified by mac
+ result = self.api.install_push_notification(0, configs, True, mac)
+ if not result:
+ self.hass.components.persistent_notification.create(
+ "Error: failed to install local push notifications
",
+ title="Wireless Sensor Tag Setup Local Push Notifications",
+ notification_id="wirelesstag_failed_push_notification")
+ else:
+ _LOGGER.info("Installed push notifications for all\
+ tags in %s.", mac)
+
+ @property
+ def local_base_url(self):
+ """Define base url of hass in local network."""
+ if self._local_base_url is None:
+ self._local_base_url = "http://{}".format(util.get_local_ip())
+
+ port = self.hass.config.api.port
+ if port is not None:
+ self._local_base_url += ':{}'.format(port)
+ return self._local_base_url
@property
def update_callback_url(self):
"""Return url for local push notifications(update event)."""
return '{}/api/events/wirelesstag_update_tags'.format(
- self.hass.config.api.base_url)
+ self.local_base_url)
@property
def binary_event_callback_url(self):
"""Return url for local push notifications(binary event)."""
return '{}/api/events/wirelesstag_binary_event'.format(
- self.hass.config.api.base_url)
+ self.local_base_url)
def handle_update_tags_event(self, event):
"""Handle push event from wireless tag manager."""
_LOGGER.info("push notification for update arrived: %s", event)
- dispatcher_send(
- self.hass,
- SIGNAL_TAG_UPDATE.format(event.data.get('id')),
- event)
+ try:
+ tag_id = event.data.get('id')
+ mac = event.data.get('mac')
+ dispatcher_send(
+ self.hass,
+ SIGNAL_TAG_UPDATE.format(tag_id, mac),
+ event)
+ except Exception as ex: # pylint: disable=broad-except
+ _LOGGER.error("Unable to handle tag update event:\
+ %s error: %s", str(event), str(ex))
def handle_binary_event(self, event):
"""Handle push notifications for binary (on/off) events."""
@@ -142,12 +162,13 @@ class WirelessTagPlatform:
try:
tag_id = event.data.get('id')
event_type = event.data.get('type')
+ mac = event.data.get('mac')
dispatcher_send(
self.hass,
- SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type),
+ SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac),
event)
except Exception as ex: # pylint: disable=broad-except
- _LOGGER.error("Unable to handle binary event:\
+ _LOGGER.error("Unable to handle tag binary event:\
%s error: %s", str(event), str(ex))
@@ -193,6 +214,7 @@ class WirelessTagBaseSensor(Entity):
self._tag = tag
self._uuid = self._tag.uuid
self.tag_id = self._tag.tag_id
+ self.tag_manager_mac = self._tag.tag_manager_mac
self._name = self._tag.name
self._state = None
@@ -251,8 +273,8 @@ class WirelessTagBaseSensor(Entity):
return {
ATTR_BATTERY_LEVEL: self._tag.battery_remaining,
ATTR_VOLTAGE: '{:.2f}V'.format(self._tag.battery_volts),
- ATTR_TAG_SIGNAL_STRAIGHT: '{}dBm'.format(
- self._tag.signal_straight),
+ ATTR_TAG_SIGNAL_STRENGTH: '{}dBm'.format(
+ self._tag.signal_strength),
ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range,
ATTR_TAG_POWER_CONSUMPTION: '{:.2f}%'.format(
self._tag.power_consumption)
diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py
index 2090f522709..f2d51d2fc2e 100644
--- a/homeassistant/components/xiaomi_aqara.py
+++ b/homeassistant/components/xiaomi_aqara.py
@@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
from homeassistant.util import slugify
-REQUIREMENTS = ['PyXiaomiGateway==0.9.5']
+REQUIREMENTS = ['PyXiaomiGateway==0.10.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/zeroconf.py b/homeassistant/components/zeroconf.py
index f3917078f34..5d6161da904 100644
--- a/homeassistant/components/zeroconf.py
+++ b/homeassistant/components/zeroconf.py
@@ -12,7 +12,7 @@ import voluptuous as vol
from homeassistant import util
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__)
-REQUIREMENTS = ['zeroconf==0.21.2']
+REQUIREMENTS = ['zeroconf==0.21.3']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py
index 7aec4333ea8..7c48a577850 100644
--- a/homeassistant/components/zha/__init__.py
+++ b/homeassistant/components/zha/__init__.py
@@ -7,6 +7,7 @@ https://home-assistant.io/components/zha/
import collections
import enum
import logging
+import time
import voluptuous as vol
@@ -14,6 +15,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant import const as ha_const
from homeassistant.helpers import discovery, entity
from homeassistant.util import slugify
+from homeassistant.helpers.entity_component import EntityComponent
REQUIREMENTS = [
'bellows==0.7.0',
@@ -139,6 +141,7 @@ class ApplicationListener:
"""Initialize the listener."""
self._hass = hass
self._config = config
+ self._component = EntityComponent(_LOGGER, DOMAIN, hass)
self._device_registry = collections.defaultdict(list)
hass.data[DISCOVERY_KEY] = hass.data.get(DISCOVERY_KEY, {})
@@ -175,11 +178,16 @@ class ApplicationListener:
import homeassistant.components.zha.const as zha_const
zha_const.populate_data()
+ device_manufacturer = device_model = None
+
for endpoint_id, endpoint in device.endpoints.items():
if endpoint_id == 0: # ZDO
continue
- discovered_info = await _discover_endpoint_info(endpoint)
+ if endpoint.manufacturer is not None:
+ device_manufacturer = endpoint.manufacturer
+ if endpoint.model is not None:
+ device_model = endpoint.model
component = None
profile_clusters = ([], [])
@@ -212,10 +220,11 @@ class ApplicationListener:
'endpoint': endpoint,
'in_clusters': {c.cluster_id: c for c in in_clusters},
'out_clusters': {c.cluster_id: c for c in out_clusters},
+ 'manufacturer': endpoint.manufacturer,
+ 'model': endpoint.model,
'new_join': join,
'unique_id': device_key,
}
- discovery_info.update(discovered_info)
self._hass.data[DISCOVERY_KEY][device_key] = discovery_info
await discovery.async_load_platform(
@@ -234,7 +243,6 @@ class ApplicationListener:
device_key,
zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
'in_clusters',
- discovered_info,
join,
)
@@ -246,10 +254,17 @@ class ApplicationListener:
device_key,
zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
'out_clusters',
- discovered_info,
join,
)
+ endpoint_entity = ZhaDeviceEntity(
+ device,
+ device_manufacturer,
+ device_model,
+ self,
+ )
+ await self._component.async_add_entities([endpoint_entity])
+
def register_entity(self, ieee, entity_obj):
"""Record the creation of a hass entity associated with ieee."""
self._device_registry[ieee].append(entity_obj)
@@ -257,7 +272,7 @@ class ApplicationListener:
async def _attempt_single_cluster_device(self, endpoint, cluster,
profile_clusters, device_key,
device_classes, discovery_attr,
- entity_info, is_new_join):
+ is_new_join):
"""Try to set up an entity from a "bare" cluster."""
if cluster.cluster_id in profile_clusters:
return
@@ -277,12 +292,13 @@ class ApplicationListener:
'endpoint': endpoint,
'in_clusters': {},
'out_clusters': {},
+ 'manufacturer': endpoint.manufacturer,
+ 'model': endpoint.model,
'new_join': is_new_join,
'unique_id': cluster_key,
'entity_suffix': '_{}'.format(cluster.cluster_id),
}
discovery_info[discovery_attr] = {cluster.cluster_id: cluster}
- discovery_info.update(entity_info)
self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info
await discovery.async_load_platform(
@@ -338,6 +354,7 @@ class Entity(entity.Entity):
self._in_listeners = {}
self._out_listeners = {}
+ self._initialized = False
application_listener.register_entity(ieee, self)
async def async_added_to_hass(self):
@@ -350,6 +367,8 @@ class Entity(entity.Entity):
for cluster_id, cluster in self._out_clusters.items():
cluster.add_listener(self._out_listeners.get(cluster_id, self))
+ self._initialized = True
+
@property
def unique_id(self) -> str:
"""Return a unique ID."""
@@ -369,38 +388,75 @@ class Entity(entity.Entity):
pass
-async def _discover_endpoint_info(endpoint):
- """Find some basic information about an endpoint."""
- extra_info = {
- 'manufacturer': None,
- 'model': None,
- }
- if 0 not in endpoint.in_clusters:
- return extra_info
+class ZhaDeviceEntity(entity.Entity):
+ """A base class for ZHA devices."""
- async def read(attributes):
- """Read attributes and update extra_info convenience function."""
- result, _ = await endpoint.in_clusters[0].read_attributes(
- attributes,
- allow_cache=True,
- )
- extra_info.update(result)
+ def __init__(self, device, manufacturer, model, application_listener,
+ keepalive_interval=7200, **kwargs):
+ """Init ZHA endpoint entity."""
+ self._device_state_attributes = {
+ 'nwk': '0x{0:04x}'.format(device.nwk),
+ 'ieee': str(device.ieee),
+ 'lqi': device.lqi,
+ 'rssi': device.rssi,
+ }
- await read(['manufacturer', 'model'])
- if extra_info['manufacturer'] is None or extra_info['model'] is None:
- # Some devices fail at returning multiple results. Attempt separately.
- await read(['manufacturer'])
- await read(['model'])
+ ieee = device.ieee
+ ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
+ if manufacturer is not None and model is not None:
+ self._unique_id = "{}_{}_{}".format(
+ slugify(manufacturer),
+ slugify(model),
+ ieeetail,
+ )
+ self._device_state_attributes['friendly_name'] = "{} {}".format(
+ manufacturer,
+ model,
+ )
+ else:
+ self._unique_id = str(ieeetail)
- for key, value in extra_info.items():
- if isinstance(value, bytes):
- try:
- extra_info[key] = value.decode('ascii').strip()
- except UnicodeDecodeError:
- # Unsure what the best behaviour here is. Unset the key?
- pass
+ self._device = device
+ self._state = 'offline'
+ self._keepalive_interval = keepalive_interval
- return extra_info
+ application_listener.register_entity(ieee, self)
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ return self._unique_id
+
+ @property
+ def state(self) -> str:
+ """Return the state of the entity."""
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return device specific state attributes."""
+ update_time = None
+ if self._device.last_seen is not None and self._state == 'offline':
+ time_struct = time.localtime(self._device.last_seen)
+ update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
+ self._device_state_attributes['last_seen'] = update_time
+ if ('last_seen' in self._device_state_attributes and
+ self._state != 'offline'):
+ del self._device_state_attributes['last_seen']
+ self._device_state_attributes['lqi'] = self._device.lqi
+ self._device_state_attributes['rssi'] = self._device.rssi
+ return self._device_state_attributes
+
+ async def async_update(self):
+ """Handle polling."""
+ if self._device.last_seen is None:
+ self._state = 'offline'
+ else:
+ difference = time.time() - self._device.last_seen
+ if difference > self._keepalive_interval:
+ self._state = 'offline'
+ else:
+ self._state = 'online'
def get_discovery_info(hass, discovery_info):
@@ -420,7 +476,7 @@ def get_discovery_info(hass, discovery_info):
return all_discovery_info.get(discovery_key, None)
-async def safe_read(cluster, attributes, allow_cache=True):
+async def safe_read(cluster, attributes, allow_cache=True, only_cache=False):
"""Swallow all exceptions from network read.
If we throw during initialization, setup fails. Rather have an entity that
@@ -431,7 +487,47 @@ async def safe_read(cluster, attributes, allow_cache=True):
result, _ = await cluster.read_attributes(
attributes,
allow_cache=allow_cache,
+ only_cache=only_cache
)
return result
except Exception: # pylint: disable=broad-except
return {}
+
+
+async def configure_reporting(entity_id, cluster, attr, skip_bind=False,
+ min_report=300, max_report=900,
+ reportable_change=1):
+ """Configure attribute reporting for a cluster.
+
+ while swallowing the DeliverError exceptions in case of unreachable
+ devices.
+ """
+ from zigpy.exceptions import DeliveryError
+
+ attr_name = cluster.attributes.get(attr, [attr])[0]
+ cluster_name = cluster.ep_attribute
+ if not skip_bind:
+ try:
+ res = await cluster.bind()
+ _LOGGER.debug(
+ "%s: bound '%s' cluster: %s", entity_id, cluster_name, res[0]
+ )
+ except DeliveryError as ex:
+ _LOGGER.debug(
+ "%s: Failed to bind '%s' cluster: %s",
+ entity_id, cluster_name, str(ex)
+ )
+
+ try:
+ res = await cluster.configure_reporting(attr, min_report,
+ max_report, reportable_change)
+ _LOGGER.debug(
+ "%s: reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'",
+ entity_id, attr_name, cluster_name, min_report, max_report,
+ reportable_change, res
+ )
+ except DeliveryError as ex:
+ _LOGGER.debug(
+ "%s: failed to set reporting for '%s' attr on '%s' cluster: %s",
+ entity_id, attr_name, cluster_name, str(ex)
+ )
diff --git a/homeassistant/components/zone/.translations/de.json b/homeassistant/components/zone/.translations/de.json
index fc1e3537f33..483c7f065a3 100644
--- a/homeassistant/components/zone/.translations/de.json
+++ b/homeassistant/components/zone/.translations/de.json
@@ -13,7 +13,7 @@
"passive": "Passiv",
"radius": "Radius"
},
- "title": "Definieren Sie die Zonenparameter"
+ "title": "Definiere die Zonenparameter"
}
},
"title": "Zone"
diff --git a/homeassistant/components/zone/.translations/id.json b/homeassistant/components/zone/.translations/id.json
new file mode 100644
index 00000000000..b84710dc408
--- /dev/null
+++ b/homeassistant/components/zone/.translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Nama sudah ada"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Ikon",
+ "latitude": "Lintang",
+ "longitude": "Garis bujur",
+ "name": "Nama",
+ "passive": "Pasif",
+ "radius": "Radius"
+ },
+ "title": "Tentukan parameter zona"
+ }
+ },
+ "title": "Zona"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/nn.json b/homeassistant/components/zone/.translations/nn.json
new file mode 100644
index 00000000000..39161f98c82
--- /dev/null
+++ b/homeassistant/components/zone/.translations/nn.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Namnet eksisterar allereie"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Ikon",
+ "latitude": "Breiddegrad",
+ "longitude": "Lengdegrad",
+ "name": "Namn",
+ "passive": "Passiv",
+ "radius": "Radius"
+ },
+ "title": "Definer soneparameterar"
+ }
+ },
+ "title": "Sone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py
index 01577de4c8f..bf221a828ad 100644
--- a/homeassistant/components/zone/config_flow.py
+++ b/homeassistant/components/zone/config_flow.py
@@ -3,7 +3,7 @@
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
-from homeassistant import config_entries, data_entry_flow
+from homeassistant import config_entries
from homeassistant.const import (
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS)
from homeassistant.core import callback
@@ -20,7 +20,7 @@ def configured_zones(hass):
@config_entries.HANDLERS.register(DOMAIN)
-class ZoneFlowHandler(data_entry_flow.FlowHandler):
+class ZoneFlowHandler(config_entries.ConfigFlow):
"""Zone config flow."""
VERSION = 1
diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py
index 5c045544456..53d6d8b2536 100644
--- a/homeassistant/components/zoneminder.py
+++ b/homeassistant/components/zoneminder.py
@@ -5,9 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zoneminder/
"""
import logging
-from urllib.parse import urljoin
-import requests
import voluptuous as vol
from homeassistant.const import (
@@ -17,6 +15,8 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
+REQUIREMENTS = ['zm-py==0.0.3']
+
CONF_PATH_ZMS = 'path_zms'
DEFAULT_PATH = '/zm/'
@@ -26,10 +26,6 @@ DEFAULT_TIMEOUT = 10
DEFAULT_VERIFY_SSL = True
DOMAIN = 'zoneminder'
-LOGIN_RETRIES = 2
-
-ZM = {}
-
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
@@ -45,8 +41,7 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config):
"""Set up the ZoneMinder component."""
- global ZM
- ZM = {}
+ from zoneminder.zm import ZoneMinder
conf = config[DOMAIN]
if conf[CONF_SSL]:
@@ -55,83 +50,11 @@ def setup(hass, config):
schema = 'http'
server_origin = '{}://{}'.format(schema, conf[CONF_HOST])
- url = urljoin(server_origin, conf[CONF_PATH])
- username = conf.get(CONF_USERNAME, None)
- password = conf.get(CONF_PASSWORD, None)
+ hass.data[DOMAIN] = ZoneMinder(server_origin,
+ conf.get(CONF_USERNAME),
+ conf.get(CONF_PASSWORD),
+ conf.get(CONF_PATH),
+ conf.get(CONF_PATH_ZMS),
+ conf.get(CONF_VERIFY_SSL))
- ssl_verification = conf.get(CONF_VERIFY_SSL)
-
- ZM['server_origin'] = server_origin
- ZM['url'] = url
- ZM['username'] = username
- ZM['password'] = password
- ZM['path_zms'] = conf.get(CONF_PATH_ZMS)
- ZM['ssl_verification'] = ssl_verification
-
- hass.data[DOMAIN] = ZM
-
- return login()
-
-
-def login():
- """Login to the ZoneMinder API."""
- _LOGGER.debug("Attempting to login to ZoneMinder")
-
- login_post = {'view': 'console', 'action': 'login'}
- if ZM['username']:
- login_post['username'] = ZM['username']
- if ZM['password']:
- login_post['password'] = ZM['password']
-
- req = requests.post(ZM['url'] + '/index.php', data=login_post,
- verify=ZM['ssl_verification'], timeout=DEFAULT_TIMEOUT)
-
- ZM['cookies'] = req.cookies
-
- # Login calls returns a 200 response on both failure and success.
- # The only way to tell if you logged in correctly is to issue an api call.
- req = requests.get(
- ZM['url'] + 'api/host/getVersion.json', cookies=ZM['cookies'],
- timeout=DEFAULT_TIMEOUT, verify=ZM['ssl_verification'])
-
- if not req.ok:
- _LOGGER.error("Connection error logging into ZoneMinder")
- return False
-
- return True
-
-
-def _zm_request(method, api_url, data=None):
- """Perform a Zoneminder request."""
- # Since the API uses sessions that expire, sometimes we need to re-auth
- # if the call fails.
- for _ in range(LOGIN_RETRIES):
- req = requests.request(
- method, urljoin(ZM['url'], api_url), data=data,
- cookies=ZM['cookies'], timeout=DEFAULT_TIMEOUT,
- verify=ZM['ssl_verification'])
-
- if not req.ok:
- login()
- else:
- break
-
- else:
- _LOGGER.error("Unable to get API response from ZoneMinder")
-
- try:
- return req.json()
- except ValueError:
- _LOGGER.exception(
- "JSON decode exception caught while attempting to decode: %s",
- req.text)
-
-
-def get_state(api_url):
- """Get a state from the ZoneMinder API service."""
- return _zm_request('get', api_url)
-
-
-def change_state(api_url, post_data):
- """Update a state using the Zoneminder API."""
- return _zm_request('post', api_url, data=post_data)
+ return hass.data[DOMAIN].login()
diff --git a/homeassistant/config.py b/homeassistant/config.py
index d742e62660b..98857d8a83d 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -6,8 +6,7 @@ import logging
import os
import re
import shutil
-# pylint: disable=unused-import
-from typing import ( # noqa: F401
+from typing import ( # noqa: F401 pylint: disable=unused-import
Any, Tuple, Optional, Dict, List, Union, Callable, Sequence, Set)
from types import ModuleType
import voluptuous as vol
@@ -172,7 +171,7 @@ def _no_duplicate_auth_mfa_module(configs: Sequence[Dict[str, Any]]) \
PACKAGES_CONFIG_SCHEMA = vol.Schema({
cv.slug: vol.Schema( # Package names are slugs
- {cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names
+ {cv.string: vol.Any(dict, list, None)}) # Component configuration
})
CUSTOMIZE_DICT_SCHEMA = vol.Schema({
@@ -476,7 +475,7 @@ async def async_process_ha_core_config(
auth_conf.append({'type': 'trusted_networks'})
mfa_conf = config.get(CONF_AUTH_MFA_MODULES, [
- {'type': 'totp', 'id': 'totp', 'name': 'Authenticator app'}
+ {'type': 'totp', 'id': 'totp', 'name': 'Authenticator app'},
])
setattr(hass, 'auth', await auth.auth_manager_from_config(
@@ -663,7 +662,10 @@ def merge_packages_config(hass: HomeAssistant, config: Dict, packages: Dict,
for comp_name, comp_conf in pack_conf.items():
if comp_name == CONF_CORE:
continue
- component = get_component(hass, comp_name)
+ # If component name is given with a trailing description, remove it
+ # when looking for component
+ domain = comp_name.split(' ')[0]
+ component = get_component(hass, domain)
if component is None:
_log_pkg_error(pack_name, comp_name, config, "does not exist")
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 15932f2c3f8..7763594e0e1 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -27,9 +27,10 @@ At a minimum, each config flow will have to define a version number and the
'user' step.
@config_entries.HANDLERS.register(DOMAIN)
- class ExampleConfigFlow(data_entry_flow.FlowHandler):
+ class ExampleConfigFlow(config_entries.ConfigFlow):
VERSION = 1
+ CONNETION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
async def async_step_user(self, user_input=None):
…
@@ -117,7 +118,7 @@ the flow from the config panel.
import logging
import uuid
-from typing import Set, Optional, List # noqa pylint: disable=unused-import
+from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import
from homeassistant import data_entry_flow
from homeassistant.core import callback, HomeAssistant
@@ -140,9 +141,12 @@ FLOWS = [
'deconz',
'homematicip_cloud',
'hue',
+ 'ios',
+ 'mqtt',
'nest',
'openuv',
'sonos',
+ 'tradfri',
'zone',
]
@@ -168,15 +172,23 @@ DISCOVERY_SOURCES = (
EVENT_FLOW_DISCOVERED = 'config_entry_discovered'
+CONN_CLASS_CLOUD_PUSH = 'cloud_push'
+CONN_CLASS_CLOUD_POLL = 'cloud_poll'
+CONN_CLASS_LOCAL_PUSH = 'local_push'
+CONN_CLASS_LOCAL_POLL = 'local_poll'
+CONN_CLASS_ASSUMED = 'assumed'
+CONN_CLASS_UNKNOWN = 'unknown'
+
class ConfigEntry:
"""Hold a configuration entry."""
__slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source',
- 'state')
+ 'connection_class', 'state')
def __init__(self, version: str, domain: str, title: str, data: dict,
- source: str, entry_id: Optional[str] = None,
+ source: str, connection_class: str,
+ entry_id: Optional[str] = None,
state: str = ENTRY_STATE_NOT_LOADED) -> None:
"""Initialize a config entry."""
# Unique id of the config entry
@@ -197,6 +209,9 @@ class ConfigEntry:
# Source of the configuration (user, discovery, cloud)
self.source = source
+ # Connection class
+ self.connection_class = connection_class
+
# State of the entry (LOADED, NOT_LOADED)
self.state = state
@@ -264,6 +279,7 @@ class ConfigEntry:
'title': self.title,
'data': self.data,
'source': self.source,
+ 'connection_class': self.connection_class,
}
@@ -350,7 +366,18 @@ class ConfigEntries:
self._entries = []
return
- self._entries = [ConfigEntry(**entry) for entry in config['entries']]
+ self._entries = [
+ ConfigEntry(
+ version=entry['version'],
+ domain=entry['domain'],
+ entry_id=entry['entry_id'],
+ data=entry['data'],
+ source=entry['source'],
+ title=entry['title'],
+ # New in 0.79
+ connection_class=entry.get('connection_class',
+ CONN_CLASS_UNKNOWN))
+ for entry in config['entries']]
async def async_forward_entry_setup(self, entry, component):
"""Forward the setup of an entry to a different component.
@@ -400,6 +427,7 @@ class ConfigEntries:
title=result['title'],
data=result['data'],
source=flow.context['source'],
+ connection_class=flow.CONNECTION_CLASS,
)
self._entries.append(entry)
self._async_schedule_save()
@@ -462,3 +490,21 @@ class ConfigEntries:
async def _old_conf_migrator(old_config):
"""Migrate the pre-0.73 config format to the latest version."""
return {'entries': old_config}
+
+
+class ConfigFlow(data_entry_flow.FlowHandler):
+ """Base class for config flows with some helpers."""
+
+ CONNECTION_CLASS = CONN_CLASS_UNKNOWN
+
+ @callback
+ def _async_current_entries(self):
+ """Return current entries."""
+ return self.hass.config_entries.async_entries(self.handler)
+
+ @callback
+ def _async_in_progress(self):
+ """Return other in progress flows for current domain."""
+ return [flw for flw in self.hass.config_entries.flow.async_progress()
+ if flw['handler'] == self.handler and
+ flw['flow_id'] != self.flow_id]
diff --git a/homeassistant/const.py b/homeassistant/const.py
index e61d406b1fb..d3888d2651e 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,8 +1,8 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 78
-PATCH_VERSION = '3'
+MINOR_VERSION = 79
+PATCH_VERSION = '0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)
diff --git a/homeassistant/core.py b/homeassistant/core.py
index b4c824fe44b..d1f811502e0 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -18,8 +18,7 @@ from time import monotonic
import uuid
from types import MappingProxyType
-# pylint: disable=unused-import
-from typing import ( # NOQA
+from typing import ( # noqa: F401 pylint: disable=unused-import
Optional, Any, Callable, List, TypeVar, Dict, Coroutine, Set,
TYPE_CHECKING, Awaitable, Iterator)
@@ -130,10 +129,7 @@ class HomeAssistant:
self,
loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None:
"""Initialize new Home Assistant object."""
- if sys.platform == 'win32':
- self.loop = loop or asyncio.ProactorEventLoop()
- else:
- self.loop = loop or asyncio.get_event_loop()
+ self.loop = loop or asyncio.get_event_loop()
executor_opts = {'max_workers': None} # type: Dict[str, Any]
if sys.version_info[:2] >= (3, 6):
@@ -155,6 +151,8 @@ class HomeAssistant:
self.state = CoreState.not_running
self.exit_code = 0 # type: int
self.config_entries = None # type: Optional[ConfigEntries]
+ # If not None, use to signal end-of-loop
+ self._stopped = None # type: Optional[asyncio.Event]
@property
def is_running(self) -> bool:
@@ -162,23 +160,45 @@ class HomeAssistant:
return self.state in (CoreState.starting, CoreState.running)
def start(self) -> int:
- """Start home assistant."""
+ """Start home assistant.
+
+ Note: This function is only used for testing.
+ For regular use, use "await hass.run()".
+ """
# Register the async start
fire_coroutine_threadsafe(self.async_start(), self.loop)
- # Run forever and catch keyboard interrupt
+ # Run forever
try:
# Block until stopped
_LOGGER.info("Starting Home Assistant core loop")
self.loop.run_forever()
- except KeyboardInterrupt:
- self.loop.call_soon_threadsafe(
- self.loop.create_task, self.async_stop())
- self.loop.run_forever()
finally:
self.loop.close()
return self.exit_code
+ async def async_run(self, *, attach_signals: bool = True) -> int:
+ """Home Assistant main entry point.
+
+ Start Home Assistant and block until stopped.
+
+ This method is a coroutine.
+ """
+ if self.state != CoreState.not_running:
+ raise RuntimeError("HASS is already running")
+
+ # _async_stop will set this instead of stopping the loop
+ self._stopped = asyncio.Event()
+
+ await self.async_start()
+ if attach_signals:
+ from homeassistant.helpers.signal \
+ import async_register_signal_handling
+ async_register_signal_handling(self)
+
+ await self._stopped.wait()
+ return self.exit_code
+
async def async_start(self) -> None:
"""Finalize startup from inside the event loop.
@@ -204,6 +224,13 @@ class HomeAssistant:
# Allow automations to set up the start triggers before changing state
await asyncio.sleep(0)
+
+ if self.state != CoreState.starting:
+ _LOGGER.warning(
+ 'Home Assistant startup has been interrupted. '
+ 'Its state may be inconsistent.')
+ return
+
self.state = CoreState.running
_async_create_timer(self)
@@ -322,13 +349,32 @@ class HomeAssistant:
def stop(self) -> None:
"""Stop Home Assistant and shuts down all threads."""
+ if self.state == CoreState.not_running: # just ignore
+ return
fire_coroutine_threadsafe(self.async_stop(), self.loop)
- async def async_stop(self, exit_code: int = 0) -> None:
+ async def async_stop(self, exit_code: int = 0, *,
+ force: bool = False) -> None:
"""Stop Home Assistant and shuts down all threads.
+ The "force" flag commands async_stop to proceed regardless of
+ Home Assistan't current state. You should not set this flag
+ unless you're testing.
+
This method is a coroutine.
"""
+ if not force:
+ # Some tests require async_stop to run,
+ # regardless of the state of the loop.
+ if self.state == CoreState.not_running: # just ignore
+ return
+ if self.state == CoreState.stopping:
+ _LOGGER.info("async_stop called twice: ignored")
+ return
+ if self.state == CoreState.starting:
+ # This may not work
+ _LOGGER.warning("async_stop called before startup is complete")
+
# stage 1
self.state = CoreState.stopping
self.async_track_tasks()
@@ -342,7 +388,11 @@ class HomeAssistant:
self.executor.shutdown()
self.exit_code = exit_code
- self.loop.stop()
+
+ if self._stopped is not None:
+ self._stopped.set()
+ else:
+ self.loop.stop()
@attr.s(slots=True, frozen=True)
@@ -477,7 +527,7 @@ class EventBus:
event = Event(event_type, event_data, origin, None, context)
if event_type != EVENT_TIME_CHANGED:
- _LOGGER.info("Bus:Handling %s", event)
+ _LOGGER.debug("Bus:Handling %s", event)
if not listeners:
return
@@ -1231,22 +1281,26 @@ def _async_create_timer(hass: HomeAssistant) -> None:
"""Create a timer that will start on HOMEASSISTANT_START."""
handle = None
- @callback
- def fire_time_event(nxt: float) -> None:
- """Fire next time event."""
+ def schedule_tick(now: datetime.datetime) -> None:
+ """Schedule a timer tick when the next second rolls around."""
nonlocal handle
+ slp_seconds = 1 - (now.microsecond / 10**6)
+ target = monotonic() + slp_seconds
+ handle = hass.loop.call_later(slp_seconds, fire_time_event, target)
+
+ @callback
+ def fire_time_event(target: float) -> None:
+ """Fire next time event."""
+ now = dt_util.utcnow()
+
hass.bus.async_fire(EVENT_TIME_CHANGED,
- {ATTR_NOW: dt_util.utcnow()})
- nxt += 1
- slp_seconds = nxt - monotonic()
+ {ATTR_NOW: now})
- if slp_seconds < 0:
+ if monotonic() > target + 1:
_LOGGER.error('Timer got out of sync. Resetting')
- nxt = monotonic() + 1
- slp_seconds = 1
- handle = hass.loop.call_later(slp_seconds, fire_time_event, nxt)
+ schedule_tick(now)
@callback
def stop_timer(_: Event) -> None:
@@ -1257,4 +1311,4 @@ def _async_create_timer(hass: HomeAssistant) -> None:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_timer)
_LOGGER.info("Timer:starting")
- fire_time_event(monotonic())
+ schedule_tick(dt_util.utcnow())
diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py
index a54c07fc1b8..ecf9850a67c 100644
--- a/homeassistant/data_entry_flow.py
+++ b/homeassistant/data_entry_flow.py
@@ -1,8 +1,8 @@
"""Classes to help gather user submissions."""
import logging
+from typing import Dict, Any, Callable, Hashable, List, Optional # noqa pylint: disable=unused-import
import uuid
import voluptuous as vol
-from typing import Dict, Any, Callable, Hashable, List, Optional # noqa pylint: disable=unused-import
from .core import callback, HomeAssistant
from .exceptions import HomeAssistantError
diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py
index ed489ed858b..abc3b7a2324 100644
--- a/homeassistant/helpers/__init__.py
+++ b/homeassistant/helpers/__init__.py
@@ -1,6 +1,5 @@
"""Helper methods for components within Home Assistant."""
import re
-
from typing import Any, Iterable, Tuple, Sequence, Dict
from homeassistant.const import CONF_PLATFORM
diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py
index e17d5071c6a..569a101b3dd 100644
--- a/homeassistant/helpers/config_entry_flow.py
+++ b/homeassistant/helpers/config_entry_flow.py
@@ -1,26 +1,28 @@
"""Helpers for data entry flows for config entries."""
from functools import partial
-from homeassistant.core import callback
-from homeassistant import config_entries, data_entry_flow
+from homeassistant import config_entries
-def register_discovery_flow(domain, title, discovery_function):
+def register_discovery_flow(domain, title, discovery_function,
+ connection_class):
"""Register flow for discovered integrations that not require auth."""
config_entries.HANDLERS.register(domain)(
- partial(DiscoveryFlowHandler, domain, title, discovery_function))
+ partial(DiscoveryFlowHandler, domain, title, discovery_function,
+ connection_class))
-class DiscoveryFlowHandler(data_entry_flow.FlowHandler):
+class DiscoveryFlowHandler(config_entries.ConfigFlow):
"""Handle a discovery config flow."""
VERSION = 1
- def __init__(self, domain, title, discovery_function):
+ def __init__(self, domain, title, discovery_function, connection_class):
"""Initialize the discovery config flow."""
self._domain = domain
self._title = title
self._discovery_function = discovery_function
+ self.CONNECTION_CLASS = connection_class # pylint: disable=C0103
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
@@ -29,40 +31,39 @@ class DiscoveryFlowHandler(data_entry_flow.FlowHandler):
reason='single_instance_allowed'
)
- # Get current discovered entries.
- in_progress = self._async_in_progress()
+ return await self.async_step_confirm()
- has_devices = in_progress
- if not has_devices:
- has_devices = await self.hass.async_add_job(
- self._discovery_function, self.hass)
-
- if not has_devices:
- return self.async_abort(
- reason='no_devices_found'
+ async def async_step_confirm(self, user_input=None):
+ """Confirm setup."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id='confirm',
)
- # Cancel the discovered one.
- for flow in in_progress:
- self.hass.config_entries.flow.async_abort(flow['flow_id'])
+ if self.context and self.context.get('source') != \
+ config_entries.SOURCE_DISCOVERY:
+ # Get current discovered entries.
+ in_progress = self._async_in_progress()
+
+ has_devices = in_progress
+ if not has_devices:
+ has_devices = await self.hass.async_add_job(
+ self._discovery_function, self.hass)
+
+ if not has_devices:
+ return self.async_abort(
+ reason='no_devices_found'
+ )
+
+ # Cancel the discovered one.
+ for flow in in_progress:
+ self.hass.config_entries.flow.async_abort(flow['flow_id'])
return self.async_create_entry(
title=self._title,
data={},
)
- async def async_step_confirm(self, user_input=None):
- """Confirm setup."""
- if user_input is not None:
- return self.async_create_entry(
- title=self._title,
- data={},
- )
-
- return self.async_show_form(
- step_id='confirm',
- )
-
async def async_step_discovery(self, discovery_info):
"""Handle a flow initialized by discovery."""
if self._async_in_progress() or self._async_current_entries():
@@ -83,15 +84,3 @@ class DiscoveryFlowHandler(data_entry_flow.FlowHandler):
title=self._title,
data={},
)
-
- @callback
- def _async_current_entries(self):
- """Return current entries."""
- return self.hass.config_entries.async_entries(self._domain)
-
- @callback
- def _async_in_progress(self):
- """Return other in progress flows for current domain."""
- return [flw for flw in self.hass.config_entries.flow.async_progress()
- if flw['handler'] == self._domain and
- flw['flow_id'] != self.flow_id]
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 90098a677a1..bb4dcf6a55f 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -7,7 +7,6 @@ from urllib.parse import urlparse
from socket import _GLOBAL_DEFAULT_TIMEOUT
import logging
import inspect
-
from typing import Any, Union, TypeVar, Callable, Sequence, Dict
import voluptuous as vol
@@ -591,7 +590,7 @@ _SCRIPT_DELAY_SCHEMA = vol.Schema({
vol.Optional(CONF_ALIAS): string,
vol.Required("delay"): vol.Any(
vol.All(time_period, positive_timedelta),
- template)
+ template, template_complex)
})
_SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema({
diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py
index e6ff45af2fe..8d4cd0a5bbf 100644
--- a/homeassistant/helpers/device_registry.py
+++ b/homeassistant/helpers/device_registry.py
@@ -10,6 +10,7 @@ from homeassistant.core import callback
from homeassistant.loader import bind_hass
_LOGGER = logging.getLogger(__name__)
+_UNDEF = object()
DATA_REGISTRY = 'device_registry'
@@ -32,6 +33,7 @@ class DeviceEntry:
model = attr.ib(type=str)
name = attr.ib(type=str, default=None)
sw_version = attr.ib(type=str, default=None)
+ hub_device_id = attr.ib(type=str, default=None)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
@@ -54,28 +56,36 @@ class DeviceRegistry:
return None
@callback
- def async_get_or_create(self, *, config_entry, connections, identifiers,
- manufacturer, model, name=None, sw_version=None):
+ def async_get_or_create(self, *, config_entry_id, connections, identifiers,
+ manufacturer, model, name=None, sw_version=None,
+ via_hub=None):
"""Get device. Create if it doesn't exist."""
if not identifiers and not connections:
return None
device = self.async_get_device(identifiers, connections)
+ if via_hub is not None:
+ hub_device = self.async_get_device({via_hub}, set())
+ hub_device_id = hub_device.id if hub_device else None
+ else:
+ hub_device_id = None
+
if device is not None:
- if config_entry not in device.config_entries:
- device.config_entries.add(config_entry)
- self.async_schedule_save()
- return device
+ return self._async_update_device(
+ device.id, config_entry_id=config_entry_id,
+ hub_device_id=hub_device_id
+ )
device = DeviceEntry(
- config_entries=[config_entry],
+ config_entries={config_entry_id},
connections=connections,
identifiers=identifiers,
manufacturer=manufacturer,
model=model,
name=name,
- sw_version=sw_version
+ sw_version=sw_version,
+ hub_device_id=hub_device_id
)
self.devices[device.id] = device
@@ -83,24 +93,64 @@ class DeviceRegistry:
return device
+ @callback
+ def _async_update_device(self, device_id, *, config_entry_id=_UNDEF,
+ remove_config_entry_id=_UNDEF,
+ hub_device_id=_UNDEF):
+ """Update device attributes."""
+ old = self.devices[device_id]
+
+ changes = {}
+
+ config_entries = old.config_entries
+
+ if (config_entry_id is not _UNDEF and
+ config_entry_id not in old.config_entries):
+ config_entries = old.config_entries | {config_entry_id}
+
+ if (remove_config_entry_id is not _UNDEF and
+ remove_config_entry_id in config_entries):
+ config_entries = set(config_entries)
+ config_entries.remove(remove_config_entry_id)
+
+ if config_entries is not old.config_entries:
+ changes['config_entries'] = config_entries
+
+ if (hub_device_id is not _UNDEF and
+ hub_device_id != old.hub_device_id):
+ changes['hub_device_id'] = hub_device_id
+
+ if not changes:
+ return old
+
+ new = self.devices[device_id] = attr.evolve(old, **changes)
+ self.async_schedule_save()
+ return new
+
async def async_load(self):
"""Load the device registry."""
- devices = await self._store.async_load()
+ data = await self._store.async_load()
- if devices is None:
- self.devices = OrderedDict()
- return
+ devices = OrderedDict()
- self.devices = {device['id']: DeviceEntry(
- config_entries=device['config_entries'],
- connections={tuple(conn) for conn in device['connections']},
- identifiers={tuple(iden) for iden in device['identifiers']},
- manufacturer=device['manufacturer'],
- model=device['model'],
- name=device['name'],
- sw_version=device['sw_version'],
- id=device['id'],
- ) for device in devices['devices']}
+ if data is not None:
+ for device in data['devices']:
+ devices[device['id']] = DeviceEntry(
+ config_entries=set(device['config_entries']),
+ connections={tuple(conn) for conn
+ in device['connections']},
+ identifiers={tuple(iden) for iden
+ in device['identifiers']},
+ manufacturer=device['manufacturer'],
+ model=device['model'],
+ name=device['name'],
+ sw_version=device['sw_version'],
+ id=device['id'],
+ # Introduced in 0.79
+ hub_device_id=device.get('hub_device_id'),
+ )
+
+ self.devices = devices
@callback
def async_schedule_save(self):
@@ -122,18 +172,19 @@ class DeviceRegistry:
'name': entry.name,
'sw_version': entry.sw_version,
'id': entry.id,
+ 'hub_device_id': entry.hub_device_id,
} for entry in self.devices.values()
]
return data
@callback
- def async_clear_config_entry(self, config_entry):
+ def async_clear_config_entry(self, config_entry_id):
"""Clear config entry from registry entries."""
- for device in self.devices.values():
- if config_entry in device.config_entries:
- device.config_entries.remove(config_entry)
- self.async_schedule_save()
+ for dev_id, device in self.devices.items():
+ if config_entry_id in device.config_entries:
+ self._async_update_device(
+ dev_id, remove_config_entry_id=config_entry_id)
@bind_hass
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index 695da5bce9c..e48af6a3365 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -4,7 +4,6 @@ from datetime import timedelta
import logging
import functools as ft
from timeit import default_timer as timer
-
from typing import Optional, List, Iterable
from homeassistant.const import (
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index 083a2946122..f2913e37339 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -273,16 +273,19 @@ class EntityPlatform:
config_entry_id = None
device_info = entity.device_info
+
if config_entry_id is not None and device_info is not None:
device = device_registry.async_get_or_create(
- config_entry=config_entry_id,
- connections=device_info.get('connections', []),
- identifiers=device_info.get('identifiers', []),
+ config_entry_id=config_entry_id,
+ connections=device_info.get('connections') or set(),
+ identifiers=device_info.get('identifiers') or set(),
manufacturer=device_info.get('manufacturer'),
model=device_info.get('model'),
name=device_info.get('name'),
- sw_version=device_info.get('sw_version'))
- device_id = device.id
+ sw_version=device_info.get('sw_version'),
+ via_hub=device_info.get('via_hub'))
+ if device:
+ device_id = device.id
else:
device_id = None
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index da3645a96fe..4a5daa182fa 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -31,7 +31,7 @@ STORAGE_VERSION = 1
STORAGE_KEY = 'core.entity_registry'
-@attr.s(slots=True)
+@attr.s(slots=True, frozen=True)
class RegistryEntry:
"""Entity Registry Entry."""
@@ -113,14 +113,9 @@ class EntityRegistry:
"""Get entity. Create if it doesn't exist."""
entity_id = self.async_get_entity_id(domain, platform, unique_id)
if entity_id:
- entry = self.entities[entity_id]
- if entry.config_entry_id == config_entry_id:
- return entry
-
- self._async_update_entity(
+ return self._async_update_entity(
entity_id, config_entry_id=config_entry_id,
device_id=device_id)
- return self.entities[entity_id]
entity_id = self.async_generate_entity_id(
domain, suggested_object_id or '{}_{}'.format(platform, unique_id))
@@ -245,6 +240,7 @@ class EntityRegistry:
'unique_id': entry.unique_id,
'platform': entry.platform,
'name': entry.name,
+ 'disabled_by': entry.disabled_by,
} for entry in self.entities.values()
]
@@ -253,10 +249,9 @@ class EntityRegistry:
@callback
def async_clear_config_entry(self, config_entry):
"""Clear config entry from registry entries."""
- for entry in self.entities.values():
+ for entity_id, entry in self.entities.items():
if config_entry == entry.config_entry_id:
- entry.config_entry_id = None
- self.async_schedule_save()
+ self._async_update_entity(entity_id, config_entry_id=None)
@bind_hass
diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py
index c28ee8c5c2c..bdb82687a32 100644
--- a/homeassistant/helpers/json.py
+++ b/homeassistant/helpers/json.py
@@ -2,7 +2,6 @@
from datetime import datetime
import json
import logging
-
from typing import Any
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index 8a33b57e9d1..96f9b2d5069 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -107,6 +107,11 @@ class Script():
cv.time_period,
cv.positive_timedelta)(
delay.async_render(variables))
+ elif isinstance(delay, dict):
+ delay_data = {}
+ delay_data.update(
+ template.render_complex(delay, variables))
+ delay = cv.time_period(delay_data)
except (TemplateError, vol.Invalid) as ex:
_LOGGER.error("Error rendering '%s' delay template: %s",
self.name, ex)
diff --git a/homeassistant/helpers/signal.py b/homeassistant/helpers/signal.py
index 824b32177cd..7496388fb52 100644
--- a/homeassistant/helpers/signal.py
+++ b/homeassistant/helpers/signal.py
@@ -17,7 +17,13 @@ def async_register_signal_handling(hass: HomeAssistant) -> None:
if sys.platform != 'win32':
@callback
def async_signal_handle(exit_code):
- """Wrap signal handling."""
+ """Wrap signal handling.
+
+ * queue call to shutdown task
+ * re-instate default handler
+ """
+ hass.loop.remove_signal_handler(signal.SIGTERM)
+ hass.loop.remove_signal_handler(signal.SIGINT)
hass.async_create_task(hass.async_stop(exit_code))
try:
@@ -26,8 +32,39 @@ def async_register_signal_handling(hass: HomeAssistant) -> None:
except ValueError:
_LOGGER.warning("Could not bind to SIGTERM")
+ try:
+ hass.loop.add_signal_handler(
+ signal.SIGINT, async_signal_handle, 0)
+ except ValueError:
+ _LOGGER.warning("Could not bind to SIGINT")
+
try:
hass.loop.add_signal_handler(
signal.SIGHUP, async_signal_handle, RESTART_EXIT_CODE)
except ValueError:
_LOGGER.warning("Could not bind to SIGHUP")
+
+ else:
+ old_sigterm = None
+ old_sigint = None
+
+ @callback
+ def async_signal_handle(exit_code, frame):
+ """Wrap signal handling.
+
+ * queue call to shutdown task
+ * re-instate default handler
+ """
+ signal.signal(signal.SIGTERM, old_sigterm)
+ signal.signal(signal.SIGINT, old_sigint)
+ hass.async_create_task(hass.async_stop(exit_code))
+
+ try:
+ old_sigterm = signal.signal(signal.SIGTERM, async_signal_handle)
+ except ValueError:
+ _LOGGER.warning("Could not bind to SIGTERM")
+
+ try:
+ old_sigint = signal.signal(signal.SIGINT, async_signal_handle)
+ except ValueError:
+ _LOGGER.warning("Could not bind to SIGINT")
diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index 3ac49e354b5..6fb003926e1 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -15,9 +15,7 @@ import importlib
import logging
import sys
from types import ModuleType
-
-# pylint: disable=unused-import
-from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar # NOQA
+from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar # noqa pylint: disable=unused-import
from homeassistant.const import PLATFORM_FORMAT
from homeassistant.util import OrderedSet
diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py
index 7aba3b2561c..02cc0bff362 100644
--- a/homeassistant/scripts/__init__.py
+++ b/homeassistant/scripts/__init__.py
@@ -5,7 +5,6 @@ import importlib
import logging
import os
import sys
-
from typing import List
from homeassistant.bootstrap import async_mount_local_lib_path
diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py
index e0c933df5bb..94add794651 100644
--- a/homeassistant/scripts/check_config.py
+++ b/homeassistant/scripts/check_config.py
@@ -6,10 +6,10 @@ import os
from collections import OrderedDict, namedtuple
from glob import glob
from platform import system
+from typing import Dict, List, Sequence
from unittest.mock import patch
import attr
-from typing import Dict, List, Sequence
import voluptuous as vol
from homeassistant import bootstrap, core, loader
diff --git a/homeassistant/scripts/influxdb_import.py b/homeassistant/scripts/influxdb_import.py
index 031df1d3a72..a6dd90920c3 100644
--- a/homeassistant/scripts/influxdb_import.py
+++ b/homeassistant/scripts/influxdb_import.py
@@ -3,7 +3,6 @@ import argparse
import json
import os
import sys
-
from typing import List
import homeassistant.config as config_util
diff --git a/homeassistant/scripts/influxdb_migrator.py b/homeassistant/scripts/influxdb_migrator.py
index a4c0df74b09..04d54cd3fa8 100644
--- a/homeassistant/scripts/influxdb_migrator.py
+++ b/homeassistant/scripts/influxdb_migrator.py
@@ -2,7 +2,6 @@
import argparse
import sys
-
from typing import List
diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py
index e48fa809d19..76a9d9318f2 100644
--- a/homeassistant/scripts/keyring.py
+++ b/homeassistant/scripts/keyring.py
@@ -5,7 +5,7 @@ import os
from homeassistant.util.yaml import _SECRET_NAMESPACE
-REQUIREMENTS = ['keyring==15.0.0', 'keyrings.alt==3.1']
+REQUIREMENTS = ['keyring==15.1.0', 'keyrings.alt==3.1']
def run(args):
diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py
index 1e74c500fc1..17849154ff7 100644
--- a/homeassistant/util/__init__.py
+++ b/homeassistant/util/__init__.py
@@ -11,7 +11,6 @@ import string
from functools import wraps
from types import MappingProxyType
from unicodedata import normalize
-
from typing import (Any, Optional, TypeVar, Callable, KeysView, Union, # noqa
Iterable, List, Dict, Iterator, Coroutine, MutableSet)
diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py
index aa030bf13c7..04456b8cb2f 100644
--- a/homeassistant/util/async_.py
+++ b/homeassistant/util/async_.py
@@ -6,12 +6,32 @@ from asyncio import coroutines
from asyncio.events import AbstractEventLoop
from asyncio.futures import Future
+import asyncio
from asyncio import ensure_future
-from typing import Any, Union, Coroutine, Callable, Generator
+from typing import Any, Union, Coroutine, Callable, Generator, TypeVar, \
+ Awaitable
_LOGGER = logging.getLogger(__name__)
+try:
+ # pylint: disable=invalid-name
+ asyncio_run = asyncio.run # type: ignore
+except AttributeError:
+ _T = TypeVar('_T')
+
+ def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T:
+ """Minimal re-implementation of asyncio.run (since 3.7)."""
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ loop.set_debug(debug)
+ try:
+ return loop.run_until_complete(main)
+ finally:
+ asyncio.set_event_loop(None) # type: ignore # not a bug
+ loop.close()
+
+
def _set_result_unless_cancelled(fut: Future, result: Any) -> None:
"""Set the result only if the Future was not cancelled."""
if fut.cancelled():
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index 729195fb3fd..5d4b10454a7 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -1,9 +1,7 @@
"""Helper methods to handle the time in Home Assistant."""
import datetime as dt
import re
-
-# pylint: disable=unused-import
-from typing import Any, Dict, Union, Optional, Tuple # NOQA
+from typing import Any, Dict, Union, Optional, Tuple # noqa pylint: disable=unused-import
import pytz
import pytz.exceptions as pytzexceptions
diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py
index 422809f7594..925755eb741 100644
--- a/homeassistant/util/package.py
+++ b/homeassistant/util/package.py
@@ -4,7 +4,6 @@ import logging
import os
from subprocess import PIPE, Popen
import sys
-
from typing import Optional
_LOGGER = logging.getLogger(__name__)
diff --git a/pylintrc b/pylintrc
index b72502248d7..be06f83e6f2 100644
--- a/pylintrc
+++ b/pylintrc
@@ -42,5 +42,8 @@ reports=no
ignored-classes=_CountingAttr
generated-members=botocore.errorfactory
+[FORMAT]
+expected-line-ending-format=LF
+
[EXCEPTIONS]
overgeneral-exceptions=Exception,HomeAssistantError
diff --git a/requirements_all.txt b/requirements_all.txt
index 5776a7c1944..61a412951f9 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -52,13 +52,13 @@ PyMata==2.14
PyQRCode==1.2.1
# homeassistant.components.sensor.rmvtransport
-PyRMVtransport==0.0.7
+PyRMVtransport==0.1
# homeassistant.components.switch.switchbot
PySwitchbot==0.3
# homeassistant.components.xiaomi_aqara
-PyXiaomiGateway==0.9.5
+PyXiaomiGateway==0.10.0
# homeassistant.components.rpi_gpio
# RPi.GPIO==0.6.1
@@ -66,9 +66,6 @@ PyXiaomiGateway==0.9.5
# homeassistant.components.remember_the_milk
RtmAPI==0.7.0
-# homeassistant.components.sonos
-SoCo==0.16
-
# homeassistant.components.sensor.travisci
TravisPy==0.3.5
@@ -175,7 +172,7 @@ beautifulsoup4==4.6.3
bellows==0.7.0
# homeassistant.components.bmw_connected_drive
-bimmer_connected==0.5.1
+bimmer_connected==0.5.2
# homeassistant.components.blink
blinkpy==0.6.0
@@ -190,7 +187,6 @@ blinkstick==1.1.8
blockchain==1.4.4
# homeassistant.components.light.decora
-# homeassistant.components.switch.switchmate
# bluepy==1.1.4
# homeassistant.components.sensor.bme680
@@ -312,6 +308,9 @@ dsmr_parser==0.11
# homeassistant.components.sensor.dweet
dweepy==0.3.0
+# homeassistant.components.edp_redy
+edp_redy==0.0.2
+
# homeassistant.components.media_player.horizon
einder==0.3.1
@@ -334,7 +333,7 @@ ephem==3.7.6.0
epson-projector==0.1.3
# homeassistant.components.netgear_lte
-eternalegypt==0.0.3
+eternalegypt==0.0.5
# homeassistant.components.keyboard_remote
# evdev==0.6.1
@@ -391,6 +390,9 @@ gearbest_parser==1.0.7
# homeassistant.components.sensor.geizhals
geizhals==0.0.7
+# homeassistant.components.geo_location.geo_json_events
+geojson_client==0.1
+
# homeassistant.components.sensor.gitter
gitterpy==0.1.7
@@ -433,6 +435,9 @@ haversine==0.4.5
# homeassistant.components.mqtt.server
hbmqtt==0.9.4
+# homeassistant.components.sensor.jewish_calendar
+hdate==0.6.3
+
# homeassistant.components.climate.heatmiser
heatmiserV3==0.9.1
@@ -446,10 +451,10 @@ hipnotify==1.0.8
hole==0.3.0
# homeassistant.components.binary_sensor.workday
-holidays==0.9.6
+holidays==0.9.7
# homeassistant.components.frontend
-home-assistant-frontend==20180916.0
+home-assistant-frontend==20180927.0
# homeassistant.components.homekit_controller
# homekit==0.10
@@ -461,6 +466,9 @@ homematicip==0.9.8
# homeassistant.components.remember_the_milk
httplib2==0.10.3
+# homeassistant.components.huawei_lte
+huawei-lte-api==1.0.12
+
# homeassistant.components.hydrawise
hydrawiser==0.1.1
@@ -499,7 +507,7 @@ jsonrpc-async==0.6
jsonrpc-websocket==0.6
# homeassistant.scripts.keyring
-keyring==15.0.0
+keyring==15.1.0
# homeassistant.scripts.keyring
keyrings.alt==3.1
@@ -524,7 +532,7 @@ libpurecoollink==0.4.2
libpyfoscam==1.0
# homeassistant.components.device_tracker.mikrotik
-librouteros==2.1.0
+librouteros==2.1.1
# homeassistant.components.media_player.soundtouch
libsoundtouch==0.7.2
@@ -551,6 +559,9 @@ lmnotify==0.0.4
# homeassistant.components.device_tracker.google_maps
locationsharinglib==2.0.11
+# homeassistant.components.logi_circle
+logi_circle==0.1.7
+
# homeassistant.components.sensor.luftdaten
luftdaten==0.2.0
@@ -652,7 +663,7 @@ orvibo==1.1.1
# homeassistant.components.mqtt
# homeassistant.components.shiftr
-paho-mqtt==1.3.1
+paho-mqtt==1.4.0
# homeassistant.components.media_player.panasonic_viera
panasonic_viera==0.3.1
@@ -719,7 +730,7 @@ pushbullet.py==0.11.0
pushetta==1.0.15
# homeassistant.components.light.rpi_gpio_pwm
-pwmled==1.2.1
+pwmled==1.3.0
# homeassistant.components.august
py-august==0.6.0
@@ -743,11 +754,17 @@ pyCEC==0.4.13
# homeassistant.components.switch.tplink
pyHS100==0.3.3
+# homeassistant.components.weather.met
+pyMetno==0.2.0
+
# homeassistant.components.rfxtrx
pyRFXtrx==0.23
+# homeassistant.components.switch.switchmate
+pySwitchmate==0.4.1
+
# homeassistant.components.sensor.tibber
-pyTibber==0.4.1
+pyTibber==0.5.1
# homeassistant.components.switch.dlink
pyW215==0.6.0
@@ -793,7 +810,7 @@ pyblackbird==0.5
# pybluez==0.22
# homeassistant.components.neato
-pybotvac==0.0.9
+pybotvac==0.0.10
# homeassistant.components.cloudflare
pycfdns==0.0.1
@@ -836,7 +853,7 @@ pydukeenergy==0.0.6
pyebox==1.1.4
# homeassistant.components.climate.econet
-pyeconet==0.0.5
+pyeconet==0.0.6
# homeassistant.components.switch.edimax
pyedimax==0.1
@@ -891,7 +908,7 @@ pyhik==0.1.8
pyhiveapi==0.2.14
# homeassistant.components.homematic
-pyhomematic==0.1.47
+pyhomematic==0.1.49
# homeassistant.components.sensor.hydroquebec
pyhydroquebec==2.2.2
@@ -933,6 +950,9 @@ pylgnetcast-homeassistant==0.2.0.dev0
# homeassistant.components.notify.webostv
pylgtv==0.1.7
+# homeassistant.components.sensor.linky
+pylinky==0.1.6
+
# homeassistant.components.litejet
pylitejet==0.1
@@ -1000,6 +1020,7 @@ pyota==2.0.5
# homeassistant.components.climate.opentherm_gw
pyotgw==0.1b0
+# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
# homeassistant.components.sensor.otp
pyotp==2.2.6
@@ -1046,6 +1067,12 @@ pysma==0.2
# homeassistant.components.switch.snmp
pysnmp==4.4.5
+# homeassistant.components.sonos
+pysonos==0.0.2
+
+# homeassistant.components.spc
+pyspcwebgw==0.4.0
+
# homeassistant.components.notify.stride
pystride==0.1.7
@@ -1147,19 +1174,19 @@ python-tado==0.2.3
python-telegram-bot==11.0.0
# homeassistant.components.sensor.twitch
-python-twitch-client==0.5.1
+python-twitch-client==0.6.0
# homeassistant.components.velbus
-python-velbus==2.0.19
+python-velbus==2.0.20
# homeassistant.components.media_player.vlc
python-vlc==1.1.2
# homeassistant.components.wink
-python-wink==1.9.1
+python-wink==1.10.1
# homeassistant.components.sensor.swiss_public_transport
-python_opendata_transport==0.1.3
+python_opendata_transport==0.1.4
# homeassistant.components.zwave
python_openzwave==0.4.9
@@ -1240,7 +1267,7 @@ raincloudy==0.0.5
regenmaschine==1.0.2
# homeassistant.components.python_script
-restrictedpython==4.0b4
+restrictedpython==4.0b5
# homeassistant.components.rflink
rflink==0.0.37
@@ -1295,7 +1322,7 @@ sense_energy==0.4.2
sharp_aquos_rc==0.3.2
# homeassistant.components.sensor.shodan
-shodan==1.10.1
+shodan==1.10.2
# homeassistant.components.notify.simplepush
simplepush==1.1.4
@@ -1355,6 +1382,9 @@ spotipy-homeassistant==2.4.4.dev1
# homeassistant.components.sensor.sql
sqlalchemy==1.2.11
+# homeassistant.components.sensor.starlingbank
+starlingbank==1.2
+
# homeassistant.components.statsd
statsd==3.2.1
@@ -1362,7 +1392,7 @@ statsd==3.2.1
steamodd==4.21
# homeassistant.components.ecovacs
-sucks==0.9.1
+sucks==0.9.3
# homeassistant.components.camera.onvif
suds-passworddigest-homeassistant==0.1.2a0.dev0
@@ -1459,7 +1489,7 @@ vultr==0.1.2
# homeassistant.components.media_player.panasonic_viera
# homeassistant.components.media_player.samsungtv
# homeassistant.components.switch.wake_on_lan
-wakeonlan==1.0.0
+wakeonlan==1.1.6
# homeassistant.components.sensor.waqi
waqiasync==1.0.0
@@ -1476,12 +1506,11 @@ waterfurnace==0.7.0
# homeassistant.components.media_player.gpmdp
websocket-client==0.37.0
-# homeassistant.components.spc
# homeassistant.components.media_player.webostv
websockets==6.0
# homeassistant.components.wirelesstag
-wirelesstagpy==0.3.0
+wirelesstagpy==0.4.0
# homeassistant.components.zigbee
xbee-helper==0.0.7
@@ -1517,13 +1546,13 @@ yeelight==0.4.0
yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2018.09.10
+youtube_dl==2018.09.18
# homeassistant.components.light.zengge
zengge==0.2
# homeassistant.components.zeroconf
-zeroconf==0.21.2
+zeroconf==0.21.3
# homeassistant.components.climate.zhong_hong
zhong_hong_hvac==1.0.9
@@ -1536,3 +1565,6 @@ zigpy-xbee==0.1.1
# homeassistant.components.zha
zigpy==0.2.0
+
+# homeassistant.components.zoneminder
+zm-py==0.0.3
diff --git a/requirements_test.txt b/requirements_test.txt
index e50ef699848..15e06c4e53d 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -6,12 +6,12 @@ coveralls==1.2.0
flake8-docstrings==1.3.0
flake8==3.5
mock-open==1.3.1
-mypy==0.620
+mypy==0.630
pydocstyle==2.1.1
pylint==2.1.1
pytest-aiohttp==0.3.0
pytest-cov==2.5.1
pytest-sugar==0.9.1
-pytest-timeout==1.3.1
-pytest==3.7.2
+pytest-timeout==1.3.2
+pytest==3.8.0
requests_mock==1.5.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index e6d64857c4c..e67da9755cd 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -7,14 +7,14 @@ coveralls==1.2.0
flake8-docstrings==1.3.0
flake8==3.5
mock-open==1.3.1
-mypy==0.620
+mypy==0.630
pydocstyle==2.1.1
pylint==2.1.1
pytest-aiohttp==0.3.0
pytest-cov==2.5.1
pytest-sugar==0.9.1
-pytest-timeout==1.3.1
-pytest==3.7.2
+pytest-timeout==1.3.2
+pytest==3.8.0
requests_mock==1.5.2
@@ -22,10 +22,7 @@ requests_mock==1.5.2
HAP-python==2.2.2
# homeassistant.components.sensor.rmvtransport
-PyRMVtransport==0.0.7
-
-# homeassistant.components.sonos
-SoCo==0.16
+PyRMVtransport==0.1
# homeassistant.components.device_tracker.automatic
aioautomatic==0.6.5
@@ -68,6 +65,9 @@ foobot_async==0.3.1
# homeassistant.components.tts.google
gTTS-token==1.1.2
+# homeassistant.components.geo_location.geo_json_events
+geojson_client==0.1
+
# homeassistant.components.ffmpeg
ha-ffmpeg==1.9
@@ -80,11 +80,14 @@ haversine==0.4.5
# homeassistant.components.mqtt.server
hbmqtt==0.9.4
+# homeassistant.components.sensor.jewish_calendar
+hdate==0.6.3
+
# homeassistant.components.binary_sensor.workday
-holidays==0.9.6
+holidays==0.9.7
# homeassistant.components.frontend
-home-assistant-frontend==20180916.0
+home-assistant-frontend==20180927.0
# homeassistant.components.homematicip_cloud
homematicip==0.9.8
@@ -109,7 +112,7 @@ numpy==1.15.1
# homeassistant.components.mqtt
# homeassistant.components.shiftr
-paho-mqtt==1.3.1
+paho-mqtt==1.4.0
# homeassistant.components.device_tracker.aruba
# homeassistant.components.device_tracker.asuswrt
@@ -157,6 +160,7 @@ pynx584==0.4
# homeassistant.components.openuv
pyopenuv==1.0.4
+# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
# homeassistant.components.sensor.otp
pyotp==2.2.6
@@ -164,6 +168,12 @@ pyotp==2.2.6
# homeassistant.components.qwikswitch
pyqwikswitch==0.8
+# homeassistant.components.sonos
+pysonos==0.0.2
+
+# homeassistant.components.spc
+pyspcwebgw==0.4.0
+
# homeassistant.components.sensor.darksky
# homeassistant.components.weather.darksky
python-forecastio==1.4.0
@@ -187,7 +197,7 @@ pyupnp-async==0.1.1.1
pywebpush==1.6.0
# homeassistant.components.python_script
-restrictedpython==4.0b4
+restrictedpython==4.0b5
# homeassistant.components.rflink
rflink==0.0.37
@@ -222,7 +232,7 @@ vultr==0.1.2
# homeassistant.components.media_player.panasonic_viera
# homeassistant.components.media_player.samsungtv
# homeassistant.components.switch.wake_on_lan
-wakeonlan==1.0.0
+wakeonlan==1.1.6
# homeassistant.components.cloud
warrant==0.6.1
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index ec024bef614..7493e523273 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -50,11 +50,13 @@ TEST_REQUIREMENTS = (
'feedparser',
'foobot_async',
'gTTS-token',
+ 'geojson_client',
'hangups',
'HAP-python',
'ha-ffmpeg',
'haversine',
'hbmqtt',
+ 'hdate',
'holidays',
'home-assistant-frontend',
'homematicip',
@@ -78,8 +80,10 @@ TEST_REQUIREMENTS = (
'pynx584',
'pyopenuv',
'pyotp',
+ 'pysonos',
'pyqwikswitch',
'PyRMVtransport',
+ 'pyspcwebgw',
'python-forecastio',
'python-nest',
'pytradfri\[async\]',
@@ -91,7 +95,6 @@ TEST_REQUIREMENTS = (
'ring_doorbell',
'rxv',
'sleepyq',
- 'SoCo',
'somecomfort',
'sqlalchemy',
'statsd',
@@ -115,8 +118,8 @@ IGNORE_REQ = (
'colorama<=1', # Windows only requirement in check_config
)
-URL_PIN = ('https://home-assistant.io/developers/code_review_platform/'
- '#1-requirements')
+URL_PIN = ('https://developers.home-assistant.io/docs/'
+ 'creating_platform_code_review.html#1-requirements')
CONSTRAINT_PATH = os.path.join(os.path.dirname(__file__),
diff --git a/setup.cfg b/setup.cfg
index 7813cc5c047..936840acfaa 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -13,6 +13,7 @@ classifier =
Operating System :: OS Independent
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
+ Programming Language :: Python :: 3.7
Topic :: Home Automation
[tool:pytest]
@@ -32,6 +33,9 @@ indent = " "
not_skip = __init__.py
# will group `import x` and `from x import` of the same module.
force_sort_within_sections = true
-# typing is stdlib on py35 but 3rd party on py34, let it hang in between
-known_inbetweens = typing
sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
+default_section = THIRDPARTY
+known_first_party = homeassistant,tests
+forced_separate = tests
+combine_as_imports = true
+use_parentheses = true
diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py
index 80109627140..d9330d5f6e8 100644
--- a/tests/auth/mfa_modules/test_insecure_example.py
+++ b/tests/auth/mfa_modules/test_insecure_example.py
@@ -12,15 +12,15 @@ async def test_validate(hass):
'data': [{'user_id': 'test-user', 'pin': '123456'}]
})
- result = await auth_module.async_validation(
+ result = await auth_module.async_validate(
'test-user', {'pin': '123456'})
assert result is True
- result = await auth_module.async_validation(
+ result = await auth_module.async_validate(
'test-user', {'pin': 'invalid'})
assert result is False
- result = await auth_module.async_validation(
+ result = await auth_module.async_validate(
'invalid-user', {'pin': '123456'})
assert result is False
@@ -36,7 +36,7 @@ async def test_setup_user(hass):
'test-user', {'pin': '123456'})
assert len(auth_module._data) == 1
- result = await auth_module.async_validation(
+ result = await auth_module.async_validate(
'test-user', {'pin': '123456'})
assert result is True
diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py
new file mode 100644
index 00000000000..ffe0b103fc9
--- /dev/null
+++ b/tests/auth/mfa_modules/test_notify.py
@@ -0,0 +1,397 @@
+"""Test the HMAC-based One Time Password (MFA) auth module."""
+from unittest.mock import patch
+
+from homeassistant import data_entry_flow
+from homeassistant.auth import models as auth_models, auth_manager_from_config
+from homeassistant.auth.mfa_modules import auth_mfa_module_from_config
+from homeassistant.components.notify import NOTIFY_SERVICE_SCHEMA
+from tests.common import MockUser, async_mock_service
+
+MOCK_CODE = '123456'
+MOCK_CODE_2 = '654321'
+
+
+async def test_validating_mfa(hass):
+ """Test validating mfa code."""
+ notify_auth_module = await auth_mfa_module_from_config(hass, {
+ 'type': 'notify'
+ })
+ await notify_auth_module.async_setup_user('test-user', {
+ 'notify_service': 'dummy'
+ })
+
+ with patch('pyotp.HOTP.verify', return_value=True):
+ assert await notify_auth_module.async_validate(
+ 'test-user', {'code': MOCK_CODE})
+
+
+async def test_validating_mfa_invalid_code(hass):
+ """Test validating an invalid mfa code."""
+ notify_auth_module = await auth_mfa_module_from_config(hass, {
+ 'type': 'notify'
+ })
+ await notify_auth_module.async_setup_user('test-user', {
+ 'notify_service': 'dummy'
+ })
+
+ with patch('pyotp.HOTP.verify', return_value=False):
+ assert await notify_auth_module.async_validate(
+ 'test-user', {'code': MOCK_CODE}) is False
+
+
+async def test_validating_mfa_invalid_user(hass):
+ """Test validating an mfa code with invalid user."""
+ notify_auth_module = await auth_mfa_module_from_config(hass, {
+ 'type': 'notify'
+ })
+ await notify_auth_module.async_setup_user('test-user', {
+ 'notify_service': 'dummy'
+ })
+
+ assert await notify_auth_module.async_validate(
+ 'invalid-user', {'code': MOCK_CODE}) is False
+
+
+async def test_validating_mfa_counter(hass):
+ """Test counter will move only after generate code."""
+ notify_auth_module = await auth_mfa_module_from_config(hass, {
+ 'type': 'notify'
+ })
+ await notify_auth_module.async_setup_user('test-user', {
+ 'counter': 0,
+ 'notify_service': 'dummy',
+ })
+
+ assert notify_auth_module._user_settings
+ notify_setting = list(notify_auth_module._user_settings.values())[0]
+ init_count = notify_setting.counter
+ assert init_count is not None
+
+ with patch('pyotp.HOTP.at', return_value=MOCK_CODE):
+ await notify_auth_module.async_initialize_login_mfa_step('test-user')
+
+ notify_setting = list(notify_auth_module._user_settings.values())[0]
+ after_generate_count = notify_setting.counter
+ assert after_generate_count != init_count
+
+ with patch('pyotp.HOTP.verify', return_value=True):
+ assert await notify_auth_module.async_validate(
+ 'test-user', {'code': MOCK_CODE})
+
+ notify_setting = list(notify_auth_module._user_settings.values())[0]
+ assert after_generate_count == notify_setting.counter
+
+ with patch('pyotp.HOTP.verify', return_value=False):
+ assert await notify_auth_module.async_validate(
+ 'test-user', {'code': MOCK_CODE}) is False
+
+ notify_setting = list(notify_auth_module._user_settings.values())[0]
+ assert after_generate_count == notify_setting.counter
+
+
+async def test_setup_depose_user(hass):
+ """Test set up and despose user."""
+ notify_auth_module = await auth_mfa_module_from_config(hass, {
+ 'type': 'notify'
+ })
+ await notify_auth_module.async_setup_user('test-user', {})
+ assert len(notify_auth_module._user_settings) == 1
+ await notify_auth_module.async_setup_user('test-user', {})
+ assert len(notify_auth_module._user_settings) == 1
+
+ await notify_auth_module.async_depose_user('test-user')
+ assert len(notify_auth_module._user_settings) == 0
+
+ await notify_auth_module.async_setup_user(
+ 'test-user2', {'secret': 'secret-code'})
+ assert len(notify_auth_module._user_settings) == 1
+
+
+async def test_login_flow_validates_mfa(hass):
+ """Test login flow with mfa enabled."""
+ hass.auth = await auth_manager_from_config(hass, [{
+ 'type': 'insecure_example',
+ 'users': [{'username': 'test-user', 'password': 'test-pass'}],
+ }], [{
+ 'type': 'notify',
+ }])
+ user = MockUser(
+ id='mock-user',
+ is_owner=False,
+ is_active=False,
+ name='Paulus',
+ ).add_to_auth_manager(hass.auth)
+ await hass.auth.async_link_user(user, auth_models.Credentials(
+ id='mock-id',
+ auth_provider_type='insecure_example',
+ auth_provider_id=None,
+ data={'username': 'test-user'},
+ is_new=False,
+ ))
+
+ notify_calls = async_mock_service(hass, 'notify', 'test-notify',
+ NOTIFY_SERVICE_SCHEMA)
+
+ await hass.auth.async_enable_user_mfa(user, 'notify', {
+ 'notify_service': 'test-notify',
+ })
+
+ provider = hass.auth.auth_providers[0]
+
+ result = await hass.auth.login_flow.async_init(
+ (provider.type, provider.id))
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+ result = await hass.auth.login_flow.async_configure(result['flow_id'], {
+ 'username': 'incorrect-user',
+ 'password': 'test-pass',
+ })
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['errors']['base'] == 'invalid_auth'
+
+ result = await hass.auth.login_flow.async_configure(result['flow_id'], {
+ 'username': 'test-user',
+ 'password': 'incorrect-pass',
+ })
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['errors']['base'] == 'invalid_auth'
+
+ with patch('pyotp.HOTP.at', return_value=MOCK_CODE):
+ result = await hass.auth.login_flow.async_configure(
+ result['flow_id'],
+ {
+ 'username': 'test-user',
+ 'password': 'test-pass',
+ })
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'mfa'
+ assert result['data_schema'].schema.get('code') == str
+
+ # wait service call finished
+ await hass.async_block_till_done()
+
+ assert len(notify_calls) == 1
+ notify_call = notify_calls[0]
+ assert notify_call.domain == 'notify'
+ assert notify_call.service == 'test-notify'
+ message = notify_call.data['message']
+ message.hass = hass
+ assert MOCK_CODE in message.async_render()
+
+ with patch('pyotp.HOTP.verify', return_value=False):
+ result = await hass.auth.login_flow.async_configure(
+ result['flow_id'], {'code': 'invalid-code'})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'mfa'
+ assert result['errors']['base'] == 'invalid_code'
+
+ # wait service call finished
+ await hass.async_block_till_done()
+
+ # would not send new code, allow user retry
+ assert len(notify_calls) == 1
+
+ # retry twice
+ with patch('pyotp.HOTP.verify', return_value=False), \
+ patch('pyotp.HOTP.at', return_value=MOCK_CODE_2):
+ result = await hass.auth.login_flow.async_configure(
+ result['flow_id'], {'code': 'invalid-code'})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'mfa'
+ assert result['errors']['base'] == 'invalid_code'
+
+ # after the 3rd failure, flow abort
+ result = await hass.auth.login_flow.async_configure(
+ result['flow_id'], {'code': 'invalid-code'})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'too_many_retry'
+
+ # wait service call finished
+ await hass.async_block_till_done()
+
+ # restart login
+ result = await hass.auth.login_flow.async_init(
+ (provider.type, provider.id))
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+ with patch('pyotp.HOTP.at', return_value=MOCK_CODE):
+ result = await hass.auth.login_flow.async_configure(
+ result['flow_id'],
+ {
+ 'username': 'test-user',
+ 'password': 'test-pass',
+ })
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'mfa'
+ assert result['data_schema'].schema.get('code') == str
+
+ # wait service call finished
+ await hass.async_block_till_done()
+
+ assert len(notify_calls) == 2
+ notify_call = notify_calls[1]
+ assert notify_call.domain == 'notify'
+ assert notify_call.service == 'test-notify'
+ message = notify_call.data['message']
+ message.hass = hass
+ assert MOCK_CODE in message.async_render()
+
+ with patch('pyotp.HOTP.verify', return_value=True):
+ result = await hass.auth.login_flow.async_configure(
+ result['flow_id'], {'code': MOCK_CODE})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['data'].id == 'mock-user'
+
+
+async def test_setup_user_notify_service(hass):
+ """Test allow select notify service during mfa setup."""
+ notify_calls = async_mock_service(
+ hass, 'notify', 'test1', NOTIFY_SERVICE_SCHEMA)
+ async_mock_service(hass, 'notify', 'test2', NOTIFY_SERVICE_SCHEMA)
+ notify_auth_module = await auth_mfa_module_from_config(hass, {
+ 'type': 'notify',
+ })
+
+ services = notify_auth_module.aync_get_available_notify_services()
+ assert services == ['test1', 'test2']
+
+ flow = await notify_auth_module.async_setup_flow('test-user')
+ step = await flow.async_step_init()
+ assert step['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert step['step_id'] == 'init'
+ schema = step['data_schema']
+ schema({'notify_service': 'test2'})
+
+ with patch('pyotp.HOTP.at', return_value=MOCK_CODE):
+ step = await flow.async_step_init({'notify_service': 'test1'})
+ assert step['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert step['step_id'] == 'setup'
+
+ # wait service call finished
+ await hass.async_block_till_done()
+
+ assert len(notify_calls) == 1
+ notify_call = notify_calls[0]
+ assert notify_call.domain == 'notify'
+ assert notify_call.service == 'test1'
+ message = notify_call.data['message']
+ message.hass = hass
+ assert MOCK_CODE in message.async_render()
+
+ with patch('pyotp.HOTP.at', return_value=MOCK_CODE_2):
+ step = await flow.async_step_setup({'code': 'invalid'})
+ assert step['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert step['step_id'] == 'setup'
+ assert step['errors']['base'] == 'invalid_code'
+
+ # wait service call finished
+ await hass.async_block_till_done()
+
+ assert len(notify_calls) == 2
+ notify_call = notify_calls[1]
+ assert notify_call.domain == 'notify'
+ assert notify_call.service == 'test1'
+ message = notify_call.data['message']
+ message.hass = hass
+ assert MOCK_CODE_2 in message.async_render()
+
+ with patch('pyotp.HOTP.verify', return_value=True):
+ step = await flow.async_step_setup({'code': MOCK_CODE_2})
+ assert step['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+
+async def test_include_exclude_config(hass):
+ """Test allow include exclude config."""
+ async_mock_service(hass, 'notify', 'include1', NOTIFY_SERVICE_SCHEMA)
+ async_mock_service(hass, 'notify', 'include2', NOTIFY_SERVICE_SCHEMA)
+ async_mock_service(hass, 'notify', 'exclude1', NOTIFY_SERVICE_SCHEMA)
+ async_mock_service(hass, 'notify', 'exclude2', NOTIFY_SERVICE_SCHEMA)
+ async_mock_service(hass, 'other', 'include3', NOTIFY_SERVICE_SCHEMA)
+ async_mock_service(hass, 'other', 'exclude3', NOTIFY_SERVICE_SCHEMA)
+
+ notify_auth_module = await auth_mfa_module_from_config(hass, {
+ 'type': 'notify',
+ 'exclude': ['exclude1', 'exclude2', 'exclude3'],
+ })
+ services = notify_auth_module.aync_get_available_notify_services()
+ assert services == ['include1', 'include2']
+
+ notify_auth_module = await auth_mfa_module_from_config(hass, {
+ 'type': 'notify',
+ 'include': ['include1', 'include2', 'include3'],
+ })
+ services = notify_auth_module.aync_get_available_notify_services()
+ assert services == ['include1', 'include2']
+
+ # exclude has high priority than include
+ notify_auth_module = await auth_mfa_module_from_config(hass, {
+ 'type': 'notify',
+ 'include': ['include1', 'include2', 'include3'],
+ 'exclude': ['exclude1', 'exclude2', 'include2'],
+ })
+ services = notify_auth_module.aync_get_available_notify_services()
+ assert services == ['include1']
+
+
+async def test_setup_user_no_notify_service(hass):
+ """Test setup flow abort if there is no avilable 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',
+ })
+
+ services = notify_auth_module.aync_get_available_notify_services()
+ assert services == []
+
+ flow = await notify_auth_module.async_setup_flow('test-user')
+ step = await flow.async_step_init()
+ assert step['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert step['reason'] == 'no_available_service'
+
+
+async def test_not_raise_exception_when_service_not_exist(hass):
+ """Test login flow will not raise exception when notify service error."""
+ hass.auth = await auth_manager_from_config(hass, [{
+ 'type': 'insecure_example',
+ 'users': [{'username': 'test-user', 'password': 'test-pass'}],
+ }], [{
+ 'type': 'notify',
+ }])
+ user = MockUser(
+ id='mock-user',
+ is_owner=False,
+ is_active=False,
+ name='Paulus',
+ ).add_to_auth_manager(hass.auth)
+ await hass.auth.async_link_user(user, auth_models.Credentials(
+ id='mock-id',
+ auth_provider_type='insecure_example',
+ auth_provider_id=None,
+ data={'username': 'test-user'},
+ is_new=False,
+ ))
+
+ await hass.auth.async_enable_user_mfa(user, 'notify', {
+ 'notify_service': 'invalid-notify',
+ })
+
+ provider = hass.auth.auth_providers[0]
+
+ result = await hass.auth.login_flow.async_init(
+ (provider.type, provider.id))
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+ with patch('pyotp.HOTP.at', return_value=MOCK_CODE):
+ result = await hass.auth.login_flow.async_configure(
+ result['flow_id'],
+ {
+ 'username': 'test-user',
+ 'password': 'test-pass',
+ })
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'mfa'
+ assert result['data_schema'].schema.get('code') == str
+
+ # wait service call finished
+ await hass.async_block_till_done()
diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py
index 6e3558ec549..d400fe80672 100644
--- a/tests/auth/mfa_modules/test_totp.py
+++ b/tests/auth/mfa_modules/test_totp.py
@@ -17,7 +17,7 @@ async def test_validating_mfa(hass):
await totp_auth_module.async_setup_user('test-user', {})
with patch('pyotp.TOTP.verify', return_value=True):
- assert await totp_auth_module.async_validation(
+ assert await totp_auth_module.async_validate(
'test-user', {'code': MOCK_CODE})
@@ -29,7 +29,7 @@ async def test_validating_mfa_invalid_code(hass):
await totp_auth_module.async_setup_user('test-user', {})
with patch('pyotp.TOTP.verify', return_value=False):
- assert await totp_auth_module.async_validation(
+ assert await totp_auth_module.async_validate(
'test-user', {'code': MOCK_CODE}) is False
@@ -40,7 +40,7 @@ async def test_validating_mfa_invalid_user(hass):
})
await totp_auth_module.async_setup_user('test-user', {})
- assert await totp_auth_module.async_validation(
+ assert await totp_auth_module.async_validate(
'invalid-user', {'code': MOCK_CODE}) is False
diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py
index 8325bd2551a..8fd9b8930e4 100644
--- a/tests/auth/test_init.py
+++ b/tests/auth/test_init.py
@@ -9,7 +9,7 @@ import voluptuous as vol
from homeassistant import auth, data_entry_flow
from homeassistant.auth import (
models as auth_models, auth_store, const as auth_const)
-from homeassistant.auth.mfa_modules import SESSION_EXPIRATION
+from homeassistant.auth.const import MFA_SESSION_EXPIRATION
from homeassistant.util import dt as dt_util
from tests.common import (
MockUser, ensure_auth_manager_loaded, flush_store, CLIENT_ID)
@@ -720,7 +720,7 @@ async def test_auth_module_expired_session(mock_hass):
assert step['step_id'] == 'mfa'
with patch('homeassistant.util.dt.utcnow',
- return_value=dt_util.utcnow() + SESSION_EXPIRATION):
+ return_value=dt_util.utcnow() + MFA_SESSION_EXPIRATION):
step = await manager.login_flow.async_configure(step['flow_id'], {
'pin': 'test-pin',
})
diff --git a/tests/common.py b/tests/common.py
index 738c51fb3f0..0cb15d683b5 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -19,7 +19,7 @@ from homeassistant.setup import setup_component, async_setup_component
from homeassistant.config import async_process_component_config
from homeassistant.helpers import (
intent, entity, restore_state, entity_registry,
- entity_platform, storage)
+ entity_platform, storage, device_registry)
from homeassistant.util.unit_system import METRIC_SYSTEM
import homeassistant.util.dt as date_util
import homeassistant.util.yaml as yaml
@@ -322,7 +322,7 @@ def mock_component(hass, component):
def mock_registry(hass, mock_entries=None):
"""Mock the Entity Registry."""
registry = entity_registry.EntityRegistry(hass)
- registry.entities = mock_entries or {}
+ registry.entities = mock_entries or OrderedDict()
async def _get_reg():
return registry
@@ -332,6 +332,19 @@ def mock_registry(hass, mock_entries=None):
return registry
+def mock_device_registry(hass, mock_entries=None):
+ """Mock the Device Registry."""
+ registry = device_registry.DeviceRegistry(hass)
+ registry.devices = mock_entries or OrderedDict()
+
+ async def _get_reg():
+ return registry
+
+ hass.data[device_registry.DATA_REGISTRY] = \
+ hass.loop.create_task(_get_reg())
+ return registry
+
+
class MockUser(auth_models.User):
"""Mock a user in Home Assistant."""
@@ -537,14 +550,16 @@ class MockConfigEntry(config_entries.ConfigEntry):
def __init__(self, *, domain='test', data=None, version=0, entry_id=None,
source=config_entries.SOURCE_USER, title='Mock Title',
- state=None):
+ state=None,
+ connection_class=config_entries.CONN_CLASS_UNKNOWN):
"""Initialize a mock config entry."""
kwargs = {
'entry_id': entry_id or 'mock-id',
'domain': domain,
'data': data or {},
'version': version,
- 'title': title
+ 'title': title,
+ 'connection_class': connection_class,
}
if source is not None:
kwargs['source'] = source
@@ -594,16 +609,18 @@ def patch_yaml_files(files_dict, endswith=True):
return patch.object(yaml, 'open', mock_open_f, create=True)
-def mock_coro(return_value=None):
- """Return a coro that returns a value."""
- return mock_coro_func(return_value)()
+def mock_coro(return_value=None, exception=None):
+ """Return a coro that returns a value or raise an exception."""
+ return mock_coro_func(return_value, exception)()
-def mock_coro_func(return_value=None):
+def mock_coro_func(return_value=None, exception=None):
"""Return a method to create a coro function that returns a value."""
@asyncio.coroutine
def coro(*args, **kwargs):
"""Fake coroutine."""
+ if exception:
+ raise exception
return return_value
return coro
@@ -748,6 +765,11 @@ class MockEntity(entity.Entity):
"""Return True if entity is available."""
return self._handle('available')
+ @property
+ def device_info(self):
+ """Info how it links to a device."""
+ return self._handle('device_info')
+
def _handle(self, attr):
"""Return attribute value."""
if attr in self._values:
diff --git a/tests/components/alarm_control_panel/test_spc.py b/tests/components/alarm_control_panel/test_spc.py
index 0a4ba0916ea..b1078e1b14f 100644
--- a/tests/components/alarm_control_panel/test_spc.py
+++ b/tests/components/alarm_control_panel/test_spc.py
@@ -1,27 +1,11 @@
"""Tests for Vanderbilt SPC alarm control panel platform."""
-import asyncio
-
-import pytest
-
-from homeassistant.components.spc import SpcRegistry
from homeassistant.components.alarm_control_panel import spc
-from tests.common import async_test_home_assistant
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED)
+from homeassistant.components.spc import (DATA_API)
-@pytest.fixture
-def hass(loop):
- """Home Assistant fixture with device mapping registry."""
- hass = loop.run_until_complete(async_test_home_assistant(loop))
- hass.data['spc_registry'] = SpcRegistry()
- hass.data['spc_api'] = None
- yield hass
- loop.run_until_complete(hass.async_stop())
-
-
-@asyncio.coroutine
-def test_setup_platform(hass):
+async def test_setup_platform(hass):
"""Test adding areas as separate alarm control panel devices."""
added_entities = []
@@ -29,7 +13,7 @@ def test_setup_platform(hass):
nonlocal added_entities
added_entities = list(entities)
- areas = {'areas': [{
+ area_defs = [{
'id': '1',
'name': 'House',
'mode': '3',
@@ -50,12 +34,18 @@ def test_setup_platform(hass):
'last_unset_time': '1483705808',
'last_unset_user_id': '9998',
'last_unset_user_name': 'Lisa'
- }]}
+ }]
- yield from spc.async_setup_platform(hass=hass,
- config={},
- async_add_entities=add_entities,
- discovery_info=areas)
+ from pyspcwebgw import Area
+
+ areas = [Area(gateway=None, spc_area=a) for a in area_defs]
+
+ hass.data[DATA_API] = None
+
+ await spc.async_setup_platform(hass=hass,
+ config={},
+ async_add_entities=add_entities,
+ discovery_info={'areas': areas})
assert len(added_entities) == 2
diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py
index 2e33e28fa57..5fd6e132e03 100644
--- a/tests/components/binary_sensor/test_deconz.py
+++ b/tests/components/binary_sensor/test_deconz.py
@@ -34,6 +34,7 @@ async def setup_bridge(hass, data, allow_clip_sensor=True):
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'}
bridge = DeconzSession(loop, session, **entry.data)
+ bridge.config = Mock()
with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)):
await bridge.async_load_parameters()
@@ -42,7 +43,8 @@ async def setup_bridge(hass, data, allow_clip_sensor=True):
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title',
- {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test')
+ {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test',
+ config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(
config_entry, 'binary_sensor')
# To flush out the service call to update the group
diff --git a/tests/components/binary_sensor/test_rest.py b/tests/components/binary_sensor/test_rest.py
index d1c26624452..db70303e217 100644
--- a/tests/components/binary_sensor/test_rest.py
+++ b/tests/components/binary_sensor/test_rest.py
@@ -1,11 +1,13 @@
"""The tests for the REST binary sensor platform."""
import unittest
+from pytest import raises
from unittest.mock import patch, Mock
import requests
from requests.exceptions import Timeout, MissingSchema
import requests_mock
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.setup import setup_component
import homeassistant.components.binary_sensor as binary_sensor
import homeassistant.components.binary_sensor.rest as rest
@@ -18,9 +20,18 @@ from tests.common import get_test_home_assistant, assert_setup_component
class TestRestBinarySensorSetup(unittest.TestCase):
"""Tests for setting up the REST binary sensor platform."""
+ DEVICES = []
+
+ def add_devices(self, devices, update_before_add=False):
+ """Mock add devices."""
+ for device in devices:
+ self.DEVICES.append(device)
+
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
+ # Reset for this test.
+ self.DEVICES = []
def tearDown(self):
"""Stop everything that was started."""
@@ -45,76 +56,80 @@ class TestRestBinarySensorSetup(unittest.TestCase):
side_effect=requests.exceptions.ConnectionError())
def test_setup_failed_connect(self, mock_req):
"""Test setup when connection error occurs."""
- self.assertFalse(rest.setup_platform(self.hass, {
- 'platform': 'rest',
- 'resource': 'http://localhost',
- }, lambda devices, update=True: None))
+ with raises(PlatformNotReady):
+ rest.setup_platform(self.hass, {
+ 'platform': 'rest',
+ 'resource': 'http://localhost',
+ }, self.add_devices, None)
+ self.assertEqual(len(self.DEVICES), 0)
@patch('requests.Session.send', side_effect=Timeout())
def test_setup_timeout(self, mock_req):
"""Test setup when connection timeout occurs."""
- self.assertFalse(rest.setup_platform(self.hass, {
- 'platform': 'rest',
- 'resource': 'http://localhost',
- }, lambda devices, update=True: None))
+ with raises(PlatformNotReady):
+ rest.setup_platform(self.hass, {
+ 'platform': 'rest',
+ 'resource': 'http://localhost',
+ }, self.add_devices, None)
+ self.assertEqual(len(self.DEVICES), 0)
@requests_mock.Mocker()
def test_setup_minimum(self, mock_req):
"""Test setup with minimum configuration."""
mock_req.get('http://localhost', status_code=200)
- self.assertTrue(setup_component(self.hass, 'binary_sensor', {
- 'binary_sensor': {
- 'platform': 'rest',
- 'resource': 'http://localhost'
- }
- }))
- self.assertEqual(2, mock_req.call_count)
- assert_setup_component(1, 'switch')
+ with assert_setup_component(1, 'binary_sensor'):
+ self.assertTrue(setup_component(self.hass, 'binary_sensor', {
+ 'binary_sensor': {
+ 'platform': 'rest',
+ 'resource': 'http://localhost'
+ }
+ }))
+ self.assertEqual(1, mock_req.call_count)
@requests_mock.Mocker()
def test_setup_get(self, mock_req):
"""Test setup with valid configuration."""
mock_req.get('http://localhost', status_code=200)
- self.assertTrue(setup_component(self.hass, 'binary_sensor', {
- 'binary_sensor': {
- 'platform': 'rest',
- 'resource': 'http://localhost',
- 'method': 'GET',
- 'value_template': '{{ value_json.key }}',
- 'name': 'foo',
- 'unit_of_measurement': 'MB',
- 'verify_ssl': 'true',
- 'authentication': 'basic',
- 'username': 'my username',
- 'password': 'my password',
- 'headers': {'Accept': 'application/json'}
- }
- }))
- self.assertEqual(2, mock_req.call_count)
- assert_setup_component(1, 'binary_sensor')
+ with assert_setup_component(1, 'binary_sensor'):
+ self.assertTrue(setup_component(self.hass, 'binary_sensor', {
+ 'binary_sensor': {
+ 'platform': 'rest',
+ 'resource': 'http://localhost',
+ 'method': 'GET',
+ 'value_template': '{{ value_json.key }}',
+ 'name': 'foo',
+ 'unit_of_measurement': 'MB',
+ 'verify_ssl': 'true',
+ 'authentication': 'basic',
+ 'username': 'my username',
+ 'password': 'my password',
+ 'headers': {'Accept': 'application/json'}
+ }
+ }))
+ self.assertEqual(1, mock_req.call_count)
@requests_mock.Mocker()
def test_setup_post(self, mock_req):
"""Test setup with valid configuration."""
mock_req.post('http://localhost', status_code=200)
- self.assertTrue(setup_component(self.hass, 'binary_sensor', {
- 'binary_sensor': {
- 'platform': 'rest',
- 'resource': 'http://localhost',
- 'method': 'POST',
- 'value_template': '{{ value_json.key }}',
- 'payload': '{ "device": "toaster"}',
- 'name': 'foo',
- 'unit_of_measurement': 'MB',
- 'verify_ssl': 'true',
- 'authentication': 'basic',
- 'username': 'my username',
- 'password': 'my password',
- 'headers': {'Accept': 'application/json'}
- }
- }))
- self.assertEqual(2, mock_req.call_count)
- assert_setup_component(1, 'binary_sensor')
+ with assert_setup_component(1, 'binary_sensor'):
+ self.assertTrue(setup_component(self.hass, 'binary_sensor', {
+ 'binary_sensor': {
+ 'platform': 'rest',
+ 'resource': 'http://localhost',
+ 'method': 'POST',
+ 'value_template': '{{ value_json.key }}',
+ 'payload': '{ "device": "toaster"}',
+ 'name': 'foo',
+ 'unit_of_measurement': 'MB',
+ 'verify_ssl': 'true',
+ 'authentication': 'basic',
+ 'username': 'my username',
+ 'password': 'my password',
+ 'headers': {'Accept': 'application/json'}
+ }
+ }))
+ self.assertEqual(1, mock_req.call_count)
class TestRestBinarySensor(unittest.TestCase):
diff --git a/tests/components/binary_sensor/test_spc.py b/tests/components/binary_sensor/test_spc.py
index 0a91b59e14d..ec0886aeed8 100644
--- a/tests/components/binary_sensor/test_spc.py
+++ b/tests/components/binary_sensor/test_spc.py
@@ -1,28 +1,12 @@
"""Tests for Vanderbilt SPC binary sensor platform."""
-import asyncio
-
-import pytest
-
-from homeassistant.components.spc import SpcRegistry
from homeassistant.components.binary_sensor import spc
-from tests.common import async_test_home_assistant
-@pytest.fixture
-def hass(loop):
- """Home Assistant fixture with device mapping registry."""
- hass = loop.run_until_complete(async_test_home_assistant(loop))
- hass.data['spc_registry'] = SpcRegistry()
- yield hass
- loop.run_until_complete(hass.async_stop())
-
-
-@asyncio.coroutine
-def test_setup_platform(hass):
+async def test_setup_platform(hass):
"""Test autodiscovery of supported device types."""
added_entities = []
- zones = {'devices': [{
+ zone_defs = [{
'id': '1',
'type': '3',
'zone_name': 'Kitchen smoke',
@@ -46,16 +30,20 @@ def test_setup_platform(hass):
'area_name': 'House',
'input': '1',
'status': '0',
- }]}
+ }]
def add_entities(entities):
nonlocal added_entities
added_entities = list(entities)
- yield from spc.async_setup_platform(hass=hass,
- config={},
- async_add_entities=add_entities,
- discovery_info=zones)
+ from pyspcwebgw import Zone
+
+ zones = [Zone(area=None, spc_zone=z) for z in zone_defs]
+
+ await spc.async_setup_platform(hass=hass,
+ config={},
+ async_add_entities=add_entities,
+ discovery_info={'devices': zones})
assert len(added_entities) == 3
assert added_entities[0].device_class == 'smoke'
diff --git a/tests/components/camera/test_mqtt.py b/tests/components/camera/test_mqtt.py
index d83054d7732..8665f26aba9 100644
--- a/tests/components/camera/test_mqtt.py
+++ b/tests/components/camera/test_mqtt.py
@@ -29,3 +29,26 @@ def test_run_camera_setup(hass, aiohttp_client):
assert resp.status == 200
body = yield from resp.text()
assert body == 'beer'
+
+
+@asyncio.coroutine
+def test_unique_id(hass):
+ """Test unique id option only creates one camera per unique_id."""
+ yield from async_mock_mqtt_component(hass)
+ yield from async_setup_component(hass, 'camera', {
+ 'camera': [{
+ 'platform': 'mqtt',
+ 'name': 'Test Camera 1',
+ 'topic': 'test-topic',
+ 'unique_id': 'TOTALLY_UNIQUE'
+ }, {
+ 'platform': 'mqtt',
+ 'name': 'Test Camera 2',
+ 'topic': 'test-topic',
+ 'unique_id': 'TOTALLY_UNIQUE'
+ }]
+ })
+
+ async_fire_mqtt_message(hass, 'test-topic', 'payload')
+ yield from hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py
index 1ffbd375b75..0121cd1c794 100644
--- a/tests/components/cast/test_init.py
+++ b/tests/components/cast/test_init.py
@@ -17,6 +17,12 @@ async def test_creating_entry_sets_up_media_player(hass):
return_value=True):
result = await hass.config_entries.flow.async_init(
cast.DOMAIN, context={'source': config_entries.SOURCE_USER})
+
+ # Confirmation form
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+ result = await hass.config_entries.flow.async_configure(
+ result['flow_id'], {})
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py
index 7a4e9f2950e..108e5c45137 100644
--- a/tests/components/cloud/__init__.py
+++ b/tests/components/cloud/__init__.py
@@ -1 +1,32 @@
"""Tests for the cloud component."""
+from unittest.mock import patch
+from homeassistant.setup import async_setup_component
+from homeassistant.components import cloud
+
+from jose import jwt
+
+from tests.common import mock_coro
+
+
+def mock_cloud(hass, config={}):
+ """Mock cloud."""
+ with patch('homeassistant.components.cloud.Cloud.async_start',
+ return_value=mock_coro()):
+ assert hass.loop.run_until_complete(async_setup_component(
+ hass, cloud.DOMAIN, {
+ 'cloud': config
+ }))
+
+ hass.data[cloud.DOMAIN]._decode_claims = \
+ lambda token: jwt.get_unverified_claims(token)
+
+
+def mock_cloud_prefs(hass, prefs={}):
+ """Fixture for cloud component."""
+ prefs_to_set = {
+ cloud.STORAGE_ENABLE_ALEXA: True,
+ cloud.STORAGE_ENABLE_GOOGLE: True,
+ }
+ prefs_to_set.update(prefs)
+ hass.data[cloud.DOMAIN]._prefs = prefs_to_set
+ return prefs_to_set
diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py
new file mode 100644
index 00000000000..81ecb7250ef
--- /dev/null
+++ b/tests/components/cloud/conftest.py
@@ -0,0 +1,11 @@
+"""Fixtures for cloud tests."""
+import pytest
+
+from . import mock_cloud, mock_cloud_prefs
+
+
+@pytest.fixture
+def mock_cloud_fixture(hass):
+ """Fixture for cloud component."""
+ mock_cloud(hass)
+ return mock_cloud_prefs(hass)
diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py
index 55c6290c158..5d4b356b9b2 100644
--- a/tests/components/cloud/test_http_api.py
+++ b/tests/components/cloud/test_http_api.py
@@ -5,32 +5,42 @@ from unittest.mock import patch, MagicMock
import pytest
from jose import jwt
-from homeassistant.bootstrap import async_setup_component
-from homeassistant.components.cloud import DOMAIN, auth_api, iot
+from homeassistant.components.cloud import (
+ DOMAIN, auth_api, iot, STORAGE_ENABLE_GOOGLE, STORAGE_ENABLE_ALEXA)
from tests.common import mock_coro
+from . import mock_cloud, mock_cloud_prefs
GOOGLE_ACTIONS_SYNC_URL = 'https://api-test.hass.io/google_actions_sync'
+SUBSCRIPTION_INFO_URL = 'https://api-test.hass.io/subscription_info'
+
+
+@pytest.fixture()
+def mock_auth():
+ """Mock check token."""
+ with patch('homeassistant.components.cloud.auth_api.check_token'):
+ yield
+
+
+@pytest.fixture(autouse=True)
+def setup_api(hass):
+ """Initialize HTTP API."""
+ mock_cloud(hass, {
+ 'mode': 'development',
+ 'cognito_client_id': 'cognito_client_id',
+ 'user_pool_id': 'user_pool_id',
+ 'region': 'region',
+ 'relayer': 'relayer',
+ 'google_actions_sync_url': GOOGLE_ACTIONS_SYNC_URL,
+ 'subscription_info_url': SUBSCRIPTION_INFO_URL,
+ })
+ return mock_cloud_prefs(hass)
@pytest.fixture
def cloud_client(hass, aiohttp_client):
"""Fixture that can fetch from the cloud client."""
- with patch('homeassistant.components.cloud.Cloud.async_start',
- return_value=mock_coro()):
- hass.loop.run_until_complete(async_setup_component(hass, 'cloud', {
- 'cloud': {
- 'mode': 'development',
- 'cognito_client_id': 'cognito_client_id',
- 'user_pool_id': 'user_pool_id',
- 'region': 'region',
- 'relayer': 'relayer',
- 'google_actions_sync_url': GOOGLE_ACTIONS_SYNC_URL,
- }
- }))
- hass.data['cloud']._decode_claims = \
- lambda token: jwt.get_unverified_claims(token)
with patch('homeassistant.components.cloud.Cloud.write_user_info'):
yield hass.loop.run_until_complete(aiohttp_client(hass.http.app))
@@ -57,31 +67,6 @@ async def test_google_actions_sync_fails(mock_cognito, cloud_client,
assert req.status == 403
-@asyncio.coroutine
-def test_account_view_no_account(cloud_client):
- """Test fetching account if no account available."""
- req = yield from cloud_client.get('/api/cloud/account')
- assert req.status == 400
-
-
-@asyncio.coroutine
-def test_account_view(hass, cloud_client):
- """Test fetching account if no account available."""
- hass.data[DOMAIN].id_token = jwt.encode({
- 'email': 'hello@home-assistant.io',
- 'custom:sub-exp': '2018-01-03'
- }, 'test')
- hass.data[DOMAIN].iot.state = iot.STATE_CONNECTED
- req = yield from cloud_client.get('/api/cloud/account')
- assert req.status == 200
- result = yield from req.json()
- assert result == {
- 'email': 'hello@home-assistant.io',
- 'sub_exp': '2018-01-03',
- 'cloud': iot.STATE_CONNECTED,
- }
-
-
@asyncio.coroutine
def test_login_view(hass, cloud_client, mock_cognito):
"""Test logging in."""
@@ -103,8 +88,7 @@ def test_login_view(hass, cloud_client, mock_cognito):
assert req.status == 200
result = yield from req.json()
- assert result['email'] == 'hello@home-assistant.io'
- assert result['sub_exp'] == '2018-01-03'
+ assert result == {'success': True}
assert len(mock_connect.mock_calls) == 1
@@ -330,3 +314,116 @@ def test_resend_confirm_view_unknown_error(mock_cognito, cloud_client):
'email': 'hello@bla.com',
})
assert req.status == 502
+
+
+async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture):
+ """Test querying the status."""
+ hass.data[DOMAIN].id_token = jwt.encode({
+ 'email': 'hello@home-assistant.io',
+ 'custom:sub-exp': '2018-01-03'
+ }, 'test')
+ hass.data[DOMAIN].iot.state = iot.STATE_CONNECTED
+ client = await hass_ws_client(hass)
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/status'
+ })
+ response = await client.receive_json()
+ assert response['result'] == {
+ 'logged_in': True,
+ 'email': 'hello@home-assistant.io',
+ 'cloud': 'connected',
+ 'alexa_enabled': True,
+ 'google_enabled': True,
+ }
+
+
+async def test_websocket_status_not_logged_in(hass, hass_ws_client):
+ """Test querying the status."""
+ client = await hass_ws_client(hass)
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/status'
+ })
+ response = await client.receive_json()
+ assert response['result'] == {
+ 'logged_in': False,
+ 'cloud': 'disconnected'
+ }
+
+
+async def test_websocket_subscription(hass, hass_ws_client, aioclient_mock,
+ mock_auth):
+ """Test querying the status."""
+ aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'return': 'value'})
+ hass.data[DOMAIN].id_token = jwt.encode({
+ 'email': 'hello@home-assistant.io',
+ 'custom:sub-exp': '2018-01-03'
+ }, 'test')
+ client = await hass_ws_client(hass)
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/subscription'
+ })
+ response = await client.receive_json()
+
+ assert response['result'] == {
+ 'return': 'value'
+ }
+
+
+async def test_websocket_subscription_fail(hass, hass_ws_client,
+ aioclient_mock, mock_auth):
+ """Test querying the status."""
+ aioclient_mock.get(SUBSCRIPTION_INFO_URL, status=500)
+ hass.data[DOMAIN].id_token = jwt.encode({
+ 'email': 'hello@home-assistant.io',
+ 'custom:sub-exp': '2018-01-03'
+ }, 'test')
+ client = await hass_ws_client(hass)
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/subscription'
+ })
+ response = await client.receive_json()
+
+ assert not response['success']
+ assert response['error']['code'] == 'request_failed'
+
+
+async def test_websocket_subscription_not_logged_in(hass, hass_ws_client):
+ """Test querying the status."""
+ client = await hass_ws_client(hass)
+ with patch('homeassistant.components.cloud.Cloud.fetch_subscription_info',
+ return_value=mock_coro({'return': 'value'})):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/subscription'
+ })
+ response = await client.receive_json()
+
+ assert not response['success']
+ assert response['error']['code'] == 'not_logged_in'
+
+
+async def test_websocket_update_preferences(hass, hass_ws_client,
+ aioclient_mock, setup_api):
+ """Test updating preference."""
+ assert setup_api[STORAGE_ENABLE_GOOGLE]
+ assert setup_api[STORAGE_ENABLE_ALEXA]
+ hass.data[DOMAIN].id_token = jwt.encode({
+ 'email': 'hello@home-assistant.io',
+ 'custom:sub-exp': '2018-01-03'
+ }, 'test')
+ client = await hass_ws_client(hass)
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/update_prefs',
+ 'alexa_enabled': False,
+ 'google_enabled': False,
+ })
+ response = await client.receive_json()
+
+ assert response['success']
+ assert not setup_api[STORAGE_ENABLE_GOOGLE]
+ assert not setup_api[STORAGE_ENABLE_ALEXA]
diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py
index 014cdb1c6c6..1fdbda496a9 100644
--- a/tests/components/cloud/test_init.py
+++ b/tests/components/cloud/test_init.py
@@ -30,6 +30,7 @@ def test_constructor_loads_info_from_constant():
'region': 'test-region',
'relayer': 'test-relayer',
'google_actions_sync_url': 'test-google_actions_sync_url',
+ 'subscription_info_url': 'test-subscription-info-url'
}
}), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset',
return_value=mock_coro(True)):
@@ -45,6 +46,7 @@ def test_constructor_loads_info_from_constant():
assert cl.region == 'test-region'
assert cl.relayer == 'test-relayer'
assert cl.google_actions_sync_url == 'test-google_actions_sync_url'
+ assert cl.subscription_info_url == 'test-subscription-info-url'
@asyncio.coroutine
@@ -139,9 +141,9 @@ def test_write_user_info():
@asyncio.coroutine
-def test_subscription_expired():
+def test_subscription_expired(hass):
"""Test subscription being expired."""
- cl = cloud.Cloud(None, cloud.MODE_DEV, None, None)
+ cl = cloud.Cloud(hass, cloud.MODE_DEV, None, None)
token_val = {
'custom:sub-exp': '2017-11-13'
}
@@ -152,9 +154,9 @@ def test_subscription_expired():
@asyncio.coroutine
-def test_subscription_not_expired():
+def test_subscription_not_expired(hass):
"""Test subscription not being expired."""
- cl = cloud.Cloud(None, cloud.MODE_DEV, None, None)
+ cl = cloud.Cloud(hass, cloud.MODE_DEV, None, None)
token_val = {
'custom:sub-exp': '2017-11-13'
}
diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py
index 1b580d0eb9b..07ec1851fbe 100644
--- a/tests/components/cloud/test_iot.py
+++ b/tests/components/cloud/test_iot.py
@@ -6,10 +6,14 @@ from aiohttp import WSMsgType, client_exceptions
import pytest
from homeassistant.setup import async_setup_component
-from homeassistant.components.cloud import Cloud, iot, auth_api, MODE_DEV
+from homeassistant.components.cloud import (
+ Cloud, iot, auth_api, MODE_DEV, STORAGE_ENABLE_ALEXA,
+ STORAGE_ENABLE_GOOGLE)
from tests.components.alexa import test_smart_home as test_alexa
from tests.common import mock_coro
+from . import mock_cloud_prefs
+
@pytest.fixture
def mock_client():
@@ -284,6 +288,8 @@ def test_handler_alexa(hass):
})
assert setup
+ mock_cloud_prefs(hass)
+
resp = yield from iot.async_handle_alexa(
hass, hass.data['cloud'],
test_alexa.get_new_request('Alexa.Discovery', 'Discover'))
@@ -299,6 +305,20 @@ def test_handler_alexa(hass):
assert device['manufacturerName'] == 'Home Assistant'
+@asyncio.coroutine
+def test_handler_alexa_disabled(hass, mock_cloud_fixture):
+ """Test handler Alexa when user has disabled it."""
+ mock_cloud_fixture[STORAGE_ENABLE_ALEXA] = False
+
+ resp = yield from iot.async_handle_alexa(
+ hass, hass.data['cloud'],
+ test_alexa.get_new_request('Alexa.Discovery', 'Discover'))
+
+ assert resp['event']['header']['namespace'] == 'Alexa'
+ assert resp['event']['header']['name'] == 'ErrorResponse'
+ assert resp['event']['payload']['type'] == 'BRIDGE_UNREACHABLE'
+
+
@asyncio.coroutine
def test_handler_google_actions(hass):
"""Test handler Google Actions."""
@@ -327,6 +347,8 @@ def test_handler_google_actions(hass):
})
assert setup
+ mock_cloud_prefs(hass)
+
reqid = '5711642932632160983'
data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]}
@@ -351,6 +373,24 @@ def test_handler_google_actions(hass):
assert device['roomHint'] == 'living room'
+async def test_handler_google_actions_disabled(hass, mock_cloud_fixture):
+ """Test handler Google Actions when user has disabled it."""
+ mock_cloud_fixture[STORAGE_ENABLE_GOOGLE] = False
+
+ with patch('homeassistant.components.cloud.Cloud.async_start',
+ return_value=mock_coro()):
+ assert await async_setup_component(hass, 'cloud', {})
+
+ reqid = '5711642932632160983'
+ data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]}
+
+ resp = await iot.async_handle_google_actions(
+ hass, hass.data['cloud'], data)
+
+ assert resp['requestId'] == reqid
+ assert resp['payload']['errorCode'] == 'deviceTurnedOff'
+
+
async def test_refresh_token_expired(hass):
"""Test handling Unauthenticated error raised if refresh token expired."""
cloud = Cloud(hass, MODE_DEV, None, None)
diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py
index ba053050f99..66d29aac757 100644
--- a/tests/components/config/test_config_entries.py
+++ b/tests/components/config/test_config_entries.py
@@ -9,7 +9,6 @@ import voluptuous as vol
from homeassistant import config_entries as core_ce
from homeassistant.config_entries import HANDLERS
-from homeassistant.data_entry_flow import FlowHandler
from homeassistant.setup import async_setup_component
from homeassistant.components.config import config_entries
from homeassistant.loader import set_component
@@ -37,13 +36,15 @@ def test_get_entries(hass, client):
MockConfigEntry(
domain='comp',
title='Test 1',
- source='bla'
+ source='bla',
+ connection_class=core_ce.CONN_CLASS_LOCAL_POLL,
).add_to_hass(hass)
MockConfigEntry(
domain='comp2',
title='Test 2',
source='bla2',
state=core_ce.ENTRY_STATE_LOADED,
+ connection_class=core_ce.CONN_CLASS_ASSUMED,
).add_to_hass(hass)
resp = yield from client.get('/api/config/config_entries/entry')
assert resp.status == 200
@@ -55,13 +56,15 @@ def test_get_entries(hass, client):
'domain': 'comp',
'title': 'Test 1',
'source': 'bla',
- 'state': 'not_loaded'
+ 'state': 'not_loaded',
+ 'connection_class': 'local_poll',
},
{
'domain': 'comp2',
'title': 'Test 2',
'source': 'bla2',
'state': 'loaded',
+ 'connection_class': 'assumed',
},
]
@@ -100,7 +103,7 @@ def test_available_flows(hass, client):
@asyncio.coroutine
def test_initialize_flow(hass, client):
"""Test we can initialize a flow."""
- class TestFlow(FlowHandler):
+ class TestFlow(core_ce.ConfigFlow):
@asyncio.coroutine
def async_step_user(self, user_input=None):
schema = OrderedDict()
@@ -155,7 +158,7 @@ def test_initialize_flow(hass, client):
@asyncio.coroutine
def test_abort(hass, client):
"""Test a flow that aborts."""
- class TestFlow(FlowHandler):
+ class TestFlow(core_ce.ConfigFlow):
@asyncio.coroutine
def async_step_user(self, user_input=None):
return self.async_abort(reason='bla')
@@ -181,7 +184,7 @@ def test_create_account(hass, client):
hass, 'test',
MockModule('test', async_setup_entry=mock_coro_func(True)))
- class TestFlow(FlowHandler):
+ class TestFlow(core_ce.ConfigFlow):
VERSION = 1
@asyncio.coroutine
@@ -213,7 +216,7 @@ def test_two_step_flow(hass, client):
hass, 'test',
MockModule('test', async_setup_entry=mock_coro_func(True)))
- class TestFlow(FlowHandler):
+ class TestFlow(core_ce.ConfigFlow):
VERSION = 1
@asyncio.coroutine
@@ -269,7 +272,7 @@ def test_two_step_flow(hass, client):
@asyncio.coroutine
def test_get_progress_index(hass, client):
"""Test querying for the flows that are in progress."""
- class TestFlow(FlowHandler):
+ class TestFlow(core_ce.ConfigFlow):
VERSION = 5
@asyncio.coroutine
@@ -301,7 +304,7 @@ def test_get_progress_index(hass, client):
@asyncio.coroutine
def test_get_progress_flow(hass, client):
"""Test we can query the API for same result as we get from init a flow."""
- class TestFlow(FlowHandler):
+ class TestFlow(core_ce.ConfigFlow):
@asyncio.coroutine
def async_step_user(self, user_input=None):
schema = OrderedDict()
diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py
new file mode 100644
index 00000000000..f8ea51cfdc8
--- /dev/null
+++ b/tests/components/config/test_device_registry.py
@@ -0,0 +1,62 @@
+"""Test entity_registry API."""
+import pytest
+
+from homeassistant.components.config import device_registry
+from tests.common import mock_device_registry
+
+
+@pytest.fixture
+def client(hass, hass_ws_client):
+ """Fixture that can interact with the config manager API."""
+ hass.loop.run_until_complete(device_registry.async_setup(hass))
+ yield hass.loop.run_until_complete(hass_ws_client(hass))
+
+
+@pytest.fixture
+def registry(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+async def test_list_devices(hass, client, registry):
+ """Test list entries."""
+ registry.async_get_or_create(
+ config_entry_id='1234',
+ connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
+ identifiers={('bridgeid', '0123')},
+ manufacturer='manufacturer', model='model')
+ registry.async_get_or_create(
+ config_entry_id='1234',
+ connections={},
+ identifiers={('bridgeid', '1234')},
+ manufacturer='manufacturer', model='model',
+ via_hub=('bridgeid', '0123'))
+
+ await client.send_json({
+ 'id': 5,
+ 'type': 'config/device_registry/list',
+ })
+ msg = await client.receive_json()
+
+ dev1, dev2 = [entry.pop('id') for entry in msg['result']]
+
+ assert msg['result'] == [
+ {
+ 'config_entries': ['1234'],
+ 'connections': [['ethernet', '12:34:56:78:90:AB:CD:EF']],
+ 'manufacturer': 'manufacturer',
+ 'model': 'model',
+ 'name': None,
+ 'sw_version': None,
+ 'hub_device_id': None,
+ },
+ {
+ 'config_entries': ['1234'],
+ 'connections': [],
+ 'manufacturer': 'manufacturer',
+ 'model': 'model',
+ 'name': None,
+ 'sw_version': None,
+ 'hub_device_id': dev1,
+ }
+ ]
diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py
index 559f29372de..cd74faf1843 100644
--- a/tests/components/config/test_entity_registry.py
+++ b/tests/components/config/test_entity_registry.py
@@ -1,4 +1,6 @@
"""Test entity_registry API."""
+from collections import OrderedDict
+
import pytest
from homeassistant.helpers.entity_registry import RegistryEntry
@@ -13,6 +15,49 @@ def client(hass, hass_ws_client):
yield hass.loop.run_until_complete(hass_ws_client(hass))
+async def test_list_entities(hass, client):
+ """Test list entries."""
+ entities = OrderedDict()
+ entities['test_domain.name'] = RegistryEntry(
+ entity_id='test_domain.name',
+ unique_id='1234',
+ platform='test_platform',
+ name='Hello World'
+ )
+ entities['test_domain.no_name'] = RegistryEntry(
+ entity_id='test_domain.no_name',
+ unique_id='6789',
+ platform='test_platform',
+ )
+
+ mock_registry(hass, entities)
+
+ await client.send_json({
+ 'id': 5,
+ 'type': 'config/entity_registry/list',
+ })
+ msg = await client.receive_json()
+
+ assert msg['result'] == [
+ {
+ 'config_entry_id': None,
+ 'device_id': None,
+ 'disabled_by': None,
+ 'entity_id': 'test_domain.name',
+ 'name': 'Hello World',
+ 'platform': 'test_platform',
+ },
+ {
+ 'config_entry_id': None,
+ 'device_id': None,
+ 'disabled_by': None,
+ 'entity_id': 'test_domain.no_name',
+ 'name': None,
+ 'platform': 'test_platform',
+ }
+ ]
+
+
async def test_get_entity(hass, client):
"""Test get entry."""
mock_registry(hass, {
diff --git a/tests/components/cover/test_deconz.py b/tests/components/cover/test_deconz.py
new file mode 100644
index 00000000000..60de9cffdc1
--- /dev/null
+++ b/tests/components/cover/test_deconz.py
@@ -0,0 +1,84 @@
+"""deCONZ cover platform tests."""
+from unittest.mock import Mock, patch
+
+from homeassistant import config_entries
+from homeassistant.components import deconz
+from homeassistant.components.deconz.const import COVER_TYPES
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+from tests.common import mock_coro
+
+SUPPORTED_COVERS = {
+ "1": {
+ "id": "Cover 1 id",
+ "name": "Cover 1 name",
+ "type": "Level controllable output",
+ "state": {}
+ }
+}
+
+UNSUPPORTED_COVER = {
+ "1": {
+ "id": "Cover id",
+ "name": "Unsupported switch",
+ "type": "Not a cover",
+ "state": {}
+ }
+}
+
+
+async def setup_bridge(hass, data):
+ """Load the deCONZ cover platform."""
+ from pydeconz import DeconzSession
+ loop = Mock()
+ session = Mock()
+ entry = Mock()
+ entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'}
+ bridge = DeconzSession(loop, session, **entry.data)
+ with patch('pydeconz.DeconzSession.async_get_state',
+ return_value=mock_coro(data)):
+ await bridge.async_load_parameters()
+ hass.data[deconz.DOMAIN] = bridge
+ hass.data[deconz.DATA_DECONZ_UNSUB] = []
+ hass.data[deconz.DATA_DECONZ_ID] = {}
+ config_entry = config_entries.ConfigEntry(
+ 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test',
+ config_entries.CONN_CLASS_LOCAL_PUSH)
+ await hass.config_entries.async_forward_entry_setup(config_entry, 'cover')
+ # To flush out the service call to update the group
+ await hass.async_block_till_done()
+
+
+async def test_no_switches(hass):
+ """Test that no cover entities are created."""
+ data = {}
+ await setup_bridge(hass, data)
+ assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0
+ assert len(hass.states.async_all()) == 0
+
+
+async def test_cover(hass):
+ """Test that all supported cover entities are created."""
+ await setup_bridge(hass, {"lights": SUPPORTED_COVERS})
+ assert "cover.cover_1_name" in hass.data[deconz.DATA_DECONZ_ID]
+ assert len(SUPPORTED_COVERS) == len(COVER_TYPES)
+ assert len(hass.states.async_all()) == 2
+
+
+async def test_add_new_cover(hass):
+ """Test successful creation of cover entity."""
+ data = {}
+ await setup_bridge(hass, data)
+ cover = Mock()
+ cover.name = 'name'
+ cover.type = "Level controllable output"
+ cover.register_async_callback = Mock()
+ async_dispatcher_send(hass, 'deconz_new_light', [cover])
+ await hass.async_block_till_done()
+ assert "cover.name" in hass.data[deconz.DATA_DECONZ_ID]
+
+
+async def test_unsupported_cover(hass):
+ """Test that unsupported covers are not created."""
+ await setup_bridge(hass, {"lights": UNSUPPORTED_COVER})
+ assert len(hass.states.async_all()) == 0
diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py
index 049a3b961b6..cfda1232e93 100644
--- a/tests/components/deconz/test_init.py
+++ b/tests/components/deconz/test_init.py
@@ -112,17 +112,21 @@ async def test_setup_entry_successful(hass):
assert hass.data[deconz.DOMAIN]
assert hass.data[deconz.DATA_DECONZ_ID] == {}
assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1
- assert len(mock_add_job.mock_calls) == 5
- assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 5
+ assert len(mock_add_job.mock_calls) == \
+ len(deconz.SUPPORTED_PLATFORMS)
+ assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == \
+ len(deconz.SUPPORTED_PLATFORMS)
assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \
(entry, 'binary_sensor')
assert mock_config_entries.async_forward_entry_setup.mock_calls[1][1] == \
- (entry, 'light')
+ (entry, 'cover')
assert mock_config_entries.async_forward_entry_setup.mock_calls[2][1] == \
- (entry, 'scene')
+ (entry, 'light')
assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \
- (entry, 'sensor')
+ (entry, 'scene')
assert mock_config_entries.async_forward_entry_setup.mock_calls[4][1] == \
+ (entry, 'sensor')
+ assert mock_config_entries.async_forward_entry_setup.mock_calls[5][1] == \
(entry, 'switch')
diff --git a/tests/components/geo_location/test_geo_json_events.py b/tests/components/geo_location/test_geo_json_events.py
new file mode 100644
index 00000000000..5ce508289dd
--- /dev/null
+++ b/tests/components/geo_location/test_geo_json_events.py
@@ -0,0 +1,136 @@
+"""The tests for the geojson platform."""
+import unittest
+from unittest import mock
+from unittest.mock import patch, MagicMock
+
+from homeassistant.components import geo_location
+from homeassistant.components.geo_location.geo_json_events import \
+ SCAN_INTERVAL, ATTR_EXTERNAL_ID
+from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_START, \
+ CONF_RADIUS, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_FRIENDLY_NAME, \
+ ATTR_UNIT_OF_MEASUREMENT
+from homeassistant.setup import setup_component
+from tests.common import get_test_home_assistant, assert_setup_component, \
+ fire_time_changed
+import homeassistant.util.dt as dt_util
+
+URL = 'http://geo.json.local/geo_json_events.json'
+CONFIG = {
+ geo_location.DOMAIN: [
+ {
+ 'platform': 'geo_json_events',
+ CONF_URL: URL,
+ CONF_RADIUS: 200
+ }
+ ]
+}
+
+
+class TestGeoJsonPlatform(unittest.TestCase):
+ """Test the geojson platform."""
+
+ def setUp(self):
+ """Initialize values for this testcase class."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @staticmethod
+ def _generate_mock_feed_entry(external_id, title, distance_to_home,
+ coordinates):
+ """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
+ return feed_entry
+
+ @mock.patch('geojson_client.generic_feed.GenericFeed')
+ def test_setup(self, mock_feed):
+ """Test the general setup of the platform."""
+ # Set up some mock feed entries for this test.
+ mock_entry_1 = self._generate_mock_feed_entry('1234', 'Title 1', 15.5,
+ (-31.0, 150.0))
+ mock_entry_2 = self._generate_mock_feed_entry('2345', 'Title 2', 20.5,
+ (-31.1, 150.1))
+ mock_entry_3 = self._generate_mock_feed_entry('3456', 'Title 3', 25.5,
+ (-31.2, 150.2))
+ mock_entry_4 = self._generate_mock_feed_entry('4567', 'Title 4', 12.5,
+ (-31.3, 150.3))
+ mock_feed.return_value.update.return_value = 'OK', [mock_entry_1,
+ mock_entry_2,
+ mock_entry_3]
+
+ utcnow = dt_util.utcnow()
+ # Patching 'utcnow' to gain more control over the timed update.
+ with patch('homeassistant.util.dt.utcnow', return_value=utcnow):
+ with assert_setup_component(1, geo_location.DOMAIN):
+ self.assertTrue(setup_component(self.hass, geo_location.DOMAIN,
+ CONFIG))
+ # Artificially trigger update.
+ self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
+ # Collect events.
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 3
+
+ state = self.hass.states.get("geo_location.title_1")
+ self.assertIsNotNone(state)
+ assert state.name == "Title 1"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "1234", ATTR_LATITUDE: -31.0,
+ ATTR_LONGITUDE: 150.0, ATTR_FRIENDLY_NAME: "Title 1",
+ ATTR_UNIT_OF_MEASUREMENT: "km"}
+ self.assertAlmostEqual(float(state.state), 15.5)
+
+ state = self.hass.states.get("geo_location.title_2")
+ self.assertIsNotNone(state)
+ assert state.name == "Title 2"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "2345", ATTR_LATITUDE: -31.1,
+ ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2",
+ ATTR_UNIT_OF_MEASUREMENT: "km"}
+ self.assertAlmostEqual(float(state.state), 20.5)
+
+ state = self.hass.states.get("geo_location.title_3")
+ self.assertIsNotNone(state)
+ assert state.name == "Title 3"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "3456", ATTR_LATITUDE: -31.2,
+ ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3",
+ ATTR_UNIT_OF_MEASUREMENT: "km"}
+ self.assertAlmostEqual(float(state.state), 25.5)
+
+ # Simulate an update - one existing, one new entry,
+ # one outdated entry
+ mock_feed.return_value.update.return_value = 'OK', [
+ mock_entry_1, mock_entry_4, mock_entry_3]
+ fire_time_changed(self.hass, utcnow + SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 3
+
+ # Simulate an update - empty data, but successful update,
+ # so no changes to entities.
+ mock_feed.return_value.update.return_value = 'OK_NO_DATA', None
+ # mock_restdata.return_value.data = None
+ fire_time_changed(self.hass, utcnow +
+ 2 * SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 3
+
+ # Simulate an update - empty data, removes all entities
+ mock_feed.return_value.update.return_value = 'ERROR', None
+ fire_time_changed(self.hass, utcnow +
+ 2 * SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 0
diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py
index c18ed4b7bf3..52dac7ddb61 100644
--- a/tests/components/google_assistant/test_trait.py
+++ b/tests/components/google_assistant/test_trait.py
@@ -457,6 +457,22 @@ async def test_color_temperature_light(hass):
}
+async def test_color_temperature_light_bad_temp(hass):
+ """Test ColorTemperature trait support for light domain."""
+ assert not trait.ColorTemperatureTrait.supported(light.DOMAIN, 0)
+ assert trait.ColorTemperatureTrait.supported(light.DOMAIN,
+ light.SUPPORT_COLOR_TEMP)
+
+ trt = trait.ColorTemperatureTrait(hass, State('light.bla', STATE_ON, {
+ light.ATTR_MIN_MIREDS: 200,
+ light.ATTR_COLOR_TEMP: 0,
+ light.ATTR_MAX_MIREDS: 500,
+ }))
+
+ assert trt.query_attributes() == {
+ }
+
+
async def test_scene_scene(hass):
"""Test Scene trait support for scene domain."""
assert trait.SceneTrait.supported(scene.DOMAIN, 0)
diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py
index ce260225097..4370c011891 100644
--- a/tests/components/hassio/test_http.py
+++ b/tests/components/hassio/test_http.py
@@ -36,9 +36,13 @@ def test_forward_request(hassio_client):
@asyncio.coroutine
-def test_auth_required_forward_request(hassio_client):
+@pytest.mark.parametrize(
+ 'build_type', [
+ 'supervisor/info', 'homeassistant/update', 'host/info'
+ ])
+def test_auth_required_forward_request(hassio_client, build_type):
"""Test auth required for normal request."""
- resp = yield from hassio_client.post('/api/hassio/beer')
+ resp = yield from hassio_client.post("/api/hassio/{}".format(build_type))
# Check we got right response
assert resp.status == 401
diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py
index 92f8736d1fe..5b76618d460 100644
--- a/tests/components/homekit/test_get_accessories.py
+++ b/tests/components/homekit/test_get_accessories.py
@@ -106,6 +106,8 @@ def test_type_covers(type_name, entity_id, state, attrs):
('AirQualitySensor', 'sensor.air_quality_pm25', '40', {}),
('AirQualitySensor', 'sensor.air_quality', '40',
{ATTR_DEVICE_CLASS: 'pm25'}),
+ ('CarbonMonoxideSensor', 'sensor.airmeter', '2',
+ {ATTR_DEVICE_CLASS: 'co'}),
('CarbonDioxideSensor', 'sensor.airmeter_co2', '500', {}),
('CarbonDioxideSensor', 'sensor.airmeter', '500',
{ATTR_DEVICE_CLASS: 'co2'}),
diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py
index 901a8e76856..ebc1c3e1306 100644
--- a/tests/components/homekit/test_type_sensors.py
+++ b/tests/components/homekit/test_type_sensors.py
@@ -1,8 +1,9 @@
"""Test different accessory types: Sensors."""
-from homeassistant.components.homekit.const import PROP_CELSIUS
+from homeassistant.components.homekit.const import (
+ PROP_CELSIUS, THRESHOLD_CO, THRESHOLD_CO2)
from homeassistant.components.homekit.type_sensors import (
- AirQualitySensor, BinarySensor, CarbonDioxideSensor, HumiditySensor,
- LightSensor, TemperatureSensor, BINARY_SENSOR_SERVICE_MAP)
+ AirQualitySensor, BinarySensor, CarbonMonoxideSensor, CarbonDioxideSensor,
+ HumiditySensor, LightSensor, TemperatureSensor, BINARY_SENSOR_SERVICE_MAP)
from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_HOME, STATE_NOT_HOME,
STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT)
@@ -94,6 +95,45 @@ async def test_air_quality(hass, hk_driver):
assert acc.char_quality.value == 5
+async def test_co(hass, hk_driver):
+ """Test if accessory is updated after state change."""
+ entity_id = 'sensor.co'
+
+ hass.states.async_set(entity_id, None)
+ await hass.async_block_till_done()
+ acc = CarbonMonoxideSensor(hass, hk_driver, 'CO', entity_id, 2, None)
+ await hass.async_add_job(acc.run)
+
+ assert acc.aid == 2
+ assert acc.category == 10 # Sensor
+
+ assert acc.char_level.value == 0
+ assert acc.char_peak.value == 0
+ assert acc.char_detected.value == 0
+
+ hass.states.async_set(entity_id, STATE_UNKNOWN)
+ await hass.async_block_till_done()
+ assert acc.char_level.value == 0
+ assert acc.char_peak.value == 0
+ assert acc.char_detected.value == 0
+
+ value = 32
+ assert value > THRESHOLD_CO
+ hass.states.async_set(entity_id, str(value))
+ await hass.async_block_till_done()
+ assert acc.char_level.value == 32
+ assert acc.char_peak.value == 32
+ assert acc.char_detected.value == 1
+
+ value = 10
+ assert value < THRESHOLD_CO
+ hass.states.async_set(entity_id, str(value))
+ await hass.async_block_till_done()
+ assert acc.char_level.value == 10
+ assert acc.char_peak.value == 32
+ assert acc.char_detected.value == 0
+
+
async def test_co2(hass, hk_driver):
"""Test if accessory is updated after state change."""
entity_id = 'sensor.co2'
@@ -106,25 +146,29 @@ async def test_co2(hass, hk_driver):
assert acc.aid == 2
assert acc.category == 10 # Sensor
- assert acc.char_co2.value == 0
+ assert acc.char_level.value == 0
assert acc.char_peak.value == 0
assert acc.char_detected.value == 0
hass.states.async_set(entity_id, STATE_UNKNOWN)
await hass.async_block_till_done()
- assert acc.char_co2.value == 0
+ assert acc.char_level.value == 0
assert acc.char_peak.value == 0
assert acc.char_detected.value == 0
- hass.states.async_set(entity_id, '1100')
+ value = 1100
+ assert value > THRESHOLD_CO2
+ hass.states.async_set(entity_id, str(value))
await hass.async_block_till_done()
- assert acc.char_co2.value == 1100
+ assert acc.char_level.value == 1100
assert acc.char_peak.value == 1100
assert acc.char_detected.value == 1
- hass.states.async_set(entity_id, '800')
+ value = 800
+ assert value < THRESHOLD_CO2
+ hass.states.async_set(entity_id, str(value))
await hass.async_block_till_done()
- assert acc.char_co2.value == 800
+ assert acc.char_level.value == 800
assert acc.char_peak.value == 1100
assert acc.char_detected.value == 0
diff --git a/tests/components/huawei_lte.py b/tests/components/huawei_lte.py
new file mode 100644
index 00000000000..0fe803208ff
--- /dev/null
+++ b/tests/components/huawei_lte.py
@@ -0,0 +1,48 @@
+"""Huawei LTE component tests."""
+import pytest
+
+from homeassistant.components import huawei_lte
+
+
+@pytest.fixture(autouse=True)
+def routerdata():
+ """Set up a router data for testing."""
+ rd = huawei_lte.RouterData(None)
+ rd.device_information = {
+ 'SoftwareVersion': '1.0',
+ 'nested': {'foo': 'bar'},
+ }
+ return rd
+
+
+async def test_routerdata_get_nonexistent_root(routerdata):
+ """Test that accessing a nonexistent root element raises KeyError."""
+ with pytest.raises(KeyError): # NOT AttributeError
+ routerdata["nonexistent_root.foo"]
+
+
+async def test_routerdata_get_nonexistent_leaf(routerdata):
+ """Test that accessing a nonexistent leaf element raises KeyError."""
+ with pytest.raises(KeyError):
+ routerdata["device_information.foo"]
+
+
+async def test_routerdata_get_nonexistent_leaf_path(routerdata):
+ """Test that accessing a nonexistent long path raises KeyError."""
+ with pytest.raises(KeyError):
+ routerdata["device_information.long.path.foo"]
+
+
+async def test_routerdata_get_simple(routerdata):
+ """Test that accessing a short, simple path works."""
+ assert routerdata["device_information.SoftwareVersion"] == "1.0"
+
+
+async def test_routerdata_get_longer(routerdata):
+ """Test that accessing a longer path works."""
+ assert routerdata["device_information.nested.foo"] == "bar"
+
+
+async def test_routerdata_get_dict(routerdata):
+ """Test that returning an intermediate dict works."""
+ assert routerdata["device_information.nested"] == {'foo': 'bar'}
diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py
index 1c4768746d5..5da6d5b709a 100644
--- a/tests/components/hue/test_init.py
+++ b/tests/components/hue/test_init.py
@@ -182,7 +182,7 @@ async def test_config_passed_to_config_entry(hass):
assert len(mock_registry.mock_calls) == 1
assert mock_registry.mock_calls[0][2] == {
- 'config_entry': entry.entry_id,
+ 'config_entry_id': entry.entry_id,
'connections': {
('mac', 'mock-mac')
},
@@ -192,7 +192,7 @@ async def test_config_passed_to_config_entry(hass):
'manufacturer': 'Signify',
'name': 'mock-name',
'model': 'mock-modelid',
- 'sw_version': 'mock-swversion'
+ 'sw_version': 'mock-swversion',
}
diff --git a/tests/components/ios/__init__.py b/tests/components/ios/__init__.py
new file mode 100644
index 00000000000..a028090473e
--- /dev/null
+++ b/tests/components/ios/__init__.py
@@ -0,0 +1 @@
+"""Tests for the iOS component."""
diff --git a/tests/components/ios/test_init.py b/tests/components/ios/test_init.py
new file mode 100644
index 00000000000..ad1ab328325
--- /dev/null
+++ b/tests/components/ios/test_init.py
@@ -0,0 +1,67 @@
+"""Tests for the iOS init file."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.setup import async_setup_component
+from homeassistant.components import ios
+
+from tests.common import mock_component, mock_coro
+
+
+@pytest.fixture(autouse=True)
+def mock_load_json():
+ """Mock load_json."""
+ with patch('homeassistant.components.ios.load_json', return_value={}):
+ yield
+
+
+@pytest.fixture(autouse=True)
+def mock_dependencies(hass):
+ """Mock dependencies loaded."""
+ mock_component(hass, 'zeroconf')
+ mock_component(hass, 'device_tracker')
+
+
+async def test_creating_entry_sets_up_sensor(hass):
+ """Test setting up iOS loads the sensor component."""
+ with patch('homeassistant.components.sensor.ios.async_setup_entry',
+ return_value=mock_coro(True)) as mock_setup:
+ result = await hass.config_entries.flow.async_init(
+ ios.DOMAIN, context={'source': config_entries.SOURCE_USER})
+
+ # Confirmation form
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+ result = await hass.config_entries.flow.async_configure(
+ result['flow_id'], {})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert len(mock_setup.mock_calls) == 1
+
+
+async def test_configuring_ios_creates_entry(hass):
+ """Test that specifying config will create an entry."""
+ with patch('homeassistant.components.ios.async_setup_entry',
+ return_value=mock_coro(True)) as mock_setup:
+ await async_setup_component(hass, ios.DOMAIN, {
+ 'ios': {
+ 'push': {}
+ }
+ })
+ await hass.async_block_till_done()
+
+ assert len(mock_setup.mock_calls) == 1
+
+
+async def test_not_configuring_ios_not_creates_entry(hass):
+ """Test that no config will not create an entry."""
+ with patch('homeassistant.components.ios.async_setup_entry',
+ return_value=mock_coro(True)) as mock_setup:
+ await async_setup_component(hass, ios.DOMAIN, {})
+ await hass.async_block_till_done()
+
+ assert len(mock_setup.mock_calls) == 0
diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py
index df088d7a1b5..96f180505b8 100644
--- a/tests/components/light/test_deconz.py
+++ b/tests/components/light/test_deconz.py
@@ -55,6 +55,7 @@ async def setup_bridge(hass, data, allow_deconz_groups=True):
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'}
bridge = DeconzSession(loop, session, **entry.data)
+ bridge.config = Mock()
with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)):
await bridge.async_load_parameters()
@@ -64,7 +65,7 @@ async def setup_bridge(hass, data, allow_deconz_groups=True):
config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title',
{'host': 'mock-host', 'allow_deconz_groups': allow_deconz_groups},
- 'test')
+ 'test', config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(config_entry, 'light')
# To flush out the service call to update the group
await hass.async_block_till_done()
diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py
index db8d7e5f1e1..ad4026e7f31 100644
--- a/tests/components/light/test_hue.py
+++ b/tests/components/light/test_hue.py
@@ -199,7 +199,7 @@ async def setup_bridge(hass, mock_bridge):
hass.data[hue.DOMAIN] = {'mock-host': mock_bridge}
config_entry = config_entries.ConfigEntry(1, hue.DOMAIN, 'Mock Title', {
'host': 'mock-host'
- }, 'test')
+ }, 'test', config_entries.CONN_CLASS_LOCAL_POLL)
await hass.config_entries.async_forward_entry_setup(config_entry, 'light')
# To flush out the service call to update the group
await hass.async_block_till_done()
diff --git a/tests/components/light/test_tradfri.py b/tests/components/light/test_tradfri.py
index 12c596f3f09..337031cf92c 100644
--- a/tests/components/light/test_tradfri.py
+++ b/tests/components/light/test_tradfri.py
@@ -5,10 +5,10 @@ from unittest.mock import Mock, MagicMock, patch, PropertyMock
import pytest
from pytradfri.device import Device, LightControl, Light
-from pytradfri import RequestError
from homeassistant.components import tradfri
-from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry
DEFAULT_TEST_FEATURES = {'can_set_dimmer': False,
@@ -199,7 +199,7 @@ def mock_gateway():
@pytest.fixture
def mock_api(mock_gateway):
"""Mock api."""
- async def api(self, command):
+ async def api(command):
"""Mock api function."""
# Store the data for "real" command objects.
if(hasattr(command, '_data') and not isinstance(command, Mock)):
@@ -213,63 +213,20 @@ async def generate_psk(self, code):
return "mock"
-async def setup_gateway(hass, mock_gateway, mock_api,
- generate_psk=generate_psk,
- known_hosts=None):
+async def setup_gateway(hass, mock_gateway, mock_api):
"""Load the Tradfri platform with a mock gateway."""
- def request_config(_, callback, description, submit_caption, fields):
- """Mock request_config."""
- hass.async_add_job(callback, {'security_code': 'mock'})
-
- if known_hosts is None:
- known_hosts = {}
-
- with patch('pytradfri.api.aiocoap_api.APIFactory.generate_psk',
- generate_psk), \
- patch('pytradfri.api.aiocoap_api.APIFactory.request', mock_api), \
- patch('pytradfri.Gateway', return_value=mock_gateway), \
- patch.object(tradfri, 'load_json', return_value=known_hosts), \
- patch.object(tradfri, 'save_json'), \
- patch.object(hass.components.configurator, 'request_config',
- request_config):
-
- await async_setup_component(hass, tradfri.DOMAIN,
- {
- tradfri.DOMAIN: {
- 'host': 'mock-host',
- 'allow_tradfri_groups': True
- }
- })
- await hass.async_block_till_done()
-
-
-async def test_setup_gateway(hass, mock_gateway, mock_api):
- """Test that the gateway can be setup without errors."""
- await setup_gateway(hass, mock_gateway, mock_api)
-
-
-async def test_setup_gateway_known_host(hass, mock_gateway, mock_api):
- """Test gateway setup with a known host."""
- await setup_gateway(hass, mock_gateway, mock_api,
- known_hosts={
- 'mock-host': {
- 'identity': 'mock',
- 'key': 'mock-key'
- }
- })
-
-
-async def test_incorrect_security_code(hass, mock_gateway, mock_api):
- """Test that an error is shown if the security code is incorrect."""
- async def psk_error(self, code):
- """Raise RequestError when called."""
- raise RequestError
-
- with patch.object(hass.components.configurator, 'async_notify_errors') \
- as notify_error:
- await setup_gateway(hass, mock_gateway, mock_api,
- generate_psk=psk_error)
- assert len(notify_error.mock_calls) > 0
+ entry = MockConfigEntry(domain=tradfri.DOMAIN, data={
+ 'host': 'mock-host',
+ 'identity': 'mock-identity',
+ 'key': 'mock-key',
+ 'import_groups': True,
+ 'gateway_id': 'mock-gateway-id',
+ })
+ hass.data[tradfri.KEY_GATEWAY] = {entry.entry_id: mock_gateway}
+ hass.data[tradfri.KEY_API] = {entry.entry_id: mock_api}
+ await hass.config_entries.async_forward_entry_setup(
+ entry, 'light'
+ )
def mock_light(test_features={}, test_state={}, n=0):
diff --git a/tests/components/light/test_zwave.py b/tests/components/light/test_zwave.py
index 62bcf834b98..5805c8eb2fb 100644
--- a/tests/components/light/test_zwave.py
+++ b/tests/components/light/test_zwave.py
@@ -105,6 +105,26 @@ def test_dimmer_turn_on(mock_openzwave):
assert entity_id == device.entity_id
+def test_dimmer_min_brightness(mock_openzwave):
+ """Test turning on a dimmable Z-Wave light to its minimum brightness."""
+ node = MockNode()
+ value = MockValue(data=0, node=node)
+ values = MockLightValues(primary=value)
+ device = zwave.get_device(node=node, values=values, node_config={})
+
+ assert not device.is_on
+
+ device.turn_on(**{ATTR_BRIGHTNESS: 1})
+
+ assert device.is_on
+ assert device.brightness == 1
+
+ device.turn_on(**{ATTR_BRIGHTNESS: 0})
+
+ assert device.is_on
+ assert device.brightness == 0
+
+
def test_dimmer_transitions(mock_openzwave):
"""Test dimming transition on a dimmable Z-Wave light."""
node = MockNode()
diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py
index 3377fcefcf5..2c69a5effa7 100644
--- a/tests/components/mailbox/test_init.py
+++ b/tests/components/mailbox/test_init.py
@@ -29,7 +29,7 @@ def test_get_platforms_from_mailbox(mock_http_client):
req = yield from mock_http_client.get(url)
assert req.status == 200
result = yield from req.json()
- assert len(result) == 1 and "DemoMailbox" in result
+ assert len(result) == 1 and "DemoMailbox" == result[0].get('name', None)
@asyncio.coroutine
diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py
index 7345fd0c158..8fd1ae18841 100644
--- a/tests/components/media_player/test_cast.py
+++ b/tests/components/media_player/test_cast.py
@@ -415,3 +415,23 @@ async def test_entry_setup_list_config(hass: HomeAssistantType):
assert len(mock_setup.mock_calls) == 2
assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'}
assert mock_setup.mock_calls[1][1][1] == {'host': 'blu'}
+
+
+async def test_entry_setup_platform_not_ready(hass: HomeAssistantType):
+ """Test failed setting up entry will raise PlatformNotReady."""
+ await async_setup_component(hass, 'cast', {
+ 'cast': {
+ 'media_player': {
+ 'host': 'bla'
+ }
+ }
+ })
+
+ with patch(
+ 'homeassistant.components.media_player.cast._async_setup_platform',
+ return_value=mock_coro(exception=Exception)) as mock_setup:
+ with pytest.raises(PlatformNotReady):
+ await cast.async_setup_entry(hass, MockConfigEntry(), None)
+
+ assert len(mock_setup.mock_calls) == 1
+ assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'}
diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py
index 5a845738fa3..cb3da3ab899 100644
--- a/tests/components/media_player/test_sonos.py
+++ b/tests/components/media_player/test_sonos.py
@@ -2,10 +2,10 @@
import datetime
import socket
import unittest
-import soco.snapshot
+import pysonos.snapshot
from unittest import mock
-import soco
-from soco import alarms
+import pysonos
+from pysonos import alarms
from homeassistant.setup import setup_component
from homeassistant.components.media_player import sonos, DOMAIN
@@ -17,16 +17,16 @@ from tests.common import get_test_home_assistant
ENTITY_ID = 'media_player.kitchen'
-class socoDiscoverMock():
- """Mock class for the soco.discover method."""
+class pysonosDiscoverMock():
+ """Mock class for the pysonos.discover method."""
def discover(interface_addr):
- """Return tuple of soco.SoCo objects representing found speakers."""
+ """Return tuple of pysonos.SoCo objects representing found speakers."""
return {SoCoMock('192.0.2.1')}
class AvTransportMock():
- """Mock class for the avTransport property on soco.SoCo object."""
+ """Mock class for the avTransport property on pysonos.SoCo object."""
def __init__(self):
"""Initialize ethe Transport mock."""
@@ -41,7 +41,7 @@ class AvTransportMock():
class MusicLibraryMock():
- """Mock class for the music_library property on soco.SoCo object."""
+ """Mock class for the music_library property on pysonos.SoCo object."""
def get_sonos_favorites(self):
"""Return favorites."""
@@ -49,10 +49,10 @@ class MusicLibraryMock():
class SoCoMock():
- """Mock class for the soco.SoCo object."""
+ """Mock class for the pysonos.SoCo object."""
def __init__(self, ip):
- """Initialize soco object."""
+ """Initialize SoCo object."""
self.ip_address = ip
self.is_visible = True
self.volume = 50
@@ -153,7 +153,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.SonosDevice.available = self.real_available
self.hass.stop()
- @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_discovery(self, *args):
"""Test a single device using the autodiscovery provided by HASS."""
@@ -165,9 +165,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
self.assertEqual(len(devices), 1)
self.assertEqual(devices[0].name, 'Kitchen')
- @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
- @mock.patch('soco.discover')
+ @mock.patch('pysonos.discover')
def test_ensure_setup_config_interface_addr(self, discover_mock, *args):
"""Test an interface address config'd by the HASS config file."""
discover_mock.return_value = {SoCoMock('192.0.2.1')}
@@ -184,7 +184,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1)
self.assertEqual(discover_mock.call_count, 1)
- @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_config_hosts_string_single(self, *args):
"""Test a single address config'd by the HASS config file."""
@@ -201,7 +201,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
self.assertEqual(len(devices), 1)
self.assertEqual(devices[0].name, 'Kitchen')
- @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_config_hosts_string_multiple(self, *args):
"""Test multiple address string config'd by the HASS config file."""
@@ -218,7 +218,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
self.assertEqual(len(devices), 2)
self.assertEqual(devices[0].name, 'Kitchen')
- @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_config_hosts_list(self, *args):
"""Test a multiple address list config'd by the HASS config file."""
@@ -235,8 +235,8 @@ class TestSonosMediaPlayer(unittest.TestCase):
self.assertEqual(len(devices), 2)
self.assertEqual(devices[0].name, 'Kitchen')
- @mock.patch('soco.SoCo', new=SoCoMock)
- @mock.patch.object(soco, 'discover', new=socoDiscoverMock.discover)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
+ @mock.patch.object(pysonos, 'discover', new=pysonosDiscoverMock.discover)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_sonos_discovery(self, *args):
"""Test a single device using the autodiscovery provided by Sonos."""
@@ -245,11 +245,11 @@ class TestSonosMediaPlayer(unittest.TestCase):
self.assertEqual(len(devices), 1)
self.assertEqual(devices[0].name, 'Kitchen')
- @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@mock.patch.object(SoCoMock, 'set_sleep_timer')
def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args):
- """Ensuring soco methods called for sonos_set_sleep_timer service."""
+ """Ensure pysonos methods called for sonos_set_sleep_timer service."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
@@ -259,11 +259,11 @@ class TestSonosMediaPlayer(unittest.TestCase):
device.set_sleep_timer(30)
set_sleep_timerMock.assert_called_once_with(30)
- @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@mock.patch.object(SoCoMock, 'set_sleep_timer')
def test_sonos_clear_sleep_timer(self, set_sleep_timerMock, *args):
- """Ensuring soco methods called for sonos_clear_sleep_timer service."""
+ """Ensure pysonos method called for sonos_clear_sleep_timer service."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
@@ -273,20 +273,20 @@ class TestSonosMediaPlayer(unittest.TestCase):
device.set_sleep_timer(None)
set_sleep_timerMock.assert_called_once_with(None)
- @mock.patch('soco.SoCo', new=SoCoMock)
- @mock.patch('soco.alarms.Alarm')
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.alarms.Alarm')
@mock.patch('socket.create_connection', side_effect=socket.error())
- def test_set_alarm(self, soco_mock, alarm_mock, *args):
- """Ensuring soco methods called for sonos_set_sleep_timer service."""
+ def test_set_alarm(self, pysonos_mock, alarm_mock, *args):
+ """Ensure pysonos methods called for sonos_set_sleep_timer service."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1]
device.hass = self.hass
- alarm1 = alarms.Alarm(soco_mock)
+ alarm1 = alarms.Alarm(pysonos_mock)
alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False,
include_linked_zones=False, volume=100)
- with mock.patch('soco.alarms.get_alarms', return_value=[alarm1]):
+ with mock.patch('pysonos.alarms.get_alarms', return_value=[alarm1]):
attrs = {
'time': datetime.time(12, 00),
'enabled': True,
@@ -303,11 +303,11 @@ class TestSonosMediaPlayer(unittest.TestCase):
self.assertEqual(alarm1.volume, 30)
alarm1.save.assert_called_once_with()
- @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
- @mock.patch.object(soco.snapshot.Snapshot, 'snapshot')
+ @mock.patch.object(pysonos.snapshot.Snapshot, 'snapshot')
def test_sonos_snapshot(self, snapshotMock, *args):
- """Ensuring soco methods called for sonos_snapshot service."""
+ """Ensure pysonos methods called for sonos_snapshot service."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
@@ -319,12 +319,12 @@ class TestSonosMediaPlayer(unittest.TestCase):
self.assertEqual(snapshotMock.call_count, 1)
self.assertEqual(snapshotMock.call_args, mock.call())
- @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
- @mock.patch.object(soco.snapshot.Snapshot, 'restore')
+ @mock.patch.object(pysonos.snapshot.Snapshot, 'restore')
def test_sonos_restore(self, restoreMock, *args):
- """Ensuring soco methods called for sonos_restor service."""
- from soco.snapshot import Snapshot
+ """Ensure pysonos methods called for sonos_restore service."""
+ from pysonos.snapshot import Snapshot
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py
new file mode 100644
index 00000000000..9f6be60c68b
--- /dev/null
+++ b/tests/components/mqtt/test_config_flow.py
@@ -0,0 +1,90 @@
+"""Test config flow."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.setup import async_setup_component
+
+from tests.common import mock_coro
+
+
+@pytest.fixture(autouse=True)
+def mock_finish_setup():
+ """Mock out the finish setup method."""
+ with patch('homeassistant.components.mqtt.MQTT.async_connect',
+ return_value=mock_coro(True)) as mock_finish:
+ yield mock_finish
+
+
+@pytest.fixture
+def mock_try_connection():
+ """Mock the try connection method."""
+ with patch(
+ 'homeassistant.components.mqtt.config_flow.try_connection'
+ ) as mock_try:
+ yield mock_try
+
+
+async def test_user_connection_works(hass, mock_try_connection,
+ mock_finish_setup):
+ """Test we can finish a config flow."""
+ mock_try_connection.return_value = True
+
+ result = await hass.config_entries.flow.async_init(
+ 'mqtt', context={'source': 'user'})
+ assert result['type'] == 'form'
+
+ result = await hass.config_entries.flow.async_configure(
+ result['flow_id'], {
+ 'broker': '127.0.0.1',
+ }
+ )
+
+ assert result['type'] == 'create_entry'
+ assert result['result'].data == {
+ 'broker': '127.0.0.1',
+ 'port': 1883,
+ 'discovery': False,
+ }
+ # Check we tried the connection
+ assert len(mock_try_connection.mock_calls) == 1
+ # Check config entry got setup
+ assert len(mock_finish_setup.mock_calls) == 1
+
+
+async def test_user_connection_fails(hass, mock_try_connection,
+ mock_finish_setup):
+ """Test if connnection cannot be made."""
+ mock_try_connection.return_value = False
+
+ result = await hass.config_entries.flow.async_init(
+ 'mqtt', context={'source': 'user'})
+ assert result['type'] == 'form'
+
+ result = await hass.config_entries.flow.async_configure(
+ result['flow_id'], {
+ 'broker': '127.0.0.1',
+ }
+ )
+
+ assert result['type'] == 'form'
+ assert result['errors']['base'] == 'cannot_connect'
+
+ # Check we tried the connection
+ assert len(mock_try_connection.mock_calls) == 1
+ # Check config entry did not setup
+ assert len(mock_finish_setup.mock_calls) == 0
+
+
+async def test_manual_config_set(hass, mock_try_connection,
+ mock_finish_setup):
+ """Test we ignore entry if manual config available."""
+ assert await async_setup_component(
+ hass, 'mqtt', {'mqtt': {'broker': 'bla'}})
+ assert len(mock_finish_setup.mock_calls) == 1
+
+ mock_try_connection.return_value = True
+
+ result = await hass.config_entries.flow.async_init(
+ 'mqtt', context={'source': 'user'})
+ assert result['type'] == 'abort'
diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py
index 9e0ef14a3fa..6de277eb48d 100644
--- a/tests/components/mqtt/test_discovery.py
+++ b/tests/components/mqtt/test_discovery.py
@@ -181,3 +181,31 @@ def test_non_duplicate_discovery(hass, mqtt_mock, caplog):
assert state_duplicate is None
assert 'Component has already been discovered: ' \
'binary_sensor bla' in caplog.text
+
+
+@asyncio.coroutine
+def test_discovery_removal(hass, mqtt_mock, caplog):
+ """Test expansion of abbreviated discovery payload."""
+ yield from async_start(hass, 'homeassistant', {})
+
+ data = (
+ '{ "name": "Beer",'
+ ' "status_topic": "test_topic",'
+ ' "command_topic": "test_topic" }'
+ )
+
+ async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config',
+ data)
+ yield from hass.async_block_till_done()
+
+ state = hass.states.get('switch.beer')
+ assert state is not None
+ assert state.name == 'Beer'
+
+ async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config',
+ '')
+ yield from hass.async_block_till_done()
+ yield from hass.async_block_till_done()
+
+ state = hass.states.get('switch.beer')
+ assert state is None
diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py
index 51bd75f66e3..831bcaa1d24 100644
--- a/tests/components/mqtt/test_init.py
+++ b/tests/components/mqtt/test_init.py
@@ -2,21 +2,29 @@
import asyncio
import unittest
from unittest import mock
-import socket
import ssl
+import pytest
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.setup import async_setup_component
-import homeassistant.components.mqtt as mqtt
+from homeassistant.components import mqtt
from homeassistant.const import (EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE,
EVENT_HOMEASSISTANT_STOP)
from tests.common import (get_test_home_assistant, mock_coro,
mock_mqtt_component,
threadsafe_coroutine_factory, fire_mqtt_message,
- async_fire_mqtt_message)
+ async_fire_mqtt_message, MockConfigEntry)
+
+
+@pytest.fixture
+def mock_MQTT():
+ """Make sure connection is established."""
+ with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
+ mock_MQTT.return_value.async_connect.return_value = mock_coro(True)
+ yield mock_MQTT
@asyncio.coroutine
@@ -533,64 +541,59 @@ def test_setup_embedded_with_embedded(hass):
assert _start.call_count == 1
-@asyncio.coroutine
-def test_setup_fails_if_no_connect_broker(hass):
+async def test_setup_fails_if_no_connect_broker(hass):
"""Test for setup failure if connection to broker is missing."""
- test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker'}}
-
- with mock.patch('homeassistant.components.mqtt.MQTT',
- side_effect=socket.error()):
- result = yield from async_setup_component(hass, mqtt.DOMAIN,
- test_broker_cfg)
- assert not result
+ entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
+ mqtt.CONF_BROKER: 'test-broker'
+ })
with mock.patch('paho.mqtt.client.Client') as mock_client:
mock_client().connect = lambda *args: 1
- result = yield from async_setup_component(hass, mqtt.DOMAIN,
- test_broker_cfg)
- assert not result
+ assert not await mqtt.async_setup_entry(hass, entry)
-@asyncio.coroutine
-def test_setup_uses_certificate_on_certificate_set_to_auto(hass):
+async def test_setup_uses_certificate_on_certificate_set_to_auto(
+ hass, mock_MQTT):
"""Test setup uses bundled certs when certificate is set to auto."""
- test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker',
- 'certificate': 'auto'}}
+ entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
+ mqtt.CONF_BROKER: 'test-broker',
+ 'certificate': 'auto'
+ })
- with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
- yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
+ assert await mqtt.async_setup_entry(hass, entry)
assert mock_MQTT.called
import requests.certs
expectedCertificate = requests.certs.where()
- assert mock_MQTT.mock_calls[0][1][7] == expectedCertificate
+ assert mock_MQTT.mock_calls[0][2]['certificate'] == expectedCertificate
-@asyncio.coroutine
-def test_setup_does_not_use_certificate_on_mqtts_port(hass):
- """Test setup doesn't use bundled certs when certificate is not set."""
- test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker',
- 'port': 8883}}
+async def test_setup_does_not_use_certificate_on_mqtts_port(hass, mock_MQTT):
+ """Test setup doesn't use bundled certs when ssl set."""
+ entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
+ mqtt.CONF_BROKER: 'test-broker',
+ 'port': 8883
+ })
- with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
- yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
+ assert await mqtt.async_setup_entry(hass, entry)
assert mock_MQTT.called
- assert mock_MQTT.mock_calls[0][1][2] == 8883
+ assert mock_MQTT.mock_calls[0][2]['port'] == 8883
import requests.certs
mqttsCertificateBundle = requests.certs.where()
- assert mock_MQTT.mock_calls[0][1][7] != mqttsCertificateBundle
+ assert mock_MQTT.mock_calls[0][2]['port'] != mqttsCertificateBundle
-@asyncio.coroutine
-def test_setup_without_tls_config_uses_tlsv1_under_python36(hass):
+async def test_setup_without_tls_config_uses_tlsv1_under_python36(
+ hass, mock_MQTT):
"""Test setup defaults to TLSv1 under python3.6."""
- test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker'}}
+ entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
+ mqtt.CONF_BROKER: 'test-broker',
+ })
- with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
- yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
+ assert await mqtt.async_setup_entry(hass, entry)
assert mock_MQTT.called
@@ -600,34 +603,35 @@ def test_setup_without_tls_config_uses_tlsv1_under_python36(hass):
else:
expectedTlsVersion = ssl.PROTOCOL_TLSv1
- assert mock_MQTT.mock_calls[0][1][14] == expectedTlsVersion
+ assert mock_MQTT.mock_calls[0][2]['tls_version'] == expectedTlsVersion
-@asyncio.coroutine
-def test_setup_with_tls_config_uses_tls_version1_2(hass):
+async def test_setup_with_tls_config_uses_tls_version1_2(hass, mock_MQTT):
"""Test setup uses specified TLS version."""
- test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker',
- 'tls_version': '1.2'}}
+ entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
+ mqtt.CONF_BROKER: 'test-broker',
+ 'tls_version': '1.2'
+ })
- with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
- yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
+ assert await mqtt.async_setup_entry(hass, entry)
assert mock_MQTT.called
- assert mock_MQTT.mock_calls[0][1][14] == ssl.PROTOCOL_TLSv1_2
+ assert mock_MQTT.mock_calls[0][2]['tls_version'] == ssl.PROTOCOL_TLSv1_2
-@asyncio.coroutine
-def test_setup_with_tls_config_of_v1_under_python36_only_uses_v1(hass):
+async def test_setup_with_tls_config_of_v1_under_python36_only_uses_v1(
+ hass, mock_MQTT):
"""Test setup uses TLSv1.0 if explicitly chosen."""
- test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker',
- 'tls_version': '1.0'}}
+ entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
+ mqtt.CONF_BROKER: 'test-broker',
+ 'tls_version': '1.0'
+ })
- with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
- yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
+ assert await mqtt.async_setup_entry(hass, entry)
assert mock_MQTT.called
- assert mock_MQTT.mock_calls[0][1][14] == ssl.PROTOCOL_TLSv1
+ assert mock_MQTT.mock_calls[0][2]['tls_version'] == ssl.PROTOCOL_TLSv1
@asyncio.coroutine
@@ -671,3 +675,8 @@ def test_mqtt_subscribes_topics_on_connect(hass):
}
calls = {call[1][1]: call[1][2] for call in hass.add_job.mock_calls}
assert calls == expected
+
+
+async def test_setup_fails_without_config(hass):
+ """Test if the MQTT component fails to load with no config."""
+ assert not await async_setup_component(hass, mqtt.DOMAIN, {})
diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py
index 976fdd3d15c..9f80f753690 100644
--- a/tests/components/mqtt/test_server.py
+++ b/tests/components/mqtt/test_server.py
@@ -57,8 +57,8 @@ class TestMQTT:
assert mock_mqtt.called
from pprint import pprint
pprint(mock_mqtt.mock_calls)
- assert mock_mqtt.mock_calls[1][1][5] == 'homeassistant'
- assert mock_mqtt.mock_calls[1][1][6] == password
+ assert mock_mqtt.mock_calls[1][2]['username'] == 'homeassistant'
+ assert mock_mqtt.mock_calls[1][2]['password'] == password
@patch('passlib.apps.custom_app_context', Mock(return_value=''))
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
@@ -82,24 +82,8 @@ class TestMQTT:
assert mock_mqtt.called
from pprint import pprint
pprint(mock_mqtt.mock_calls)
- assert mock_mqtt.mock_calls[1][1][5] == 'homeassistant'
- assert mock_mqtt.mock_calls[1][1][6] == password
-
- @patch('passlib.apps.custom_app_context', Mock(return_value=''))
- @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
- @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock()))
- @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro()))
- @patch('homeassistant.components.mqtt.MQTT')
- def test_creating_config_without_pass(self, mock_mqtt):
- """Test if the MQTT server gets started without password."""
- mock_mqtt().async_connect.return_value = mock_coro(True)
- self.hass.bus.listen_once = MagicMock()
-
- self.hass.config.api = MagicMock(api_password=None)
- assert setup_component(self.hass, mqtt.DOMAIN, {})
- assert mock_mqtt.called
- assert mock_mqtt.mock_calls[1][1][5] is None
- assert mock_mqtt.mock_calls[1][1][6] is None
+ assert mock_mqtt.mock_calls[1][2]['username'] == 'homeassistant'
+ assert mock_mqtt.mock_calls[1][2]['password'] == password
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
@patch('hbmqtt.broker.Broker.start', return_value=mock_coro())
diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py
index a609247b839..6acc796a108 100644
--- a/tests/components/persistent_notification/test_init.py
+++ b/tests/components/persistent_notification/test_init.py
@@ -1,5 +1,6 @@
"""The tests for the persistent notification component."""
-from homeassistant.setup import setup_component
+from homeassistant.components import websocket_api
+from homeassistant.setup import setup_component, async_setup_component
import homeassistant.components.persistent_notification as pn
from tests.common import get_test_home_assistant
@@ -19,7 +20,9 @@ class TestPersistentNotification:
def test_create(self):
"""Test creating notification without title or notification id."""
+ notifications = self.hass.data[pn.DOMAIN]['notifications']
assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+ assert len(notifications) == 0
pn.create(self.hass, 'Hello World {{ 1 + 1 }}',
title='{{ 1 + 1 }} beers')
@@ -27,54 +30,170 @@ class TestPersistentNotification:
entity_ids = self.hass.states.entity_ids(pn.DOMAIN)
assert len(entity_ids) == 1
+ assert len(notifications) == 1
state = self.hass.states.get(entity_ids[0])
assert state.state == pn.STATE
assert state.attributes.get('message') == 'Hello World 2'
assert state.attributes.get('title') == '2 beers'
+ notification = notifications.get(entity_ids[0])
+ assert notification['status'] == pn.STATUS_UNREAD
+ assert notification['message'] == 'Hello World 2'
+ assert notification['title'] == '2 beers'
+ notifications.clear()
+
def test_create_notification_id(self):
"""Ensure overwrites existing notification with same id."""
+ notifications = self.hass.data[pn.DOMAIN]['notifications']
assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+ assert len(notifications) == 0
pn.create(self.hass, 'test', notification_id='Beer 2')
self.hass.block_till_done()
assert len(self.hass.states.entity_ids()) == 1
- state = self.hass.states.get('persistent_notification.beer_2')
+ assert len(notifications) == 1
+
+ entity_id = 'persistent_notification.beer_2'
+ state = self.hass.states.get(entity_id)
assert state.attributes.get('message') == 'test'
+ notification = notifications.get(entity_id)
+ assert notification['message'] == 'test'
+ assert notification['title'] is None
+
pn.create(self.hass, 'test 2', notification_id='Beer 2')
self.hass.block_till_done()
# We should have overwritten old one
assert len(self.hass.states.entity_ids()) == 1
- state = self.hass.states.get('persistent_notification.beer_2')
+ state = self.hass.states.get(entity_id)
assert state.attributes.get('message') == 'test 2'
+ notification = notifications.get(entity_id)
+ assert notification['message'] == 'test 2'
+ notifications.clear()
+
def test_create_template_error(self):
"""Ensure we output templates if contain error."""
+ notifications = self.hass.data[pn.DOMAIN]['notifications']
assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+ assert len(notifications) == 0
pn.create(self.hass, '{{ message + 1 }}', '{{ title + 1 }}')
self.hass.block_till_done()
entity_ids = self.hass.states.entity_ids(pn.DOMAIN)
assert len(entity_ids) == 1
+ assert len(notifications) == 1
state = self.hass.states.get(entity_ids[0])
assert state.attributes.get('message') == '{{ message + 1 }}'
assert state.attributes.get('title') == '{{ title + 1 }}'
+ notification = notifications.get(entity_ids[0])
+ assert notification['message'] == '{{ message + 1 }}'
+ assert notification['title'] == '{{ title + 1 }}'
+ notifications.clear()
+
def test_dismiss_notification(self):
"""Ensure removal of specific notification."""
+ notifications = self.hass.data[pn.DOMAIN]['notifications']
assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+ assert len(notifications) == 0
pn.create(self.hass, 'test', notification_id='Beer 2')
self.hass.block_till_done()
assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 1
+ assert len(notifications) == 1
pn.dismiss(self.hass, notification_id='Beer 2')
self.hass.block_till_done()
assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+ assert len(notifications) == 0
+ notifications.clear()
+
+ def test_mark_read(self):
+ """Ensure notification is marked as Read."""
+ notifications = self.hass.data[pn.DOMAIN]['notifications']
+ assert len(notifications) == 0
+
+ pn.create(self.hass, 'test', notification_id='Beer 2')
+ self.hass.block_till_done()
+
+ entity_id = 'persistent_notification.beer_2'
+ assert len(notifications) == 1
+ notification = notifications.get(entity_id)
+ assert notification['status'] == pn.STATUS_UNREAD
+
+ self.hass.services.call(pn.DOMAIN, pn.SERVICE_MARK_READ, {
+ 'notification_id': 'Beer 2'
+ })
+ self.hass.block_till_done()
+
+ assert len(notifications) == 1
+ notification = notifications.get(entity_id)
+ assert notification['status'] == pn.STATUS_READ
+ notifications.clear()
+
+
+async def test_ws_get_notifications(hass, hass_ws_client):
+ """Test websocket endpoint for retrieving persistent notifications."""
+ await async_setup_component(hass, pn.DOMAIN, {})
+
+ client = await hass_ws_client(hass)
+
+ await client.send_json({
+ 'id': 5,
+ 'type': 'persistent_notification/get'
+ })
+ msg = await client.receive_json()
+ assert msg['id'] == 5
+ assert msg['type'] == websocket_api.TYPE_RESULT
+ assert msg['success']
+ notifications = msg['result']
+ assert len(notifications) == 0
+
+ # Create
+ hass.components.persistent_notification.async_create(
+ 'test', notification_id='Beer 2')
+ await client.send_json({
+ 'id': 6,
+ 'type': 'persistent_notification/get'
+ })
+ msg = await client.receive_json()
+ assert msg['id'] == 6
+ assert msg['type'] == websocket_api.TYPE_RESULT
+ assert msg['success']
+ notifications = msg['result']
+ assert len(notifications) == 1
+ notification = notifications[0]
+ assert notification['notification_id'] == 'Beer 2'
+ assert notification['message'] == 'test'
+ assert notification['title'] is None
+ assert notification['status'] == pn.STATUS_UNREAD
+
+ # Mark Read
+ await hass.services.async_call(pn.DOMAIN, pn.SERVICE_MARK_READ, {
+ 'notification_id': 'Beer 2'
+ })
+ await client.send_json({
+ 'id': 7,
+ 'type': 'persistent_notification/get'
+ })
+ msg = await client.receive_json()
+ notifications = msg['result']
+ assert len(notifications) == 1
+ assert notifications[0]['status'] == pn.STATUS_READ
+
+ # Dismiss
+ hass.components.persistent_notification.async_dismiss('Beer 2')
+ await client.send_json({
+ 'id': 8,
+ 'type': 'persistent_notification/get'
+ })
+ msg = await client.receive_json()
+ notifications = msg['result']
+ assert len(notifications) == 0
diff --git a/tests/components/scene/test_deconz.py b/tests/components/scene/test_deconz.py
index 8c22f718fa0..89bb5297e78 100644
--- a/tests/components/scene/test_deconz.py
+++ b/tests/components/scene/test_deconz.py
@@ -36,7 +36,8 @@ async def setup_bridge(hass, data):
hass.data[deconz.DATA_DECONZ_UNSUB] = []
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
- 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test')
+ 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test',
+ config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(config_entry, 'scene')
# To flush out the service call to update the group
await hass.async_block_till_done()
diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py
index d7cdb458646..ae9e75d6a41 100644
--- a/tests/components/sensor/test_deconz.py
+++ b/tests/components/sensor/test_deconz.py
@@ -49,6 +49,7 @@ async def setup_bridge(hass, data, allow_clip_sensor=True):
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'}
bridge = DeconzSession(loop, session, **entry.data)
+ bridge.config = Mock()
with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)):
await bridge.async_load_parameters()
@@ -58,7 +59,8 @@ async def setup_bridge(hass, data, allow_clip_sensor=True):
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title',
- {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test')
+ {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test',
+ config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(config_entry, 'sensor')
# To flush out the service call to update the group
await hass.async_block_till_done()
diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py
index e5fca461a23..9ab8d61f739 100644
--- a/tests/components/sensor/test_dsmr.py
+++ b/tests/components/sensor/test_dsmr.py
@@ -182,10 +182,14 @@ def test_reconnect(hass, monkeypatch, mock_connection_factory):
# mock waiting coroutine while connection lasts
closed = asyncio.Event(loop=hass.loop)
+ # Handshake so that `hass.async_block_till_done()` doesn't cycle forever
+ closed2 = asyncio.Event(loop=hass.loop)
@asyncio.coroutine
def wait_closed():
yield from closed.wait()
+ closed2.set()
+ closed.clear()
protocol.wait_closed = wait_closed
yield from async_setup_component(hass, 'sensor', {'sensor': config})
@@ -195,8 +199,11 @@ def test_reconnect(hass, monkeypatch, mock_connection_factory):
# indicate disconnect, release wait lock and allow reconnect to happen
closed.set()
# wait for lock set to resolve
- yield from hass.async_block_till_done()
- # wait for sleep to resolve
+ yield from closed2.wait()
+ closed2.clear()
+ assert not closed.is_set()
+
+ closed.set()
yield from hass.async_block_till_done()
assert connection_factory.call_count >= 2, \
diff --git a/tests/components/sensor/test_dyson.py b/tests/components/sensor/test_dyson.py
index 5b5be4a587d..baab96a61f0 100644
--- a/tests/components/sensor/test_dyson.py
+++ b/tests/components/sensor/test_dyson.py
@@ -70,11 +70,11 @@ class DysonTest(unittest.TestCase):
"""Test setup component with devices."""
def _add_device(devices):
assert len(devices) == 5
- assert devices[0].name == "Device_name filter life"
- assert devices[1].name == "Device_name dust"
- assert devices[2].name == "Device_name humidity"
- assert devices[3].name == "Device_name temperature"
- assert devices[4].name == "Device_name air quality"
+ assert devices[0].name == "Device_name Filter Life"
+ assert devices[1].name == "Device_name Dust"
+ assert devices[2].name == "Device_name Humidity"
+ assert devices[3].name == "Device_name Temperature"
+ assert devices[4].name == "Device_name AQI"
device_fan = _get_device_without_state()
device_non_fan = _get_with_state()
@@ -83,143 +83,147 @@ class DysonTest(unittest.TestCase):
def test_dyson_filter_life_sensor(self):
"""Test filter life sensor with no value."""
- sensor = dyson.DysonFilterLifeSensor(self.hass,
- _get_device_without_state())
+ sensor = dyson.DysonFilterLifeSensor(_get_device_without_state())
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertIsNone(sensor.state)
self.assertEqual(sensor.unit_of_measurement, "hours")
- self.assertEqual(sensor.name, "Device_name filter life")
+ self.assertEqual(sensor.name, "Device_name Filter Life")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
sensor.on_message('message')
def test_dyson_filter_life_sensor_with_values(self):
"""Test filter sensor with values."""
- sensor = dyson.DysonFilterLifeSensor(self.hass, _get_with_state())
+ sensor = dyson.DysonFilterLifeSensor(_get_with_state())
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertEqual(sensor.state, 100)
self.assertEqual(sensor.unit_of_measurement, "hours")
- self.assertEqual(sensor.name, "Device_name filter life")
+ self.assertEqual(sensor.name, "Device_name Filter Life")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
sensor.on_message('message')
def test_dyson_dust_sensor(self):
"""Test dust sensor with no value."""
- sensor = dyson.DysonDustSensor(self.hass,
- _get_device_without_state())
+ sensor = dyson.DysonDustSensor(_get_device_without_state())
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertIsNone(sensor.state)
- self.assertEqual(sensor.unit_of_measurement, 'level')
- self.assertEqual(sensor.name, "Device_name dust")
+ self.assertEqual(sensor.unit_of_measurement, None)
+ self.assertEqual(sensor.name, "Device_name Dust")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
def test_dyson_dust_sensor_with_values(self):
"""Test dust sensor with values."""
- sensor = dyson.DysonDustSensor(self.hass, _get_with_state())
+ sensor = dyson.DysonDustSensor(_get_with_state())
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertEqual(sensor.state, 5)
- self.assertEqual(sensor.unit_of_measurement, 'level')
- self.assertEqual(sensor.name, "Device_name dust")
+ self.assertEqual(sensor.unit_of_measurement, None)
+ self.assertEqual(sensor.name, "Device_name Dust")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
def test_dyson_humidity_sensor(self):
"""Test humidity sensor with no value."""
- sensor = dyson.DysonHumiditySensor(self.hass,
- _get_device_without_state())
+ sensor = dyson.DysonHumiditySensor(_get_device_without_state())
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertIsNone(sensor.state)
self.assertEqual(sensor.unit_of_measurement, '%')
- self.assertEqual(sensor.name, "Device_name humidity")
+ self.assertEqual(sensor.name, "Device_name Humidity")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
def test_dyson_humidity_sensor_with_values(self):
"""Test humidity sensor with values."""
- sensor = dyson.DysonHumiditySensor(self.hass, _get_with_state())
+ sensor = dyson.DysonHumiditySensor(_get_with_state())
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertEqual(sensor.state, 45)
self.assertEqual(sensor.unit_of_measurement, '%')
- self.assertEqual(sensor.name, "Device_name humidity")
+ self.assertEqual(sensor.name, "Device_name Humidity")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
def test_dyson_humidity_standby_monitoring(self):
"""Test humidity sensor while device is in standby monitoring."""
- sensor = dyson.DysonHumiditySensor(self.hass,
- _get_with_standby_monitoring())
+ sensor = dyson.DysonHumiditySensor(_get_with_standby_monitoring())
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertEqual(sensor.state, STATE_OFF)
self.assertEqual(sensor.unit_of_measurement, '%')
- self.assertEqual(sensor.name, "Device_name humidity")
+ self.assertEqual(sensor.name, "Device_name Humidity")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
def test_dyson_temperature_sensor(self):
"""Test temperature sensor with no value."""
- sensor = dyson.DysonTemperatureSensor(self.hass,
- _get_device_without_state(),
+ sensor = dyson.DysonTemperatureSensor(_get_device_without_state(),
TEMP_CELSIUS)
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertIsNone(sensor.state)
self.assertEqual(sensor.unit_of_measurement, '°C')
- self.assertEqual(sensor.name, "Device_name temperature")
+ self.assertEqual(sensor.name, "Device_name Temperature")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
def test_dyson_temperature_sensor_with_values(self):
"""Test temperature sensor with values."""
- sensor = dyson.DysonTemperatureSensor(self.hass,
- _get_with_state(),
+ sensor = dyson.DysonTemperatureSensor(_get_with_state(),
TEMP_CELSIUS)
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertEqual(sensor.state, 21.9)
self.assertEqual(sensor.unit_of_measurement, '°C')
- self.assertEqual(sensor.name, "Device_name temperature")
+ self.assertEqual(sensor.name, "Device_name Temperature")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
- sensor = dyson.DysonTemperatureSensor(self.hass,
- _get_with_state(),
+ sensor = dyson.DysonTemperatureSensor(_get_with_state(),
TEMP_FAHRENHEIT)
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertEqual(sensor.state, 71.3)
self.assertEqual(sensor.unit_of_measurement, '°F')
- self.assertEqual(sensor.name, "Device_name temperature")
+ self.assertEqual(sensor.name, "Device_name Temperature")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
def test_dyson_temperature_standby_monitoring(self):
"""Test temperature sensor while device is in standby monitoring."""
- sensor = dyson.DysonTemperatureSensor(self.hass,
- _get_with_standby_monitoring(),
+ sensor = dyson.DysonTemperatureSensor(_get_with_standby_monitoring(),
TEMP_CELSIUS)
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertEqual(sensor.state, STATE_OFF)
self.assertEqual(sensor.unit_of_measurement, '°C')
- self.assertEqual(sensor.name, "Device_name temperature")
+ self.assertEqual(sensor.name, "Device_name Temperature")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
def test_dyson_air_quality_sensor(self):
"""Test air quality sensor with no value."""
- sensor = dyson.DysonAirQualitySensor(self.hass,
- _get_device_without_state())
+ sensor = dyson.DysonAirQualitySensor(_get_device_without_state())
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertIsNone(sensor.state)
- self.assertEqual(sensor.unit_of_measurement, 'level')
- self.assertEqual(sensor.name, "Device_name air quality")
+ self.assertEqual(sensor.unit_of_measurement, None)
+ self.assertEqual(sensor.name, "Device_name AQI")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
def test_dyson_air_quality_sensor_with_values(self):
"""Test air quality sensor with values."""
- sensor = dyson.DysonAirQualitySensor(self.hass, _get_with_state())
+ sensor = dyson.DysonAirQualitySensor(_get_with_state())
+ sensor.hass = self.hass
sensor.entity_id = "sensor.dyson_1"
self.assertFalse(sensor.should_poll)
self.assertEqual(sensor.state, 2)
- self.assertEqual(sensor.unit_of_measurement, 'level')
- self.assertEqual(sensor.name, "Device_name air quality")
+ self.assertEqual(sensor.unit_of_measurement, None)
+ self.assertEqual(sensor.name, "Device_name AQI")
self.assertEqual(sensor.entity_id, "sensor.dyson_1")
diff --git a/tests/components/sensor/test_jewish_calendar.py b/tests/components/sensor/test_jewish_calendar.py
new file mode 100644
index 00000000000..990f26d6ea7
--- /dev/null
+++ b/tests/components/sensor/test_jewish_calendar.py
@@ -0,0 +1,120 @@
+"""The tests for the Jewish calendar sensor platform."""
+import unittest
+from datetime import datetime as dt
+from unittest.mock import patch
+
+from homeassistant.util.async_ import run_coroutine_threadsafe
+from homeassistant.setup import setup_component
+from homeassistant.components.sensor.jewish_calendar import JewishCalSensor
+from tests.common import get_test_home_assistant
+
+
+class TestJewishCalenderSensor(unittest.TestCase):
+ """Test the Jewish Calendar sensor."""
+
+ TEST_LATITUDE = 31.778
+ TEST_LONGITUDE = 35.235
+
+ def setUp(self):
+ """Set up things to run when tests begin."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ def test_jewish_calendar_min_config(self):
+ """Test minimum jewish calendar configuration."""
+ config = {
+ 'sensor': {
+ 'platform': 'jewish_calendar'
+ }
+ }
+ assert setup_component(self.hass, 'sensor', config)
+
+ def test_jewish_calendar_hebrew(self):
+ """Test jewish calendar sensor with language set to hebrew."""
+ config = {
+ 'sensor': {
+ 'platform': 'jewish_calendar',
+ 'language': 'hebrew',
+ }
+ }
+
+ assert setup_component(self.hass, 'sensor', config)
+
+ def test_jewish_calendar_multiple_sensors(self):
+ """Test jewish calendar sensor with multiple sensors setup."""
+ config = {
+ 'sensor': {
+ 'platform': 'jewish_calendar',
+ 'sensors': [
+ 'date', 'weekly_portion', 'holiday_name',
+ 'holyness', 'first_light', 'gra_end_shma',
+ 'mga_end_shma', 'plag_mincha', 'first_stars'
+ ]
+ }
+ }
+
+ assert setup_component(self.hass, 'sensor', config)
+
+ def test_jewish_calendar_sensor_date_output(self):
+ """Test Jewish calendar sensor date output."""
+ test_time = dt(2018, 9, 3)
+ sensor = JewishCalSensor(
+ name='test', language='english', sensor_type='date',
+ latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE,
+ diaspora=False)
+ with patch('homeassistant.util.dt.now', return_value=test_time):
+ run_coroutine_threadsafe(
+ sensor.async_update(),
+ self.hass.loop).result()
+ self.assertEqual(sensor.state, '23 Elul 5778')
+
+ def test_jewish_calendar_sensor_date_output_hebrew(self):
+ """Test Jewish calendar sensor date output in hebrew."""
+ test_time = dt(2018, 9, 3)
+ sensor = JewishCalSensor(
+ name='test', language='hebrew', sensor_type='date',
+ latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE,
+ diaspora=False)
+ with patch('homeassistant.util.dt.now', return_value=test_time):
+ run_coroutine_threadsafe(
+ sensor.async_update(), self.hass.loop).result()
+ self.assertEqual(sensor.state, "כ\"ג באלול ה\' תשע\"ח")
+
+ def test_jewish_calendar_sensor_holiday_name(self):
+ """Test Jewish calendar sensor date output in hebrew."""
+ test_time = dt(2018, 9, 10)
+ sensor = JewishCalSensor(
+ name='test', language='hebrew', sensor_type='holiday_name',
+ latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE,
+ diaspora=False)
+ with patch('homeassistant.util.dt.now', return_value=test_time):
+ run_coroutine_threadsafe(
+ sensor.async_update(), self.hass.loop).result()
+ self.assertEqual(sensor.state, "א\' ראש השנה")
+
+ def test_jewish_calendar_sensor_holyness(self):
+ """Test Jewish calendar sensor date output in hebrew."""
+ test_time = dt(2018, 9, 10)
+ sensor = JewishCalSensor(
+ name='test', language='hebrew', sensor_type='holyness',
+ latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE,
+ diaspora=False)
+ with patch('homeassistant.util.dt.now', return_value=test_time):
+ run_coroutine_threadsafe(
+ sensor.async_update(), self.hass.loop).result()
+ self.assertEqual(sensor.state, 1)
+
+ def test_jewish_calendar_sensor_torah_reading(self):
+ """Test Jewish calendar sensor date output in hebrew."""
+ test_time = dt(2018, 9, 8)
+ sensor = JewishCalSensor(
+ name='test', language='hebrew', sensor_type='weekly_portion',
+ latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE,
+ diaspora=False)
+ with patch('homeassistant.util.dt.now', return_value=test_time):
+ run_coroutine_threadsafe(
+ sensor.async_update(), self.hass.loop).result()
+ self.assertEqual(sensor.state, "פרשת נצבים")
diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py
index 4d40ad394cd..7f818193a29 100644
--- a/tests/components/sensor/test_rest.py
+++ b/tests/components/sensor/test_rest.py
@@ -1,11 +1,13 @@
"""The tests for the REST sensor platform."""
import unittest
+from pytest import raises
from unittest.mock import patch, Mock
import requests
from requests.exceptions import Timeout, MissingSchema, RequestException
import requests_mock
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.setup import setup_component
import homeassistant.components.sensor as sensor
import homeassistant.components.sensor.rest as rest
@@ -45,76 +47,78 @@ class TestRestSensorSetup(unittest.TestCase):
side_effect=requests.exceptions.ConnectionError())
def test_setup_failed_connect(self, mock_req):
"""Test setup when connection error occurs."""
- self.assertTrue(rest.setup_platform(self.hass, {
- 'platform': 'rest',
- 'resource': 'http://localhost',
- }, lambda devices, update=True: None) is None)
+ with raises(PlatformNotReady):
+ rest.setup_platform(self.hass, {
+ 'platform': 'rest',
+ 'resource': 'http://localhost',
+ }, lambda devices, update=True: None)
@patch('requests.Session.send', side_effect=Timeout())
def test_setup_timeout(self, mock_req):
"""Test setup when connection timeout occurs."""
- self.assertTrue(rest.setup_platform(self.hass, {
- 'platform': 'rest',
- 'resource': 'http://localhost',
- }, lambda devices, update=True: None) is None)
+ with raises(PlatformNotReady):
+ rest.setup_platform(self.hass, {
+ 'platform': 'rest',
+ 'resource': 'http://localhost',
+ }, lambda devices, update=True: None)
@requests_mock.Mocker()
def test_setup_minimum(self, mock_req):
"""Test setup with minimum configuration."""
mock_req.get('http://localhost', status_code=200)
- self.assertTrue(setup_component(self.hass, 'sensor', {
- 'sensor': {
- 'platform': 'rest',
- 'resource': 'http://localhost'
- }
- }))
+ with assert_setup_component(1, 'sensor'):
+ self.assertTrue(setup_component(self.hass, 'sensor', {
+ 'sensor': {
+ 'platform': 'rest',
+ 'resource': 'http://localhost'
+ }
+ }))
self.assertEqual(2, mock_req.call_count)
- assert_setup_component(1, 'switch')
@requests_mock.Mocker()
def test_setup_get(self, mock_req):
"""Test setup with valid configuration."""
mock_req.get('http://localhost', status_code=200)
- self.assertTrue(setup_component(self.hass, 'sensor', {
- 'sensor': {
- 'platform': 'rest',
- 'resource': 'http://localhost',
- 'method': 'GET',
- 'value_template': '{{ value_json.key }}',
- 'name': 'foo',
- 'unit_of_measurement': 'MB',
- 'verify_ssl': 'true',
- 'authentication': 'basic',
- 'username': 'my username',
- 'password': 'my password',
- 'headers': {'Accept': 'application/json'}
- }
- }))
+ with assert_setup_component(1, 'sensor'):
+ self.assertTrue(setup_component(self.hass, 'sensor', {
+ 'sensor': {
+ 'platform': 'rest',
+ 'resource': 'http://localhost',
+ 'method': 'GET',
+ 'value_template': '{{ value_json.key }}',
+ 'name': 'foo',
+ 'unit_of_measurement': 'MB',
+ 'verify_ssl': 'true',
+ 'authentication': 'basic',
+ 'username': 'my username',
+ 'password': 'my password',
+ 'headers': {'Accept': 'application/json'}
+ }
+ }))
self.assertEqual(2, mock_req.call_count)
- assert_setup_component(1, 'sensor')
@requests_mock.Mocker()
def test_setup_post(self, mock_req):
"""Test setup with valid configuration."""
mock_req.post('http://localhost', status_code=200)
- self.assertTrue(setup_component(self.hass, 'sensor', {
- 'sensor': {
- 'platform': 'rest',
- 'resource': 'http://localhost',
- 'method': 'POST',
- 'value_template': '{{ value_json.key }}',
- 'payload': '{ "device": "toaster"}',
- 'name': 'foo',
- 'unit_of_measurement': 'MB',
- 'verify_ssl': 'true',
- 'authentication': 'basic',
- 'username': 'my username',
- 'password': 'my password',
- 'headers': {'Accept': 'application/json'}
- }
- }))
+ with assert_setup_component(1, 'sensor'):
+ self.assertTrue(setup_component(self.hass, 'sensor', {
+ 'sensor': {
+ 'platform': 'rest',
+ 'resource': 'http://localhost',
+ 'method': 'POST',
+ 'value_template': '{{ value_json.key }}',
+ 'payload': '{ "device": "toaster"}',
+ 'name': 'foo',
+ 'unit_of_measurement': 'MB',
+ 'verify_ssl': 'true',
+ 'authentication': 'basic',
+ 'username': 'my username',
+ 'password': 'my password',
+ 'headers': {'Accept': 'application/json'}
+ }
+ }))
self.assertEqual(2, mock_req.call_count)
- assert_setup_component(1, 'sensor')
class TestRestSensor(unittest.TestCase):
diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py
index ab4eed31fee..8d46f4d57a3 100644
--- a/tests/components/sonos/test_init.py
+++ b/tests/components/sonos/test_init.py
@@ -12,9 +12,15 @@ async def test_creating_entry_sets_up_media_player(hass):
"""Test setting up Sonos loads the media player."""
with patch('homeassistant.components.media_player.sonos.async_setup_entry',
return_value=mock_coro(True)) as mock_setup, \
- patch('soco.discover', return_value=True):
+ patch('pysonos.discover', return_value=True):
result = await hass.config_entries.flow.async_init(
sonos.DOMAIN, context={'source': config_entries.SOURCE_USER})
+
+ # Confirmation form
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+ result = await hass.config_entries.flow.async_configure(
+ result['flow_id'], {})
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
@@ -26,7 +32,7 @@ async def test_configuring_sonos_creates_entry(hass):
"""Test that specifying config will create an entry."""
with patch('homeassistant.components.sonos.async_setup_entry',
return_value=mock_coro(True)) as mock_setup, \
- patch('soco.discover', return_value=True):
+ patch('pysonos.discover', return_value=True):
await async_setup_component(hass, sonos.DOMAIN, {
'sonos': {
'some_config': 'to_trigger_import'
@@ -41,7 +47,7 @@ async def test_not_configuring_sonos_not_creates_entry(hass):
"""Test that no config will not create an entry."""
with patch('homeassistant.components.sonos.async_setup_entry',
return_value=mock_coro(True)) as mock_setup, \
- patch('soco.discover', return_value=True):
+ patch('pysonos.discover', return_value=True):
await async_setup_component(hass, sonos.DOMAIN, {})
await hass.async_block_till_done()
diff --git a/tests/components/switch/test_deconz.py b/tests/components/switch/test_deconz.py
index 57fc8b3bcd9..6833cab33d7 100644
--- a/tests/components/switch/test_deconz.py
+++ b/tests/components/switch/test_deconz.py
@@ -47,6 +47,7 @@ async def setup_bridge(hass, data):
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'}
bridge = DeconzSession(loop, session, **entry.data)
+ bridge.config = Mock()
with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)):
await bridge.async_load_parameters()
@@ -54,7 +55,8 @@ async def setup_bridge(hass, data):
hass.data[deconz.DATA_DECONZ_UNSUB] = []
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
- 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test')
+ 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test',
+ config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(config_entry, 'switch')
# To flush out the service call to update the group
await hass.async_block_till_done()
@@ -69,7 +71,7 @@ async def test_no_switches(hass):
async def test_switch(hass):
- """Test that all supported switch entities and switch group are created."""
+ """Test that all supported switch entities are created."""
await setup_bridge(hass, {"lights": SUPPORTED_SWITCHES})
assert "switch.switch_1_name" in hass.data[deconz.DATA_DECONZ_ID]
assert "switch.switch_2_name" in hass.data[deconz.DATA_DECONZ_ID]
diff --git a/tests/components/test_spc.py b/tests/components/test_spc.py
index 7837abd8007..d4bedda4e96 100644
--- a/tests/components/test_spc.py
+++ b/tests/components/test_spc.py
@@ -1,167 +1,74 @@
"""Tests for Vanderbilt SPC component."""
-import asyncio
+from unittest.mock import patch, PropertyMock, Mock
-import pytest
-
-from homeassistant.components import spc
from homeassistant.bootstrap import async_setup_component
-from tests.common import async_test_home_assistant
-from tests.test_util.aiohttp import mock_aiohttp_client
-from homeassistant.const import (
- STATE_ON, STATE_OFF, STATE_ALARM_ARMED_AWAY,
- STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
+from homeassistant.components.spc import DATA_API
+from homeassistant.const import (STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED)
+
+from tests.common import mock_coro
-@pytest.fixture
-def hass(loop):
- """Home Assistant fixture with device mapping registry."""
- hass = loop.run_until_complete(async_test_home_assistant(loop))
- hass.data[spc.DATA_REGISTRY] = spc.SpcRegistry()
- hass.data[spc.DATA_API] = None
- yield hass
- loop.run_until_complete(hass.async_stop())
-
-
-@pytest.fixture
-def spcwebgw(hass):
- """Fixture for the SPC Web Gateway API configured for localhost."""
- yield spc.SpcWebGateway(hass=hass,
- api_url='http://localhost/',
- ws_url='ws://localhost/')
-
-
-@pytest.fixture
-def aioclient_mock():
- """HTTP client mock for areas and zones."""
- areas = """{"status":"success","data":{"area":[{"id":"1","name":"House",
- "mode":"0","last_set_time":"1485759851","last_set_user_id":"1",
- "last_set_user_name":"Pelle","last_unset_time":"1485800564",
- "last_unset_user_id":"1","last_unset_user_name":"Pelle","last_alarm":
- "1478174896"},{"id":"3","name":"Garage","mode":"0","last_set_time":
- "1483705803","last_set_user_id":"9998","last_set_user_name":"Lisa",
- "last_unset_time":"1483705808","last_unset_user_id":"9998",
- "last_unset_user_name":"Lisa"}]}}"""
-
- zones = """{"status":"success","data":{"zone":[{"id":"1","type":"3",
- "zone_name":"Kitchen smoke","area":"1","area_name":"House","input":"0",
- "logic_input":"0","status":"0","proc_state":"0","inhibit_allowed":"1",
- "isolate_allowed":"1"},{"id":"3","type":"0","zone_name":"Hallway PIR",
- "area":"1","area_name":"House","input":"0","logic_input":"0","status":
- "0","proc_state":"0","inhibit_allowed":"1","isolate_allowed":"1"},
- {"id":"5","type":"1","zone_name":"Front door","area":"1","area_name":
- "House","input":"1","logic_input":"0","status":"0","proc_state":"0",
- "inhibit_allowed":"1","isolate_allowed":"1"}]}}"""
-
- with mock_aiohttp_client() as mock_session:
- mock_session.get('http://localhost/spc/area', text=areas)
- mock_session.get('http://localhost/spc/zone', text=zones)
- yield mock_session
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize("sia_code,state", [
- ('NL', STATE_ALARM_ARMED_HOME),
- ('CG', STATE_ALARM_ARMED_AWAY),
- ('OG', STATE_ALARM_DISARMED)
-])
-def test_update_alarm_device(hass, aioclient_mock, monkeypatch,
- sia_code, state):
- """Test that alarm panel state changes on incoming websocket data."""
- monkeypatch.setattr("homeassistant.components.spc.SpcWebGateway."
- "start_listener", lambda x, *args: None)
+async def test_valid_device_config(hass, monkeypatch):
+ """Test valid device config."""
config = {
'spc': {
'api_url': 'http://localhost/',
'ws_url': 'ws://localhost/'
}
}
- yield from async_setup_component(hass, 'spc', config)
- yield from hass.async_block_till_done()
+ with patch('pyspcwebgw.SpcWebGateway.async_load_parameters',
+ return_value=mock_coro(True)):
+ assert await async_setup_component(hass, 'spc', config) is True
+
+
+async def test_invalid_device_config(hass, monkeypatch):
+ """Test valid device config."""
+ config = {
+ 'spc': {
+ 'api_url': 'http://localhost/'
+ }
+ }
+
+ with patch('pyspcwebgw.SpcWebGateway.async_load_parameters',
+ return_value=mock_coro(True)):
+ assert await async_setup_component(hass, 'spc', config) is False
+
+
+async def test_update_alarm_device(hass):
+ """Test that alarm panel state changes on incoming websocket data."""
+ import pyspcwebgw
+ from pyspcwebgw.const import AreaMode
+
+ config = {
+ 'spc': {
+ 'api_url': 'http://localhost/',
+ 'ws_url': 'ws://localhost/'
+ }
+ }
+
+ area_mock = Mock(spec=pyspcwebgw.area.Area, id='1',
+ mode=AreaMode.FULL_SET, last_changed_by='Sven')
+ area_mock.name = 'House'
+ area_mock.verified_alarm = False
+
+ with patch('pyspcwebgw.SpcWebGateway.areas',
+ new_callable=PropertyMock) as mock_areas:
+ mock_areas.return_value = {'1': area_mock}
+ with patch('pyspcwebgw.SpcWebGateway.async_load_parameters',
+ return_value=mock_coro(True)):
+ assert await async_setup_component(hass, 'spc', config) is True
+
+ await hass.async_block_till_done()
entity_id = 'alarm_control_panel.house'
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
+ assert hass.states.get(entity_id).attributes['changed_by'] == 'Sven'
+
+ area_mock.mode = AreaMode.UNSET
+ area_mock.last_changed_by = 'Anna'
+ await hass.data[DATA_API]._async_callback(area_mock)
+ await hass.async_block_till_done()
+
assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
-
- msg = {"sia_code": sia_code, "sia_address": "1",
- "description": "House¦Sam¦1"}
- yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY])
- yield from hass.async_block_till_done()
-
- state_obj = hass.states.get(entity_id)
- assert state_obj.state == state
- assert state_obj.attributes['changed_by'] == 'Sam'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize("sia_code,state", [
- ('ZO', STATE_ON),
- ('ZC', STATE_OFF)
-])
-def test_update_sensor_device(hass, aioclient_mock, monkeypatch,
- sia_code, state):
- """
- Test that sensors change state on incoming websocket data.
-
- Note that we don't test for the ZD (disconnected) and ZX (problem/short)
- codes since the binary sensor component is hardcoded to only
- let on/off states through.
- """
- monkeypatch.setattr("homeassistant.components.spc.SpcWebGateway."
- "start_listener", lambda x, *args: None)
- config = {
- 'spc': {
- 'api_url': 'http://localhost/',
- 'ws_url': 'ws://localhost/'
- }
- }
- yield from async_setup_component(hass, 'spc', config)
- yield from hass.async_block_till_done()
-
- assert hass.states.get('binary_sensor.hallway_pir').state == STATE_OFF
-
- msg = {"sia_code": sia_code, "sia_address": "3",
- "description": "Hallway PIR"}
- yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY])
- yield from hass.async_block_till_done()
- assert hass.states.get('binary_sensor.hallway_pir').state == state
-
-
-class TestSpcRegistry:
- """Test the device mapping registry."""
-
- def test_sensor_device(self):
- """Test retrieving device based on ID."""
- r = spc.SpcRegistry()
- r.register_sensor_device('1', 'dummy')
- assert r.get_sensor_device('1') == 'dummy'
-
- def test_alarm_device(self):
- """Test retrieving device based on zone name."""
- r = spc.SpcRegistry()
- r.register_alarm_device('Area 51', 'dummy')
- assert r.get_alarm_device('Area 51') == 'dummy'
-
-
-class TestSpcWebGateway:
- """Test the SPC Web Gateway API wrapper."""
-
- @asyncio.coroutine
- def test_get_areas(self, spcwebgw, aioclient_mock):
- """Test area retrieval."""
- result = yield from spcwebgw.get_areas()
- assert aioclient_mock.call_count == 1
- assert len(list(result)) == 2
-
- @asyncio.coroutine
- @pytest.mark.parametrize("url_command,command", [
- ('set', spc.SpcWebGateway.AREA_COMMAND_SET),
- ('unset', spc.SpcWebGateway.AREA_COMMAND_UNSET),
- ('set_a', spc.SpcWebGateway.AREA_COMMAND_PART_SET)
- ])
- def test_area_commands(self, spcwebgw, url_command, command):
- """Test alarm arming/disarming."""
- with mock_aiohttp_client() as aioclient_mock:
- url = "http://localhost/spc/area/1/{}".format(url_command)
- aioclient_mock.put(url, text='{}')
- yield from spcwebgw.send_area_command('1', command)
- assert aioclient_mock.call_count == 1
+ assert hass.states.get(entity_id).attributes['changed_by'] == 'Anna'
diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py
index 199a9d804f8..cf74081adb1 100644
--- a/tests/components/test_websocket_api.py
+++ b/tests/components/test_websocket_api.py
@@ -1,6 +1,6 @@
"""Tests for the Home Assistant Websocket API."""
import asyncio
-from unittest.mock import patch
+from unittest.mock import patch, Mock
from aiohttp import WSMsgType
from async_timeout import timeout
@@ -539,3 +539,20 @@ async def test_call_service_context_no_user(hass, aiohttp_client):
assert call.service == 'test_service'
assert call.data == {'hello': 'world'}
assert call.context.user_id is None
+
+
+async def test_handler_failing(hass, websocket_client):
+ """Test a command that raises."""
+ hass.components.websocket_api.async_register_command(
+ 'bla', Mock(side_effect=TypeError),
+ wapi.BASE_COMMAND_MESSAGE_SCHEMA.extend({'type': 'bla'}))
+ await websocket_client.send_json({
+ 'id': 5,
+ 'type': 'bla',
+ })
+
+ msg = await websocket_client.receive_json()
+ assert msg['id'] == 5
+ assert msg['type'] == wapi.TYPE_RESULT
+ assert not msg['success']
+ assert msg['error']['code'] == wapi.ERR_UNKNOWN_ERROR
diff --git a/tests/components/tradfri/__init__.py b/tests/components/tradfri/__init__.py
new file mode 100644
index 00000000000..4d1b505abc9
--- /dev/null
+++ b/tests/components/tradfri/__init__.py
@@ -0,0 +1 @@
+"""Tests for the tradfri component."""
diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py
new file mode 100644
index 00000000000..99566356f61
--- /dev/null
+++ b/tests/components/tradfri/test_config_flow.py
@@ -0,0 +1,219 @@
+"""Test the Tradfri config flow."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant import data_entry_flow
+from homeassistant.components.tradfri import config_flow
+
+from tests.common import mock_coro, MockConfigEntry
+
+
+@pytest.fixture
+def mock_auth():
+ """Mock authenticate."""
+ with patch('homeassistant.components.tradfri.config_flow.'
+ 'authenticate') as mock_auth:
+ yield mock_auth
+
+
+@pytest.fixture
+def mock_gateway_info():
+ """Mock get_gateway_info."""
+ with patch('homeassistant.components.tradfri.config_flow.'
+ 'get_gateway_info') as mock_gateway:
+ yield mock_gateway
+
+
+@pytest.fixture
+def mock_entry_setup():
+ """Mock entry setup."""
+ with patch('homeassistant.components.tradfri.'
+ 'async_setup_entry') as mock_setup:
+ mock_setup.return_value = mock_coro(True)
+ yield mock_setup
+
+
+async def test_user_connection_successful(hass, mock_auth, mock_entry_setup):
+ """Test a successful connection."""
+ mock_auth.side_effect = lambda hass, host, code: mock_coro({
+ 'host': host,
+ 'gateway_id': 'bla'
+ })
+
+ flow = await hass.config_entries.flow.async_init(
+ 'tradfri', context={'source': 'user'})
+
+ result = await hass.config_entries.flow.async_configure(flow['flow_id'], {
+ 'host': '123.123.123.123',
+ 'security_code': 'abcd',
+ })
+
+ assert len(mock_entry_setup.mock_calls) == 1
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['result'].data == {
+ 'host': '123.123.123.123',
+ 'gateway_id': 'bla',
+ 'import_groups': False
+ }
+
+
+async def test_user_connection_timeout(hass, mock_auth, mock_entry_setup):
+ """Test a connection timeout."""
+ mock_auth.side_effect = config_flow.AuthError('timeout')
+
+ flow = await hass.config_entries.flow.async_init(
+ 'tradfri', context={'source': 'user'})
+
+ result = await hass.config_entries.flow.async_configure(flow['flow_id'], {
+ 'host': '127.0.0.1',
+ 'security_code': 'abcd',
+ })
+
+ assert len(mock_entry_setup.mock_calls) == 0
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['errors'] == {
+ 'base': 'timeout'
+ }
+
+
+async def test_user_connection_bad_key(hass, mock_auth, mock_entry_setup):
+ """Test a connection with bad key."""
+ mock_auth.side_effect = config_flow.AuthError('invalid_security_code')
+
+ flow = await hass.config_entries.flow.async_init(
+ 'tradfri', context={'source': 'user'})
+
+ result = await hass.config_entries.flow.async_configure(flow['flow_id'], {
+ 'host': '127.0.0.1',
+ 'security_code': 'abcd',
+ })
+
+ assert len(mock_entry_setup.mock_calls) == 0
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['errors'] == {
+ 'security_code': 'invalid_security_code'
+ }
+
+
+async def test_discovery_connection(hass, mock_auth, mock_entry_setup):
+ """Test a connection via discovery."""
+ mock_auth.side_effect = lambda hass, host, code: mock_coro({
+ 'host': host,
+ 'gateway_id': 'bla'
+ })
+
+ flow = await hass.config_entries.flow.async_init(
+ 'tradfri', context={'source': 'discovery'}, data={
+ 'host': '123.123.123.123'
+ })
+
+ result = await hass.config_entries.flow.async_configure(flow['flow_id'], {
+ 'security_code': 'abcd',
+ })
+
+ assert len(mock_entry_setup.mock_calls) == 1
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['result'].data == {
+ 'host': '123.123.123.123',
+ 'gateway_id': 'bla',
+ 'import_groups': False
+ }
+
+
+async def test_import_connection(hass, mock_gateway_info, mock_entry_setup):
+ """Test a connection via import."""
+ mock_gateway_info.side_effect = \
+ lambda hass, host, identity, key: mock_coro({
+ 'host': host,
+ 'identity': identity,
+ 'key': key,
+ 'gateway_id': 'mock-gateway'
+ })
+
+ result = await hass.config_entries.flow.async_init(
+ 'tradfri', context={'source': 'import'}, data={
+ 'host': '123.123.123.123',
+ 'identity': 'mock-iden',
+ 'key': 'mock-key',
+ 'import_groups': True
+ })
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['result'].data == {
+ 'host': '123.123.123.123',
+ 'gateway_id': 'mock-gateway',
+ 'identity': 'mock-iden',
+ 'key': 'mock-key',
+ 'import_groups': True
+ }
+
+ assert len(mock_gateway_info.mock_calls) == 1
+ assert len(mock_entry_setup.mock_calls) == 1
+
+
+async def test_import_connection_legacy(hass, mock_gateway_info,
+ mock_entry_setup):
+ """Test a connection via import."""
+ mock_gateway_info.side_effect = \
+ lambda hass, host, identity, key: mock_coro({
+ 'host': host,
+ 'identity': identity,
+ 'key': key,
+ 'gateway_id': 'mock-gateway'
+ })
+
+ result = await hass.config_entries.flow.async_init(
+ 'tradfri', context={'source': 'import'}, data={
+ 'host': '123.123.123.123',
+ 'key': 'mock-key',
+ 'import_groups': True
+ })
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['result'].data == {
+ 'host': '123.123.123.123',
+ 'gateway_id': 'mock-gateway',
+ 'identity': 'homeassistant',
+ 'key': 'mock-key',
+ 'import_groups': True
+ }
+
+ assert len(mock_gateway_info.mock_calls) == 1
+ assert len(mock_entry_setup.mock_calls) == 1
+
+
+async def test_discovery_duplicate_aborted(hass):
+ """Test a duplicate discovery host is ignored."""
+ MockConfigEntry(
+ domain='tradfri',
+ data={'host': 'some-host'}
+ ).add_to_hass(hass)
+
+ flow = await hass.config_entries.flow.async_init(
+ 'tradfri', context={'source': 'discovery'}, data={
+ 'host': 'some-host'
+ })
+
+ assert flow['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert flow['reason'] == 'already_configured'
+
+
+async def test_import_duplicate_aborted(hass):
+ """Test a duplicate discovery host is ignored."""
+ MockConfigEntry(
+ domain='tradfri',
+ data={'host': 'some-host'}
+ ).add_to_hass(hass)
+
+ flow = await hass.config_entries.flow.async_init(
+ 'tradfri', context={'source': 'import'}, data={
+ 'host': 'some-host'
+ })
+
+ assert flow['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert flow['reason'] == 'already_configured'
diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py
new file mode 100644
index 00000000000..4527e87f605
--- /dev/null
+++ b/tests/components/tradfri/test_init.py
@@ -0,0 +1,72 @@
+"""Tests for Tradfri setup."""
+from unittest.mock import patch
+
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry
+
+
+async def test_config_yaml_host_not_imported(hass):
+ """Test that we don't import a configured host."""
+ MockConfigEntry(
+ domain='tradfri',
+ data={'host': 'mock-host'}
+ ).add_to_hass(hass)
+
+ with patch('homeassistant.components.tradfri.load_json',
+ return_value={}), \
+ patch.object(hass.config_entries.flow, 'async_init') as mock_init:
+ assert await async_setup_component(hass, 'tradfri', {
+ 'tradfri': {
+ 'host': 'mock-host'
+ }
+ })
+
+ assert len(mock_init.mock_calls) == 0
+
+
+async def test_config_yaml_host_imported(hass):
+ """Test that we import a configured host."""
+ with patch('homeassistant.components.tradfri.load_json',
+ return_value={}):
+ assert await async_setup_component(hass, 'tradfri', {
+ 'tradfri': {
+ 'host': 'mock-host'
+ }
+ })
+
+ progress = hass.config_entries.flow.async_progress()
+ assert len(progress) == 1
+ assert progress[0]['handler'] == 'tradfri'
+ assert progress[0]['context'] == {'source': 'import'}
+
+
+async def test_config_json_host_not_imported(hass):
+ """Test that we don't import a configured host."""
+ MockConfigEntry(
+ domain='tradfri',
+ data={'host': 'mock-host'}
+ ).add_to_hass(hass)
+
+ with patch('homeassistant.components.tradfri.load_json',
+ return_value={'mock-host': {'key': 'some-info'}}), \
+ patch.object(hass.config_entries.flow, 'async_init') as mock_init:
+ assert await async_setup_component(hass, 'tradfri', {
+ 'tradfri': {}
+ })
+
+ assert len(mock_init.mock_calls) == 0
+
+
+async def test_config_json_host_imported(hass):
+ """Test that we import a configured host."""
+ with patch('homeassistant.components.tradfri.load_json',
+ return_value={'mock-host': {'key': 'some-info'}}):
+ assert await async_setup_component(hass, 'tradfri', {
+ 'tradfri': {}
+ })
+
+ progress = hass.config_entries.flow.async_progress()
+ assert len(progress) == 1
+ assert progress[0]['handler'] == 'tradfri'
+ assert progress[0]['context'] == {'source': 'import'}
diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py
index c9224885bbc..1857d14ad84 100644
--- a/tests/components/zwave/test_init.py
+++ b/tests/components/zwave/test_init.py
@@ -1180,7 +1180,7 @@ class TestZWaveServices(unittest.TestCase):
self.zwave_network.nodes = {14: node}
- with self.assertLogs(level='INFO') as mock_logger:
+ with self.assertLogs(level='DEBUG') as mock_logger:
self.hass.services.call('zwave', 'print_node', {
const.ATTR_NODE_ID: 14
})
diff --git a/tests/conftest.py b/tests/conftest.py
index 61c5c1c7dd5..84b72189a8d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -73,7 +73,7 @@ def hass(loop, hass_storage):
yield hass
- loop.run_until_complete(hass.async_stop())
+ loop.run_until_complete(hass.async_stop(force=True))
@pytest.fixture
diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py
index a9b4dc158e0..9d858e31a06 100644
--- a/tests/helpers/test_config_entry_flow.py
+++ b/tests/helpers/test_config_entry_flow.py
@@ -21,7 +21,8 @@ def flow_conf(hass):
with patch.dict(config_entries.HANDLERS):
config_entry_flow.register_discovery_flow(
- 'test', 'Test', has_discovered_devices)
+ 'test', 'Test', has_discovered_devices,
+ config_entries.CONN_CLASS_LOCAL_POLL)
yield handler_conf
@@ -41,22 +42,24 @@ async def test_user_no_devices_found(hass, flow_conf):
"""Test if no devices found."""
flow = config_entries.HANDLERS['test']()
flow.hass = hass
-
- result = await flow.async_step_user()
+ flow.context = {
+ 'source': config_entries.SOURCE_USER
+ }
+ result = await flow.async_step_confirm(user_input={})
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'no_devices_found'
-async def test_user_no_confirmation(hass, flow_conf):
- """Test user requires no confirmation to set up."""
+async def test_user_has_confirmation(hass, flow_conf):
+ """Test user requires no confirmation to setup."""
flow = config_entries.HANDLERS['test']()
flow.hass = hass
flow_conf['discovered'] = True
result = await flow.async_step_user()
- assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
async def test_discovery_single_instance(hass, flow_conf):
@@ -99,7 +102,7 @@ async def test_multiple_discoveries(hass, flow_conf):
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
-async def test_user_init_trumps_discovery(hass, flow_conf):
+async def test_only_one_in_progress(hass, flow_conf):
"""Test a user initialized one will finish and cancel discovered one."""
loader.set_component(hass, 'test', MockModule('test'))
@@ -111,9 +114,16 @@ async def test_user_init_trumps_discovery(hass, flow_conf):
# User starts flow
result = await hass.config_entries.flow.async_init(
'test', context={'source': config_entries.SOURCE_USER}, data={})
- assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- # Discovery flow has been aborted
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+ # Discovery flow has not been aborted
+ assert len(hass.config_entries.flow.async_progress()) == 2
+
+ # Discovery should be aborted once user confirms
+ result = await hass.config_entries.flow.async_configure(
+ result['flow_id'], {})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert len(hass.config_entries.flow.async_progress()) == 0
diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py
index a9132529bc3..b251846c491 100644
--- a/tests/helpers/test_device_registry.py
+++ b/tests/helpers/test_device_registry.py
@@ -1,68 +1,54 @@
"""Tests for the Device Registry."""
import pytest
-from collections import OrderedDict
-
from homeassistant.helpers import device_registry
-
-
-def mock_registry(hass, mock_entries=None):
- """Mock the Device Registry."""
- registry = device_registry.DeviceRegistry(hass)
- registry.devices = mock_entries or OrderedDict()
-
- async def _get_reg():
- return registry
-
- hass.data[device_registry.DATA_REGISTRY] = \
- hass.loop.create_task(_get_reg())
- return registry
+from tests.common import mock_device_registry, flush_store
@pytest.fixture
def registry(hass):
"""Return an empty, loaded, registry."""
- return mock_registry(hass)
+ return mock_device_registry(hass)
async def test_get_or_create_returns_same_entry(registry):
"""Make sure we do not duplicate entries."""
entry = registry.async_get_or_create(
- config_entry='1234',
+ config_entry_id='1234',
connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
identifiers={('bridgeid', '0123')},
manufacturer='manufacturer', model='model')
entry2 = registry.async_get_or_create(
- config_entry='1234',
+ config_entry_id='1234',
connections={('ethernet', '11:22:33:44:55:66:77:88')},
identifiers={('bridgeid', '0123')},
manufacturer='manufacturer', model='model')
entry3 = registry.async_get_or_create(
- config_entry='1234',
+ config_entry_id='1234',
connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
identifiers={('bridgeid', '1234')},
manufacturer='manufacturer', model='model')
assert len(registry.devices) == 1
- assert entry is entry2
- assert entry is entry3
+ assert entry.id == entry2.id
+ assert entry.id == entry3.id
assert entry.identifiers == {('bridgeid', '0123')}
async def test_requirement_for_identifier_or_connection(registry):
"""Make sure we do require some descriptor of device."""
entry = registry.async_get_or_create(
- config_entry='1234',
+ config_entry_id='1234',
connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
identifiers=set(),
manufacturer='manufacturer', model='model')
entry2 = registry.async_get_or_create(
- config_entry='1234',
+ config_entry_id='1234',
connections=set(),
identifiers={('bridgeid', '0123')},
manufacturer='manufacturer', model='model')
entry3 = registry.async_get_or_create(
- config_entry='1234',
+ config_entry_id='1234',
connections=set(),
identifiers=set(),
manufacturer='manufacturer', model='model')
@@ -76,25 +62,25 @@ async def test_requirement_for_identifier_or_connection(registry):
async def test_multiple_config_entries(registry):
"""Make sure we do not get duplicate entries."""
entry = registry.async_get_or_create(
- config_entry='123',
+ config_entry_id='123',
connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
identifiers={('bridgeid', '0123')},
manufacturer='manufacturer', model='model')
entry2 = registry.async_get_or_create(
- config_entry='456',
+ config_entry_id='456',
connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
identifiers={('bridgeid', '0123')},
manufacturer='manufacturer', model='model')
entry3 = registry.async_get_or_create(
- config_entry='123',
+ config_entry_id='123',
connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
identifiers={('bridgeid', '0123')},
manufacturer='manufacturer', model='model')
assert len(registry.devices) == 1
- assert entry is entry2
- assert entry is entry3
- assert entry.config_entries == {'123', '456'}
+ assert entry.id == entry2.id
+ assert entry.id == entry3.id
+ assert entry2.config_entries == {'123', '456'}
async def test_loading_from_storage(hass, hass_storage):
@@ -132,7 +118,7 @@ async def test_loading_from_storage(hass, hass_storage):
registry = await device_registry.async_get_registry(hass)
entry = registry.async_get_or_create(
- config_entry='1234',
+ config_entry_id='1234',
connections={('Zigbee', '01.23.45.67.89')},
identifiers={('serial', '12:34:56:78:90:AB:CD:EF')},
manufacturer='manufacturer', model='model')
@@ -143,25 +129,106 @@ async def test_loading_from_storage(hass, hass_storage):
async def test_removing_config_entries(registry):
"""Make sure we do not get duplicate entries."""
entry = registry.async_get_or_create(
- config_entry='123',
+ config_entry_id='123',
connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
identifiers={('bridgeid', '0123')},
manufacturer='manufacturer', model='model')
entry2 = registry.async_get_or_create(
- config_entry='456',
+ config_entry_id='456',
connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
identifiers={('bridgeid', '0123')},
manufacturer='manufacturer', model='model')
entry3 = registry.async_get_or_create(
- config_entry='123',
+ config_entry_id='123',
connections={('ethernet', '34:56:78:90:AB:CD:EF:12')},
identifiers={('bridgeid', '4567')},
manufacturer='manufacturer', model='model')
assert len(registry.devices) == 2
- assert entry is entry2
- assert entry is not entry3
- assert entry.config_entries == {'123', '456'}
+ assert entry.id == entry2.id
+ assert entry.id != entry3.id
+ assert entry2.config_entries == {'123', '456'}
+
registry.async_clear_config_entry('123')
+ entry = registry.async_get_device({('bridgeid', '0123')}, set())
+ entry3 = registry.async_get_device({('bridgeid', '4567')}, set())
+
assert entry.config_entries == {'456'}
assert entry3.config_entries == set()
+
+
+async def test_specifying_hub_device_create(registry):
+ """Test specifying a hub and updating."""
+ hub = registry.async_get_or_create(
+ config_entry_id='123',
+ connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
+ identifiers={('hue', '0123')},
+ manufacturer='manufacturer', model='hub')
+
+ light = registry.async_get_or_create(
+ config_entry_id='456',
+ connections=set(),
+ identifiers={('hue', '456')},
+ manufacturer='manufacturer', model='light',
+ via_hub=('hue', '0123'))
+
+ assert light.hub_device_id == hub.id
+
+
+async def test_specifying_hub_device_update(registry):
+ """Test specifying a hub and updating."""
+ light = registry.async_get_or_create(
+ config_entry_id='456',
+ connections=set(),
+ identifiers={('hue', '456')},
+ manufacturer='manufacturer', model='light',
+ via_hub=('hue', '0123'))
+
+ assert light.hub_device_id is None
+
+ hub = registry.async_get_or_create(
+ config_entry_id='123',
+ connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
+ identifiers={('hue', '0123')},
+ manufacturer='manufacturer', model='hub')
+
+ light = registry.async_get_or_create(
+ config_entry_id='456',
+ connections=set(),
+ identifiers={('hue', '456')},
+ manufacturer='manufacturer', model='light',
+ via_hub=('hue', '0123'))
+
+ assert light.hub_device_id == hub.id
+
+
+async def test_loading_saving_data(hass, registry):
+ """Test that we load/save data correctly."""
+ orig_hub = registry.async_get_or_create(
+ config_entry_id='123',
+ connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
+ identifiers={('hue', '0123')},
+ manufacturer='manufacturer', model='hub')
+
+ orig_light = registry.async_get_or_create(
+ config_entry_id='456',
+ connections=set(),
+ identifiers={('hue', '456')},
+ manufacturer='manufacturer', model='light',
+ via_hub=('hue', '0123'))
+
+ assert len(registry.devices) == 2
+
+ # Now load written data in new registry
+ registry2 = device_registry.DeviceRegistry(hass)
+ await flush_store(registry._store)
+ await registry2.async_load()
+
+ # Ensure same order
+ assert list(registry.devices) == list(registry2.devices)
+
+ new_hub = registry2.async_get_device({('hue', '0123')}, set())
+ new_light = registry2.async_get_device({('hue', '456')}, set())
+
+ assert orig_hub == new_hub
+ assert orig_light == new_light
diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py
index a8d78bde1f4..64f90ee7452 100644
--- a/tests/helpers/test_discovery.py
+++ b/tests/helpers/test_discovery.py
@@ -154,7 +154,7 @@ class TestHelpersDiscovery:
assert 'test_component' in self.hass.config.components
assert 'switch' in self.hass.config.components
- @patch('homeassistant.bootstrap.async_register_signal_handling')
+ @patch('homeassistant.helpers.signal.async_register_signal_handling')
def test_1st_discovers_2nd_component(self, mock_signal):
"""Test that we don't break if one component discovers the other.
diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py
index b51219ddbed..631d446d186 100644
--- a/tests/helpers/test_entity_platform.py
+++ b/tests/helpers/test_entity_platform.py
@@ -676,3 +676,55 @@ async def test_entity_registry_updates_invalid_entity_id(hass):
assert hass.states.get('test_domain.world') is not None
assert hass.states.get('invalid_entity_id') is None
assert hass.states.get('diff_domain.world') is None
+
+
+async def test_device_info_called(hass):
+ """Test device info is forwarded correctly."""
+ registry = await hass.helpers.device_registry.async_get_registry()
+ hub = registry.async_get_or_create(
+ config_entry_id='123',
+ connections=set(),
+ identifiers={('hue', 'hub-id')},
+ manufacturer='manufacturer', model='hub'
+ )
+
+ async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Mock setup entry method."""
+ async_add_entities([
+ # Invalid device info
+ MockEntity(unique_id='abcd', device_info={}),
+ # Valid device info
+ MockEntity(unique_id='qwer', device_info={
+ 'identifiers': {('hue', '1234')},
+ 'connections': {('mac', 'abcd')},
+ 'manufacturer': 'test-manuf',
+ 'model': 'test-model',
+ 'name': 'test-name',
+ 'sw_version': 'test-sw',
+ 'via_hub': ('hue', 'hub-id'),
+ }),
+ ])
+ return True
+
+ platform = MockPlatform(
+ async_setup_entry=async_setup_entry
+ )
+ config_entry = MockConfigEntry(entry_id='super-mock-id')
+ entity_platform = MockEntityPlatform(
+ hass,
+ platform_name=config_entry.domain,
+ platform=platform
+ )
+
+ assert await entity_platform.async_setup_entry(config_entry)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({('hue', '1234')}, set())
+ assert device is not None
+ assert device.identifiers == {('hue', '1234')}
+ assert device.connections == {('mac', 'abcd')}
+ assert device.manufacturer == 'test-manuf'
+ assert device.model == 'test-model'
+ assert device.name == 'test-name'
+ assert device.sw_version == 'test-sw'
+ assert device.hub_device_id == hub.id
diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py
index bb28287ddd8..a8c9086b2d2 100644
--- a/tests/helpers/test_entity_registry.py
+++ b/tests/helpers/test_entity_registry.py
@@ -6,7 +6,7 @@ import pytest
from homeassistant.helpers import entity_registry
-from tests.common import mock_registry
+from tests.common import mock_registry, flush_store
YAML__OPEN_PATH = 'homeassistant.util.yaml.open'
@@ -77,8 +77,7 @@ async def test_loading_saving_data(hass, registry):
# Now load written data in new registry
registry2 = entity_registry.EntityRegistry(hass)
- registry2._store = registry._store
-
+ await flush_store(registry._store)
await registry2.async_load()
# Ensure same order
@@ -192,6 +191,8 @@ async def test_removing_config_entry_id(registry):
'light', 'hue', '5678', config_entry_id='mock-id-1')
assert entry.config_entry_id == 'mock-id-1'
registry.async_clear_config_entry('mock-id-1')
+
+ entry = registry.entities[entry.entity_id]
assert entry.config_entry_id is None
diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py
index 2956d82c2ba..d217b99b3a8 100644
--- a/tests/helpers/test_script.py
+++ b/tests/helpers/test_script.py
@@ -255,6 +255,70 @@ class TestScriptHelper(unittest.TestCase):
assert not script_obj.is_running
assert len(events) == 1
+ def test_delay_complex_template(self):
+ """Test the delay with a working complex template."""
+ event = 'test_event'
+ events = []
+
+ @callback
+ def record_event(event):
+ """Add recorded event to set."""
+ events.append(event)
+
+ self.hass.bus.listen(event, record_event)
+
+ script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([
+ {'event': event},
+ {'delay': {
+ 'seconds': '{{ 5 }}'
+ }},
+ {'event': event}]))
+
+ script_obj.run()
+ self.hass.block_till_done()
+
+ assert script_obj.is_running
+ assert script_obj.can_cancel
+ assert script_obj.last_action == event
+ assert len(events) == 1
+
+ future = dt_util.utcnow() + timedelta(seconds=5)
+ fire_time_changed(self.hass, future)
+ self.hass.block_till_done()
+
+ assert not script_obj.is_running
+ assert len(events) == 2
+
+ def test_delay_complex_invalid_template(self):
+ """Test the delay with a complex template that fails."""
+ event = 'test_event'
+ events = []
+
+ @callback
+ def record_event(event):
+ """Add recorded event to set."""
+ events.append(event)
+
+ self.hass.bus.listen(event, record_event)
+
+ script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([
+ {'event': event},
+ {'delay': {
+ 'seconds': '{{ invalid_delay }}'
+ }},
+ {'delay': {
+ 'seconds': '{{ 5 }}'
+ }},
+ {'event': event}]))
+
+ with mock.patch.object(script, '_LOGGER') as mock_logger:
+ script_obj.run()
+ self.hass.block_till_done()
+ assert mock_logger.error.called
+
+ assert not script_obj.is_running
+ assert len(events) == 1
+
def test_cancel_while_delay(self):
"""Test the cancelling while the delay is present."""
event = 'test_event'
diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py
index 4f258bc2b09..978b0b9d450 100644
--- a/tests/test_bootstrap.py
+++ b/tests/test_bootstrap.py
@@ -22,7 +22,6 @@ _LOGGER = logging.getLogger(__name__)
'homeassistant.bootstrap.conf_util.process_ha_config_upgrade', Mock())
@patch('homeassistant.util.location.detect_location_info',
Mock(return_value=None))
-@patch('homeassistant.bootstrap.async_register_signal_handling', Mock())
@patch('os.path.isfile', Mock(return_value=True))
@patch('os.access', Mock(return_value=True))
@patch('homeassistant.bootstrap.async_enable_logging',
@@ -41,7 +40,6 @@ def test_from_config_file(hass):
@patch('homeassistant.bootstrap.async_enable_logging', Mock())
-@patch('homeassistant.bootstrap.async_register_signal_handling', Mock())
@asyncio.coroutine
def test_home_assistant_core_config_validation(hass):
"""Test if we pass in wrong information for HA conf."""
diff --git a/tests/test_config.py b/tests/test_config.py
index e4a6798093f..0e53bc0cdfb 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -965,3 +965,21 @@ async def test_disallowed_duplicated_auth_mfa_module_config(hass):
}
with pytest.raises(Invalid):
await config_util.async_process_ha_core_config(hass, core_config)
+
+
+def test_merge_split_component_definition(hass):
+ """Test components with trailing description in packages are merged."""
+ packages = {
+ 'pack_1': {'light one': {'l1': None}},
+ 'pack_2': {'light two': {'l2': None},
+ 'light three': {'l3': None}},
+ }
+ config = {
+ config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
+ }
+ config_util.merge_packages_config(hass, config, packages)
+
+ assert len(config) == 4
+ assert len(config['light one']) == 1
+ assert len(config['light two']) == 1
+ assert len(config['light three']) == 1
diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py
index d8756d87a19..57d63eb8271 100644
--- a/tests/test_config_entries.py
+++ b/tests/test_config_entries.py
@@ -103,7 +103,7 @@ def test_add_entry_calls_setup_entry(hass, manager):
hass, 'comp',
MockModule('comp', async_setup_entry=mock_setup_entry))
- class TestFlow(data_entry_flow.FlowHandler):
+ class TestFlow(config_entries.ConfigFlow):
VERSION = 1
@@ -159,8 +159,9 @@ async def test_saving_and_loading(hass):
hass, 'test',
MockModule('test', async_setup_entry=lambda *args: mock_coro(True)))
- class TestFlow(data_entry_flow.FlowHandler):
+ class TestFlow(config_entries.ConfigFlow):
VERSION = 5
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
@asyncio.coroutine
def async_step_user(self, user_input=None):
@@ -175,8 +176,9 @@ async def test_saving_and_loading(hass):
await hass.config_entries.flow.async_init(
'test', context={'source': config_entries.SOURCE_USER})
- class Test2Flow(data_entry_flow.FlowHandler):
+ class Test2Flow(config_entries.ConfigFlow):
VERSION = 3
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
@asyncio.coroutine
def async_step_user(self, user_input=None):
@@ -209,6 +211,7 @@ async def test_saving_and_loading(hass):
assert orig.title == loaded.title
assert orig.data == loaded.data
assert orig.source == loaded.source
+ assert orig.connection_class == loaded.connection_class
async def test_forward_entry_sets_up_component(hass):
@@ -252,7 +255,7 @@ async def test_discovery_notification(hass):
loader.set_component(hass, 'test', MockModule('test'))
await async_setup_component(hass, 'persistent_notification', {})
- class TestFlow(data_entry_flow.FlowHandler):
+ class TestFlow(config_entries.ConfigFlow):
VERSION = 5
async def async_step_discovery(self, user_input=None):
@@ -289,7 +292,7 @@ async def test_discovery_notification_not_created(hass):
loader.set_component(hass, 'test', MockModule('test'))
await async_setup_component(hass, 'persistent_notification', {})
- class TestFlow(data_entry_flow.FlowHandler):
+ class TestFlow(config_entries.ConfigFlow):
VERSION = 5
async def async_step_discovery(self, user_input=None):
diff --git a/tests/test_core.py b/tests/test_core.py
index 7e6d57136e4..d88257abfb4 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -4,7 +4,7 @@ import asyncio
import logging
import os
import unittest
-from unittest.mock import patch, MagicMock, sentinel
+from unittest.mock import patch, MagicMock
from datetime import datetime, timedelta
from tempfile import TemporaryDirectory
@@ -858,32 +858,42 @@ def test_create_timer(mock_monotonic, loop):
funcs.append(func)
return orig_callback(func)
- mock_monotonic.side_effect = 10.2, 10.3
+ mock_monotonic.side_effect = 10.2, 10.8, 11.3
with patch.object(ha, 'callback', mock_callback), \
patch('homeassistant.core.dt_util.utcnow',
- return_value=sentinel.mock_date):
+ return_value=datetime(2018, 12, 31, 3, 4, 5, 333333)):
ha._async_create_timer(hass)
- assert len(funcs) == 2
- fire_time_event, stop_timer = funcs
+ assert len(funcs) == 2
+ fire_time_event, stop_timer = funcs
+
+ assert len(hass.loop.call_later.mock_calls) == 1
+ delay, callback, target = hass.loop.call_later.mock_calls[0][1]
+ assert abs(delay - 0.666667) < 0.001
+ assert callback is fire_time_event
+ assert abs(target - 10.866667) < 0.001
+
+ with patch('homeassistant.core.dt_util.utcnow',
+ return_value=datetime(2018, 12, 31, 3, 4, 6, 100000)):
+ callback(target)
assert len(hass.bus.async_listen_once.mock_calls) == 1
assert len(hass.bus.async_fire.mock_calls) == 1
- assert len(hass.loop.call_later.mock_calls) == 1
+ assert len(hass.loop.call_later.mock_calls) == 2
event_type, callback = hass.bus.async_listen_once.mock_calls[0][1]
assert event_type == EVENT_HOMEASSISTANT_STOP
assert callback is stop_timer
- slp_seconds, callback, nxt = hass.loop.call_later.mock_calls[0][1]
- assert abs(slp_seconds - 0.9) < 0.001
+ delay, callback, target = hass.loop.call_later.mock_calls[1][1]
+ assert abs(delay - 0.9) < 0.001
assert callback is fire_time_event
- assert abs(nxt - 11.2) < 0.001
+ assert abs(target - 12.2) < 0.001
event_type, event_data = hass.bus.async_fire.mock_calls[0][1]
assert event_type == EVENT_TIME_CHANGED
- assert event_data[ATTR_NOW] is sentinel.mock_date
+ assert event_data[ATTR_NOW] == datetime(2018, 12, 31, 3, 4, 6, 100000)
@patch('homeassistant.core.monotonic')
@@ -897,22 +907,31 @@ def test_timer_out_of_sync(mock_monotonic, loop):
funcs.append(func)
return orig_callback(func)
- mock_monotonic.side_effect = 10.2, 11.3, 11.3
+ mock_monotonic.side_effect = 10.2, 13.3, 13.4
with patch.object(ha, 'callback', mock_callback), \
patch('homeassistant.core.dt_util.utcnow',
- return_value=sentinel.mock_date):
+ return_value=datetime(2018, 12, 31, 3, 4, 5, 333333)):
ha._async_create_timer(hass)
+ delay, callback, target = hass.loop.call_later.mock_calls[0][1]
+
+ with patch.object(ha, '_LOGGER', MagicMock()) as mock_logger, \
+ patch('homeassistant.core.dt_util.utcnow',
+ return_value=datetime(2018, 12, 31, 3, 4, 8, 200000)):
+ callback(target)
+
+ assert len(mock_logger.error.mock_calls) == 1
+
assert len(funcs) == 2
fire_time_event, stop_timer = funcs
- assert len(hass.loop.call_later.mock_calls) == 1
+ assert len(hass.loop.call_later.mock_calls) == 2
- slp_seconds, callback, nxt = hass.loop.call_later.mock_calls[0][1]
- assert slp_seconds == 1
+ delay, callback, target = hass.loop.call_later.mock_calls[1][1]
+ assert abs(delay - 0.8) < 0.001
assert callback is fire_time_event
- assert abs(nxt - 12.3) < 0.001
+ assert abs(target - 14.2) < 0.001
@asyncio.coroutine
diff --git a/tox.ini b/tox.ini
index e1261457c47..60dacd5d8cb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -49,8 +49,8 @@ deps =
-r{toxinidir}/requirements_test.txt
commands =
python script/gen_requirements_all.py validate
- flake8
- pydocstyle homeassistant tests
+ flake8 {posargs}
+ pydocstyle {posargs:homeassistant tests}
[testenv:typing]
basepython = {env:PYTHON3_PATH:python3}