diff --git a/.coveragerc b/.coveragerc index eae6498cd0a..d2192ca2e46 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,7 +29,7 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py - homeassistant/components/bmw_connected_drive.py + homeassistant/components/bmw_connected_drive/*.py homeassistant/components/*/bmw_connected_drive.py homeassistant/components/android_ip_webcam.py @@ -166,6 +166,9 @@ omit = homeassistant/components/mailgun.py homeassistant/components/*/mailgun.py + homeassistant/components/matrix.py + homeassistant/components/*/matrix.py + homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py @@ -208,6 +211,9 @@ omit = homeassistant/components/raincloud.py homeassistant/components/*/raincloud.py + homeassistant/components/rainmachine.py + homeassistant/components/*/rainmachine.py + homeassistant/components/raspihats.py homeassistant/components/*/raspihats.py @@ -516,7 +522,6 @@ omit = homeassistant/components/notify/lannouncer.py homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/mastodon.py - homeassistant/components/notify/matrix.py homeassistant/components/notify/message_bird.py homeassistant/components/notify/mycroft.py homeassistant/components/notify/nfandroidtv.py @@ -574,6 +579,7 @@ omit = homeassistant/components/sensor/discogs.py homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dovado.py + homeassistant/components/sensor/domain_expiry.py homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/dwd_weather_warnings.py @@ -615,6 +621,7 @@ omit = homeassistant/components/sensor/lyft.py homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/miflora.py + homeassistant/components/sensor/mitemp_bt.py homeassistant/components/sensor/modem_callerid.py homeassistant/components/sensor/mopar.py homeassistant/components/sensor/mqtt_room.py @@ -635,6 +642,7 @@ omit = homeassistant/components/sensor/plex.py homeassistant/components/sensor/pocketcasts.py homeassistant/components/sensor/pollen.py + homeassistant/components/sensor/postnl.py homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pyload.py @@ -656,6 +664,7 @@ omit = homeassistant/components/sensor/sma.py homeassistant/components/sensor/snmp.py homeassistant/components/sensor/sochain.py + homeassistant/components/sensor/socialblade.py homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/spotcrime.py @@ -710,7 +719,6 @@ omit = homeassistant/components/switch/orvibo.py homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/rainbird.py - homeassistant/components/switch/rainmachine.py homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 00000000000..2c418c6f63e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,50 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + + + +**Home Assistant release with the issue:** + + + +**Last working Home Assistant release (if known):** + + +**Operating environment (Hass.io/Docker/Windows/etc.):** + + +**Component/platform:** + + + +**Description of problem:** + + + +**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** +```yaml + +``` + +**Traceback (if applicable):** +``` + +``` + +**Additional information:** diff --git a/CODEOWNERS b/CODEOWNERS index 528716e174d..33966d1badb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,8 +54,11 @@ homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti +homeassistant/components/lock/nello.py @pschmitt +homeassistant/components/lock/nuki.py @pschmitt homeassistant/components/media_player/emby.py @mezz64 homeassistant/components/media_player/kodi.py @armills +homeassistant/components/media_player/liveboxplaytv.py @pschmitt homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/sonos.py @amelchio @@ -77,6 +80,7 @@ homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/tplink.py @rytilahti +homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/axis.py @kane610 @@ -90,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 +homeassistant/components/matrix.py @tinloaf +homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza homeassistant/components/*/rfxtrx.py @danielhiversen diff --git a/homeassistant/auth.py b/homeassistant/auth.py new file mode 100644 index 00000000000..55de9309954 --- /dev/null +++ b/homeassistant/auth.py @@ -0,0 +1,505 @@ +"""Provide an authentication layer for Home Assistant.""" +import asyncio +import binascii +from collections import OrderedDict +from datetime import datetime, timedelta +import os +import importlib +import logging +import uuid + +import attr +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant import data_entry_flow, requirements +from homeassistant.core import callback +from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.decorator import Registry +from homeassistant.util import dt as dt_util + + +_LOGGER = logging.getLogger(__name__) + + +AUTH_PROVIDERS = Registry() + +AUTH_PROVIDER_SCHEMA = vol.Schema({ + vol.Required(CONF_TYPE): str, + vol.Optional(CONF_NAME): str, + # Specify ID if you have two auth providers for same type. + vol.Optional(CONF_ID): str, +}, extra=vol.ALLOW_EXTRA) + +ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) +DATA_REQS = 'auth_reqs_processed' + + +class AuthError(HomeAssistantError): + """Generic authentication error.""" + + +class InvalidUser(AuthError): + """Raised when an invalid user has been specified.""" + + +class InvalidPassword(AuthError): + """Raised when an invalid password has been supplied.""" + + +class UnknownError(AuthError): + """When an unknown error occurs.""" + + +def generate_secret(entropy=32): + """Generate a secret. + + Backport of secrets.token_hex from Python 3.6 + + Event loop friendly. + """ + return binascii.hexlify(os.urandom(entropy)).decode('ascii') + + +class AuthProvider: + """Provider of user authentication.""" + + DEFAULT_TITLE = 'Unnamed auth provider' + + initialized = False + + def __init__(self, store, config): + """Initialize an auth provider.""" + self.store = store + self.config = config + + @property + def id(self): # pylint: disable=invalid-name + """Return id of the auth provider. + + Optional, can be None. + """ + return self.config.get(CONF_ID) + + @property + def type(self): + """Return type of the provider.""" + return self.config[CONF_TYPE] + + @property + def name(self): + """Return the name of the auth provider.""" + return self.config.get(CONF_NAME, self.DEFAULT_TITLE) + + async def async_credentials(self): + """Return all credentials of this provider.""" + return await self.store.credentials_for_provider(self.type, self.id) + + @callback + def async_create_credentials(self, data): + """Create credentials.""" + return Credentials( + auth_provider_type=self.type, + auth_provider_id=self.id, + data=data, + ) + + # Implement by extending class + + async def async_initialize(self): + """Initialize the auth provider. + + Optional. + """ + + async def async_credential_flow(self): + """Return the data flow for logging in with auth provider.""" + raise NotImplementedError + + async def async_get_or_create_credentials(self, flow_result): + """Get credentials based on the flow result.""" + raise NotImplementedError + + async def async_user_meta_for_credentials(self, credentials): + """Return extra user metadata for credentials. + + Will be used to populate info when creating a new user. + """ + return {} + + +@attr.s(slots=True) +class User: + """A user.""" + + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + is_owner = attr.ib(type=bool, default=False) + is_active = attr.ib(type=bool, default=False) + name = attr.ib(type=str, default=None) + # For persisting and see if saved? + # store = attr.ib(type=AuthStore, default=None) + + # List of credentials of a user. + credentials = attr.ib(type=list, default=attr.Factory(list)) + + # Tokens associated with a user. + refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict)) + + def as_dict(self): + """Convert user object to a dictionary.""" + return { + 'id': self.id, + 'is_owner': self.is_owner, + 'is_active': self.is_active, + 'name': self.name, + } + + +@attr.s(slots=True) +class RefreshToken: + """RefreshToken for a user to grant new access tokens.""" + + user = attr.ib(type=User) + client_id = attr.ib(type=str) + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) + access_token_expiration = attr.ib(type=timedelta, + default=ACCESS_TOKEN_EXPIRATION) + token = attr.ib(type=str, + default=attr.Factory(lambda: generate_secret(64))) + access_tokens = attr.ib(type=list, default=attr.Factory(list)) + + +@attr.s(slots=True) +class AccessToken: + """Access token to access the API. + + These will only ever be stored in memory and not be persisted. + """ + + refresh_token = attr.ib(type=RefreshToken) + created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) + token = attr.ib(type=str, + default=attr.Factory(generate_secret)) + + @property + def expires(self): + """Return datetime when this token expires.""" + return self.created_at + self.refresh_token.access_token_expiration + + +@attr.s(slots=True) +class Credentials: + """Credentials for a user on an auth provider.""" + + auth_provider_type = attr.ib(type=str) + auth_provider_id = attr.ib(type=str) + + # Allow the auth provider to store data to represent their auth. + data = attr.ib(type=dict) + + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + is_new = attr.ib(type=bool, default=True) + + +@attr.s(slots=True) +class Client: + """Client that interacts with Home Assistant on behalf of a user.""" + + name = attr.ib(type=str) + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + secret = attr.ib(type=str, default=attr.Factory(generate_secret)) + + +async def load_auth_provider_module(hass, provider): + """Load an auth provider.""" + try: + module = importlib.import_module( + 'homeassistant.auth_providers.{}'.format(provider)) + except ImportError: + _LOGGER.warning('Unable to find auth provider %s', provider) + return None + + if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): + return module + + processed = hass.data.get(DATA_REQS) + + if processed is None: + processed = hass.data[DATA_REQS] = set() + elif provider in processed: + return module + + req_success = await requirements.async_process_requirements( + hass, 'auth provider {}'.format(provider), module.REQUIREMENTS) + + if not req_success: + return None + + return module + + +async def auth_manager_from_config(hass, provider_configs): + """Initialize an auth manager from config.""" + store = AuthStore(hass) + if provider_configs: + providers = await asyncio.gather( + *[_auth_provider_from_config(hass, store, config) + for config in provider_configs]) + else: + providers = [] + # So returned auth providers are in same order as config + provider_hash = OrderedDict() + for provider in providers: + if provider is None: + continue + + key = (provider.type, provider.id) + + if key in provider_hash: + _LOGGER.error( + 'Found duplicate provider: %s. Please add unique IDs if you ' + 'want to have the same provider twice.', key) + continue + + provider_hash[key] = provider + manager = AuthManager(hass, store, provider_hash) + return manager + + +async def _auth_provider_from_config(hass, store, config): + """Initialize an auth provider from a config.""" + provider_name = config[CONF_TYPE] + module = await load_auth_provider_module(hass, provider_name) + + if module is None: + return None + + try: + config = module.CONFIG_SCHEMA(config) + except vol.Invalid as err: + _LOGGER.error('Invalid configuration for auth provider %s: %s', + provider_name, humanize_error(config, err)) + return None + + return AUTH_PROVIDERS[provider_name](store, config) + + +class AuthManager: + """Manage the authentication for Home Assistant.""" + + def __init__(self, hass, store, providers): + """Initialize the auth manager.""" + self._store = store + self._providers = providers + self.login_flow = data_entry_flow.FlowManager( + hass, self._async_create_login_flow, + self._async_finish_login_flow) + self.access_tokens = {} + + @property + def async_auth_providers(self): + """Return a list of available auth providers.""" + return self._providers.values() + + async def async_get_user(self, user_id): + """Retrieve a user.""" + return await self._store.async_get_user(user_id) + + async def async_get_or_create_user(self, credentials): + """Get or create a user.""" + return await self._store.async_get_or_create_user( + credentials, self._async_get_auth_provider(credentials)) + + async def async_link_user(self, user, credentials): + """Link credentials to an existing user.""" + await self._store.async_link_user(user, credentials) + + async def async_remove_user(self, user): + """Remove a user.""" + await self._store.async_remove_user(user) + + async def async_create_refresh_token(self, user, client_id): + """Create a new refresh token for a user.""" + return await self._store.async_create_refresh_token(user, client_id) + + async def async_get_refresh_token(self, token): + """Get refresh token by token.""" + return await self._store.async_get_refresh_token(token) + + @callback + def async_create_access_token(self, refresh_token): + """Create a new access token.""" + access_token = AccessToken(refresh_token) + self.access_tokens[access_token.token] = access_token + return access_token + + @callback + def async_get_access_token(self, token): + """Get an access token.""" + return self.access_tokens.get(token) + + async def async_create_client(self, name): + """Create a new client.""" + return await self._store.async_create_client(name) + + async def async_get_client(self, client_id): + """Get a client.""" + return await self._store.async_get_client(client_id) + + async def _async_create_login_flow(self, handler, *, source, data): + """Create a login flow.""" + auth_provider = self._providers[handler] + + if not auth_provider.initialized: + auth_provider.initialized = True + await auth_provider.async_initialize() + + return await auth_provider.async_credential_flow() + + async def _async_finish_login_flow(self, result): + """Result of a credential login flow.""" + auth_provider = self._providers[result['handler']] + return await auth_provider.async_get_or_create_credentials( + result['data']) + + @callback + def _async_get_auth_provider(self, credentials): + """Helper to get auth provider from a set of credentials.""" + auth_provider_key = (credentials.auth_provider_type, + credentials.auth_provider_id) + return self._providers[auth_provider_key] + + +class AuthStore: + """Stores authentication info. + + Any mutation to an object should happen inside the auth store. + + The auth store is lazy. It won't load the data from disk until a method is + called that needs it. + """ + + def __init__(self, hass): + """Initialize the auth store.""" + self.hass = hass + self.users = None + self.clients = None + self._load_lock = asyncio.Lock(loop=hass.loop) + + async def credentials_for_provider(self, provider_type, provider_id): + """Return credentials for specific auth provider type and id.""" + if self.users is None: + await self.async_load() + + return [ + credentials + for user in self.users.values() + for credentials in user.credentials + if (credentials.auth_provider_type == provider_type and + credentials.auth_provider_id == provider_id) + ] + + async def async_get_user(self, user_id): + """Retrieve a user.""" + if self.users is None: + await self.async_load() + + return self.users.get(user_id) + + async def async_get_or_create_user(self, credentials, auth_provider): + """Get or create a new user for given credentials. + + If link_user is passed in, the credentials will be linked to the passed + in user if the credentials are new. + """ + if self.users is None: + await self.async_load() + + # New credentials, store in user + if credentials.is_new: + info = await auth_provider.async_user_meta_for_credentials( + credentials) + # Make owner and activate user if it's the first user. + if self.users: + is_owner = False + is_active = False + else: + is_owner = True + is_active = True + + new_user = User( + is_owner=is_owner, + is_active=is_active, + name=info.get('name'), + ) + self.users[new_user.id] = new_user + await self.async_link_user(new_user, credentials) + return new_user + + for user in self.users.values(): + for creds in user.credentials: + if (creds.auth_provider_type == credentials.auth_provider_type + and creds.auth_provider_id == + credentials.auth_provider_id): + return user + + raise ValueError('We got credentials with ID but found no user') + + async def async_link_user(self, user, credentials): + """Add credentials to an existing user.""" + user.credentials.append(credentials) + await self.async_save() + credentials.is_new = False + + async def async_remove_user(self, user): + """Remove a user.""" + self.users.pop(user.id) + await self.async_save() + + async def async_create_refresh_token(self, user, client_id): + """Create a new token for a user.""" + refresh_token = RefreshToken(user, client_id) + user.refresh_tokens[refresh_token.token] = refresh_token + await self.async_save() + return refresh_token + + async def async_get_refresh_token(self, token): + """Get refresh token by token.""" + if self.users is None: + await self.async_load() + + for user in self.users.values(): + refresh_token = user.refresh_tokens.get(token) + if refresh_token is not None: + return refresh_token + + return None + + async def async_create_client(self, name): + """Create a new client.""" + if self.clients is None: + await self.async_load() + + client = Client(name) + self.clients[client.id] = client + await self.async_save() + return client + + async def async_get_client(self, client_id): + """Get a client.""" + if self.clients is None: + await self.async_load() + + return self.clients.get(client_id) + + async def async_load(self): + """Load the users.""" + async with self._load_lock: + self.users = {} + self.clients = {} + + async def async_save(self): + """Save users.""" + pass diff --git a/homeassistant/auth_providers/__init__.py b/homeassistant/auth_providers/__init__.py new file mode 100644 index 00000000000..4705e7580ca --- /dev/null +++ b/homeassistant/auth_providers/__init__.py @@ -0,0 +1 @@ +"""Auth providers for Home Assistant.""" diff --git a/homeassistant/auth_providers/insecure_example.py b/homeassistant/auth_providers/insecure_example.py new file mode 100644 index 00000000000..8538e8c2f3e --- /dev/null +++ b/homeassistant/auth_providers/insecure_example.py @@ -0,0 +1,116 @@ +"""Example auth provider.""" +from collections import OrderedDict +import hmac + +import voluptuous as vol + +from homeassistant import auth, data_entry_flow +from homeassistant.core import callback + + +USER_SCHEMA = vol.Schema({ + vol.Required('username'): str, + vol.Required('password'): str, + vol.Optional('name'): str, +}) + + +CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ + vol.Required('users'): [USER_SCHEMA] +}, extra=vol.PREVENT_EXTRA) + + +@auth.AUTH_PROVIDERS.register('insecure_example') +class ExampleAuthProvider(auth.AuthProvider): + """Example auth provider based on hardcoded usernames and passwords.""" + + async def async_credential_flow(self): + """Return a flow to login.""" + return LoginFlow(self) + + @callback + def async_validate_login(self, username, password): + """Helper to validate a username and password.""" + user = None + + # Compare all users to avoid timing attacks. + for usr in self.config['users']: + if hmac.compare_digest(username.encode('utf-8'), + usr['username'].encode('utf-8')): + user = usr + + if user is None: + # Do one more compare to make timing the same as if user was found. + hmac.compare_digest(password.encode('utf-8'), + password.encode('utf-8')) + raise auth.InvalidUser + + if not hmac.compare_digest(user['password'].encode('utf-8'), + password.encode('utf-8')): + raise auth.InvalidPassword + + async def async_get_or_create_credentials(self, flow_result): + """Get credentials based on the flow result.""" + username = flow_result['username'] + password = flow_result['password'] + + self.async_validate_login(username, password) + + for credential in await self.async_credentials(): + if credential.data['username'] == username: + return credential + + # Create new credentials. + return self.async_create_credentials({ + 'username': username + }) + + async def async_user_meta_for_credentials(self, credentials): + """Return extra user metadata for credentials. + + Will be used to populate info when creating a new user. + """ + username = credentials.data['username'] + + for user in self.config['users']: + if user['username'] == username: + return { + 'name': user.get('name') + } + + return {} + + +class LoginFlow(data_entry_flow.FlowHandler): + """Handler for the login flow.""" + + def __init__(self, auth_provider): + """Initialize the login flow.""" + self._auth_provider = auth_provider + + async def async_step_init(self, user_input=None): + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + self._auth_provider.async_validate_login( + user_input['username'], user_input['password']) + except (auth.InvalidUser, auth.InvalidPassword): + errors['base'] = 'invalid_auth' + + if not errors: + return self.async_create_entry( + title=self._auth_provider.name, + data=user_input + ) + + schema = OrderedDict() + schema['username'] = str + schema['password'] = str + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e0962568a66..826cc563e82 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -12,8 +12,7 @@ from typing import Any, Optional, Dict import voluptuous as vol from homeassistant import ( - core, config as conf_util, config_entries, loader, - components as core_components) + core, config as conf_util, config_entries, components as core_components) from homeassistant.components import persistent_notification from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.setup import async_setup_component @@ -67,16 +66,15 @@ def from_config_dict(config: Dict[str, Any], return hass -@asyncio.coroutine -def async_from_config_dict(config: Dict[str, Any], - hass: core.HomeAssistant, - config_dir: Optional[str] = None, - enable_log: bool = True, - verbose: bool = False, - skip_pip: bool = False, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False) \ +async def async_from_config_dict(config: Dict[str, Any], + hass: core.HomeAssistant, + config_dir: Optional[str] = None, + enable_log: bool = True, + verbose: bool = False, + skip_pip: bool = False, + log_rotate_days: Any = None, + log_file: Any = None, + log_no_color: bool = False) \ -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a configuration dictionary. @@ -92,27 +90,24 @@ def async_from_config_dict(config: Dict[str, Any], core_config = config.get(core.DOMAIN, {}) try: - yield from conf_util.async_process_ha_core_config(hass, core_config) + await conf_util.async_process_ha_core_config(hass, core_config) except vol.Invalid as ex: conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) return None - yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass) + await hass.async_add_job(conf_util.process_ha_config_upgrade, hass) hass.config.skip_pip = skip_pip if skip_pip: _LOGGER.warning("Skipping pip installation of required modules. " "This may cause issues") - if not loader.PREPARED: - yield from hass.async_add_job(loader.prepare, hass) - # Make a copy because we are mutating it. config = OrderedDict(config) # Merge packages conf_util.merge_packages_config( - config, core_config.get(conf_util.CONF_PACKAGES, {})) + hass, config, core_config.get(conf_util.CONF_PACKAGES, {})) # Ensure we have no None values after merge for key, value in config.items(): @@ -120,7 +115,7 @@ def async_from_config_dict(config: Dict[str, Any], config[key] = {} hass.config_entries = config_entries.ConfigEntries(hass, config) - yield from hass.config_entries.async_load() + await hass.config_entries.async_load() # Filter out the repeating and common config section [homeassistant] components = set(key.split(' ')[0] for key in config.keys() @@ -129,13 +124,13 @@ def async_from_config_dict(config: Dict[str, Any], # setup components # pylint: disable=not-an-iterable - res = yield from core_components.async_setup(hass, config) + res = await core_components.async_setup(hass, config) if not res: _LOGGER.error("Home Assistant core failed to initialize. " "further initialization aborted") return hass - yield from persistent_notification.async_setup(hass, config) + await persistent_notification.async_setup(hass, config) _LOGGER.info("Home Assistant core initialized") @@ -145,7 +140,7 @@ def async_from_config_dict(config: Dict[str, Any], continue hass.async_add_job(async_setup_component(hass, component, config)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() # stage 2 for component in components: @@ -153,7 +148,7 @@ def async_from_config_dict(config: Dict[str, Any], continue hass.async_add_job(async_setup_component(hass, component, config)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop-start) @@ -187,14 +182,13 @@ def from_config_file(config_path: str, return hass -@asyncio.coroutine -def async_from_config_file(config_path: str, - hass: core.HomeAssistant, - verbose: bool = False, - skip_pip: bool = True, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False): +async def async_from_config_file(config_path: str, + hass: core.HomeAssistant, + verbose: bool = False, + skip_pip: bool = True, + log_rotate_days: Any = None, + log_file: Any = None, + log_no_color: bool = False): """Read the configuration file and try to start all the functionality. Will add functionality to 'hass' parameter. @@ -203,13 +197,13 @@ def async_from_config_file(config_path: str, # Set config dir to directory holding config file config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = config_dir - yield from async_mount_local_lib_path(config_dir, hass.loop) + await async_mount_local_lib_path(config_dir, hass.loop) async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) try: - config_dict = yield from hass.async_add_job( + config_dict = await hass.async_add_job( conf_util.load_yaml_config_file, config_path) except HomeAssistantError as err: _LOGGER.error("Error loading %s: %s", config_path, err) @@ -217,7 +211,7 @@ def async_from_config_file(config_path: str, finally: clear_secret_cache() - hass = yield from async_from_config_dict( + hass = await async_from_config_dict( config_dict, hass, enable_log=False, skip_pip=skip_pip) return hass @@ -294,11 +288,10 @@ def async_enable_logging(hass: core.HomeAssistant, async_handler = AsyncHandler(hass.loop, err_handler) - @asyncio.coroutine - def async_stop_async_handler(event): + async def async_stop_async_handler(event): """Cleanup async handler.""" logging.getLogger('').removeHandler(async_handler) - yield from async_handler.async_close(blocking=True) + await async_handler.async_close(blocking=True) hass.bus.async_listen_once( EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) @@ -323,15 +316,14 @@ def mount_local_lib_path(config_dir: str) -> str: return deps_dir -@asyncio.coroutine -def async_mount_local_lib_path(config_dir: str, - loop: asyncio.AbstractEventLoop) -> str: +async def async_mount_local_lib_path(config_dir: str, + loop: asyncio.AbstractEventLoop) -> str: """Add local library to Python Path. This function is a coroutine. """ deps_dir = os.path.join(config_dir, 'deps') - lib_dir = yield from async_get_user_site(deps_dir, loop=loop) + lib_dir = await async_get_user_site(deps_dir, loop=loop) if lib_dir not in sys.path: sys.path.insert(0, lib_dir) return deps_dir diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index 2f56bb7c2b5..6d5feb87dc2 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -81,7 +81,7 @@ TRIGGER_SCHEMA = vol.Schema({ ABODE_PLATFORMS = [ 'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', - 'camera', 'light' + 'camera', 'light', 'sensor' ] diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 0e96e6448ff..31d93373286 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyalarmdotcom==0.3.1'] +REQUIREMENTS = ['pyalarmdotcom==0.3.2'] _LOGGER = logging.getLogger(__name__) @@ -93,6 +93,13 @@ class AlarmDotCom(alarm.AlarmControlPanel): return STATE_ALARM_ARMED_AWAY return STATE_UNKNOWN + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + 'sensor_status': self._alarm.sensor_status + } + @asyncio.coroutine def async_alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 6fdf0c027a4..83e05dae641 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -76,8 +76,7 @@ class APIEventStream(HomeAssistantView): url = URL_API_STREAM name = "api:stream" - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Provide a streaming interface for the event bus.""" # pylint: disable=no-self-use hass = request.app['hass'] @@ -88,8 +87,7 @@ class APIEventStream(HomeAssistantView): if restrict: restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] - @asyncio.coroutine - def forward_events(event): + async def forward_events(event): """Forward events to the open request.""" if event.event_type == EVENT_TIME_CHANGED: return @@ -104,11 +102,11 @@ class APIEventStream(HomeAssistantView): else: data = json.dumps(event, cls=rem.JSONEncoder) - yield from to_write.put(data) + await to_write.put(data) response = web.StreamResponse() response.content_type = 'text/event-stream' - yield from response.prepare(request) + await response.prepare(request) unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) @@ -116,13 +114,13 @@ class APIEventStream(HomeAssistantView): _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) # Fire off one message so browsers fire open event right away - yield from to_write.put(STREAM_PING_PAYLOAD) + await to_write.put(STREAM_PING_PAYLOAD) while True: try: with async_timeout.timeout(STREAM_PING_INTERVAL, loop=hass.loop): - payload = yield from to_write.get() + payload = await to_write.get() if payload is stop_obj: break @@ -130,9 +128,9 @@ class APIEventStream(HomeAssistantView): msg = "data: {}\n\n".format(payload) _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), msg.strip()) - yield from response.write(msg.encode("UTF-8")) + await response.write(msg.encode("UTF-8")) except asyncio.TimeoutError: - yield from to_write.put(STREAM_PING_PAYLOAD) + await to_write.put(STREAM_PING_PAYLOAD) except asyncio.CancelledError: _LOGGER.debug('STREAM %s ABORT', id(stop_obj)) @@ -200,12 +198,11 @@ class APIEntityStateView(HomeAssistantView): return self.json(state) return self.json_message('Entity not found', HTTP_NOT_FOUND) - @asyncio.coroutine - def post(self, request, entity_id): + async def post(self, request, entity_id): """Update state of entity.""" hass = request.app['hass'] try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON specified', HTTP_BAD_REQUEST) @@ -257,10 +254,9 @@ class APIEventView(HomeAssistantView): url = '/api/events/{event_type}' name = "api:event" - @asyncio.coroutine - def post(self, request, event_type): + async def post(self, request, event_type): """Fire events.""" - body = yield from request.text() + body = await request.text() try: event_data = json.loads(body) if body else None except ValueError: @@ -292,10 +288,9 @@ class APIServicesView(HomeAssistantView): url = URL_API_SERVICES name = "api:services" - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Get registered services.""" - services = yield from async_services_json(request.app['hass']) + services = await async_services_json(request.app['hass']) return self.json(services) @@ -305,14 +300,13 @@ class APIDomainServicesView(HomeAssistantView): url = "/api/services/{domain}/{service}" name = "api:domain-services" - @asyncio.coroutine - def post(self, request, domain, service): + async def post(self, request, domain, service): """Call a service. Returns a list of changed states. """ hass = request.app['hass'] - body = yield from request.text() + body = await request.text() try: data = json.loads(body) if body else None except ValueError: @@ -320,7 +314,7 @@ class APIDomainServicesView(HomeAssistantView): HTTP_BAD_REQUEST) with AsyncTrackStates(hass) as changed_states: - yield from hass.services.async_call(domain, service, data, True) + await hass.services.async_call(domain, service, data, True) return self.json(changed_states) @@ -343,11 +337,10 @@ class APITemplateView(HomeAssistantView): url = URL_API_TEMPLATE name = "api:template" - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Render a template.""" try: - data = yield from request.json() + data = await request.json() tpl = template.Template(data['template'], request.app['hass']) return tpl.async_render(data.get('variables')) except (ValueError, TemplateError) as ex: @@ -366,10 +359,9 @@ class APIErrorLog(HomeAssistantView): return await self.file(request, request.app['hass'].data[DATA_LOGGING]) -@asyncio.coroutine -def async_services_json(hass): +async def async_services_json(hass): """Generate services data to JSONify.""" - descriptions = yield from async_get_all_descriptions(hass) + descriptions = await async_get_all_descriptions(hass) return [{"domain": key, "services": value} for key, value in descriptions.items()] diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py new file mode 100644 index 00000000000..d4b4b0f4591 --- /dev/null +++ b/homeassistant/components/auth/__init__.py @@ -0,0 +1,344 @@ +"""Component to allow users to login and get tokens. + +All requests will require passing in a valid client ID and secret via HTTP +Basic Auth. + +# GET /auth/providers + +Return a list of auth providers. Example: + +[ + { + "name": "Local", + "id": null, + "type": "local_provider", + } +] + +# POST /auth/login_flow + +Create a login flow. Will return the first step of the flow. + +Pass in parameter 'handler' to specify the auth provider to use. Auth providers +are identified by type and id. + +{ + "handler": ["local_provider", null] +} + +Return value will be a step in a data entry flow. See the docs for data entry +flow for details. + +{ + "data_schema": [ + {"name": "username", "type": "string"}, + {"name": "password", "type": "string"} + ], + "errors": {}, + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "step_id": "init", + "type": "form" +} + +# POST /auth/login_flow/{flow_id} + +Progress the flow. Most flows will be 1 page, but could optionally add extra +login challenges, like TFA. Once the flow has finished, the returned step will +have type "create_entry" and "result" key will contain an authorization code. + +{ + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "result": "411ee2f916e648d691e937ae9344681e", + "source": "user", + "title": "Example", + "type": "create_entry", + "version": 1 +} + +# POST /auth/token + +This is an OAuth2 endpoint for granting tokens. We currently support the grant +types "authorization_code" and "refresh_token". Because we follow the OAuth2 +spec, data should be send in formatted as x-www-form-urlencoded. Examples will +be in JSON as it's more readable. + +## Grant type authorization_code + +Exchange the authorization code retrieved from the login flow for tokens. + +{ + "grant_type": "authorization_code", + "code": "411ee2f916e648d691e937ae9344681e" +} + +Return value will be the access and refresh tokens. The access token will have +a limited expiration. New access tokens can be requested using the refresh +token. + +{ + "access_token": "ABCDEFGH", + "expires_in": 1800, + "refresh_token": "IJKLMNOPQRST", + "token_type": "Bearer" +} + +## Grant type refresh_token + +Request a new access token using a refresh token. + +{ + "grant_type": "refresh_token", + "refresh_token": "IJKLMNOPQRST" +} + +Return value will be a new access token. The access token will have +a limited expiration. + +{ + "access_token": "ABCDEFGH", + "expires_in": 1800, + "token_type": "Bearer" +} +""" +import logging +import uuid + +import aiohttp.web +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.core import callback +from homeassistant.helpers.data_entry_flow import ( + FlowManagerIndexView, FlowManagerResourceView) +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator + +from .client import verify_client + +DOMAIN = 'auth' +DEPENDENCIES = ['http'] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Component to allow users to login.""" + store_credentials, retrieve_credentials = _create_cred_store() + + hass.http.register_view(AuthProvidersView) + hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow)) + hass.http.register_view( + LoginFlowResourceView(hass.auth.login_flow, store_credentials)) + hass.http.register_view(GrantTokenView(retrieve_credentials)) + hass.http.register_view(LinkUserView(retrieve_credentials)) + + return True + + +class AuthProvidersView(HomeAssistantView): + """View to get available auth providers.""" + + url = '/auth/providers' + name = 'api:auth:providers' + requires_auth = False + + @verify_client + async def get(self, request, client_id): + """Get available auth providers.""" + return self.json([{ + 'name': provider.name, + 'id': provider.id, + 'type': provider.type, + } for provider in request.app['hass'].auth.async_auth_providers]) + + +class LoginFlowIndexView(FlowManagerIndexView): + """View to create a config flow.""" + + url = '/auth/login_flow' + name = 'api:auth:login_flow' + requires_auth = False + + async def get(self, request): + """Do not allow index of flows in progress.""" + return aiohttp.web.Response(status=405) + + # pylint: disable=arguments-differ + @verify_client + async def post(self, request, client_id): + """Create a new login flow.""" + # pylint: disable=no-value-for-parameter + return await super().post(request) + + +class LoginFlowResourceView(FlowManagerResourceView): + """View to interact with the flow manager.""" + + url = '/auth/login_flow/{flow_id}' + name = 'api:auth:login_flow:resource' + requires_auth = False + + def __init__(self, flow_mgr, store_credentials): + """Initialize the login flow resource view.""" + super().__init__(flow_mgr) + self._store_credentials = store_credentials + + # pylint: disable=arguments-differ + async def get(self, request): + """Do not allow getting status of a flow in progress.""" + return self.json_message('Invalid flow specified', 404) + + # pylint: disable=arguments-differ + @verify_client + @RequestDataValidator(vol.Schema(dict), allow_empty=True) + async def post(self, request, client_id, flow_id, data): + """Handle progressing a login flow request.""" + try: + result = await self._flow_mgr.async_configure(flow_id, data) + except data_entry_flow.UnknownFlow: + return self.json_message('Invalid flow specified', 404) + except vol.Invalid: + return self.json_message('User input malformed', 400) + + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return self.json(self._prepare_result_json(result)) + + result.pop('data') + result['result'] = self._store_credentials(client_id, result['result']) + + return self.json(result) + + +class GrantTokenView(HomeAssistantView): + """View to grant tokens.""" + + url = '/auth/token' + name = 'api:auth:token' + requires_auth = False + + def __init__(self, retrieve_credentials): + """Initialize the grant token view.""" + self._retrieve_credentials = retrieve_credentials + + @verify_client + async def post(self, request, client_id): + """Grant a token.""" + hass = request.app['hass'] + data = await request.post() + grant_type = data.get('grant_type') + + if grant_type == 'authorization_code': + return await self._async_handle_auth_code( + hass, client_id, data) + + elif grant_type == 'refresh_token': + return await self._async_handle_refresh_token( + hass, client_id, data) + + return self.json({ + 'error': 'unsupported_grant_type', + }, status_code=400) + + async def _async_handle_auth_code(self, hass, client_id, data): + """Handle authorization code request.""" + code = data.get('code') + + if code is None: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + + credentials = self._retrieve_credentials(client_id, code) + + if credentials is None: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + + user = await hass.auth.async_get_or_create_user(credentials) + refresh_token = await hass.auth.async_create_refresh_token(user, + client_id) + access_token = hass.auth.async_create_access_token(refresh_token) + + return self.json({ + 'access_token': access_token.token, + 'token_type': 'Bearer', + 'refresh_token': refresh_token.token, + 'expires_in': + int(refresh_token.access_token_expiration.total_seconds()), + }) + + async def _async_handle_refresh_token(self, hass, client_id, data): + """Handle authorization code request.""" + token = data.get('refresh_token') + + if token is None: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + + refresh_token = await hass.auth.async_get_refresh_token(token) + + if refresh_token is None or refresh_token.client_id != client_id: + return self.json({ + 'error': 'invalid_grant', + }, status_code=400) + + access_token = hass.auth.async_create_access_token(refresh_token) + + return self.json({ + 'access_token': access_token.token, + 'token_type': 'Bearer', + 'expires_in': + int(refresh_token.access_token_expiration.total_seconds()), + }) + + +class LinkUserView(HomeAssistantView): + """View to link existing users to new credentials.""" + + url = '/auth/link_user' + name = 'api:auth:link_user' + + def __init__(self, retrieve_credentials): + """Initialize the link user view.""" + self._retrieve_credentials = retrieve_credentials + + @RequestDataValidator(vol.Schema({ + 'code': str, + 'client_id': str, + })) + async def post(self, request, data): + """Link a user.""" + hass = request.app['hass'] + user = request['hass_user'] + + credentials = self._retrieve_credentials( + data['client_id'], data['code']) + + if credentials is None: + return self.json_message('Invalid code', status_code=400) + + await hass.auth.async_link_user(user, credentials) + return self.json_message('User linked') + + +@callback +def _create_cred_store(): + """Create a credential store.""" + temp_credentials = {} + + @callback + def store_credentials(client_id, credentials): + """Store credentials and return a code to retrieve it.""" + code = uuid.uuid4().hex + temp_credentials[(client_id, code)] = credentials + return code + + @callback + def retrieve_credentials(client_id, code): + """Retrieve credentials.""" + return temp_credentials.pop((client_id, code), None) + + return store_credentials, retrieve_credentials diff --git a/homeassistant/components/auth/client.py b/homeassistant/components/auth/client.py new file mode 100644 index 00000000000..28d72aefe0f --- /dev/null +++ b/homeassistant/components/auth/client.py @@ -0,0 +1,63 @@ +"""Helpers to resolve client ID/secret.""" +import base64 +from functools import wraps +import hmac + +import aiohttp.hdrs + + +def verify_client(method): + """Decorator to verify client id/secret on requests.""" + @wraps(method) + async def wrapper(view, request, *args, **kwargs): + """Verify client id/secret before doing request.""" + client_id = await _verify_client(request) + + if client_id is None: + return view.json({ + 'error': 'invalid_client', + }, status_code=401) + + return await method( + view, request, *args, client_id=client_id, **kwargs) + + return wrapper + + +async def _verify_client(request): + """Method to verify the client id/secret in consistent time. + + By using a consistent time for looking up client id and comparing the + secret, we prevent attacks by malicious actors trying different client ids + and are able to derive from the time it takes to process the request if + they guessed the client id correctly. + """ + if aiohttp.hdrs.AUTHORIZATION not in request.headers: + return None + + auth_type, auth_value = \ + request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1) + + if auth_type != 'Basic': + return None + + decoded = base64.b64decode(auth_value).decode('utf-8') + try: + client_id, client_secret = decoded.split(':', 1) + except ValueError: + # If no ':' in decoded + return None + + client = await request.app['hass'].auth.async_get_client(client_id) + + if client is None: + # Still do a compare so we run same time as if a client was found. + hmac.compare_digest(client_secret.encode('utf-8'), + client_secret.encode('utf-8')) + return None + + if hmac.compare_digest(client_secret.encode('utf-8'), + client.secret.encode('utf-8')): + return client_id + + return None diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 8c490754f40..2f510fd33d6 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/automation/ """ import asyncio from functools import partial +import importlib import logging import voluptuous as vol @@ -22,7 +23,6 @@ from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import get_platform from homeassistant.util.dt import utcnow import homeassistant.helpers.config_validation as cv @@ -58,12 +58,14 @@ _LOGGER = logging.getLogger(__name__) def _platform_validator(config): """Validate it is a valid platform.""" - platform = get_platform(DOMAIN, config[CONF_PLATFORM]) + try: + platform = importlib.import_module( + 'homeassistant.components.automation.{}'.format( + config[CONF_PLATFORM])) + except ImportError: + raise vol.Invalid('Invalid platform specified') from None - if not hasattr(platform, 'TRIGGER_SCHEMA'): - return config - - return getattr(platform, 'TRIGGER_SCHEMA')(config) + return platform.TRIGGER_SCHEMA(config) _TRIGGER_SCHEMA = vol.All( @@ -71,7 +73,7 @@ _TRIGGER_SCHEMA = vol.All( [ vol.All( vol.Schema({ - vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) + vol.Required(CONF_PLATFORM): str }, extra=vol.ALLOW_EXTRA), _platform_validator ), diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index ad475be76ca..d72211d5ad1 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -50,13 +50,23 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): """Track states and offer events for binary sensors.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True +async def async_setup_entry(hass, entry): + """Setup 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) + + # pylint: disable=no-self-use class BinarySensorDevice(Entity): """Represent a binary sensor.""" diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py index 53f148fe97f..3080cc65532 100644 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ b/homeassistant/components/binary_sensor/bloomsky.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -31,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available BloomSky weather binary sensors.""" - bloomsky = get_component('bloomsky') + bloomsky = hass.components.bloomsky # Default needed in case of discovery sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index ef3ec506e3a..9faa703d13c 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -6,27 +6,35 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Old way of setting up deCONZ binary sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the deCONZ binary sensor.""" - if discovery_info is None: - return + @callback + def async_add_sensor(sensors): + """Add binary sensor from deCONZ.""" + from pydeconz.sensor import DECONZ_BINARY_SENSOR + entities = [] + for sensor in sensors: + if sensor.type in DECONZ_BINARY_SENSOR: + entities.append(DeconzBinarySensor(sensor)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) - from pydeconz.sensor import DECONZ_BINARY_SENSOR - sensors = hass.data[DATA_DECONZ].sensors - entities = [] - - for sensor in sensors.values(): - if sensor and sensor.type in DECONZ_BINARY_SENSOR: - entities.append(DeconzBinarySensor(sensor)) - async_add_devices(entities, True) + async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) class DeconzBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 06079d6aa3b..9cb87b31749 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -23,7 +23,7 @@ SENSOR_TYPES = {'openClosedSensor': 'opening', @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index 7997e4e60db..fd0e30ccebc 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -13,7 +13,6 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.netatmo import CameraData -from homeassistant.loader import get_component from homeassistant.const import CONF_TIMEOUT from homeassistant.helpers import config_validation as cv @@ -61,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the access to Netatmo binary sensor.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo home = config.get(CONF_HOME) timeout = config.get(CONF_TIMEOUT) if timeout is None: diff --git a/homeassistant/components/binary_sensor/tapsaff.py b/homeassistant/components/binary_sensor/tapsaff.py index 09d28b96f72..c0f6ca3f112 100644 --- a/homeassistant/components/binary_sensor/tapsaff.py +++ b/homeassistant/components/binary_sensor/tapsaff.py @@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['tapsaff==0.1.3'] +REQUIREMENTS = ['tapsaff==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 9b4598f3c42..5405a6a77ba 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.14.2'] +REQUIREMENTS = ['numpy==1.14.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index cc1f602d871..30a7e291401 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/binary_sensor.wemo/ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.loader import get_component DEPENDENCIES = ['wemo'] @@ -25,18 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device = discovery.device_from_description(location, mac) if device: - add_devices_callback([WemoBinarySensor(device)]) + add_devices_callback([WemoBinarySensor(hass, device)]) class WemoBinarySensor(BinarySensorDevice): """Representation a WeMo binary sensor.""" - def __init__(self, device): + def __init__(self, hass, device): """Initialize the WeMo sensor.""" self.wemo = device self._state = None - wemo = get_component('wemo') + wemo = hass.components.wemo wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 8935ad5115d..b37be3f6cb6 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -17,16 +17,17 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['holidays==0.9.4'] +REQUIREMENTS = ['holidays==0.9.5'] # List of all countries currently supported by holidays # There seems to be no way to get the list out at runtime -ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada', - 'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', - 'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland', - 'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland', - 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', - 'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', +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', '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', diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 2ed0de66b18..49f716b9eb7 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -25,30 +25,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): for device in gateway.devices['binary_sensor']: model = device['model'] - if model in ['motion', 'sensor_motion.aq2']: + if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']: devices.append(XiaomiMotionSensor(device, hass, gateway)) - elif model in ['magnet', 'sensor_magnet.aq2']: + elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']: devices.append(XiaomiDoorSensor(device, gateway)) elif model == 'sensor_wleak.aq1': devices.append(XiaomiWaterLeakSensor(device, gateway)) - elif model == 'smoke': + elif model in ['smoke', 'sensor_smoke']: devices.append(XiaomiSmokeSensor(device, gateway)) - elif model == 'natgas': + elif model in ['natgas', 'sensor_natgas']: devices.append(XiaomiNatgasSensor(device, gateway)) - elif model in ['switch', 'sensor_switch.aq2', 'sensor_switch.aq3']: - devices.append(XiaomiButton(device, 'Switch', 'status', + elif model in ['switch', 'sensor_switch', + 'sensor_switch.aq2', 'sensor_switch.aq3']: + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'channel_0' + devices.append(XiaomiButton(device, 'Switch', data_key, hass, gateway)) - elif model == '86sw1': + elif model in ['86sw1', 'sensor_86sw1.aq1']: devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0', hass, gateway)) - elif model == '86sw2': + elif model in ['86sw2', 'sensor_86sw2.aq1']: devices.append(XiaomiButton(device, 'Wall Switch (Left)', 'channel_0', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Right)', 'channel_1', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Both)', 'dual_channel', hass, gateway)) - elif model == 'cube': + elif model in ['cube', 'sensor_cube']: devices.append(XiaomiCube(device, hass, gateway)) add_devices(devices) @@ -129,8 +134,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor): """Initialize the XiaomiMotionSensor.""" self._hass = hass self._no_motion_since = 0 + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'motion_status' XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub, - 'status', 'motion') + data_key, 'motion') @property def device_state_attributes(self): diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index bf038a62465..756323f41d9 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -31,12 +31,21 @@ async def async_setup_platform(hass, config, async_add_devices, if discovery_info is None: return + from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasZone + if IasZone.cluster_id in discovery_info['in_clusters']: + await _async_setup_iaszone(hass, config, async_add_devices, + discovery_info) + elif OnOff.cluster_id in discovery_info['out_clusters']: + await _async_setup_remote(hass, config, async_add_devices, + discovery_info) - in_clusters = discovery_info['in_clusters'] +async def _async_setup_iaszone(hass, config, async_add_devices, + discovery_info): device_class = None - cluster = in_clusters[IasZone.cluster_id] + from zigpy.zcl.clusters.security import IasZone + cluster = discovery_info['in_clusters'][IasZone.cluster_id] if discovery_info['new_join']: await cluster.bind() ieee = cluster.endpoint.device.application.ieee @@ -53,8 +62,34 @@ async def async_setup_platform(hass, config, async_add_devices, async_add_devices([sensor], update_before_add=True) +async def _async_setup_remote(hass, config, async_add_devices, 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) + + 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)) + 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)) + + sensor = Switch(**discovery_info) + async_add_devices([sensor], update_before_add=True) + + class BinarySensor(zha.Entity, BinarySensorDevice): - """THe ZHA Binary Sensor.""" + """The ZHA Binary Sensor.""" _domain = DOMAIN @@ -102,3 +137,113 @@ class BinarySensor(zha.Entity, BinarySensorDevice): 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): + """ZHA switch/remote controller/button.""" + + _domain = DOMAIN + + class OnOffListener: + """Listener for the OnOff ZigBee cluster.""" + + def __init__(self, entity): + """Initialize OnOffListener.""" + self._entity = entity + + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + if command_id in (0x0000, 0x0040): + self._entity.set_state(False) + elif command_id in (0x0001, 0x0041, 0x0042): + self._entity.set_state(True) + elif command_id == 0x0002: + self._entity.set_state(not self._entity.is_on) + + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == 0: + self._entity.set_state(value) + + def zdo_command(self, *args, **kwargs): + """Handle ZDO commands on this cluster.""" + pass + + class LevelListener: + """Listener for the LevelControl ZigBee cluster.""" + + def __init__(self, entity): + """Initialize LevelListener.""" + self._entity = entity + + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off + self._entity.set_level(args[0]) + elif command_id in (0x0001, 0x0005): # move, -with_on_off + # We should dim slowly -- for now, just step once + rate = args[1] + if args[0] == 0xff: + rate = 10 # Should read default move rate + self._entity.move_level(-rate if args[0] else rate) + elif command_id == 0x0002: # step + # Step (technically shouldn't change on/off) + self._entity.move_level(-args[1] if args[0] else args[1]) + + def attribute_update(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == 0: + self._entity.set_level(value) + + def zdo_command(self, *args, **kwargs): + """Handle ZDO commands on this cluster.""" + pass + + def __init__(self, **kwargs): + """Initialize Switch.""" + super().__init__(**kwargs) + self._state = True + self._level = 255 + from zigpy.zcl.clusters import general + self._out_listeners = { + general.OnOff.cluster_id: self.OnOffListener(self), + general.LevelControl.cluster_id: self.LevelListener(self), + } + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return {'level': self._state and self._level or 0} + + def move_level(self, change): + """Increment the level, setting state if appropriate.""" + if not self._state and change > 0: + self._level = 0 + self._level = min(255, max(0, self._level + change)) + self._state = bool(self._level) + self.async_schedule_update_ha_state() + + def set_level(self, level): + """Set the level, setting state if appropriate.""" + self._level = level + self._state = bool(self._level) + self.async_schedule_update_ha_state() + + def set_state(self, state): + """Set the state.""" + self._state = state + if self._level == 0: + self._level = 255 + self.async_schedule_update_ha_state() + + async def async_update(self): + """Retrieve latest state.""" + from zigpy.zcl.clusters.general import OnOff + result = await zha.safe_read( + self._endpoint.out_clusters[OnOff.cluster_id], ['on_off']) + self._state = result.get('on_off', self._state) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5321ec3d860..c1f92965198 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/camera/ """ import asyncio +import base64 import collections from contextlib import suppress from datetime import timedelta @@ -13,20 +14,20 @@ import logging import hashlib from random import SystemRandom -import aiohttp +import attr from aiohttp import web import async_timeout import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED +from homeassistant.components import websocket_api import homeassistant.helpers.config_validation as cv DOMAIN = 'camera' @@ -53,6 +54,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) _RND = SystemRandom() +FALLBACK_STREAM_INTERVAL = 1 # seconds +MIN_STREAM_INTERVAL = 0.5 # seconds + CAMERA_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -61,6 +65,20 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({ vol.Required(ATTR_FILENAME): cv.template }) +WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail' +SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + 'type': WS_TYPE_CAMERA_THUMBNAIL, + 'entity_id': cv.entity_id +}) + + +@attr.s +class Image: + """Represent an image.""" + + content_type = attr.ib(type=str) + content = attr.ib(type=bytes) + @bind_hass def enable_motion_detection(hass, entity_id=None): @@ -89,43 +107,40 @@ def async_snapshot(hass, filename, entity_id=None): @bind_hass -@asyncio.coroutine -def async_get_image(hass, entity_id, timeout=10): +async def async_get_image(hass, entity_id, timeout=10): """Fetch an image from a camera entity.""" - websession = async_get_clientsession(hass) - state = hass.states.get(entity_id) + component = hass.data.get(DOMAIN) - if state is None: - raise HomeAssistantError( - "No entity '{0}' for grab an image".format(entity_id)) + if component is None: + raise HomeAssistantError('Camera component not setup') - url = "{0}{1}".format( - hass.config.api.base_url, - state.attributes.get(ATTR_ENTITY_PICTURE) - ) + camera = component.get_entity(entity_id) - try: + if camera is None: + raise HomeAssistantError('Camera not found') + + with suppress(asyncio.CancelledError, asyncio.TimeoutError): with async_timeout.timeout(timeout, loop=hass.loop): - response = yield from websession.get(url) + image = await camera.async_camera_image() - if response.status != 200: - raise HomeAssistantError("Error {0} on {1}".format( - response.status, url)) + if image: + return Image(camera.content_type, image) - image = yield from response.read() - return image - - except (asyncio.TimeoutError, aiohttp.ClientError): - raise HomeAssistantError("Can't connect to {0}".format(url)) + raise HomeAssistantError('Unable to get image') @asyncio.coroutine def async_setup(hass, config): """Set up the camera component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = hass.data[DOMAIN] = \ + EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) hass.http.register_view(CameraImageView(component)) hass.http.register_view(CameraMjpegStream(component)) + hass.components.websocket_api.async_register_command( + WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail, + SCHEMA_WS_CAMERA_THUMBNAIL + ) yield from component.async_setup(config) @@ -252,19 +267,21 @@ class Camera(Entity): """ return self.hass.async_add_job(self.camera_image) - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_still_stream(self, request, interval): """Generate an HTTP MJPEG stream from camera images. This method must be run in the event loop. """ - response = web.StreamResponse() + if interval < MIN_STREAM_INTERVAL: + raise ValueError("Stream interval must be be > {}" + .format(MIN_STREAM_INTERVAL)) + response = web.StreamResponse() response.content_type = ('multipart/x-mixed-replace; ' 'boundary=--frameboundary') - yield from response.prepare(request) + await response.prepare(request) - async def write(img_bytes): + async def write_to_mjpeg_stream(img_bytes): """Write image to stream.""" await response.write(bytes( '--frameboundary\r\n' @@ -277,21 +294,21 @@ class Camera(Entity): try: while True: - img_bytes = yield from self.async_camera_image() + img_bytes = await self.async_camera_image() if not img_bytes: break if img_bytes and img_bytes != last_image: - yield from write(img_bytes) + await write_to_mjpeg_stream(img_bytes) # Chrome seems to always ignore first picture, # print it twice. if last_image is None: - yield from write(img_bytes) + await write_to_mjpeg_stream(img_bytes) last_image = img_bytes - yield from asyncio.sleep(.5) + await asyncio.sleep(interval) except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") @@ -299,7 +316,17 @@ class Camera(Entity): finally: if response is not None: - yield from response.write_eof() + await response.write_eof() + + async def handle_async_mjpeg_stream(self, request): + """Serve an HTTP MJPEG stream from the camera. + + This method can be overridden by camera plaforms to proxy + a direct stream from the camera. + This method must be run in the event loop. + """ + await self.handle_async_still_stream(request, + FALLBACK_STREAM_INTERVAL) @property def state(self): @@ -329,20 +356,20 @@ class Camera(Entity): @property def state_attributes(self): """Return the camera state attributes.""" - attr = { + attrs = { 'access_token': self.access_tokens[-1], } if self.model: - attr['model_name'] = self.model + attrs['model_name'] = self.model if self.brand: - attr['brand'] = self.brand + attrs['brand'] = self.brand if self.motion_detection_enabled: - attr['motion_detection'] = self.motion_detection_enabled + attrs['motion_detection'] = self.motion_detection_enabled - return attr + return attrs @callback def async_update_token(self): @@ -411,7 +438,40 @@ class CameraMjpegStream(CameraView): url = '/api/camera_proxy_stream/{entity_id}' name = 'api:camera:stream' - @asyncio.coroutine - def handle(self, request, camera): - """Serve camera image.""" - yield from camera.handle_async_mjpeg_stream(request) + async def handle(self, request, camera): + """Serve camera stream, possibly with interval.""" + interval = request.query.get('interval') + if interval is None: + await camera.handle_async_mjpeg_stream(request) + return + + try: + # Compose camera stream from stills + interval = float(request.query.get('interval')) + await camera.handle_async_still_stream(request, interval) + return + except ValueError: + return web.Response(status=400) + + +@callback +def websocket_camera_thumbnail(hass, connection, msg): + """Handle get camera thumbnail websocket command. + + Async friendly. + """ + async def send_camera_still(): + """Send a camera still.""" + try: + image = await async_get_image(hass, msg['entity_id']) + connection.send_message_outside(websocket_api.result_message( + msg['id'], { + 'content_type': image.content_type, + 'content': base64.b64encode(image.content).decode('utf-8') + } + )) + except HomeAssistantError: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'image_fetch_failed', 'Unable to fetch image')) + + hass.async_add_job(send_camera_still()) diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py index c3b4775b593..ef70692215d 100644 --- a/homeassistant/components/camera/bloomsky.py +++ b/homeassistant/components/camera/bloomsky.py @@ -9,7 +9,6 @@ import logging import requests from homeassistant.components.camera import Camera -from homeassistant.loader import get_component DEPENDENCIES = ['bloomsky'] @@ -17,7 +16,7 @@ DEPENDENCIES = ['bloomsky'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up access to BloomSky cameras.""" - bloomsky = get_component('bloomsky') + bloomsky = hass.components.bloomsky for device in bloomsky.BLOOMSKY.devices.values(): add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) diff --git a/homeassistant/components/camera/local_file.py b/homeassistant/components/camera/local_file.py index 95d24c7d42e..95eade48568 100644 --- a/homeassistant/components/camera/local_file.py +++ b/homeassistant/components/camera/local_file.py @@ -11,31 +11,44 @@ import os import voluptuous as vol from homeassistant.const import CONF_NAME -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import ( + Camera, CAMERA_SERVICE_SCHEMA, DOMAIN, PLATFORM_SCHEMA) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_FILE_PATH = 'file_path' - DEFAULT_NAME = 'Local File' +SERVICE_UPDATE_FILE_PATH = 'local_file_update_file_path' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FILE_PATH): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string }) +CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend({ + vol.Required(CONF_FILE_PATH): cv.string +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Camera that works with local files.""" file_path = config[CONF_FILE_PATH] + camera = LocalFile(config[CONF_NAME], file_path) - # check filepath given is readable - if not os.access(file_path, os.R_OK): - _LOGGER.warning("Could not read camera %s image from file: %s", - config[CONF_NAME], file_path) + def update_file_path_service(call): + """Update the file path.""" + file_path = call.data.get(CONF_FILE_PATH) + camera.update_file_path(file_path) + return True - add_devices([LocalFile(config[CONF_NAME], file_path)]) + hass.services.register( + DOMAIN, + SERVICE_UPDATE_FILE_PATH, + update_file_path_service, + schema=CAMERA_SERVICE_UPDATE_FILE_PATH) + + add_devices([camera]) class LocalFile(Camera): @@ -46,6 +59,7 @@ class LocalFile(Camera): super().__init__() self._name = name + self.check_file_path_access(file_path) self._file_path = file_path # Set content type of local file content, _ = mimetypes.guess_type(file_path) @@ -61,7 +75,26 @@ class LocalFile(Camera): _LOGGER.warning("Could not read camera %s image from file: %s", self._name, self._file_path) + def check_file_path_access(self, file_path): + """Check that filepath given is readable.""" + if not os.access(file_path, os.R_OK): + _LOGGER.warning("Could not read camera %s image from file: %s", + self._name, file_path) + + def update_file_path(self, file_path): + """Update the file_path.""" + self.check_file_path_access(file_path) + self._file_path = file_path + self.schedule_update_ha_state() + @property def name(self): """Return the name of this camera.""" return self._name + + @property + def device_state_attributes(self): + """Return the camera state attributes.""" + return { + 'file_path': self._file_path, + } diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index 48f2710ce2e..bf2dfe39bd8 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.const import CONF_VERIFY_SSL from homeassistant.components.netatmo import CameraData from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) -from homeassistant.loader import get_component from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['netatmo'] @@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up access to Netatmo cameras.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo home = config.get(CONF_HOME) verify_ssl = config.get(CONF_VERIFY_SSL, True) import lnetatmo diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index b548f3d1ada..544fd0e6b8a 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -24,6 +24,16 @@ snapshot: description: Template of a Filename. Variable is entity_id. example: '/tmp/snapshot_{{ entity_id }}' +local_file_update_file_path: + description: Update the file_path for a local_file camera. + fields: + entity_id: + description: Name(s) of entities to update. + example: 'camera.local_file' + file_path: + description: Path to the new image file. + example: '/images/newimage.jpg' + onvif_ptz: description: Pan/Tilt/Zoom service for ONVIF camera. fields: @@ -39,4 +49,3 @@ onvif_ptz: zoom: description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT" example: "ZOOM_IN" - diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index 5d54b39e773..49452662fc4 100644 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -13,7 +13,6 @@ from homeassistant.components.climate import ( STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) from homeassistant.util import Throttle -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['netatmo'] @@ -42,7 +41,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the NetAtmo Thermostat.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo device = config.get(CONF_RELAY) import lnetatmo diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index e73d043d366..8c1a9751c19 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -21,6 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util 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 .const import CONFIG_DIR, DOMAIN, SERVERS @@ -52,7 +53,8 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({ GOOGLE_ENTITY_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]) + vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ga_c.CONF_ROOM_HINT): cv.string, }) ASSISTANT_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py index 99da248b094..688df62ca6a 100644 --- a/homeassistant/components/cover/gogogate2.py +++ b/homeassistant/components/cover/gogogate2.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pygogogate2==0.0.3'] +REQUIREMENTS = ['pygogogate2==0.0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index c99076de851..20625143daf 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -79,5 +79,7 @@ class TahomaCover(TahomaDevice, CoverDevice): if self.tahoma_device.type == \ 'io:RollerShutterWithLowSpeedManagementIOComponent': self.apply_action('setPosition', 'secured') + elif self.tahoma_device.type == 'rts:BlindRTSComponent': + self.apply_action('my') else: self.apply_action('stopIdentify') diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json new file mode 100644 index 00000000000..91727cae257 --- /dev/null +++ b/homeassistant/components/deconz/.translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ" + }, + "error": { + "no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442 (\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: '80')" + }, + "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437" + }, + "link": { + "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"", + "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/cy.json b/homeassistant/components/deconz/.translations/cy.json new file mode 100644 index 00000000000..fff54bb3f6c --- /dev/null +++ b/homeassistant/components/deconz/.translations/cy.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Pont eisoes wedi'i ffurfweddu", + "no_bridges": "Dim pontydd deCONZ wedi eu darganfod", + "one_instance_only": "Elfen dim ond yn cefnogi enghraifft deCONZ" + }, + "error": { + "no_key": "Methu cael allwedd API" + }, + "step": { + "init": { + "data": { + "host": "Gwesteiwr", + "port": "Port (gwerth diofyn: '80')" + }, + "title": "Diffiniwch porth dad-adeiladu" + }, + "link": { + "description": "Datgloi eich porth deCONZ i gofrestru gyda Cynorthwydd Cartref.\n\n1. Ewch i osodiadau system deCONZ \n2. Bwyso botwm \"Datgloi porth\"", + "title": "Cysylltu \u00e2 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json new file mode 100644 index 00000000000..698f55c59ec --- /dev/null +++ b/homeassistant/components/deconz/.translations/da.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "host": "V\u00e6rt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json new file mode 100644 index 00000000000..9d3dc9e6e62 --- /dev/null +++ b/homeassistant/components/deconz/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge ist bereits konfiguriert", + "no_bridges": "Keine deCON-Bridges entdeckt", + "one_instance_only": "Komponente unterst\u00fctzt nur eine deCONZ-Instanz" + }, + "error": { + "no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (Standartwert : '80')" + }, + "title": "Definieren Sie den 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\"", + "title": "Mit deCONZ verbinden" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 7ea68af01c1..0009986d45f 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -1,26 +1,26 @@ { "config": { - "title": "deCONZ", - "step": { - "init": { - "title": "Define deCONZ gateway", - "data": { - "host": "Host", - "port": "Port (default value: '80')" - } - }, - "link": { - "title": "Link with deCONZ", - "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" - } - }, - "error": { - "no_key": "Couldn't get an API key" - }, "abort": { "already_configured": "Bridge is already configured", "no_bridges": "No deCONZ bridges discovered", "one_instance_only": "Component only supports one deCONZ instance" - } + }, + "error": { + "no_key": "Couldn't get an API key" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (default value: '80')" + }, + "title": "Define deCONZ gateway" + }, + "link": { + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button", + "title": "Link with deCONZ" + } + }, + "title": "deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json new file mode 100644 index 00000000000..42aab9c6d7e --- /dev/null +++ b/homeassistant/components/deconz/.translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat" + }, + "error": { + "no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt" + }, + "step": { + "init": { + "data": { + "host": "H\u00e1zigazda (Host)", + "port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')" + } + }, + "link": { + "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json new file mode 100644 index 00000000000..d6de1028218 --- /dev/null +++ b/homeassistant/components/deconz/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "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" + }, + "error": { + "no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8 (\uae30\ubcf8\uac12: '80')" + }, + "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758" + }, + "link": { + "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ", + "title": "deCONZ \uc640 \uc5f0\uacb0" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json new file mode 100644 index 00000000000..2a9dfc5e543 --- /dev/null +++ b/homeassistant/components/deconz/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge ass schon konfigur\u00e9iert", + "no_bridges": "Keng dECONZ bridges fonnt", + "one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng deCONZ Instanz" + }, + "error": { + "no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (Standard Wert: '80')" + }, + "title": "deCONZ gateway d\u00e9fin\u00e9ieren" + }, + "link": { + "description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen", + "title": "Link mat deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json new file mode 100644 index 00000000000..90d13bb39b4 --- /dev/null +++ b/homeassistant/components/deconz/.translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge is al geconfigureerd", + "no_bridges": "Geen deCONZ bruggen ontdekt", + "one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n deCONZ instance" + }, + "error": { + "no_key": "Kon geen API-sleutel ophalen" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Poort (standaard: '80')" + }, + "title": "Definieer deCONZ gateway" + }, + "link": { + "description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"", + "title": "Koppel met deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json new file mode 100644 index 00000000000..25e3b0b7d68 --- /dev/null +++ b/homeassistant/components/deconz/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Broen er allerede konfigurert", + "no_bridges": "Ingen deCONZ broer oppdaget", + "one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst" + }, + "error": { + "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" + }, + "step": { + "init": { + "data": { + "host": "Vert", + "port": "Port (standardverdi: '80')" + }, + "title": "Definer deCONZ-gatewayen" + }, + "link": { + "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen", + "title": "Koble til deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json new file mode 100644 index 00000000000..bb7488fcbec --- /dev/null +++ b/homeassistant/components/deconz/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Mostek jest ju\u017c skonfigurowany", + "no_bridges": "Nie odkryto mostk\u00f3w deCONZ", + "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ" + }, + "error": { + "no_key": "Nie mo\u017cna uzyska\u0107 klucza API" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (warto\u015b\u0107 domy\u015blna: \"80\")" + }, + "title": "Zdefiniuj bramk\u0119 deCONZ" + }, + "link": { + "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"", + "title": "Po\u0142\u0105cz z deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json new file mode 100644 index 00000000000..2a00c698691 --- /dev/null +++ b/homeassistant/components/deconz/.translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge j\u00e1 est\u00e1 configurada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json new file mode 100644 index 00000000000..b0dc6a8a4a8 --- /dev/null +++ b/homeassistant/components/deconz/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ" + }, + "error": { + "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: '80')" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" + }, + "link": { + "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb", + "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json new file mode 100644 index 00000000000..b738002b273 --- /dev/null +++ b/homeassistant/components/deconz/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Most je \u017ee nastavljen", + "no_bridges": "Ni odkritih mostov deCONZ", + "one_instance_only": "Komponenta podpira le en primerek deCONZ" + }, + "error": { + "no_key": "Klju\u010da API ni mogo\u010de dobiti" + }, + "step": { + "init": { + "data": { + "host": "Gostitelj", + "port": "Vrata (privzeta vrednost: '80')" + }, + "title": "Dolo\u010dite deCONZ prehod" + }, + "link": { + "description": "Odklenite va\u0161 deCONZ gateway za registracijo z Home Assistant-om. \n1. Pojdite v deCONT sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"", + "title": "Povezava z deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json new file mode 100644 index 00000000000..f41b5b5111c --- /dev/null +++ b/homeassistant/components/deconz/.translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u6865\u63a5\u5668\u5df2\u914d\u7f6e\u5b8c\u6210", + "no_bridges": "\u6ca1\u6709\u53d1\u73b0 deCONZ \u7684\u6865\u63a5\u8bbe\u5907", + "one_instance_only": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a deCONZ \u5b9e\u4f8b" + }, + "error": { + "no_key": "\u65e0\u6cd5\u83b7\u53d6 API \u5bc6\u94a5" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u673a", + "port": "\u7aef\u53e3\uff08\u9ed8\u8ba4\u503c\uff1a'80'\uff09" + }, + "title": "\u5b9a\u4e49 deCONZ \u7f51\u5173" + }, + "link": { + "description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae", + "title": "\u8fde\u63a5 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json new file mode 100644 index 00000000000..33be3846eb8 --- /dev/null +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", + "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b" + }, + "error": { + "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0\uff08\u9810\u8a2d\u503c\uff1a'80'\uff09" + }, + "title": "\u5b9a\u7fa9 deCONZ \u7db2\u95dc" + }, + "link": { + "description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215", + "title": "\u9023\u7d50\u81f3 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 064725eda95..47573be6add 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -7,17 +7,22 @@ https://home-assistant.io/components/deconz/ import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) -from homeassistant.core import callback -from homeassistant.helpers import ( - aiohttp_client, discovery, config_validation as cv) + CONF_API_KEY, CONF_EVENT, CONF_HOST, + CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import EventOrigin, callback +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) +from homeassistant.util import slugify from homeassistant.util.json import load_json # Loading the config flow file will register the flow from .config_flow import configured_hosts -from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER +from .const import ( + CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==36'] +REQUIREMENTS = ['pydeconz==37'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -27,6 +32,8 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +SERVICE_DECONZ = 'configure' + SERVICE_FIELD = 'field' SERVICE_ENTITY = 'entity' SERVICE_DATA = 'data' @@ -58,28 +65,27 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): - """Set up a deCONZ bridge for a config entry.""" - if DOMAIN in hass.data: - _LOGGER.error( - "Config entry failed since one deCONZ instance already exists") - return False - result = await async_setup_deconz(hass, None, entry.data) - if result: - return True - return False - - -async def async_setup_deconz(hass, config, deconz_config): - """Set up a deCONZ session. +async def async_setup_entry(hass, config_entry): + """Set up a deCONZ bridge for a config entry. Load config, group, light and sensor data for server information. Start websocket for push notification of state changes from deCONZ. """ - _LOGGER.debug("deCONZ config %s", deconz_config) from pydeconz import DeconzSession + if DOMAIN in hass.data: + _LOGGER.error( + "Config entry failed since one deCONZ instance already exists") + return False + + @callback + def async_add_device_callback(device_type, device): + """Called when a new device has been created in deCONZ.""" + async_dispatcher_send( + hass, 'deconz_new_{}'.format(device_type), [device]) + session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, session, **deconz_config) + deconz = DeconzSession(hass.loop, session, **config_entry.data, + async_add_device=async_add_device_callback) result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") @@ -87,10 +93,25 @@ async def async_setup_deconz(hass, config, deconz_config): hass.data[DOMAIN] = deconz hass.data[DATA_DECONZ_ID] = {} + hass.data[DATA_DECONZ_EVENT] = [] + hass.data[DATA_DECONZ_UNSUB] = [] for component in ['binary_sensor', 'light', 'scene', 'sensor']: - hass.async_add_job(discovery.async_load_platform( - hass, component, DOMAIN, {}, config)) + hass.async_add_job(hass.config_entries.async_forward_entry_setup( + config_entry, component)) + + @callback + def async_add_remote(sensors): + """Setup remote from deCONZ.""" + from pydeconz.sensor import SWITCH as DECONZ_REMOTE + for sensor in sensors: + if sensor.type in DECONZ_REMOTE: + hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor)) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote)) + + async_add_remote(deconz.sensors.values()) + deconz.start() async def async_configure(call): @@ -121,7 +142,7 @@ async def async_setup_deconz(hass, config, deconz_config): return await deconz.async_put_state(field, data) hass.services.async_register( - DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) + DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA) @callback def deconz_shutdown(event): @@ -136,3 +157,43 @@ async def async_setup_deconz(hass, config, deconz_config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown) return True + + +async def async_unload_entry(hass, config_entry): + """Unload deCONZ config entry.""" + deconz = hass.data.pop(DOMAIN) + hass.services.async_remove(DOMAIN, SERVICE_DECONZ) + deconz.close() + for component in ['binary_sensor', 'light', 'scene', 'sensor']: + await hass.config_entries.async_forward_entry_unload( + config_entry, component) + dispatchers = hass.data[DATA_DECONZ_UNSUB] + for unsub_dispatcher in dispatchers: + unsub_dispatcher() + hass.data[DATA_DECONZ_UNSUB] = [] + hass.data[DATA_DECONZ_EVENT] = [] + hass.data[DATA_DECONZ_ID] = [] + return True + + +class DeconzEvent(object): + """When you want signals instead of entities. + + Stateless sensors such as remotes are expected to generate an event + instead of a sensor entity in hass. + """ + + def __init__(self, hass, device): + """Register callback that will be used for signals.""" + self._hass = hass + self._device = device + self._device.register_async_callback(self.async_update_callback) + self._event = 'deconz_{}'.format(CONF_EVENT) + self._id = slugify(self._device.name) + + @callback + def async_update_callback(self, reason): + """Fire the event if reason is that state is updated.""" + if reason['state']: + data = {CONF_ID: self._id, CONF_EVENT: self._device.state} + self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index c5820c971f6..48e5ea75d68 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -5,4 +5,6 @@ _LOGGER = logging.getLogger('homeassistant.components.deconz') DOMAIN = 'deconz' CONFIG_FILE = 'deconz.conf' +DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' +DATA_DECONZ_UNSUB = 'deconz_dispatchers' diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index a1297c5c118..641ade7308b 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -16,7 +16,6 @@ from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change) from homeassistant.helpers.sun import is_up, get_astral_event_next -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv DOMAIN = 'device_sun_light_trigger' @@ -48,9 +47,9 @@ CONFIG_SCHEMA = vol.Schema({ def async_setup(hass, config): """Set up the triggers to control lights based on device presence.""" logger = logging.getLogger(__name__) - device_tracker = get_component('device_tracker') - group = get_component('group') - light = get_component('light') + device_tracker = hass.components.device_tracker + group = hass.components.group + light = hass.components.light conf = config[DOMAIN] disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF) light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) @@ -58,14 +57,14 @@ def async_setup(hass, config): device_group = conf.get( CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES) device_entity_ids = group.get_entity_ids( - hass, device_group, device_tracker.DOMAIN) + device_group, device_tracker.DOMAIN) if not device_entity_ids: logger.error("No devices found to track") return False # Get the light IDs from the specified group - light_ids = group.get_entity_ids(hass, light_group, light.DOMAIN) + light_ids = group.get_entity_ids(light_group, light.DOMAIN) if not light_ids: logger.error("No lights found to turn on") @@ -85,9 +84,9 @@ def async_setup(hass, config): def async_turn_on_before_sunset(light_id): """Turn on lights.""" - if not device_tracker.is_on(hass) or light.is_on(hass, light_id): + if not device_tracker.is_on() or light.is_on(light_id): return - light.async_turn_on(hass, light_id, + light.async_turn_on(light_id, transition=LIGHT_TRANSITION_TIME.seconds, profile=light_profile) @@ -129,7 +128,7 @@ def async_setup(hass, config): @callback def check_light_on_dev_state_change(entity, old_state, new_state): """Handle tracked device state changes.""" - lights_are_on = group.is_on(hass, light_group) + lights_are_on = group.is_on(light_group) light_needed = not (lights_are_on or is_up(hass)) # These variables are needed for the elif check @@ -139,7 +138,7 @@ def async_setup(hass, config): # Do we need lights? if light_needed: logger.info("Home coming event for %s. Turning lights on", entity) - light.async_turn_on(hass, light_ids, profile=light_profile) + light.async_turn_on(light_ids, profile=light_profile) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -152,7 +151,7 @@ def async_setup(hass, config): # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - light.async_turn_on(hass, light_id) + light.async_turn_on(light_id) else: # If this light didn't happen to be turned on yet so @@ -169,12 +168,12 @@ def async_setup(hass, config): @callback def turn_off_lights_when_all_leave(entity, old_state, new_state): """Handle device group state change.""" - if not group.is_on(hass, light_group): + if not group.is_on(light_group): return logger.info( "Everyone has left but there are lights on. Turning them off") - light.async_turn_off(hass, light_ids) + light.async_turn_off(light_ids) async_track_state_change( hass, device_group, turn_off_lights_when_all_leave, diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index b24f7784faf..e1dd52a28ea 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,6 +15,7 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group, zone +from homeassistant.components.zone.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -23,7 +24,6 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv -from homeassistant.loader import get_component import homeassistant.util as util from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util @@ -321,7 +321,7 @@ class DeviceTracker(object): # During init, we ignore the group if self.group and self.track_new: self.group.async_set_group( - self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, + util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) self.hass.bus.async_fire(EVENT_NEW_DEVICE, { @@ -356,9 +356,9 @@ class DeviceTracker(object): entity_ids = [dev.entity_id for dev in self.devices.values() if dev.track] - self.group = get_component('group') + self.group = self.hass.components.group self.group.async_set_group( - self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, + util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids) @callback @@ -541,7 +541,7 @@ class Device(Entity): elif self.location_name: self._state = self.location_name elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: - zone_state = zone.async_active_zone( + zone_state = async_active_zone( self.hass, self.gps[0], self.gps[1], self.gps_accuracy) if zone_state is None: self._state = STATE_NOT_HOME diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index d1e59293365..1d0058ed229 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -19,7 +19,7 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['locationsharinglib==1.2.1'] +REQUIREMENTS = ['locationsharinglib==1.2.2'] CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' @@ -79,5 +79,6 @@ class GoogleMapsScanner(object): gps=(person.latitude, person.longitude), picture=person.picture_url, source_type=SOURCE_TYPE_GPS, + gps_accuracy=person.accuracy, attributes=attrs ) diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 1952e6d676d..68ea9ac88ae 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -4,7 +4,6 @@ Support for the GPSLogger platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.gpslogger/ """ -import asyncio import logging from hmac import compare_digest @@ -22,6 +21,7 @@ from homeassistant.components.http import ( from homeassistant.components.device_tracker import ( # NOQA DOMAIN, PLATFORM_SCHEMA ) +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -32,8 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType, + async_see, discovery_info=None): """Set up an endpoint for the GPSLogger application.""" hass.http.register_view(GPSLoggerView(async_see, config)) @@ -54,8 +54,7 @@ class GPSLoggerView(HomeAssistantView): # password is set self.requires_auth = self._password is None - @asyncio.coroutine - def get(self, request: Request): + async def get(self, request: Request): """Handle for GPSLogger message received as GET.""" hass = request.app['hass'] data = request.query diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 781e3674550..5d40f5d533a 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner) -from homeassistant.components.zone import active_zone +from homeassistant.components.zone.zone import active_zone from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 25d5d38b2a7..0e48e3072b2 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -12,21 +12,27 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, + CONF_DEVICES, CONF_EXCLUDE) -REQUIREMENTS = ['pynetgear==0.3.3'] +REQUIREMENTS = ['pynetgear==0.4.0'] _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = 'routerlogin.net' -DEFAULT_USER = 'admin' -DEFAULT_PORT = 5000 +CONF_APS = 'accesspoints' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USER): cv.string, + vol.Optional(CONF_HOST, default=''): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_USERNAME, default=''): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port + vol.Optional(CONF_PORT, default=None): vol.Any(None, cv.port), + vol.Optional(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCLUDE, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_APS, default=[]): + vol.All(cv.ensure_list, [cv.string]), }) @@ -34,11 +40,16 @@ def get_scanner(hass, config): """Validate the configuration and returns a Netgear scanner.""" info = config[DOMAIN] host = info.get(CONF_HOST) + ssl = info.get(CONF_SSL) username = info.get(CONF_USERNAME) password = info.get(CONF_PASSWORD) port = info.get(CONF_PORT) + devices = info.get(CONF_DEVICES) + excluded_devices = info.get(CONF_EXCLUDE) + accesspoints = info.get(CONF_APS) - scanner = NetgearDeviceScanner(host, username, password, port) + scanner = NetgearDeviceScanner(host, ssl, username, password, port, + devices, excluded_devices, accesspoints) return scanner if scanner.success_init else None @@ -46,16 +57,21 @@ def get_scanner(hass, config): class NetgearDeviceScanner(DeviceScanner): """Queries a Netgear wireless router using the SOAP-API.""" - def __init__(self, host, username, password, port): + def __init__(self, host, ssl, username, password, port, devices, + excluded_devices, accesspoints): """Initialize the scanner.""" import pynetgear + self.tracked_devices = devices + self.excluded_devices = excluded_devices + self.tracked_accesspoints = accesspoints + self.last_results = [] - self._api = pynetgear.Netgear(password, host, username, port) + self._api = pynetgear.Netgear(password, host, username, port, ssl) _LOGGER.info("Logging in") - results = self._api.get_attached_devices() + results = self.get_attached_devices() self.success_init = results is not None @@ -68,15 +84,50 @@ class NetgearDeviceScanner(DeviceScanner): """Scan for new devices and return a list with found device IDs.""" self._update_info() - return (device.mac for device in self.last_results) + devices = [] + + for dev in self.last_results: + tracked = (not self.tracked_devices or + dev.mac in self.tracked_devices or + dev.name in self.tracked_devices) + tracked = tracked and (not self.excluded_devices or not( + dev.mac in self.excluded_devices or + dev.name in self.excluded_devices)) + if tracked: + devices.append(dev.mac) + if (self.tracked_accesspoints and + dev.conn_ap_mac in self.tracked_accesspoints): + devices.append(dev.mac + "_" + dev.conn_ap_mac) + + return devices def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - try: - return next(result.name for result in self.last_results - if result.mac == device) - except StopIteration: - return None + """Return the name of the given device or the MAC if we don't know.""" + parts = device.split("_") + mac = parts[0] + ap_mac = None + if len(parts) > 1: + ap_mac = parts[1] + + name = None + for dev in self.last_results: + if dev.mac == mac: + name = dev.name + break + + if not name or name == "--": + name = mac + + if ap_mac: + ap_name = "Router" + for dev in self.last_results: + if dev.mac == ap_mac: + ap_name = dev.name + break + + return name + " on " + ap_name + + return name def _update_info(self): """Retrieve latest information from the Netgear router. @@ -88,9 +139,21 @@ class NetgearDeviceScanner(DeviceScanner): _LOGGER.info("Scanning") - results = self._api.get_attached_devices() + results = self.get_attached_devices() if results is None: _LOGGER.warning("Error scanning devices") self.last_results = results or [] + + def get_attached_devices(self): + """ + List attached devices with pynetgear. + + The v2 method takes more time and is more heavy on the router + so we only use it if we need connected AP info. + """ + if self.tracked_accesspoints: + return self._api.get_attached_devices_2() + + return self._api.get_attached_devices() diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 3d7ef5cef6e..f265014657b 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -207,7 +207,7 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params): try: res = requests.post(url, data=data, timeout=5) - except requests.exceptions.Timeout: + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): return if res.status_code == 200: diff --git a/homeassistant/components/dialogflow.py b/homeassistant/components/dialogflow.py index 63205c5479c..7a0918aab25 100644 --- a/homeassistant/components/dialogflow.py +++ b/homeassistant/components/dialogflow.py @@ -4,7 +4,6 @@ Support for Dialogflow webhook. For more details about this component, please refer to the documentation at https://home-assistant.io/components/dialogflow/ """ -import asyncio import logging import voluptuous as vol @@ -37,8 +36,7 @@ class DialogFlowError(HomeAssistantError): """Raised when a DialogFlow error happens.""" -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up Dialogflow component.""" hass.http.register_view(DialogflowIntentsView) @@ -51,16 +49,15 @@ class DialogflowIntentsView(HomeAssistantView): url = INTENTS_API_ENDPOINT name = 'api:dialogflow' - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle Dialogflow.""" hass = request.app['hass'] - message = yield from request.json() + message = await request.json() _LOGGER.debug("Received Dialogflow request: %s", message) try: - response = yield from async_handle_message(hass, message) + response = await async_handle_message(hass, message) return b'' if response is None else self.json(response) except DialogFlowError as err: @@ -93,8 +90,7 @@ def dialogflow_error_response(hass, message, error): return dialogflow_response.as_dict() -@asyncio.coroutine -def async_handle_message(hass, message): +async def async_handle_message(hass, message): """Handle a DialogFlow message.""" req = message.get('result') action_incomplete = req['actionIncomplete'] @@ -110,7 +106,7 @@ def async_handle_message(hass, message): raise DialogFlowError( "You have not defined an action in your Dialogflow intent.") - intent_response = yield from intent.async_handle( + intent_response = await intent.async_handle( hass, DOMAIN, action, {key: {'value': value} for key, value in parameters.items()}) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index f0ebcba8366..65d0a1c76f3 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.3.1'] +REQUIREMENTS = ['netdisco==1.4.1'] DOMAIN = 'discovery' @@ -79,6 +79,7 @@ SERVICE_HANDLERS = { 'bluesound': ('media_player', 'bluesound'), 'songpal': ('media_player', 'songpal'), 'kodi': ('media_player', 'kodi'), + 'volumio': ('media_player', 'volumio'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index 7ae4ec862bb..3478d5cd08e 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -22,7 +22,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pyeight==0.0.7'] +REQUIREMENTS = ['pyeight==0.0.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index fa558cf299f..fd7f7147fdb 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json from .hue_api import ( HueUsernameView, HueAllLightsStateView, HueOneLightStateView, - HueOneLightChangeView) + HueOneLightChangeView, HueGroupView) from .upnp import DescriptionXmlView, UPNPResponderThread DOMAIN = 'emulated_hue' @@ -104,6 +104,7 @@ def setup(hass, yaml_config): server.register_view(HueAllLightsStateView(config)) server.register_view(HueOneLightStateView(config)) server.register_view(HueOneLightChangeView(config)) + server.register_view(HueGroupView(config)) upnp_listener = UPNPResponderThread( config.host_ip_addr, config.listen_port, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 5d97ef3cea4..2b74984e4ca 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -51,6 +51,29 @@ class HueUsernameView(HomeAssistantView): return self.json([{'success': {'username': '12345678901234567890'}}]) +class HueGroupView(HomeAssistantView): + """Group handler to get Logitech Pop working.""" + + url = '/api/{username}/groups/0/action' + name = 'emulated_hue:groups:state' + requires_auth = False + + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def put(self, request, username): + """Process a request to make the Logitech Pop working.""" + return self.json([{ + 'error': { + 'address': '/groups/0/action/scene', + 'type': 7, + 'description': 'invalid value, dummy for parameter, scene' + } + }]) + + class HueAllLightsStateView(HomeAssistantView): """Handle requests for getting and setting info about entities.""" diff --git a/homeassistant/components/fan/insteon_plm.py b/homeassistant/components/fan/insteon_plm.py index f30abdbaa30..0911295d090 100644 --- a/homeassistant/components/fan/insteon_plm.py +++ b/homeassistant/components/fan/insteon_plm.py @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 95ff587c613..6fa506edec6 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT fans. For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -19,6 +18,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, @@ -77,8 +77,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up the MQTT fan platform.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -149,10 +149,9 @@ class MqttFan(MqttAvailability, FanEntity): self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() templates = {} for key, tpl in list(self._templates.items()): @@ -173,7 +172,7 @@ class MqttFan(MqttAvailability, FanEntity): self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) @@ -190,7 +189,7 @@ class MqttFan(MqttAvailability, FanEntity): self.async_schedule_update_ha_state() if self._topic[CONF_SPEED_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received, self._qos) self._speed = SPEED_OFF @@ -206,7 +205,7 @@ class MqttFan(MqttAvailability, FanEntity): self.async_schedule_update_ha_state() if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC], oscillation_received, self._qos) self._oscillation = False @@ -251,8 +250,7 @@ class MqttFan(MqttAvailability, FanEntity): """Return the oscillation state.""" return self._oscillation - @asyncio.coroutine - def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the entity. This method is a coroutine. @@ -261,10 +259,9 @@ class MqttFan(MqttAvailability, FanEntity): self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload[STATE_ON], self._qos, self._retain) if speed: - 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 off the entity. This method is a coroutine. @@ -273,8 +270,7 @@ class MqttFan(MqttAvailability, FanEntity): self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload[STATE_OFF], self._qos, self._retain) - @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. This method is a coroutine. @@ -299,8 +295,7 @@ class MqttFan(MqttAvailability, FanEntity): self._speed = speed self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_oscillate(self, oscillating: bool) -> None: + async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation. This method is a coroutine. diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py new file mode 100644 index 00000000000..31b335eb2bc --- /dev/null +++ b/homeassistant/components/fan/template.py @@ -0,0 +1,324 @@ +""" +Support for Template fans. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/fan.template/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import ( + CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, + STATE_ON, STATE_OFF, MATCH_ALL, EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN) + +from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, + SPEED_HIGH, SUPPORT_SET_SPEED, + SUPPORT_OSCILLATE, FanEntity, + ATTR_SPEED, ATTR_OSCILLATING, + ENTITY_ID_FORMAT) + +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.script import Script + +_LOGGER = logging.getLogger(__name__) + +CONF_FANS = 'fans' +CONF_SPEED_LIST = 'speeds' +CONF_SPEED_TEMPLATE = 'speed_template' +CONF_OSCILLATING_TEMPLATE = 'oscillating_template' +CONF_ON_ACTION = 'turn_on' +CONF_OFF_ACTION = 'turn_off' +CONF_SET_SPEED_ACTION = 'set_speed' +CONF_SET_OSCILLATING_ACTION = 'set_oscillating' + +_VALID_STATES = [STATE_ON, STATE_OFF] +_VALID_OSC = [True, False] + +FAN_SCHEMA = vol.Schema({ + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, + + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + + vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, + + vol.Optional( + CONF_SPEED_LIST, + default=[SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + ): cv.ensure_list, + + vol.Optional(CONF_ENTITY_ID): cv.entity_ids +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FANS): vol.Schema({cv.slug: FAN_SCHEMA}), +}) + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None +): + """Set up the Template Fans.""" + fans = [] + + for device, device_config in config[CONF_FANS].items(): + friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) + + state_template = device_config[CONF_VALUE_TEMPLATE] + speed_template = device_config.get(CONF_SPEED_TEMPLATE) + oscillating_template = device_config.get( + CONF_OSCILLATING_TEMPLATE + ) + + on_action = device_config[CONF_ON_ACTION] + off_action = device_config[CONF_OFF_ACTION] + set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) + set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) + + speed_list = device_config[CONF_SPEED_LIST] + + entity_ids = set() + manual_entity_ids = device_config.get(CONF_ENTITY_ID) + + for template in (state_template, speed_template, oscillating_template): + if template is None: + continue + template.hass = hass + + if entity_ids == MATCH_ALL or manual_entity_ids is not None: + continue + + template_entity_ids = template.extract_entities() + if template_entity_ids == MATCH_ALL: + entity_ids = MATCH_ALL + else: + entity_ids |= set(template_entity_ids) + + if manual_entity_ids is not None: + entity_ids = manual_entity_ids + elif entity_ids != MATCH_ALL: + entity_ids = list(entity_ids) + + fans.append( + TemplateFan( + hass, device, friendly_name, + state_template, speed_template, oscillating_template, + on_action, off_action, set_speed_action, + set_oscillating_action, speed_list, entity_ids + ) + ) + + async_add_devices(fans) + + +class TemplateFan(FanEntity): + """A template fan component.""" + + def __init__(self, hass, device_id, friendly_name, + state_template, speed_template, oscillating_template, + on_action, off_action, set_speed_action, + set_oscillating_action, speed_list, entity_ids): + """Initialize the fan.""" + self.hass = hass + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, device_id, hass=hass) + self._name = friendly_name + + self._template = state_template + self._speed_template = speed_template + self._oscillating_template = oscillating_template + self._supported_features = 0 + + self._on_script = Script(hass, on_action) + self._off_script = Script(hass, off_action) + + self._set_speed_script = None + if set_speed_action: + self._set_speed_script = Script(hass, set_speed_action) + + self._set_oscillating_script = None + if set_oscillating_action: + self._set_oscillating_script = Script(hass, set_oscillating_action) + + self._state = STATE_OFF + self._speed = None + self._oscillating = None + + self._template.hass = self.hass + if self._speed_template: + self._speed_template.hass = self.hass + self._supported_features |= SUPPORT_SET_SPEED + if self._oscillating_template: + self._oscillating_template.hass = self.hass + self._supported_features |= SUPPORT_OSCILLATE + + self._entities = entity_ids + # List of valid speeds + self._speed_list = speed_list + + @property + def name(self): + """Return the display name of this fan.""" + return self._name + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def speed_list(self: ToggleEntity) -> list: + """Get the list of available speeds.""" + return self._speed_list + + @property + def is_on(self): + """Return true if device is on.""" + return self._state == STATE_ON + + @property + def speed(self): + """Return the current speed.""" + return self._speed + + @property + def oscillating(self): + """Return the oscillation state.""" + return self._oscillating + + @property + def should_poll(self): + """Return the polling state.""" + return False + + # pylint: disable=arguments-differ + async def async_turn_on(self, speed: str = None) -> None: + """Turn on the fan.""" + await self._on_script.async_run() + self._state = STATE_ON + + if speed is not None: + await self.async_set_speed(speed) + + # pylint: disable=arguments-differ + async def async_turn_off(self) -> None: + """Turn off the fan.""" + await self._off_script.async_run() + self._state = STATE_OFF + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + if self._set_speed_script is None: + return + + if speed in self._speed_list: + self._speed = speed + await self._set_speed_script.async_run({ATTR_SPEED: speed}) + else: + _LOGGER.error( + 'Received invalid speed: %s. ' + + 'Expected: %s.', + speed, self._speed_list) + + async def async_oscillate(self, oscillating: bool) -> None: + """Set oscillation of the fan.""" + if self._set_oscillating_script is None: + return + + await self._set_oscillating_script.async_run( + {ATTR_OSCILLATING: oscillating} + ) + self._oscillating = oscillating + + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def template_fan_state_listener(entity, old_state, new_state): + """Handle target device state changes.""" + self.async_schedule_update_ha_state(True) + + @callback + def template_fan_startup(event): + """Update template on startup.""" + self.hass.helpers.event.async_track_state_change( + self._entities, template_fan_state_listener) + + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, template_fan_startup) + + async def async_update(self): + """Update the state from the template.""" + # Update state + try: + state = self._template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + state = None + self._state = None + + # Validate state + if state in _VALID_STATES: + self._state = state + elif state == STATE_UNKNOWN: + self._state = None + else: + _LOGGER.error( + 'Received invalid fan is_on state: %s. ' + + 'Expected: %s.', + state, ', '.join(_VALID_STATES)) + self._state = None + + # Update speed if 'speed_template' is configured + if self._speed_template is not None: + try: + speed = self._speed_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + speed = None + self._state = None + + # Validate speed + if speed in self._speed_list: + self._speed = speed + elif speed == STATE_UNKNOWN: + self._speed = None + else: + _LOGGER.error( + 'Received invalid speed: %s. ' + + 'Expected: %s.', + speed, self._speed_list) + self._speed = None + + # Update oscillating if 'oscillating_template' is configured + if self._oscillating_template is not None: + try: + oscillating = self._oscillating_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + self._state = None + + # Validate osc + if oscillating == 'True' or oscillating is True: + self._oscillating = True + elif oscillating == 'False' or oscillating is False: + self._oscillating = False + elif oscillating == STATE_UNKNOWN: + self._oscillating = None + else: + _LOGGER.error( + 'Received invalid oscillating: %s. ' + + 'Expected: True/False.', oscillating) + self._oscillating = None diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 4a181c00c02..0d267077991 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -16,15 +16,16 @@ import voluptuous as vol import jinja2 import homeassistant.helpers.config_validation as cv -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.const import KEY_AUTHENTICATED +from homeassistant.components import websocket_api from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180426.0'] +REQUIREMENTS = ['home-assistant-frontend==20180509.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -94,6 +95,10 @@ SERVICE_RELOAD_THEMES = 'reload_themes' SERVICE_SET_THEME_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, }) +WS_TYPE_GET_PANELS = 'get_panels' +SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_PANELS, +}) class AbstractPanel: @@ -291,6 +296,8 @@ def add_manifest_json_key(key, val): @asyncio.coroutine def async_setup(hass, config): """Set up the serving of the frontend.""" + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -597,3 +604,19 @@ def _is_latest(js_option, request): useragent = request.headers.get('User-Agent') return useragent and hass_frontend.version(useragent) + + +@callback +def websocket_handle_get_panels(hass, connection, msg): + """Handle get panels command. + + Async friendly. + """ + panels = { + panel: + connection.hass.data[DATA_PANELS][panel].to_response( + connection.hass, connection.request) + for panel in connection.hass.data[DATA_PANELS]} + + connection.to_write.put_nowait(websocket_api.result_message( + msg['id'], panels)) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 676654c2c91..1c6d11a7c99 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -70,8 +70,7 @@ def request_sync(hass): hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC) -@asyncio.coroutine -def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): +async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) agent_user_id = config.get(CONF_AGENT_USER_ID) @@ -79,20 +78,19 @@ def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): hass.http.register_view(GoogleAssistantAuthView(hass, config)) async_register_http(hass, config) - @asyncio.coroutine - def request_sync_service_handler(call): + async def request_sync_service_handler(call): """Handle request sync service calls.""" websession = async_get_clientsession(hass) try: with async_timeout.timeout(5, loop=hass.loop): - res = yield from websession.post( + res = await websession.post( REQUEST_SYNC_BASE_URL, params={'key': api_key}, json={'agent_user_id': agent_user_id}) _LOGGER.info("Submitted request_sync request to Google") res.raise_for_status() except aiohttp.ClientResponseError: - body = yield from res.read() + body = await res.read() _LOGGER.error( 'request_sync request failed: %d %s', res.status, body) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py index 1ed27403797..a21dd0e6738 100644 --- a/homeassistant/components/google_assistant/auth.py +++ b/homeassistant/components/google_assistant/auth.py @@ -1,6 +1,5 @@ """Google Assistant OAuth View.""" -import asyncio import logging # Typing imports @@ -44,8 +43,7 @@ class GoogleAssistantAuthView(HomeAssistantView): self.client_id = cfg.get(CONF_CLIENT_ID) self.access_token = cfg.get(CONF_ACCESS_TOKEN) - @asyncio.coroutine - def get(self, request: Request) -> Response: + async def get(self, request: Request) -> Response: """Handle oauth token request.""" query = request.query redirect_uri = query.get('redirect_uri') diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 0caea3aadf4..0ea5f7d9fa4 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -4,7 +4,6 @@ Support for Google Actions Smart Home Control. For more details about this component, please refer to the documentation at https://home-assistant.io/components/google_assistant/ """ -import asyncio import logging from aiohttp.hdrs import AUTHORIZATION @@ -77,14 +76,13 @@ class GoogleAssistantView(HomeAssistantView): self.access_token = access_token self.gass_config = gass_config - @asyncio.coroutine - def post(self, request: Request) -> Response: + async def post(self, request: Request) -> Response: """Handle Google Assistant requests.""" auth = request.headers.get(AUTHORIZATION, None) if 'Bearer {}'.format(self.access_token) != auth: return self.json_message("missing authorization", status_code=401) - message = yield from request.json() # type: dict - result = yield from async_handle_message( + message = await request.json() # type: dict + result = await async_handle_message( request.app['hass'], self.gass_config, message) return self.json(result) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 67ad8066aff..f70a2d29351 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -245,34 +245,31 @@ def get_entity_ids(hass, entity_id, domain_filter=None): if ent_id.startswith(domain_filter)] -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up all groups found defined in the configuration.""" component = hass.data.get(DOMAIN) if component is None: component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) - yield from _async_process_config(hass, config, component) + await _async_process_config(hass, config, component) - @asyncio.coroutine - def reload_service_handler(service): + async def reload_service_handler(service): """Remove all user-defined groups and load new ones from config.""" auto = list(filter(lambda e: not e.user_defined, component.entities)) - conf = yield from component.async_prepare_reload() + conf = await component.async_prepare_reload() if conf is None: return - yield from _async_process_config(hass, conf, component) + await _async_process_config(hass, conf, component) - yield from component.async_add_entities(auto) + await component.async_add_entities(auto) hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA) - @asyncio.coroutine - def groups_service_handler(service): + async def groups_service_handler(service): """Handle dynamic group service functions.""" object_id = service.data[ATTR_OBJECT_ID] entity_id = ENTITY_ID_FORMAT.format(object_id) @@ -287,7 +284,7 @@ def async_setup(hass, config): ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL ) if service.data.get(attr) is not None} - yield from Group.async_create_group( + await Group.async_create_group( hass, service.data.get(ATTR_NAME, object_id), object_id=object_id, entity_ids=entity_ids, @@ -308,11 +305,11 @@ def async_setup(hass, config): if ATTR_ADD_ENTITIES in service.data: delta = service.data[ATTR_ADD_ENTITIES] entity_ids = set(group.tracking) | set(delta) - yield from group.async_update_tracked_entity_ids(entity_ids) + await group.async_update_tracked_entity_ids(entity_ids) if ATTR_ENTITIES in service.data: entity_ids = service.data[ATTR_ENTITIES] - yield from group.async_update_tracked_entity_ids(entity_ids) + await group.async_update_tracked_entity_ids(entity_ids) if ATTR_NAME in service.data: group.name = service.data[ATTR_NAME] @@ -335,13 +332,13 @@ def async_setup(hass, config): need_update = True if need_update: - yield from group.async_update_ha_state() + await group.async_update_ha_state() return # remove group if service.service == SERVICE_REMOVE: - yield from component.async_remove_entity(entity_id) + await component.async_remove_entity(entity_id) hass.services.async_register( DOMAIN, SERVICE_SET, groups_service_handler, @@ -351,8 +348,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_REMOVE, groups_service_handler, schema=REMOVE_SERVICE_SCHEMA) - @asyncio.coroutine - def visibility_service_handler(service): + async def visibility_service_handler(service): """Change visibility of a group.""" visible = service.data.get(ATTR_VISIBLE) @@ -363,7 +359,7 @@ def async_setup(hass, config): tasks.append(group.async_update_ha_state()) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, @@ -372,8 +368,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _async_process_config(hass, config, component): +async def _async_process_config(hass, config, component): """Process group configuration.""" for object_id, conf in config.get(DOMAIN, {}).items(): name = conf.get(CONF_NAME, object_id) @@ -384,7 +379,7 @@ def _async_process_config(hass, config, component): # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. - yield from Group.async_create_group( + await Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, control=control, object_id=object_id) @@ -428,10 +423,9 @@ class Group(Entity): hass.loop).result() @staticmethod - @asyncio.coroutine - def async_create_group(hass, name, entity_ids=None, user_defined=True, - visible=True, icon=None, view=False, control=None, - object_id=None): + async def async_create_group(hass, name, entity_ids=None, + user_defined=True, visible=True, icon=None, + view=False, control=None, object_id=None): """Initialize a group. This method must be run in the event loop. @@ -453,7 +447,7 @@ class Group(Entity): component = hass.data[DOMAIN] = \ EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([group], True) + await component.async_add_entities([group], True) return group @@ -520,17 +514,16 @@ class Group(Entity): self.async_update_tracked_entity_ids(entity_ids), self.hass.loop ).result() - @asyncio.coroutine - def async_update_tracked_entity_ids(self, entity_ids): + async def async_update_tracked_entity_ids(self, entity_ids): """Update the member entity IDs. This method must be run in the event loop. """ - yield from self.async_stop() + await self.async_stop() self.tracking = tuple(ent_id.lower() for ent_id in entity_ids) self.group_on, self.group_off = None, None - yield from self.async_update_ha_state(True) + await self.async_update_ha_state(True) self.async_start() @callback @@ -544,8 +537,7 @@ class Group(Entity): self.hass, self.tracking, self._async_state_changed_listener ) - @asyncio.coroutine - def async_stop(self): + async def async_stop(self): """Unregister the group from Home Assistant. This method must be run in the event loop. @@ -554,27 +546,24 @@ class Group(Entity): self._async_unsub_state_changed() self._async_unsub_state_changed = None - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Query all members and determine current group state.""" self._state = STATE_UNKNOWN self._async_update_group_state() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Callback when added to HASS.""" if self.tracking: self.async_start() - @asyncio.coroutine - def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self): """Callback when removed from HASS.""" if self._async_unsub_state_changed: self._async_unsub_state_changed() self._async_unsub_state_changed = None - @asyncio.coroutine - def _async_state_changed_listener(self, entity_id, old_state, new_state): + async def _async_state_changed_listener(self, entity_id, old_state, + new_state): """Respond to a member state changing. This method must be run in the event loop. @@ -584,7 +573,7 @@ class Group(Entity): return self._async_update_group_state(new_state) - yield from self.async_update_ha_state() + await self.async_update_ha_state() @property def _tracking_states(self): diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index b5ac37b1451..c27e394ce28 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -4,7 +4,6 @@ Provide pre-made queries on top of the recorder component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/history/ """ -import asyncio from collections import defaultdict from datetime import timedelta from itertools import groupby @@ -259,8 +258,7 @@ def get_state(hass, utc_point_in_time, entity_id, run=None): return states[0] if states else None -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the history hooks.""" filters = Filters() conf = config.get(DOMAIN, {}) @@ -275,7 +273,7 @@ def async_setup(hass, config): use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'history', 'history', 'mdi:poll-box') return True @@ -293,8 +291,7 @@ class HistoryPeriodView(HomeAssistantView): self.filters = filters self.use_include_order = use_include_order - @asyncio.coroutine - def get(self, request, datetime=None): + async def get(self, request, datetime=None): """Return history over a period of time.""" timer_start = time.perf_counter() if datetime: @@ -330,7 +327,7 @@ class HistoryPeriodView(HomeAssistantView): hass = request.app['hass'] - result = yield from hass.async_add_job( + result = await hass.async_add_job( get_significant_states, hass, start_time, end_time, entity_ids, self.filters, include_start_time_state) result = list(result.values()) @@ -353,8 +350,7 @@ class HistoryPeriodView(HomeAssistantView): sorted_result.extend(result) result = sorted_result - response = yield from hass.async_add_job(self.json, result) - return response + return await hass.async_add_job(self.json, result) class Filters(object): diff --git a/homeassistant/components/history_graph.py b/homeassistant/components/history_graph.py index e6977d60c30..fa7d615dce2 100644 --- a/homeassistant/components/history_graph.py +++ b/homeassistant/components/history_graph.py @@ -4,7 +4,6 @@ Support to graphs card in the UI. For more details about this component, please refer to the documentation at https://home-assistant.io/components/history_graph/ """ -import asyncio import logging import voluptuous as vol @@ -39,8 +38,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Load graph configurations.""" component = EntityComponent( _LOGGER, DOMAIN, hass) @@ -51,7 +49,7 @@ def async_setup(hass, config): graph = HistoryGraphEntity(name, cfg) graphs.append(graph) - yield from component.async_add_entities(graphs) + await component.async_add_entities(graphs) return True diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 6af470e80be..c31093a5eb8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -14,7 +14,8 @@ from homeassistant.components.cover import ( from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, - TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip @@ -22,15 +23,20 @@ from homeassistant.util.decorator import Registry from .const import ( DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START, - DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE) + DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) from .util import ( validate_entity_config, show_setup_message) TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==1.1.9'] +REQUIREMENTS = ['HAP-python==2.0.0'] + +# #### Driver Status #### +STATUS_READY = 0 +STATUS_RUNNING = 1 +STATUS_STOPPED = 2 +STATUS_WAIT = 3 CONFIG_SCHEMA = vol.Schema({ @@ -57,7 +63,7 @@ async def async_setup(hass, config): entity_config = conf[CONF_ENTITY_CONFIG] homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config) - homekit.setup() + await hass.async_add_job(homekit.setup) if auto_start: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) @@ -65,8 +71,10 @@ async def async_setup(hass, config): def handle_homekit_service_start(service): """Handle start HomeKit service call.""" - if homekit.started: - _LOGGER.warning('HomeKit is already running') + if homekit.status != STATUS_READY: + _LOGGER.warning( + 'HomeKit is not ready. Either it is already running or has ' + 'been stopped.') return homekit.start() @@ -118,10 +126,10 @@ def get_accessory(hass, state, aid, config): unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) device_class = state.attributes.get(ATTR_DEVICE_CLASS) - if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \ - or unit == TEMP_FAHRENHEIT: + if device_class == DEVICE_CLASS_TEMPERATURE or \ + unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT): a_type = 'TemperatureSensor' - elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%': + elif device_class == DEVICE_CLASS_HUMIDITY and unit == '%': a_type = 'HumiditySensor' elif device_class == DEVICE_CLASS_PM25 \ or DEVICE_CLASS_PM25 in state.entity_id: @@ -129,12 +137,10 @@ def get_accessory(hass, state, aid, config): elif device_class == DEVICE_CLASS_CO2 \ or DEVICE_CLASS_CO2 in state.entity_id: a_type = 'CarbonDioxideSensor' - elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \ - unit == 'lux': + elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): a_type = 'LightSensor' - elif state.domain == 'switch' or state.domain == 'remote' \ - or state.domain == 'input_boolean' or state.domain == 'script': + elif state.domain in ('switch', 'remote', 'input_boolean', 'script'): a_type = 'Switch' if a_type is None: @@ -162,7 +168,7 @@ class HomeKit(): self._ip_address = ip_address self._filter = entity_filter self._config = entity_config - self.started = False + self.status = STATUS_READY self.bridge = None self.driver = None @@ -191,9 +197,9 @@ class HomeKit(): def start(self, *args): """Start the accessory driver.""" - if self.started: + if self.status != STATUS_READY: return - self.started = True + self.status = STATUS_WAIT # pylint: disable=unused-variable from . import ( # noqa F401 @@ -202,19 +208,20 @@ class HomeKit(): for state in self.hass.states.all(): self.add_bridge_accessory(state) - self.bridge.set_broker(self.driver) + self.bridge.set_driver(self.driver) if not self.bridge.paired: show_setup_message(self.hass, self.bridge) _LOGGER.debug('Driver start') - self.driver.start() + self.hass.add_job(self.driver.start) + self.status = STATUS_RUNNING def stop(self, *args): """Stop the accessory driver.""" - if not self.started: + if self.status != STATUS_RUNNING: return + self.status = STATUS_STOPPED _LOGGER.debug('Driver stop') - if self.driver and self.driver.run_sentinel: - self.driver.stop() + self.hass.add_job(self.driver.stop) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index d9b90a77d68..c47c3f8fbe7 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -4,18 +4,20 @@ from functools import wraps from inspect import getmodule import logging -from pyhap.accessory import Accessory, Bridge, Category +from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver +from pyhap.const import CATEGORY_OTHER +from homeassistant.const import __version__ from homeassistant.core import callback as ha_callback +from homeassistant.core import split_entity_id from homeassistant.helpers.event import ( async_track_state_change, track_point_in_utc_time) from homeassistant.util import dt as dt_util from .const import ( - DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER, - SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, - CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, + BRIDGE_SERIAL_NUMBER, MANUFACTURER) from .util import ( show_setup_message, dismiss_setup_message) @@ -59,55 +61,20 @@ def debounce(func): return wrapper -def add_preload_service(acc, service, chars=None): - """Define and return a service to be available for the accessory.""" - from pyhap.loader import get_serv_loader, get_char_loader - service = get_serv_loader().get(service) - if chars: - chars = chars if isinstance(chars, list) else [chars] - for char_name in chars: - char = get_char_loader().get(char_name) - service.add_characteristic(char) - acc.add_service(service) - return service - - -def setup_char(char_name, service, value=None, properties=None, callback=None): - """Helper function to return fully configured characteristic.""" - char = service.get_characteristic(char_name) - if value: - char.value = value - if properties: - char.override_properties(properties) - if callback: - char.setter_callback = callback - return char - - -def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, - serial_number='0000'): - """Set the default accessory information.""" - service = acc.get_service(SERV_ACCESSORY_INFO) - service.get_characteristic(CHAR_NAME).set_value(name) - service.get_characteristic(CHAR_MODEL).set_value(model) - service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) - service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) - - class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, name, entity_id, aid, category): + def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER): """Initialize a Accessory object.""" super().__init__(name, aid=aid) - set_accessory_info(self, name, model=entity_id) - self.category = getattr(Category, category, Category.OTHER) + domain = split_entity_id(entity_id)[0].replace("_", " ").title() + self.set_info_service( + firmware_revision=__version__, manufacturer=MANUFACTURER, + model=domain, serial_number=entity_id) + self.category = category self.entity_id = entity_id self.hass = hass - def _set_services(self): - add_preload_service(self, SERV_ACCESSORY_INFO) - def run(self): """Method called by accessory after driver is started.""" state = self.hass.states.get(self.entity_id) @@ -137,12 +104,11 @@ class HomeBridge(Bridge): def __init__(self, hass, name=BRIDGE_NAME): """Initialize a Bridge object.""" super().__init__(name) - set_accessory_info(self, name, model=BRIDGE_MODEL) + self.set_info_service( + firmware_revision=__version__, manufacturer=MANUFACTURER, + model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER) self.hass = hass - def _set_services(self): - add_preload_service(self, SERV_ACCESSORY_INFO) - def setup_message(self): """Prevent print of pyhap setup message to terminal.""" pass diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 59444c75421..ce46e84a2ef 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -18,20 +18,10 @@ DEFAULT_PORT = 51827 SERVICE_HOMEKIT_START = 'start' # #### STRING CONSTANTS #### -BRIDGE_MODEL = 'homekit.bridge' -BRIDGE_NAME = 'Home Assistant' -MANUFACTURER = 'HomeAssistant' - -# #### Categories #### -CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' -CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER' -CATEGORY_LIGHT = 'LIGHTBULB' -CATEGORY_LOCK = 'DOOR_LOCK' -CATEGORY_SENSOR = 'SENSOR' -CATEGORY_SWITCH = 'SWITCH' -CATEGORY_THERMOSTAT = 'THERMOSTAT' -CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' - +BRIDGE_MODEL = 'Bridge' +BRIDGE_NAME = 'Home Assistant Bridge' +BRIDGE_SERIAL_NUMBER = 'homekit.bridge' +MANUFACTURER = 'Home Assistant' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' @@ -55,7 +45,6 @@ SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' # CurrentPosition, TargetPosition, PositionState - # #### Characteristics #### CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' @@ -74,6 +63,7 @@ CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' +CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_LEAK_DETECTED = 'LeakDetected' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 8ec715e0e01..3de87cf63e8 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,6 +1,8 @@ """Class to hold all cover accessories.""" import logging +from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( @@ -9,12 +11,11 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory, debounce from .const import ( - CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, - CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE, - CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER, - CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) + SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, + CHAR_TARGET_POSITION, CHAR_POSITION_STATE, + SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) _LOGGER = logging.getLogger(__name__) @@ -32,12 +33,11 @@ class GarageDoorOpener(HomeAccessory): super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER) self.flag_target_state = False - serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER) - self.char_current_state = setup_char( - CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0) - self.char_target_state = setup_char( - CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0, - callback=self.set_state) + serv_garage_door = self.add_preload_service(SERV_GARAGE_DOOR_OPENER) + self.char_current_state = serv_garage_door.configure_char( + CHAR_CURRENT_DOOR_STATE, value=0) + self.char_target_state = serv_garage_door.configure_char( + CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state) def set_state(self, value): """Change garage state if call came from HomeKit.""" @@ -74,13 +74,13 @@ class WindowCovering(HomeAccessory): super().__init__(*args, category=CATEGORY_WINDOW_COVERING) self.homekit_target = None - serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = setup_char( - CHAR_CURRENT_POSITION, serv_cover, value=0) - self.char_target_position = setup_char( - CHAR_TARGET_POSITION, serv_cover, value=0, - callback=self.move_cover) + serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) + self.char_current_position = serv_cover.configure_char( + CHAR_CURRENT_POSITION, value=0) + self.char_target_position = serv_cover.configure_char( + CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) @@ -115,15 +115,15 @@ class WindowCoveringBasic(HomeAccessory): .attributes.get(ATTR_SUPPORTED_FEATURES) self.supports_stop = features & SUPPORT_STOP - serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = setup_char( - CHAR_CURRENT_POSITION, serv_cover, value=0) - self.char_target_position = setup_char( - CHAR_TARGET_POSITION, serv_cover, value=0, - callback=self.move_cover) - self.char_position_state = setup_char( - CHAR_POSITION_STATE, serv_cover, value=2) + serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) + self.char_current_position = serv_cover.configure_char( + CHAR_CURRENT_POSITION, value=0) + self.char_target_position = serv_cover.configure_char( + CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + self.char_position_state = serv_cover.configure_char( + CHAR_POSITION_STATE, value=2) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 9a7bce76fba..3efb0e99df6 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -1,16 +1,17 @@ """Class to hold all light accessories.""" import logging +from pyhap.const import CATEGORY_LIGHTBULB + from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF from . import TYPES -from .accessories import ( - HomeAccessory, add_preload_service, debounce, setup_char) +from .accessories import HomeAccessory, debounce from .const import ( - CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, + SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) _LOGGER = logging.getLogger(__name__) @@ -27,7 +28,7 @@ class Light(HomeAccessory): def __init__(self, *args, config): """Initialize a new Light accessory object.""" - super().__init__(*args, category=CATEGORY_LIGHT) + super().__init__(*args, category=CATEGORY_LIGHTBULB) self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False} @@ -46,30 +47,28 @@ class Light(HomeAccessory): self._hue = None self._saturation = None - serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) - self.char_on = setup_char( - CHAR_ON, serv_light, value=self._state, callback=self.set_state) + serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.configure_char( + CHAR_ON, value=self._state, setter_callback=self.set_state) if CHAR_BRIGHTNESS in self.chars: - self.char_brightness = setup_char( - CHAR_BRIGHTNESS, serv_light, value=0, - callback=self.set_brightness) + self.char_brightness = serv_light.configure_char( + CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness) if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_MIN_MIREDS, 153) max_mireds = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_MAX_MIREDS, 500) - self.char_color_temperature = setup_char( - CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds, + self.char_color_temperature = serv_light.configure_char( + CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={'minValue': min_mireds, 'maxValue': max_mireds}, - callback=self.set_color_temperature) + setter_callback=self.set_color_temperature) if CHAR_HUE in self.chars: - self.char_hue = setup_char( - CHAR_HUE, serv_light, value=0, callback=self.set_hue) + self.char_hue = serv_light.configure_char( + CHAR_HUE, value=0, setter_callback=self.set_hue) if CHAR_SATURATION in self.chars: - self.char_saturation = setup_char( - CHAR_SATURATION, serv_light, value=75, - callback=self.set_saturation) + self.char_saturation = serv_light.configure_char( + CHAR_SATURATION, value=75, setter_callback=self.set_saturation) def set_state(self, value): """Set state if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index f34fc6c6a7f..e7f18d44805 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -1,13 +1,15 @@ """Class to hold all lock accessories.""" import logging +from pyhap.const import CATEGORY_DOOR_LOCK + from homeassistant.components.lock import ( ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) + SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) _LOGGER = logging.getLogger(__name__) @@ -29,16 +31,16 @@ class Lock(HomeAccessory): def __init__(self, *args, config): """Initialize a Lock accessory object.""" - super().__init__(*args, category=CATEGORY_LOCK) + super().__init__(*args, category=CATEGORY_DOOR_LOCK) self.flag_target_state = False - serv_lock_mechanism = add_preload_service(self, SERV_LOCK) - self.char_current_state = setup_char( - CHAR_LOCK_CURRENT_STATE, serv_lock_mechanism, + serv_lock_mechanism = self.add_preload_service(SERV_LOCK) + self.char_current_state = serv_lock_mechanism.configure_char( + CHAR_LOCK_CURRENT_STATE, value=HASS_TO_HOMEKIT[STATE_UNKNOWN]) - self.char_target_state = setup_char( - CHAR_LOCK_TARGET_STATE, serv_lock_mechanism, - value=HASS_TO_HOMEKIT[STATE_LOCKED], callback=self.set_state) + self.char_target_state = serv_lock_mechanism.configure_char( + CHAR_LOCK_TARGET_STATE, value=HASS_TO_HOMEKIT[STATE_LOCKED], + setter_callback=self.set_state) def set_state(self, value): """Set lock state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 0762e0f25f9..ab16f921e99 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -1,26 +1,31 @@ """Class to hold all alarm control panel accessories.""" import logging +from pyhap.const import CATEGORY_ALARM_SYSTEM + from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - ATTR_ENTITY_ID, ATTR_CODE) + STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM, - CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE) + SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, + CHAR_TARGET_SECURITY_STATE) _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = {STATE_ALARM_DISARMED: 3, STATE_ALARM_ARMED_HOME: 0, - STATE_ALARM_ARMED_AWAY: 1, STATE_ALARM_ARMED_NIGHT: 2} +HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0, + STATE_ALARM_ARMED_AWAY: 1, + STATE_ALARM_ARMED_NIGHT: 2, + STATE_ALARM_DISARMED: 3, + STATE_ALARM_TRIGGERED: 4} HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} -STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', - STATE_ALARM_ARMED_HOME: 'alarm_arm_home', +STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home', STATE_ALARM_ARMED_AWAY: 'alarm_arm_away', - STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night'} + STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night', + STATE_ALARM_DISARMED: 'alarm_disarm'} @TYPES.register('SecuritySystem') @@ -33,12 +38,12 @@ class SecuritySystem(HomeAccessory): self._alarm_code = config.get(ATTR_CODE) self.flag_target_state = False - serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) - self.char_current_state = setup_char( - CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3) - self.char_target_state = setup_char( - CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3, - callback=self.set_security_state) + serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) + self.char_current_state = serv_alarm.configure_char( + CHAR_CURRENT_SECURITY_STATE, value=3) + self.char_target_state = serv_alarm.configure_char( + CHAR_TARGET_SECURITY_STATE, value=3, + setter_callback=self.set_security_state) def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" @@ -62,7 +67,8 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Updated current state to %s (%d)', self.entity_id, hass_state, current_security_state) - if not self.flag_target_state: + # SecuritySystemTargetState does not support triggered + if not self.flag_target_state and \ + hass_state != STATE_ALARM_TRIGGERED: self.char_target_state.set_value(current_security_state) - if self.char_target_state.value == self.char_current_state.value: - self.flag_target_state = False + self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 7d7bbc5edd6..393b6beffd6 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -1,14 +1,16 @@ """Class to hold all sensor accessories.""" import logging +from pyhap.const import CATEGORY_SENSOR + from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, + SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, @@ -52,10 +54,9 @@ class TemperatureSensor(HomeAccessory): def __init__(self, *args, config): """Initialize a TemperatureSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) - self.char_temp = setup_char( - CHAR_CURRENT_TEMPERATURE, serv_temp, value=0, - properties=PROP_CELSIUS) + serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) + self.char_temp = serv_temp.configure_char( + CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS) self.unit = None def update_state(self, new_state): @@ -76,9 +77,9 @@ class HumiditySensor(HomeAccessory): def __init__(self, *args, config): """Initialize a HumiditySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) - self.char_humidity = setup_char( - CHAR_CURRENT_HUMIDITY, serv_humidity, value=0) + serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR) + self.char_humidity = serv_humidity.configure_char( + CHAR_CURRENT_HUMIDITY, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -97,12 +98,12 @@ class AirQualitySensor(HomeAccessory): """Initialize a AirQualitySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR, - [CHAR_AIR_PARTICULATE_DENSITY]) - self.char_quality = setup_char( - CHAR_AIR_QUALITY, serv_air_quality, value=0) - self.char_density = setup_char( - CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0) + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY]) + self.char_quality = serv_air_quality.configure_char( + CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char( + CHAR_AIR_PARTICULATE_DENSITY, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -121,14 +122,14 @@ class CarbonDioxideSensor(HomeAccessory): """Initialize a CarbonDioxideSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [ + serv_co2 = self.add_preload_service(SERV_CARBON_DIOXIDE_SENSOR, [ CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL]) - self.char_co2 = setup_char( - CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0) - self.char_peak = setup_char( - CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0) - self.char_detected = setup_char( - CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0) + self.char_co2 = serv_co2.configure_char( + CHAR_CARBON_DIOXIDE_LEVEL, value=0) + self.char_peak = serv_co2.configure_char( + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0) + self.char_detected = serv_co2.configure_char( + CHAR_CARBON_DIOXIDE_DETECTED, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -149,9 +150,9 @@ class LightSensor(HomeAccessory): """Initialize a LightSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_light = add_preload_service(self, SERV_LIGHT_SENSOR) - self.char_light = setup_char( - CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0) + serv_light = self.add_preload_service(SERV_LIGHT_SENSOR) + self.char_light = serv_light.configure_char( + CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -174,8 +175,8 @@ class BinarySensor(HomeAccessory): if device_class in BINARY_SENSOR_SERVICE_MAP \ else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] - service = add_preload_service(self, service_char[0]) - self.char_detected = setup_char(service_char[1], service, value=0) + service = self.add_preload_service(service_char[0]) + self.char_detected = service.configure_char(service_char[1], value=0) def update_state(self, new_state): """Update accessory after state change.""" diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index aaf13e4ea7e..68a4fcdab0a 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,13 +1,15 @@ """Class to hold all switch accessories.""" import logging +from pyhap.const import CATEGORY_SWITCH + from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char -from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON +from .accessories import HomeAccessory +from .const import SERV_SWITCH, CHAR_ON _LOGGER = logging.getLogger(__name__) @@ -22,9 +24,9 @@ class Switch(HomeAccessory): self._domain = split_entity_id(self.entity_id)[0] self.flag_target_state = False - serv_switch = add_preload_service(self, SERV_SWITCH) - self.char_on = setup_char( - CHAR_ON, serv_switch, value=False, callback=self.set_state) + serv_switch = self.add_preload_service(SERV_SWITCH) + self.char_on = serv_switch.configure_char( + CHAR_ON, value=False, setter_callback=self.set_state) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index ce10b96c51c..15fd8160a7e 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -1,21 +1,22 @@ """Class to hold all thermostat accessories.""" import logging +from pyhap.const import CATEGORY_THERMOSTAT + from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - STATE_HEAT, STATE_COOL, STATE_AUTO, + STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import ( - HomeAccessory, add_preload_service, debounce, setup_char) +from .accessories import HomeAccessory, debounce from .const import ( - CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, + SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -41,6 +42,7 @@ class Thermostat(HomeAccessory): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = TEMP_CELSIUS + self.support_power_state = False self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False self.coolingthresh_flag_target_state = False @@ -50,42 +52,43 @@ class Thermostat(HomeAccessory): self.chars = [] features = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_SUPPORTED_FEATURES) + if features & SUPPORT_ON_OFF: + self.support_power_state = True if features & SUPPORT_TEMP_RANGE: self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)) - serv_thermostat = add_preload_service( - self, SERV_THERMOSTAT, self.chars) + serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) # Current and target mode characteristics - self.char_current_heat_cool = setup_char( - CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0) - self.char_target_heat_cool = setup_char( - CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0, - callback=self.set_heat_cool) + self.char_current_heat_cool = serv_thermostat.configure_char( + CHAR_CURRENT_HEATING_COOLING, value=0) + self.char_target_heat_cool = serv_thermostat.configure_char( + CHAR_TARGET_HEATING_COOLING, value=0, + setter_callback=self.set_heat_cool) # Current and target temperature characteristics - self.char_current_temp = setup_char( - CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0) - self.char_target_temp = setup_char( - CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0, - callback=self.set_target_temperature) + self.char_current_temp = serv_thermostat.configure_char( + CHAR_CURRENT_TEMPERATURE, value=21.0) + self.char_target_temp = serv_thermostat.configure_char( + CHAR_TARGET_TEMPERATURE, value=21.0, + setter_callback=self.set_target_temperature) # Display units characteristic - self.char_display_units = setup_char( - CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0) + self.char_display_units = serv_thermostat.configure_char( + CHAR_TEMP_DISPLAY_UNITS, value=0) # If the device supports it: high and low temperature characteristics self.char_cooling_thresh_temp = None self.char_heating_thresh_temp = None if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: - self.char_cooling_thresh_temp = setup_char( - CHAR_COOLING_THRESHOLD_TEMPERATURE, serv_thermostat, - value=23.0, callback=self.set_cooling_threshold) + self.char_cooling_thresh_temp = serv_thermostat.configure_char( + CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, + setter_callback=self.set_cooling_threshold) if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: - self.char_heating_thresh_temp = setup_char( - CHAR_HEATING_THRESHOLD_TEMPERATURE, serv_thermostat, - value=19.0, callback=self.set_heating_threshold) + self.char_heating_thresh_temp = serv_thermostat.configure_char( + CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, + setter_callback=self.set_heating_threshold) def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" @@ -93,6 +96,13 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) self.heat_cool_flag_target_state = True hass_value = HC_HOMEKIT_TO_HASS[value] + if self.support_power_state is True: + params = {ATTR_ENTITY_ID: self.entity_id} + if hass_value == STATE_OFF: + self.hass.services.call('climate', 'turn_off', params) + return + else: + self.hass.services.call('climate', 'turn_on', params) self.hass.components.climate.set_operation_mode( operation_mode=hass_value, entity_id=self.entity_id) @@ -178,15 +188,19 @@ class Thermostat(HomeAccessory): # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) - if operation_mode \ - and operation_mode in HC_HASS_TO_HOMEKIT: + if self.support_power_state is True and new_state.state == STATE_OFF: + self.char_target_heat_cool.set_value( + HC_HASS_TO_HOMEKIT[STATE_OFF]) + elif operation_mode and operation_mode in HC_HASS_TO_HOMEKIT: if not self.heat_cool_flag_target_state: self.char_target_heat_cool.set_value( HC_HASS_TO_HOMEKIT[operation_mode]) self.heat_cool_flag_target_state = False # Set current operation mode based on temperatures and target mode - if operation_mode == STATE_HEAT: + if self.support_power_state is True and new_state.state == STATE_OFF: + current_operation_mode = STATE_OFF + elif operation_mode == STATE_HEAT: if isinstance(target_temp, float) and current_temp < target_temp: current_operation_mode = STATE_HEAT else: diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 1528943a7f9..0291cc28fed 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.41'] +REQUIREMENTS = ['pyhomematic==0.1.42'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index 180d6943d8a..0b15d7a3dfe 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -5,143 +5,239 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/homematicip_cloud/ """ +import asyncio import logging -from socket import timeout import voluptuous as vol -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import (dispatcher_send, - async_dispatcher_connect) -from homeassistant.helpers.discovery import load_platform +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity +from homeassistant.core import callback -REQUIREMENTS = ['homematicip==0.8'] +REQUIREMENTS = ['homematicip==0.9.2.4'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'homematicip_cloud' +COMPONENTS = [ + 'sensor' +] + CONF_NAME = 'name' CONF_ACCESSPOINT = 'accesspoint' CONF_AUTHTOKEN = 'authtoken' CONFIG_SCHEMA = vol.Schema({ - vol.Optional(DOMAIN): [vol.Schema({ - vol.Optional(CONF_NAME, default=''): cv.string, + vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ + vol.Optional(CONF_NAME): vol.Any(cv.string), vol.Required(CONF_ACCESSPOINT): cv.string, vol.Required(CONF_AUTHTOKEN): cv.string, - })], + })]), }, extra=vol.ALLOW_EXTRA) -EVENT_HOME_CHANGED = 'homematicip_home_changed' -EVENT_DEVICE_CHANGED = 'homematicip_device_changed' -EVENT_GROUP_CHANGED = 'homematicip_group_changed' -EVENT_SECURITY_CHANGED = 'homematicip_security_changed' -EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed' +HMIP_ACCESS_POINT = 'Access Point' +HMIP_HUB = 'HmIP-HUB' ATTR_HOME_ID = 'home_id' -ATTR_HOME_LABEL = 'home_label' +ATTR_HOME_NAME = 'home_name' ATTR_DEVICE_ID = 'device_id' ATTR_DEVICE_LABEL = 'device_label' ATTR_STATUS_UPDATE = 'status_update' ATTR_FIRMWARE_STATE = 'firmware_state' +ATTR_UNREACHABLE = 'unreachable' ATTR_LOW_BATTERY = 'low_battery' +ATTR_MODEL_TYPE = 'model_type' +ATTR_GROUP_TYPE = 'group_type' +ATTR_DEVICE_RSSI = 'device_rssi' +ATTR_DUTY_CYCLE = 'duty_cycle' +ATTR_CONNECTED = 'connected' ATTR_SABOTAGE = 'sabotage' -ATTR_RSSI = 'rssi' -ATTR_TYPE = 'type' +ATTR_OPERATION_LOCK = 'operation_lock' -def setup(hass, config): +async def async_setup(hass, config): """Set up the HomematicIP component.""" - # pylint: disable=import-error, no-name-in-module - from homematicip.home import Home + from homematicip.base.base_connection import HmipConnectionError hass.data.setdefault(DOMAIN, {}) - homes = hass.data[DOMAIN] accesspoints = config.get(DOMAIN, []) - - def _update_event(events): - """Handle incoming HomeMaticIP events.""" - for event in events: - etype = event['eventType'] - edata = event['data'] - if etype == 'DEVICE_CHANGED': - dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id) - elif etype == 'GROUP_CHANGED': - dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id) - elif etype == 'HOME_CHANGED': - dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id) - elif etype == 'JOURNAL_CHANGED': - dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id) - return True - - for device in accesspoints: - name = device.get(CONF_NAME) - accesspoint = device.get(CONF_ACCESSPOINT) - authtoken = device.get(CONF_AUTHTOKEN) - - home = Home() - if name.lower() == 'none': - name = '' - home.label = name + for conf in accesspoints: + _websession = async_get_clientsession(hass) + _hmip = HomematicipConnector(hass, conf, _websession) try: - home.set_auth_token(authtoken) - home.init(accesspoint) - if home.get_current_state(): - _LOGGER.info("Connection to HMIP established") - else: - _LOGGER.warning("Connection to HMIP could not be established") - return False - except timeout: - _LOGGER.warning("Connection to HMIP could not be established") + await _hmip.init() + except HmipConnectionError: + _LOGGER.error('Failed to connect to the HomematicIP server, %s.', + conf.get(CONF_ACCESSPOINT)) return False - homes[home.id] = home - home.onEvent += _update_event - home.enable_events() - _LOGGER.info('HUB name: %s, id: %s', home.label, home.id) - for component in ['sensor']: - load_platform(hass, component, DOMAIN, {'homeid': home.id}, config) + home = _hmip.home + home.name = conf.get(CONF_NAME) + home.label = HMIP_ACCESS_POINT + home.modelType = HMIP_HUB + hass.data[DOMAIN][home.id] = home + _LOGGER.info('Connected to the HomematicIP server, %s.', + conf.get(CONF_ACCESSPOINT)) + homeid = {ATTR_HOME_ID: home.id} + for component in COMPONENTS: + hass.async_add_job(async_load_platform(hass, component, DOMAIN, + homeid, config)) + + hass.loop.create_task(_hmip.connect()) return True +class HomematicipConnector: + """Manages HomematicIP http and websocket connection.""" + + def __init__(self, hass, config, websession): + """Initialize HomematicIP cloud connection.""" + from homematicip.async.home import AsyncHome + + self._hass = hass + self._ws_close_requested = False + self._retry_task = None + self._tries = 0 + self._accesspoint = config.get(CONF_ACCESSPOINT) + _authtoken = config.get(CONF_AUTHTOKEN) + + self.home = AsyncHome(hass.loop, websession) + self.home.set_auth_token(_authtoken) + + self.home.on_update(self.async_update) + self._accesspoint_connected = True + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close()) + + async def init(self): + """Initialize connection.""" + await self.home.init(self._accesspoint) + await self.home.get_current_state() + + @callback + def async_update(self, *args, **kwargs): + """Async update the home device. + + Triggered when the hmip HOME_CHANGED event has fired. + There are several occasions for this event to happen. + We are only interested to check whether the access point + is still connected. If not, device state changes cannot + be forwarded to hass. So if access point is disconnected all devices + are set to unavailable. + """ + if not self.home.connected: + _LOGGER.error( + "HMIP access point has lost connection with the cloud") + self._accesspoint_connected = False + self.set_all_to_unavailable() + elif not self._accesspoint_connected: + # Explicitly getting an update as device states might have + # changed during access point disconnect.""" + + job = self._hass.async_add_job(self.get_state()) + job.add_done_callback(self.get_state_finished) + + async def get_state(self): + """Update hmip state and tell hass.""" + await self.home.get_current_state() + self.update_all() + + def get_state_finished(self, future): + """Execute when get_state coroutine has finished.""" + from homematicip.base.base_connection import HmipConnectionError + + try: + future.result() + except HmipConnectionError: + # Somehow connection could not recover. Will disconnect and + # so reconnect loop is taking over. + _LOGGER.error( + "updating state after himp access point reconnect failed.") + self._hass.async_add_job(self.home.disable_events()) + + def set_all_to_unavailable(self): + """Set all devices to unavailable and tell Hass.""" + for device in self.home.devices: + device.unreach = True + self.update_all() + + def update_all(self): + """Signal all devices to update their state.""" + for device in self.home.devices: + device.fire_update_event() + + async def _handle_connection(self): + """Handle websocket connection.""" + from homematicip.base.base_connection import HmipConnectionError + + await self.home.get_current_state() + hmip_events = await self.home.enable_events() + try: + await hmip_events + except HmipConnectionError: + return + + async def connect(self): + """Start websocket connection.""" + self._tries = 0 + while True: + await self._handle_connection() + if self._ws_close_requested: + break + self._ws_close_requested = False + self._tries += 1 + try: + self._retry_task = self._hass.async_add_job(asyncio.sleep( + 2 ** min(9, self._tries), loop=self._hass.loop)) + await self._retry_task + except asyncio.CancelledError: + break + _LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.', + self._tries) + + async def close(self): + """Close the websocket connection.""" + self._ws_close_requested = True + if self._retry_task is not None: + self._retry_task.cancel() + await self.home.disable_events() + _LOGGER.info("Closed connection to HomematicIP cloud server.") + + class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, home, device): + def __init__(self, home, device, post=None): """Initialize the generic device.""" self._home = home self._device = device + self.post = post + _LOGGER.info('Setting up %s (%s)', self.name, + self._device.modelType) async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, EVENT_DEVICE_CHANGED, self._device_changed) + self._device.on_update(self._device_changed) - @callback - def _device_changed(self, deviceid): + def _device_changed(self, json, **kwargs): """Handle device state changes.""" - if deviceid is None or deviceid == self._device.id: - _LOGGER.debug('Event device %s', self._device.label) - self.async_schedule_update_ha_state() - - def _name(self, addon=''): - """Return the name of the device.""" - name = '' - if self._home.label != '': - name += self._home.label + ' ' - name += self._device.label - if addon != '': - name += ' ' + addon - return name + _LOGGER.debug('Event %s (%s)', self.name, self._device.modelType) + self.async_schedule_update_ha_state() @property def name(self): """Return the name of the generic device.""" - return self._name() + name = self._device.label + if self._home.name is not None: + name = "{} {}".format(self._home.name, name) + if self.post is not None: + name = "{} {}".format(name, self.post) + return name @property def should_poll(self): @@ -153,24 +249,10 @@ class HomematicipGenericDevice(Entity): """Device available.""" return not self._device.unreach - def _generic_state_attributes(self): - """Return the state attributes of the generic device.""" - laststatus = '' - if self._device.lastStatusUpdate is not None: - laststatus = self._device.lastStatusUpdate.isoformat() - return { - ATTR_HOME_LABEL: self._home.label, - ATTR_DEVICE_LABEL: self._device.label, - ATTR_HOME_ID: self._device.homeId, - ATTR_DEVICE_ID: self._device.id.lower(), - ATTR_STATUS_UPDATE: laststatus, - ATTR_FIRMWARE_STATE: self._device.updateState.lower(), - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue, - ATTR_TYPE: self._device.modelType - } - @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - return self._generic_state_attributes() + return { + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_MODEL_TYPE: self._device.modelType + } diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 65c70c37bd2..5558063c5c4 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -32,17 +32,19 @@ def setup_auth(app, trusted_networks, api_password): if (HTTP_HEADER_HA_AUTH in request.headers and hmac.compare_digest( - api_password, request.headers[HTTP_HEADER_HA_AUTH])): + api_password.encode('utf-8'), + request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): # A valid auth header has been set authenticated = True elif (DATA_API_PASSWORD in request.query and - hmac.compare_digest(api_password, - request.query[DATA_API_PASSWORD])): + hmac.compare_digest( + api_password.encode('utf-8'), + request.query[DATA_API_PASSWORD].encode('utf-8'))): authenticated = True elif (hdrs.AUTHORIZATION in request.headers and - validate_authorization_header(api_password, request)): + await async_validate_auth_header(api_password, request)): authenticated = True elif _is_trusted_ip(request, trusted_networks): @@ -70,23 +72,38 @@ def _is_trusted_ip(request, trusted_networks): def validate_password(request, api_password): """Test if password is valid.""" return hmac.compare_digest( - api_password, request.app['hass'].http.api_password) + api_password.encode('utf-8'), + request.app['hass'].http.api_password.encode('utf-8')) -def validate_authorization_header(api_password, request): +async def async_validate_auth_header(api_password, request): """Test an authorization header if valid password.""" if hdrs.AUTHORIZATION not in request.headers: return False - auth_type, auth = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) - if auth_type != 'Basic': + if auth_type == 'Basic': + decoded = base64.b64decode(auth_val).decode('utf-8') + try: + username, password = decoded.split(':', 1) + except ValueError: + # If no ':' in decoded + return False + + if username != 'homeassistant': + return False + + return hmac.compare_digest(api_password.encode('utf-8'), + password.encode('utf-8')) + + if auth_type != 'Bearer': return False - decoded = base64.b64decode(auth).decode('utf-8') - username, password = decoded.split(':', 1) - - if username != 'homeassistant': + hass = request.app['hass'] + access_token = hass.auth.async_get_access_token(auth_val) + if access_token is None: return False - return hmac.compare_digest(api_password, password) + request['hass_user'] = access_token.refresh_token.user + return True diff --git a/homeassistant/components/hue/.translations/bg.json b/homeassistant/components/hue/.translations/bg.json new file mode 100644 index 00000000000..276f5053bf7 --- /dev/null +++ b/homeassistant/components/hue/.translations/bg.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 Philips Hue \u0441\u0430 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", + "already_configured": "\u0411\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "cannot_connect": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f", + "discover_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e \u0435 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue", + "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "linking": "\u041f\u043e\u044f\u0432\u0438 \u0441\u0435 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e.", + "register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue" + }, + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u0437\u0430 \u0434\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0442\u0435 Philips Hue \u0441 Home Assistant. \n\n![\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f](/static/images/config_philips_hue.jpg)", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431" + } + }, + "title": "\u0411\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/cy.json b/homeassistant/components/hue/.translations/cy.json new file mode 100644 index 00000000000..f5476f73edb --- /dev/null +++ b/homeassistant/components/hue/.translations/cy.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Mae holl bontydd Philips Hue eisoes wedi eu ffurfweddu", + "already_configured": "Pont eisoes wedi'i ffurfweddu", + "cannot_connect": "Methu cysylltu i'r bont", + "discover_timeout": "Methu darganfod pontydd Hue", + "no_bridges": "Dim pontydd Philips Hue wedi'i ddarganfod", + "unknown": "Digwyddodd gwall anhysbys" + }, + "error": { + "linking": "Digwyddodd gwall cysylltu anhysbys.", + "register_failed": "Wedi methu \u00e2 chofrestru, pl\u00eds ceisiwch eto" + }, + "step": { + "init": { + "data": { + "host": "Gwesteiwr" + }, + "title": "Dewiswch bont Hue" + }, + "link": { + "description": "Pwyswch y botwm ar y bont i gofrestru Philips Hue gyda Cynorthwydd Cartref.\n\n![Lleoliad botwm ar bont](/static/images/config_philips_hue.jpg)", + "title": "Hwb cyswllt" + } + }, + "title": "Pont Phillips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/da.json b/homeassistant/components/hue/.translations/da.json new file mode 100644 index 00000000000..3e5e2b1d3d7 --- /dev/null +++ b/homeassistant/components/hue/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "no_bridges": "Ingen Philips Hue bridge fundet" + }, + "step": { + "init": { + "data": { + "host": "V\u00e6rt" + }, + "title": "V\u00e6lg Hue bridge" + }, + "link": { + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index f11af7756c7..d466488e9fc 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert", + "already_configured": "Bridge ist bereits konfiguriert", + "cannot_connect": "Verbindung zur Bridge nicht m\u00f6glich", "discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken", - "no_bridges": "Philips Hue Bridges entdeckt" + "no_bridges": "Keine Philips Hue Bridges entdeckt", + "unknown": "Unbekannter Fehler ist aufgetreten" }, "error": { "linking": "Unbekannter Link-Fehler aufgetreten.", diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index cbf63301da2..b0459ec3916 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "All Philips Hue bridges are already configured", + "already_configured": "Bridge is already configured", + "cannot_connect": "Unable to connect to the bridge", "discover_timeout": "Unable to discover Hue bridges", - "no_bridges": "No Philips Hue bridges discovered" + "no_bridges": "No Philips Hue bridges discovered", + "unknown": "Unknown error occurred" }, "error": { "linking": "Unknown linking error occurred.", diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json new file mode 100644 index 00000000000..d58469af044 --- /dev/null +++ b/homeassistant/components/hue/.translations/es.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "unknown": "Se produjo un error desconocido" + }, + "error": { + "linking": "Se produjo un error de enlace desconocido.", + "register_failed": "No se pudo registrar, intente de nuevo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/hu.json b/homeassistant/components/hue/.translations/hu.json new file mode 100644 index 00000000000..a4032dcbcfc --- /dev/null +++ b/homeassistant/components/hue/.translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", + "already_configured": "A bridge m\u00e1r konfigur\u00e1lt", + "cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.", + "discover_timeout": "Nem tal\u00e1ltam a Hue bridget", + "no_bridges": "Nem tal\u00e1ltam Philips Hue bridget", + "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" + }, + "error": { + "linking": "Ismeretlen \u00f6sszekapcsol\u00e1si hiba t\u00f6rt\u00e9nt.", + "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" + }, + "step": { + "init": { + "data": { + "host": "H\u00e1zigazda (Host)" + }, + "title": "V\u00e1lassz Hue bridge-t" + }, + "link": { + "title": "Kapcsol\u00f3d\u00e1s a hubhoz" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json new file mode 100644 index 00000000000..2c7a8c1924d --- /dev/null +++ b/homeassistant/components/hue/.translations/it.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati", + "discover_timeout": "Impossibile trovare i bridge Hue", + "no_bridges": "Nessun bridge Hue di Philips trovato" + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json index 226ae8ba1f6..47306a35414 100644 --- a/homeassistant/components/hue/.translations/ko.json +++ b/homeassistant/components/hue/.translations/ko.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" + "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/hue/.translations/lb.json b/homeassistant/components/hue/.translations/lb.json new file mode 100644 index 00000000000..c4ad10da278 --- /dev/null +++ b/homeassistant/components/hue/.translations/lb.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "All Philips Hue Bridge si scho\u00a0konfigur\u00e9iert", + "already_configured": "Bridge ass scho konfigur\u00e9iert", + "cannot_connect": "Keng Verbindung mat der bridge m\u00e9iglech", + "discover_timeout": "Keng Hue bridge fonnt", + "no_bridges": "Keng Philips Hue Bridge fonnt", + "unknown": "Onbekannten Feeler opgetrueden" + }, + "error": { + "linking": "Onbekannte Liaisoun's Feeler opgetrueden", + "register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9iert w.e.g. nach emol" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Hue Bridge auswielen" + }, + "link": { + "description": "Dr\u00e9ckt de Kn\u00e4ppchen un der Bridge fir den Philips Hue mam Home Assistant ze registr\u00e9ieren.\n\n![Kn\u00e4ppchen un der Bridge](/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/nl.json b/homeassistant/components/hue/.translations/nl.json index 750ae39db12..88c611b1633 100644 --- a/homeassistant/components/hue/.translations/nl.json +++ b/homeassistant/components/hue/.translations/nl.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Alle Philips Hue bridges zijn al geconfigureerd", + "already_configured": "Bridge is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken met bridge", "discover_timeout": "Hue bridges kunnen niet worden gevonden", - "no_bridges": "Geen Philips Hue bridges ontdekt" + "no_bridges": "Geen Philips Hue bridges ontdekt", + "unknown": "Onbekende fout opgetreden" }, "error": { "linking": "Er is een onbekende verbindingsfout opgetreden.", @@ -17,7 +20,7 @@ "title": "Kies Hue bridge" }, "link": { - "description": "Druk op de knop van de bridge om Philips Hue te registreren met de Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", + "description": "Druk op de knop van de bridge om Philips Hue te registreren met Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", "title": "Link Hub" } }, diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json index 604475d2ff2..309e9f6a299 100644 --- a/homeassistant/components/hue/.translations/no.json +++ b/homeassistant/components/hue/.translations/no.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Alle Philips Hue Bridger er allerede konfigurert", + "already_configured": "Bridge er allerede konfigurert", + "cannot_connect": "Kan ikke koble til Bridge", "discover_timeout": "Kunne ikke oppdage Hue Bridger", - "no_bridges": "Ingen Philips Hue Bridger oppdaget" + "no_bridges": "Ingen Philips Hue Bridger oppdaget", + "unknown": "Ukjent feil oppstod" }, "error": { "linking": "Ukjent koblingsfeil oppstod.", diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index e364b7033a1..784fa0d99a6 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", + "already_configured": "Mostek jest ju\u017c skonfigurowany", + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem", "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", - "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue" + "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue", + "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json new file mode 100644 index 00000000000..8c4c45f9c89 --- /dev/null +++ b/homeassistant/components/hue/.translations/pt.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json new file mode 100644 index 00000000000..ea1e4fff1bf --- /dev/null +++ b/homeassistant/components/hue/.translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", + "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "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", + "discover_timeout": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437\u044b Philips Hue", + "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + }, + "error": { + "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Hue" + }, + "link": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435 \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 Philips Hue \u0432 Home Assistant.\n\n![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435](/static/images/config_philips_hue.jpg)", + "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" + } + }, + "title": "\u0428\u043b\u044e\u0437 Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/sl.json b/homeassistant/components/hue/.translations/sl.json index a6c858e0e40..4245ce02c66 100644 --- a/homeassistant/components/hue/.translations/sl.json +++ b/homeassistant/components/hue/.translations/sl.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Vsi mostovi Philips Hue so \u017ee konfigurirani", + "already_configured": "Most je \u017ee konfiguriran", + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z mostom", "discover_timeout": "Ni bilo mogo\u010de odkriti Hue mostov", - "no_bridges": "Ni odkritih mostov Philips Hue" + "no_bridges": "Ni odkritih mostov Philips Hue", + "unknown": "Pri\u0161lo je do neznane napake" }, "error": { "linking": "Pri\u0161lo je do neznane napake pri povezavi.", diff --git a/homeassistant/components/hue/.translations/zh-Hans.json b/homeassistant/components/hue/.translations/zh-Hans.json index 5a94e084dd2..1d904070b81 100644 --- a/homeassistant/components/hue/.translations/zh-Hans.json +++ b/homeassistant/components/hue/.translations/zh-Hans.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "\u5168\u90e8\u98de\u5229\u6d66 Hue \u6865\u63a5\u5668\u5df2\u914d\u7f6e", + "already_configured": "\u98de\u5229\u6d66 Hue Bridge \u5df2\u914d\u7f6e\u5b8c\u6210", + "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 \u98de\u5229\u6d66 Hue Bridge", "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2 Hue \u6865\u63a5\u5668", - "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge" + "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge", + "unknown": "\u51fa\u73b0\u672a\u77e5\u7684\u9519\u8bef" }, "error": { "linking": "\u53d1\u751f\u672a\u77e5\u7684\u8fde\u63a5\u9519\u8bef\u3002", @@ -17,7 +20,7 @@ "title": "\u9009\u62e9 Hue Bridge" }, "link": { - "description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue ![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", + "description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u4ee5\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue\u3002\n\n![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", "title": "\u8fde\u63a5\u4e2d\u67a2" } }, diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json new file mode 100644 index 00000000000..eae4c09da49 --- /dev/null +++ b/homeassistant/components/hue/.translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Bridge", + "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", + "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", + "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4" + }, + "error": { + "linking": "\u767c\u751f\u672a\u77e5\u9023\u7d50\u932f\u8aa4\u3002", + "register_failed": "\u8a3b\u518a\u5931\u6557\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "\u9078\u64c7 Hue Bridge" + }, + "link": { + "description": "\u6309\u4e0b Bridge \u4e0a\u7684\u6309\u9215\uff0c\u4ee5\u5c07 Philips Hue \u8a3b\u518a\u81f3 Home Assistant\u3002\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", + "title": "\u9023\u7d50 Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 061fd5d7074..c6100ff701d 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -10,6 +10,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID) @@ -17,7 +18,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.loader import get_component +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -34,7 +35,16 @@ DEVICE_CLASSES = [ SERVICE_SCAN = 'scan' +EVENT_DETECT_FACE = 'image_processing.detect_face' + +ATTR_AGE = 'age' ATTR_CONFIDENCE = 'confidence' +ATTR_FACES = 'faces' +ATTR_GENDER = 'gender' +ATTR_GLASSES = 'glasses' +ATTR_NAME = 'name' +ATTR_MOTION = 'motion' +ATTR_TOTAL_FACES = 'total_faces' CONF_SOURCE = 'source' CONF_CONFIDENCE = 'confidence' @@ -121,16 +131,103 @@ class ImageProcessingEntity(Entity): This method is a coroutine. """ - camera = get_component('camera') + camera = self.hass.components.camera image = None try: image = yield from camera.async_get_image( - self.hass, self.camera_entity, timeout=self.timeout) + self.camera_entity, timeout=self.timeout) except HomeAssistantError as err: _LOGGER.error("Error on receive image from entity: %s", err) return # process image data - yield from self.async_process_image(image) + yield from self.async_process_image(image.content) + + +class ImageProcessingFaceEntity(ImageProcessingEntity): + """Base entity class for face image processing.""" + + def __init__(self): + """Initialize base face identify/verify entity.""" + self.faces = [] + self.total_faces = 0 + + @property + def state(self): + """Return the state of the entity.""" + confidence = 0 + state = None + + # No confidence support + if not self.confidence: + return self.total_faces + + # Search high confidence + for face in self.faces: + if ATTR_CONFIDENCE not in face: + continue + + f_co = face[ATTR_CONFIDENCE] + if f_co > confidence: + confidence = f_co + for attr in [ATTR_NAME, ATTR_MOTION]: + if attr in face: + state = face[attr] + break + + return state + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'face' + + @property + def state_attributes(self): + """Return device specific state attributes.""" + attr = { + ATTR_FACES: self.faces, + ATTR_TOTAL_FACES: self.total_faces, + } + + return attr + + def process_faces(self, faces, total): + """Send event with detected faces and store data.""" + run_callback_threadsafe( + self.hass.loop, self.async_process_faces, faces, total).result() + + @callback + def async_process_faces(self, faces, total): + """Send event with detected faces and store data. + + known are a dict in follow format: + [ + { + ATTR_CONFIDENCE: 80, + ATTR_NAME: 'Name', + ATTR_AGE: 12.0, + ATTR_GENDER: 'man', + ATTR_MOTION: 'smile', + ATTR_GLASSES: 'sunglasses' + }, + ] + + This method must be run in the event loop. + """ + # Send events + for face in faces: + if ATTR_CONFIDENCE in face and self.confidence: + if face[ATTR_CONFIDENCE] < self.confidence: + continue + + face.update({ATTR_ENTITY_ID: self.entity_id}) + self.hass.async_add_job( + self.hass.bus.async_fire, EVENT_DETECT_FACE, face + ) + + # Update entity store + self.faces = faces + self.total_faces = total diff --git a/homeassistant/components/image_processing/demo.py b/homeassistant/components/image_processing/demo.py index 788d12520f5..e225113b5b1 100644 --- a/homeassistant/components/image_processing/demo.py +++ b/homeassistant/components/image_processing/demo.py @@ -4,11 +4,12 @@ Support for the demo image processing. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/demo/ """ -from homeassistant.components.image_processing import ATTR_CONFIDENCE +from homeassistant.components.image_processing import ( + ImageProcessingFaceEntity, ATTR_CONFIDENCE, ATTR_NAME, ATTR_AGE, + ATTR_GENDER + ) from homeassistant.components.image_processing.openalpr_local import ( ImageProcessingAlprEntity) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity, ATTR_NAME, ATTR_AGE, ATTR_GENDER) def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/image_processing/dlib_face_detect.py b/homeassistant/components/image_processing/dlib_face_detect.py index 65705feb7f7..d4a20da253c 100644 --- a/homeassistant/components/image_processing/dlib_face_detect.py +++ b/homeassistant/components/image_processing/dlib_face_detect.py @@ -11,9 +11,7 @@ from homeassistant.core import split_entity_id # pylint: disable=unused-import from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa from homeassistant.components.image_processing import ( - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity) + ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) REQUIREMENTS = ['face_recognition==1.0.0'] diff --git a/homeassistant/components/image_processing/dlib_face_identify.py b/homeassistant/components/image_processing/dlib_face_identify.py index 22594aa2547..bf34eb4c2da 100644 --- a/homeassistant/components/image_processing/dlib_face_identify.py +++ b/homeassistant/components/image_processing/dlib_face_identify.py @@ -11,9 +11,8 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity) + ImageProcessingFaceEntity, PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, + CONF_NAME) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['face_recognition==1.0.0'] diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index 6770ff1bdf6..cd1e341a218 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -13,9 +13,8 @@ from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity, ATTR_GENDER, ATTR_AGE, ATTR_GLASSES) + PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_AGE, ATTR_GENDER, + ATTR_GLASSES, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 51f1cd42f47..32f02e1820e 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -9,30 +9,18 @@ import logging import voluptuous as vol -from homeassistant.core import split_entity_id, callback -from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, - CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) + PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_NAME, + CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_CONFIDENCE) import homeassistant.helpers.config_validation as cv -from homeassistant.util.async_ import run_callback_threadsafe DEPENDENCIES = ['microsoft_face'] _LOGGER = logging.getLogger(__name__) -EVENT_DETECT_FACE = 'image_processing.detect_face' - -ATTR_NAME = 'name' -ATTR_TOTAL_FACES = 'total_faces' -ATTR_AGE = 'age' -ATTR_GENDER = 'gender' -ATTR_MOTION = 'motion' -ATTR_GLASSES = 'glasses' -ATTR_FACES = 'faces' - CONF_GROUP = 'group' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -57,93 +45,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices(entities) -class ImageProcessingFaceEntity(ImageProcessingEntity): - """Base entity class for face image processing.""" - - def __init__(self): - """Initialize base face identify/verify entity.""" - self.faces = [] - self.total_faces = 0 - - @property - def state(self): - """Return the state of the entity.""" - confidence = 0 - state = STATE_UNKNOWN - - # No confidence support - if not self.confidence: - return self.total_faces - - # Search high confidence - for face in self.faces: - if ATTR_CONFIDENCE not in face: - continue - - f_co = face[ATTR_CONFIDENCE] - if f_co > confidence: - confidence = f_co - for attr in [ATTR_NAME, ATTR_MOTION]: - if attr in face: - state = face[attr] - break - - return state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return 'face' - - @property - def state_attributes(self): - """Return device specific state attributes.""" - attr = { - ATTR_FACES: self.faces, - ATTR_TOTAL_FACES: self.total_faces, - } - - return attr - - def process_faces(self, faces, total): - """Send event with detected faces and store data.""" - run_callback_threadsafe( - self.hass.loop, self.async_process_faces, faces, total).result() - - @callback - def async_process_faces(self, faces, total): - """Send event with detected faces and store data. - - known are a dict in follow format: - [ - { - ATTR_CONFIDENCE: 80, - ATTR_NAME: 'Name', - ATTR_AGE: 12.0, - ATTR_GENDER: 'man', - ATTR_MOTION: 'smile', - ATTR_GLASSES: 'sunglasses' - }, - ] - - This method must be run in the event loop. - """ - # Send events - for face in faces: - if ATTR_CONFIDENCE in face and self.confidence: - if face[ATTR_CONFIDENCE] < self.confidence: - continue - - face.update({ATTR_ENTITY_ID: self.entity_id}) - self.hass.async_add_job( - self.hass.bus.async_fire, EVENT_DETECT_FACE, face - ) - - # Update entity store - self.faces = faces - self.total_faces = total - - class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): """Representation of the Microsoft Face API entity for identify.""" diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index 18e74966a59..c3e34b4d42b 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.14.2'] +REQUIREMENTS = ['numpy==1.14.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 1f7f9f6262f..6d54324542a 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -9,6 +9,7 @@ import re import queue import threading import time +import math import requests.exceptions import voluptuous as vol @@ -220,9 +221,12 @@ def setup(hass, config): json['fields'][key] = float( RE_DECIMAL.sub('', new_value)) - # Infinity is not a valid float in InfluxDB - if (key, float("inf")) in json['fields'].items(): - del json['fields'][key] + # Infinity and NaN are not valid floats in InfluxDB + try: + if not math.isfinite(json['fields'][key]): + del json['fields'][key] + except (KeyError, TypeError): + pass json['tags'].update(tags) diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 56761b5af4e..9c8435614a2 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -65,8 +65,7 @@ def toggle(hass, entity_id): hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -85,8 +84,7 @@ def async_setup(hass, config): if not entities: return False - @asyncio.coroutine - def async_handler_service(service): + async def async_handler_service(service): """Handle a calls to the input boolean services.""" target_inputs = component.async_extract_from_service(service) @@ -99,7 +97,7 @@ def async_setup(hass, config): tasks = [getattr(input_b, attr)() for input_b in target_inputs] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handler_service, @@ -111,7 +109,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_TOGGLE, async_handler_service, schema=SERVICE_SCHEMA) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -145,24 +143,21 @@ class InputBoolean(ToggleEntity): """Return true if entity is on.""" return self._state - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added to hass.""" # If not None, we got an initial value. if self._state is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) self._state = state and state.state == STATE_ON - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the entity on.""" self._state = True - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the entity off.""" self._state = False - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm/__init__.py similarity index 57% rename from homeassistant/components/insteon_plm.py rename to homeassistant/components/insteon_plm/__init__.py index d867f0c3d28..246e84ec71f 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -11,12 +11,13 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP, - CONF_PLATFORM) + CONF_PLATFORM, + CONF_ENTITY_ID) import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.6'] +REQUIREMENTS = ['insteonplm==0.9.1'] _LOGGER = logging.getLogger(__name__) @@ -29,6 +30,17 @@ CONF_SUBCAT = 'subcat' CONF_FIRMWARE = 'firmware' CONF_PRODUCT_KEY = 'product_key' +SRV_ADD_ALL_LINK = 'add_all_link' +SRV_DEL_ALL_LINK = 'delete_all_link' +SRV_LOAD_ALDB = 'load_all_link_database' +SRV_PRINT_ALDB = 'print_all_link_database' +SRV_PRINT_IM_ALDB = 'print_im_all_link_database' +SRV_ALL_LINK_GROUP = 'group' +SRV_ALL_LINK_MODE = 'mode' +SRV_LOAD_DB_RELOAD = 'reload' +SRV_CONTROLLER = 'controller' +SRV_RESPONDER = 'responder' + CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( cv.deprecated(CONF_PLATFORM), vol.Schema({ vol.Required(CONF_ADDRESS): cv.string, @@ -47,6 +59,24 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +ADD_ALL_LINK_SCHEMA = vol.Schema({ + vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), + vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]), + }) + +DEL_ALL_LINK_SCHEMA = vol.Schema({ + vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), + }) + +LOAD_ALDB_SCHEMA = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(SRV_LOAD_DB_RELOAD, default='false'): cv.boolean, + }) + +PRINT_ALDB_SCHEMA = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + }) + @asyncio.coroutine def async_setup(hass, config): @@ -54,6 +84,7 @@ def async_setup(hass, config): import insteonplm ipdb = IPDB() + plm = None conf = config[DOMAIN] port = conf.get(CONF_PORT) @@ -79,6 +110,60 @@ def async_setup(hass, config): 'state_key': state_key}, hass_config=config)) + def add_all_link(service): + """Add an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + mode = service.data.get(SRV_ALL_LINK_MODE) + link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0 + plm.start_all_linking(link_mode, group) + + def del_all_link(service): + """Delete an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + plm.start_all_linking(255, group) + + def load_aldb(service): + """Load the device All-Link database.""" + entity_id = service.data.get(CONF_ENTITY_ID) + reload = service.data.get(SRV_LOAD_DB_RELOAD) + entities = hass.data[DOMAIN].get('entities') + entity = entities.get(entity_id) + if entity: + entity.load_aldb(reload) + else: + _LOGGER.error('Entity %s is not an INSTEON device', entity_id) + + def print_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + entity_id = service.data.get(CONF_ENTITY_ID) + entities = hass.data[DOMAIN].get('entities') + entity = entities.get(entity_id) + if entity: + entity.print_aldb() + else: + _LOGGER.error('Entity %s is not an INSTEON device', entity_id) + + def print_im_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + print_aldb_to_log(plm.aldb) + + def _register_services(): + hass.services.register(DOMAIN, SRV_ADD_ALL_LINK, add_all_link, + schema=ADD_ALL_LINK_SCHEMA) + hass.services.register(DOMAIN, SRV_DEL_ALL_LINK, del_all_link, + schema=DEL_ALL_LINK_SCHEMA) + hass.services.register(DOMAIN, SRV_LOAD_ALDB, load_aldb, + schema=LOAD_ALDB_SCHEMA) + hass.services.register(DOMAIN, SRV_PRINT_ALDB, print_aldb, + schema=PRINT_ALDB_SCHEMA) + hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, + schema=None) + _LOGGER.debug("Insteon_plm Services registered") + _LOGGER.info("Looking for PLM on %s", port) conn = yield from insteonplm.Connection.create( device=port, @@ -100,11 +185,14 @@ def async_setup(hass, config): plm.devices.add_override(address, CONF_PRODUCT_KEY, device_override[prop]) - hass.data['insteon_plm'] = plm + hass.data[DOMAIN] = {} + hass.data[DOMAIN]['plm'] = plm + hass.data[DOMAIN]['entities'] = {} hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) plm.devices.add_device_callback(async_plm_new_device) + hass.async_add_job(_register_services) return True @@ -169,6 +257,7 @@ class InsteonPLMEntity(Entity): """Initialize the INSTEON PLM binary sensor.""" self._insteon_device_state = device.states[state_key] self._insteon_device = device + self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) @property def should_poll(self): @@ -215,3 +304,44 @@ class InsteonPLMEntity(Entity): """Register INSTEON update events.""" self._insteon_device_state.register_updates( self.async_entity_update) + self.hass.data[DOMAIN]['entities'][self.entity_id] = self + + def load_aldb(self, reload=False): + """Load the device All-Link Database.""" + if reload: + self._insteon_device.aldb.clear() + self._insteon_device.read_aldb() + + def print_aldb(self): + """Print the device ALDB to the log file.""" + print_aldb_to_log(self._insteon_device.aldb) + + @callback + def _aldb_loaded(self): + """All-Link Database loaded for the device.""" + self.print_aldb() + + +def print_aldb_to_log(aldb): + """Print the All-Link Database to the log file.""" + from insteonplm.devices import ALDBStatus + _LOGGER.info('ALDB load status is %s', aldb.status.name) + if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: + _LOGGER.warning('Device All-Link database not loaded') + _LOGGER.warning('Use service insteon_plm.load_aldb first') + return + + _LOGGER.info('RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3') + _LOGGER.info('----- ------ ---- --- ----- -------- ------ ------ ------') + for mem_addr in aldb: + rec = aldb[mem_addr] + # For now we write this to the log + # Roadmap is to create a configuration panel + in_use = 'Y' if rec.control_flags.is_in_use else 'N' + mode = 'C' if rec.control_flags.is_controller else 'R' + hwm = 'Y' if rec.control_flags.is_high_water_mark else 'N' + _LOGGER.info(' {:04x} {:s} {:s} {:s} {:3d} {:s}' + ' {:3d} {:3d} {:3d}'.format( + rec.mem_addr, in_use, mode, hwm, + rec.group, rec.address.human, + rec.data1, rec.data2, rec.data3)) diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml new file mode 100644 index 00000000000..a0e250fef1f --- /dev/null +++ b/homeassistant/components/insteon_plm/services.yaml @@ -0,0 +1,32 @@ +add_all_link: + description: Tells the Insteom Modem (IM) start All-Linking mode. Once the the IM is in All-Linking mode, press the link button on the device to complete All-Linking. + fields: + group: + description: All-Link group number. + example: 1 + mode: + description: Linking mode controller - IM is controller responder - IM is responder + example: 'controller' +delete_all_link: + description: Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link Database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process. + fields: + group: + description: All-Link group number. + example: 1 +load_all_link_database: + description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistant. This may take a LONG time and may need to be repeated to obtain all records. + fields: + entity_id: + description: Name of the device to print + example: 'light.1a2b3c' + reload: + description: Reload all records. If true the current records are cleared from memory (does not effect the device) and the records are reloaded. If false the existing records are left in place and only missing records are added. Default is false. + example: 'true' +print_all_link_database: + description: Print the All-Link Database for a device. Requires that the All-Link Database is loaded into memory. + fields: + entity_id: + description: Name of the device to print + example: 'light.1a2b3c' +print_im_all_link_database: + description: Print the All-Link Database for the INSTEON Modem (IM). diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 020f43d9935..916e60c00b1 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -5,13 +5,14 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) 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, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, Light) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util DEPENDENCIES = ['deconz'] @@ -19,21 +20,35 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ light.""" - if discovery_info is None: - return + """Old way of setting up deCONZ lights and group.""" + pass - lights = hass.data[DATA_DECONZ].lights - groups = hass.data[DATA_DECONZ].groups - entities = [] - for light in lights.values(): - entities.append(DeconzLight(light)) +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ lights and groups from a config entry.""" + @callback + def async_add_light(lights): + """Add light from deCONZ.""" + entities = [] + for light in lights: + entities.append(DeconzLight(light)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_light', async_add_light)) - for group in groups.values(): - if group.lights: # Don't create entity for group not containing light - entities.append(DeconzLight(group)) - async_add_devices(entities, True) + @callback + def async_add_group(groups): + """Add group from deCONZ.""" + entities = [] + for group in groups: + if group.lights: + entities.append(DeconzLight(group)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_group', async_add_group)) + + async_add_light(hass.data[DATA_DECONZ].lights.values()) + async_add_group(hass.data[DATA_DECONZ].groups.values()) class DeconzLight(Light): diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 6ffdcc0bb4a..6c7f2e98e37 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP, - EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, - SUPPORT_COLOR, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE, + EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, + SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -191,8 +191,16 @@ class FluxLight(Light): @property def supported_features(self): """Flag supported features.""" + if self._mode is MODE_RGBW: + return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE + return SUPPORT_FLUX_LED + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._bulb.getRgbw()[3] + @property def effect_list(self): """Return the list of supported effects.""" @@ -212,24 +220,31 @@ class FluxLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) + white = kwargs.get(ATTR_WHITE_VALUE) - if rgb is not None and brightness is not None: - self._bulb.setRgb(*tuple(rgb), brightness=brightness) - elif rgb is not None: - self._bulb.setRgb(*tuple(rgb)) + # color change only + if rgb is not None: + self._bulb.setRgb(*tuple(rgb), brightness=self.brightness) + + # brightness change only elif brightness is not None: - if self._mode == MODE_RGBW: - self._bulb.setWarmWhite255(brightness) - elif self._mode == MODE_RGB: - (red, green, blue) = self._bulb.getRgb() - self._bulb.setRgb(red, green, blue, brightness=brightness) + (red, green, blue) = self._bulb.getRgb() + self._bulb.setRgb(red, green, blue, brightness=brightness) + + # random color effect elif effect == EFFECT_RANDOM: self._bulb.setRgb(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + + # effect selection elif effect in EFFECT_MAP: self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) + # white change only + elif white is not None: + self._bulb.setWarmWhite255(white) + def turn_off(self, **kwargs): """Turn the specified or all lights off.""" self._bulb.turnOff() diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 9f662718514..837a6f82510 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -245,7 +245,7 @@ class HueLight(Light): mode = self._color_mode source = self.light.action if self.is_group else self.light.state - if mode in ('xy', 'hs'): + if mode in ('xy', 'hs') and 'xy' in source: return color.color_xy_to_hs(*source['xy']) return None diff --git a/homeassistant/components/light/insteon_plm.py b/homeassistant/components/light/insteon_plm.py index 40453da38e5..8a3b463c2bd 100644 --- a/homeassistant/components/light/insteon_plm.py +++ b/homeassistant/components/light/insteon_plm.py @@ -21,7 +21,7 @@ MAX_BRIGHTNESS = 255 @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Insteon PLM device.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 20e49e40bae..ca5c76e905f 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -4,7 +4,6 @@ Support for MQTT JSON lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_json/ """ -import asyncio import logging import json import voluptuous as vol @@ -26,6 +25,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ConfigType import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -44,12 +44,14 @@ DEFAULT_OPTIMISTIC = False DEFAULT_RGB = False DEFAULT_WHITE_VALUE = False DEFAULT_XY = False +DEFAULT_HS = False DEFAULT_BRIGHTNESS_SCALE = 255 CONF_EFFECT_LIST = 'effect_list' CONF_FLASH_TIME_LONG = 'flash_time_long' CONF_FLASH_TIME_SHORT = 'flash_time_short' +CONF_HS = 'hs' # Stealing some of these from the base MQTT configs. PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -72,12 +74,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, + vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up a MQTT JSON Light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -99,6 +102,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_RGB), config.get(CONF_WHITE_VALUE), config.get(CONF_XY), + config.get(CONF_HS), { key: config.get(key) for key in ( CONF_FLASH_TIME_SHORT, @@ -116,7 +120,7 @@ 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, + brightness, color_temp, effect, rgb, white_value, xy, hs, flash_times, availability_topic, payload_available, payload_not_available, brightness_scale): """Initialize MQTT JSON light.""" @@ -131,6 +135,7 @@ class MqttJson(MqttAvailability, Light): self._state = False self._rgb = rgb self._xy = xy + self._hs_support = hs if brightness: self._brightness = 255 else: @@ -146,7 +151,7 @@ class MqttJson(MqttAvailability, Light): else: self._effect = None - if rgb or xy: + if hs or rgb or xy: self._hs = [0, 0] else: self._hs = None @@ -166,11 +171,11 @@ class MqttJson(MqttAvailability, Light): self._supported_features |= (effect and SUPPORT_EFFECT) self._supported_features |= (white_value and SUPPORT_WHITE_VALUE) self._supported_features |= (xy and SUPPORT_COLOR) + self._supported_features |= (hs and SUPPORT_COLOR) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def state_received(topic, payload, qos): @@ -193,6 +198,7 @@ class MqttJson(MqttAvailability, Light): pass except ValueError: _LOGGER.warning("Invalid RGB color value received") + try: x_color = float(values['color']['x']) y_color = float(values['color']['y']) @@ -203,6 +209,16 @@ class MqttJson(MqttAvailability, Light): except ValueError: _LOGGER.warning("Invalid XY color value received") + try: + hue = float(values['color']['h']) + saturation = float(values['color']['s']) + + self._hs = (hue, saturation) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid HS color value received") + if self._brightness is not None: try: self._brightness = int(values['brightness'] / @@ -240,7 +256,7 @@ class MqttJson(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) @@ -299,8 +315,7 @@ class MqttJson(MqttAvailability, Light): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -309,7 +324,8 @@ class MqttJson(MqttAvailability, Light): message = {'state': 'ON'} - if ATTR_HS_COLOR in kwargs and (self._rgb or self._xy): + if ATTR_HS_COLOR in kwargs and (self._hs_support + or self._rgb or self._xy): hs_color = kwargs[ATTR_HS_COLOR] message['color'] = {} if self._rgb: @@ -325,6 +341,9 @@ class MqttJson(MqttAvailability, Light): xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) message['color']['x'] = xy_color[0] message['color']['y'] = xy_color[1] + if self._hs_support: + message['color']['h'] = hs_color[0] + message['color']['s'] = hs_color[1] if self._optimistic: self._hs = kwargs[ATTR_HS_COLOR] @@ -383,8 +402,7 @@ class MqttJson(MqttAvailability, Light): if should_update: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 95082bb4d19..ab53c3669cb 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -253,6 +253,8 @@ class TradfriLight(Light): params[ATTR_BRIGHTNESS] = brightness hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360)) sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100)) + if brightness is None: + params[ATTR_TRANSITION_TIME] = transition_time await self._api( self._light_control.set_hsb(hue, sat, **params)) return diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index d0575105235..fcf3d2f7a7d 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -12,7 +12,6 @@ import homeassistant.util as util from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION) -from homeassistant.loader import get_component import homeassistant.util.color as color_util DEPENDENCIES = ['wemo'] @@ -151,7 +150,7 @@ class WemoDimmer(Light): @asyncio.coroutine def async_added_to_hass(self): """Register update callback.""" - wemo = get_component('wemo') + wemo = self.hass.components.wemo # The register method uses a threading condition, so call via executor. # and yield from to wait until the task is done. yield from self.hass.async_add_job( diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index fd957f8f11d..04e9c34b0f6 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.wink/ """ import asyncio -import colorsys from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, @@ -54,29 +53,19 @@ class WinkLight(WinkDevice, Light): return int(self.wink.brightness() * 255) return None - @property - def rgb_color(self): - """Define current bulb color in RGB.""" - if not self.wink.supports_hue_saturation(): - return None - else: - hue = self.wink.color_hue() - saturation = self.wink.color_saturation() - value = int(self.wink.brightness() * 255) - if hue is None or saturation is None or value is None: - return None - rgb = colorsys.hsv_to_rgb(hue, saturation, value) - r_value = int(round(rgb[0])) - g_value = int(round(rgb[1])) - b_value = int(round(rgb[2])) - return r_value, g_value, b_value - @property def hs_color(self): """Define current bulb color.""" - if not self.wink.supports_xy_color(): - return None - return color_util.color_xy_to_hs(*self.wink.color_xy()) + if self.wink.supports_xy_color(): + return color_util.color_xy_to_hs(*self.wink.color_xy()) + + if self.wink.supports_hue_saturation(): + hue = self.wink.color_hue() + saturation = self.wink.color_saturation() + if hue is not None and saturation is not None: + return hue*360, saturation*100 + + return None @property def color_temp(self): @@ -104,7 +93,8 @@ class WinkLight(WinkDevice, Light): xy_color = color_util.color_hs_to_xy(*hs_color) state_kwargs['color_xy'] = xy_color if self.wink.supports_hue_saturation(): - state_kwargs['color_hue_saturation'] = hs_color + hs_scaled = hs_color[0]/360, hs_color[1]/100 + state_kwargs['color_hue_saturation'] = hs_scaled if color_temp_mired: state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired) diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index 125e791829f..37ae60e3494 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): for device in gateway.devices['light']: model = device['model'] - if model == 'gateway': + if model in ['gateway', 'gateway.v3']: devices.append(XiaomiGatewayLight(device, 'Gateway Light', gateway)) add_devices(devices) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index d6d860cbd9e..202c6ac594d 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -32,16 +32,17 @@ LEGACY_DEVICE_TYPE_MAP = { 'ceiling1': 'ceiling', } -CONF_TRANSITION = 'transition' +DEFAULT_NAME = 'Yeelight' DEFAULT_TRANSITION = 350 +CONF_TRANSITION = 'transition' CONF_SAVE_ON_CHANGE = 'save_on_change' CONF_MODE_MUSIC = 'use_music_mode' DATA_KEY = 'light.yeelight' DEVICE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, + 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, @@ -136,20 +137,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Not using hostname, as it seems to vary. name = "yeelight_%s_%s" % (device_type, discovery_info['properties']['mac']) - device = {'name': name, 'ipaddr': discovery_info['host']} + host = discovery_info['host'] + device = {'name': name, 'ipaddr': host} light = YeelightLight(device, DEVICE_SCHEMA({})) lights.append(light) - hass.data[DATA_KEY][name] = light + hass.data[DATA_KEY][host] = light else: - for ipaddr, device_config in config[CONF_DEVICES].items(): - name = device_config[CONF_NAME] - _LOGGER.debug("Adding configured %s", name) - - device = {'name': name, 'ipaddr': ipaddr} + for host, device_config in config[CONF_DEVICES].items(): + device = {'name': device_config[CONF_NAME], 'ipaddr': host} light = YeelightLight(device, device_config) lights.append(light) - hass.data[DATA_KEY][name] = light + hass.data[DATA_KEY][host] = light add_devices(lights, True) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 286ce73f1ed..04216780c80 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -61,7 +61,7 @@ def get_device(node, values, node_config, **kwargs): def brightness_state(value): """Return the brightness and state.""" if value.data > 0: - return round((value.data / 99) * 255, 0), STATE_ON + return round((value.data / 99) * 255), STATE_ON return 0, STATE_OFF diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 1c3e8ed1f19..8bab6fe0440 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -4,7 +4,6 @@ Event parser and human readable log generator. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logbook/ """ -import asyncio import logging from datetime import timedelta from itertools import groupby @@ -88,8 +87,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None): hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) -@asyncio.coroutine -def setup(hass, config): +async def setup(hass, config): """Listen for download events to download files.""" @callback def log_message(service): @@ -105,7 +103,7 @@ def setup(hass, config): hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'logbook', 'logbook', 'mdi:format-list-bulleted-type') hass.services.async_register( @@ -124,8 +122,7 @@ class LogbookView(HomeAssistantView): """Initialize the logbook view.""" self.config = config - @asyncio.coroutine - def get(self, request, datetime=None): + async def get(self, request, datetime=None): """Retrieve logbook entries.""" if datetime: datetime = dt_util.parse_datetime(datetime) @@ -144,8 +141,7 @@ class LogbookView(HomeAssistantView): return self.json(list( _get_events(hass, self.config, start_day, end_day))) - response = yield from hass.async_add_job(json_events) - return response + return await hass.async_add_job(json_events) class Entry(object): diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index c2309401977..6e8995a0444 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -4,7 +4,6 @@ Component that will help set the level of logging for components. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logger/ """ -import asyncio import logging from collections import OrderedDict @@ -73,8 +72,7 @@ class HomeAssistantLogFilter(logging.Filter): return record.levelno >= default -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the logger component.""" logfilter = {} @@ -116,8 +114,7 @@ def async_setup(hass, config): if LOGGER_LOGS in config.get(DOMAIN): set_log_levels(config.get(DOMAIN)[LOGGER_LOGS]) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle logger services.""" set_log_levels(service.data) diff --git a/homeassistant/components/map.py b/homeassistant/components/map.py index b8293f64fc0..30cb00af69e 100644 --- a/homeassistant/components/map.py +++ b/homeassistant/components/map.py @@ -4,14 +4,11 @@ Provides a map panel for showing device locations. For more details about this component, please refer to the documentation at https://home-assistant.io/components/map/ """ -import asyncio - DOMAIN = 'map' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Register the built-in map panel.""" - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'map', 'map', 'mdi:account-location') return True diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix.py new file mode 100644 index 00000000000..569b012b484 --- /dev/null +++ b/homeassistant/components/matrix.py @@ -0,0 +1,351 @@ +""" +The matrix bot component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/matrix/ +""" +import logging +import os +from functools import partial + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import (ATTR_TARGET, ATTR_MESSAGE) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, + CONF_VERIFY_SSL, CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START) +from homeassistant.util.json import load_json, save_json +from homeassistant.exceptions import HomeAssistantError + +REQUIREMENTS = ['matrix-client==0.2.0'] + +_LOGGER = logging.getLogger(__name__) + +SESSION_FILE = '.matrix.conf' + +CONF_HOMESERVER = 'homeserver' +CONF_ROOMS = 'rooms' +CONF_COMMANDS = 'commands' +CONF_WORD = 'word' +CONF_EXPRESSION = 'expression' + +EVENT_MATRIX_COMMAND = 'matrix_command' + +DOMAIN = 'matrix' + +COMMAND_SCHEMA = vol.All( + # Basic Schema + vol.Schema({ + vol.Exclusive(CONF_WORD, 'trigger'): cv.string, + vol.Exclusive(CONF_EXPRESSION, 'trigger'): cv.is_regex, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, + [cv.string]), + }), + # Make sure it's either a word or an expression command + cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION) +) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOMESERVER): cv.url, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Required(CONF_USERNAME): cv.matches_regex("@[^:]*:.*"), + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, + [cv.string]), + vol.Optional(CONF_COMMANDS, default=[]): [COMMAND_SCHEMA] + }) +}, extra=vol.ALLOW_EXTRA) + +SERVICE_SEND_MESSAGE = 'send_message' + +SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.string, + vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup(hass, config): + """Set up the Matrix bot component.""" + from matrix_client.client import MatrixRequestError + + config = config[DOMAIN] + + try: + bot = MatrixBot( + hass, + os.path.join(hass.config.path(), SESSION_FILE), + config[CONF_HOMESERVER], + config[CONF_VERIFY_SSL], + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_ROOMS], + config[CONF_COMMANDS]) + hass.data[DOMAIN] = bot + except MatrixRequestError as exception: + _LOGGER.error("Matrix failed to log in: %s", str(exception)) + return False + + hass.services.register( + DOMAIN, SERVICE_SEND_MESSAGE, bot.handle_send_message, + schema=SERVICE_SCHEMA_SEND_MESSAGE) + + return True + + +class MatrixBot(object): + """The Matrix Bot.""" + + def __init__(self, hass, config_file, homeserver, verify_ssl, + username, password, listening_rooms, commands): + """Set up the client.""" + self.hass = hass + + self._session_filepath = config_file + self._auth_tokens = self._get_auth_tokens() + + self._homeserver = homeserver + self._verify_tls = verify_ssl + self._mx_id = username + self._password = password + + self._listening_rooms = listening_rooms + + # Logging in is deferred b/c it does I/O + self._setup_done = False + + # We have to fetch the aliases for every room to make sure we don't + # join it twice by accident. However, fetching aliases is costly, + # so we only do it once per room. + self._aliases_fetched_for = set() + + # word commands are stored dict-of-dict: First dict indexes by room ID + # / alias, second dict indexes by the word + self._word_commands = {} + + # regular expression commands are stored as a list of commands per + # room, i.e., a dict-of-list + self._expression_commands = {} + + for command in commands: + if not command.get(CONF_ROOMS): + command[CONF_ROOMS] = listening_rooms + + if command.get(CONF_WORD): + for room_id in command[CONF_ROOMS]: + if room_id not in self._word_commands: + self._word_commands[room_id] = {} + self._word_commands[room_id][command[CONF_WORD]] = command + else: + for room_id in command[CONF_ROOMS]: + if room_id not in self._expression_commands: + self._expression_commands[room_id] = [] + self._expression_commands[room_id].append(command) + + # Log in. This raises a MatrixRequestError if login is unsuccessful + self._client = self._login() + + def handle_matrix_exception(exception): + """Handle exceptions raised inside the Matrix SDK.""" + _LOGGER.error("Matrix exception:\n %s", str(exception)) + + self._client.start_listener_thread( + exception_handler=handle_matrix_exception) + + def stop_client(_): + """Run once when Home Assistant stops.""" + self._client.stop_listener_thread() + + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_client) + + # Joining rooms potentially does a lot of I/O, so we defer it + def handle_startup(_): + """Run once when Home Assistant finished startup.""" + self._join_rooms() + + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, handle_startup) + + def _handle_room_message(self, room_id, room, event): + """Handle a message sent to a room.""" + if event['content']['msgtype'] != 'm.text': + return + + if event['sender'] == self._mx_id: + return + + _LOGGER.debug("Handling message: %s", event['content']['body']) + + if event['content']['body'][0] == "!": + # Could trigger a single-word command. + pieces = event['content']['body'].split(' ') + cmd = pieces[0][1:] + + command = self._word_commands.get(room_id, {}).get(cmd) + if command: + event_data = { + 'command': command[CONF_NAME], + 'sender': event['sender'], + 'room': room_id, + 'args': pieces[1:] + } + self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) + + # After single-word commands, check all regex commands in the room + for command in self._expression_commands.get(room_id, []): + match = command[CONF_EXPRESSION].match(event['content']['body']) + if not match: + continue + event_data = { + 'command': command[CONF_NAME], + 'sender': event['sender'], + 'room': room_id, + 'args': match.groupdict() + } + self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) + + def _join_or_get_room(self, room_id_or_alias): + """Join a room or get it, if we are already in the room. + + We can't just always call join_room(), since that seems to crash + the client if we're already in the room. + """ + rooms = self._client.get_rooms() + if room_id_or_alias in rooms: + _LOGGER.debug("Already in room %s", room_id_or_alias) + return rooms[room_id_or_alias] + + for room in rooms.values(): + if room.room_id not in self._aliases_fetched_for: + room.update_aliases() + self._aliases_fetched_for.add(room.room_id) + + if room_id_or_alias in room.aliases: + _LOGGER.debug("Already in room %s (known as %s)", + room.room_id, room_id_or_alias) + return room + + room = self._client.join_room(room_id_or_alias) + _LOGGER.info("Joined room %s (known as %s)", room.room_id, + room_id_or_alias) + return room + + def _join_rooms(self): + """Join the rooms that we listen for commands in.""" + from matrix_client.client import MatrixRequestError + + for room_id in self._listening_rooms: + try: + room = self._join_or_get_room(room_id) + room.add_listener(partial(self._handle_room_message, room_id), + "m.room.message") + + except MatrixRequestError as ex: + _LOGGER.error("Could not join room %s: %s", room_id, ex) + + def _get_auth_tokens(self): + """ + Read sorted authentication tokens from disk. + + Returns the auth_tokens dictionary. + """ + try: + auth_tokens = load_json(self._session_filepath) + + return auth_tokens + except HomeAssistantError as ex: + _LOGGER.warning( + "Loading authentication tokens from file '%s' failed: %s", + self._session_filepath, str(ex)) + return {} + + def _store_auth_token(self, token): + """Store authentication token to session and persistent storage.""" + self._auth_tokens[self._mx_id] = token + + save_json(self._session_filepath, self._auth_tokens) + + def _login(self): + """Login to the matrix homeserver and return the client instance.""" + from matrix_client.client import MatrixRequestError + + # Attempt to generate a valid client using either of the two possible + # login methods: + client = None + + # If we have an authentication token + if self._mx_id in self._auth_tokens: + try: + client = self._login_by_token() + _LOGGER.debug("Logged in using stored token.") + + except MatrixRequestError as ex: + _LOGGER.warning( + "Login by token failed, falling back to password. " + "login_by_token raised: (%d) %s", + ex.code, ex.content) + + # If we still don't have a client try password. + if not client: + try: + client = self._login_by_password() + _LOGGER.debug("Logged in using password.") + + except MatrixRequestError as ex: + _LOGGER.error( + "Login failed, both token and username/password invalid " + "login_by_password raised: (%d) %s", + ex.code, ex.content) + + # re-raise the error so _setup can catch it. + raise + + return client + + def _login_by_token(self): + """Login using authentication token and return the client.""" + from matrix_client.client import MatrixClient + + return MatrixClient( + base_url=self._homeserver, + token=self._auth_tokens[self._mx_id], + user_id=self._mx_id, + valid_cert_check=self._verify_tls) + + def _login_by_password(self): + """Login using password authentication and return the client.""" + from matrix_client.client import MatrixClient + + _client = MatrixClient( + base_url=self._homeserver, + valid_cert_check=self._verify_tls) + + _client.login_with_password(self._mx_id, self._password) + + self._store_auth_token(_client.token) + + return _client + + def _send_message(self, message, target_rooms): + """Send the message to the matrix server.""" + from matrix_client.client import MatrixRequestError + + for target_room in target_rooms: + try: + room = self._join_or_get_room(target_room) + _LOGGER.debug(room.send_text(message)) + except MatrixRequestError as ex: + _LOGGER.error( + "Unable to deliver message to room '%s': (%d): %s", + target_room, ex.code, ex.content) + + def handle_send_message(self, service): + """Handle the send_message service.""" + if not self._setup_done: + _LOGGER.warning("Could not send message: setup is not done!") + return + + self._send_message(service.data[ATTR_MESSAGE], + service.data[ATTR_TARGET]) diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py index cf5091fc308..bca7a1b4ab7 100644 --- a/homeassistant/components/maxcube.py +++ b/homeassistant/components/maxcube.py @@ -13,7 +13,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL REQUIREMENTS = ['maxcube-api==0.1.0'] @@ -32,6 +32,7 @@ CONF_GATEWAYS = 'gateways' CONFIG_GATEWAY = vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SCAN_INTERVAL, default=300): cv.time_period, }) CONFIG_SCHEMA = vol.Schema({ @@ -54,10 +55,11 @@ def setup(hass, config): for gateway in gateways: host = gateway[CONF_HOST] port = gateway[CONF_PORT] + scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds() try: cube = MaxCube(MaxCubeConnection(host, port)) - hass.data[DATA_KEY][host] = MaxCubeHandle(cube) + hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) except timeout as ex: _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) hass.components.persistent_notification.create( @@ -80,9 +82,10 @@ def setup(hass, config): class MaxCubeHandle(object): """Keep the cube instance in one place and centralize the update.""" - def __init__(self, cube): + def __init__(self, cube, scan_interval): """Initialize the Cube Handle.""" self.cube = cube + self.scan_interval = scan_interval self.mutex = Lock() self._updatets = time.time() @@ -90,8 +93,8 @@ class MaxCubeHandle(object): """Pull the latest data from the MAX! Cube.""" # Acquire mutex to prevent simultaneous update from multiple threads with self.mutex: - # Only update every 60s - if (time.time() - self._updatets) >= 60: + # Only update every update_interval + if (time.time() - self._updatets) >= self.scan_interval: _LOGGER.debug("Updating") try: diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index b5fd26b0bcb..fe6ebe8e618 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.04.16'] +REQUIREMENTS = ['youtube_dl==2018.04.25'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 615c758cd1a..20a1a473ba8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/media_player/ """ import asyncio +import base64 from datetime import timedelta import functools as ft import collections @@ -17,6 +18,7 @@ from aiohttp.hdrs import CONTENT_TYPE, CACHE_CONTROL import async_timeout import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.const import ( STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, ATTR_ENTITY_ID, @@ -31,6 +33,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass +from homeassistant.components import websocket_api _LOGGER = logging.getLogger(__name__) _RND = SystemRandom() @@ -361,18 +364,27 @@ def set_shuffle(hass, shuffle, entity_id=None): hass.services.call(DOMAIN, SERVICE_SHUFFLE_SET, data) -@asyncio.coroutine -def async_setup(hass, config): +WS_TYPE_MEDIA_PLAYER_THUMBNAIL = 'media_player_thumbnail' +SCHEMA_WEBSOCKET_GET_THUMBNAIL = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + 'type': WS_TYPE_MEDIA_PLAYER_THUMBNAIL, + 'entity_id': cv.entity_id + }) + + +async def async_setup(hass, config): """Track states and offer events for media_players.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) + hass.components.websocket_api.async_register_command( + WS_TYPE_MEDIA_PLAYER_THUMBNAIL, websocket_handle_thumbnail, + SCHEMA_WEBSOCKET_GET_THUMBNAIL) hass.http.register_view(MediaPlayerImageView(component)) - yield from component.async_setup(config) + await component.async_setup(config) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on MediaPlayerDevice.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -400,13 +412,13 @@ def async_setup(hass, config): update_tasks = [] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) if not player.should_poll: continue update_tasks.append(player.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service].get( @@ -490,14 +502,13 @@ class MediaPlayerDevice(Entity): return None - @asyncio.coroutine - def async_get_media_image(self): + async def async_get_media_image(self): """Fetch media image of current playing image.""" url = self.media_image_url if url is None: return None, None - return (yield from _async_fetch_image(self.hass, url)) + return await _async_fetch_image(self.hass, url) @property def media_title(self): @@ -808,34 +819,31 @@ class MediaPlayerDevice(Entity): return self.async_turn_on() return self.async_turn_off() - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Turn volume up for media player. This method is a coroutine. """ if hasattr(self, 'volume_up'): # pylint: disable=no-member - yield from self.hass.async_add_job(self.volume_up) + await self.hass.async_add_job(self.volume_up) return if self.volume_level < 1: - yield from self.async_set_volume_level( - min(1, self.volume_level + .1)) + await self.async_set_volume_level(min(1, self.volume_level + .1)) - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Turn volume down for media player. This method is a coroutine. """ if hasattr(self, 'volume_down'): # pylint: disable=no-member - yield from self.hass.async_add_job(self.volume_down) + await self.hass.async_add_job(self.volume_down) return if self.volume_level > 0: - yield from self.async_set_volume_level( + await self.async_set_volume_level( max(0, self.volume_level - .1)) def async_media_play_pause(self): @@ -879,8 +887,7 @@ class MediaPlayerDevice(Entity): return state_attr -@asyncio.coroutine -def _async_fetch_image(hass, url): +async def _async_fetch_image(hass, url): """Fetch image. Images are cached in memory (the images are typically 10-100kB in size). @@ -891,7 +898,7 @@ def _async_fetch_image(hass, url): if url not in cache_images: cache_images[url] = {CACHE_LOCK: asyncio.Lock(loop=hass.loop)} - with (yield from cache_images[url][CACHE_LOCK]): + async with cache_images[url][CACHE_LOCK]: if CACHE_CONTENT in cache_images[url]: return cache_images[url][CACHE_CONTENT] @@ -899,10 +906,10 @@ def _async_fetch_image(hass, url): websession = async_get_clientsession(hass) try: with async_timeout.timeout(10, loop=hass.loop): - response = yield from websession.get(url) + response = await websession.get(url) if response.status == 200: - content = yield from response.read() + content = await response.read() content_type = response.headers.get(CONTENT_TYPE) if content_type: content_type = content_type.split(';')[0] @@ -928,8 +935,7 @@ class MediaPlayerImageView(HomeAssistantView): """Initialize a media player view.""" self.component = component - @asyncio.coroutine - def get(self, request, entity_id): + async def get(self, request, entity_id): """Start a get request.""" player = self.component.get_entity(entity_id) if player is None: @@ -942,7 +948,7 @@ class MediaPlayerImageView(HomeAssistantView): if not authenticated: return web.Response(status=401) - data, content_type = yield from player.async_get_media_image() + data, content_type = await player.async_get_media_image() if data is None: return web.Response(status=500) @@ -950,3 +956,36 @@ class MediaPlayerImageView(HomeAssistantView): headers = {CACHE_CONTROL: 'max-age=3600'} return web.Response( body=data, content_type=content_type, headers=headers) + + +@callback +def websocket_handle_thumbnail(hass, connection, msg): + """Handle get media player cover command. + + Async friendly. + """ + component = hass.data[DOMAIN] + player = component.get_entity(msg['entity_id']) + + if player is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'entity_not_found', 'Entity not found')) + return + + async def send_image(): + """Send image.""" + data, content_type = await player.async_get_media_image() + + if data is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'thumbnail_fetch_failed', + 'Failed to fetch thumbnail')) + return + + connection.send_message_outside(websocket_api.result_message( + msg['id'], { + 'content_type': content_type, + 'content': base64.b64encode(data).decode('utf-8') + })) + + hass.async_add_job(send_image()) diff --git a/homeassistant/components/media_player/blackbird.py b/homeassistant/components/media_player/blackbird.py index 37b3c0ff819..1c976f5eecd 100644 --- a/homeassistant/components/media_player/blackbird.py +++ b/homeassistant/components/media_player/blackbird.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.blackbird """ import logging +import socket import voluptuous as vol @@ -50,71 +51,68 @@ ZONE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) # Valid source ids: 1-8 SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TYPE): vol.In(['serial', 'socket']), - vol.Optional(CONF_PORT): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), - vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), -}) +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_PORT, CONF_HOST), + PLATFORM_SCHEMA.extend({ + vol.Exclusive(CONF_PORT, CONF_TYPE): cv.string, + vol.Exclusive(CONF_HOST, CONF_TYPE): cv.string, + vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), + vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), + })) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Monoprice Blackbird 4k 8x8 HDBaseT Matrix platform.""" + if DATA_BLACKBIRD not in hass.data: + hass.data[DATA_BLACKBIRD] = {} + port = config.get(CONF_PORT) host = config.get(CONF_HOST) - device_type = config.get(CONF_TYPE) - import socket from pyblackbird import get_blackbird from serial import SerialException - if device_type == 'serial': - if port is None: - _LOGGER.error("No port configured") - return + connection = None + if port is not None: try: blackbird = get_blackbird(port) + connection = port except SerialException: _LOGGER.error("Error connecting to the Blackbird controller") return - elif device_type == 'socket': + if host is not None: try: - if host is None: - _LOGGER.error("No host configured") - return blackbird = get_blackbird(host, False) + connection = host except socket.timeout: _LOGGER.error("Error connecting to the Blackbird controller") return - else: - _LOGGER.error("Incorrect device type specified") - return - sources = {source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items()} - hass.data[DATA_BLACKBIRD] = [] + devices = [] for zone_id, extra in config[CONF_ZONES].items(): _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) - hass.data[DATA_BLACKBIRD].append(BlackbirdZone( - blackbird, sources, zone_id, extra[CONF_NAME])) + unique_id = "{}-{}".format(connection, zone_id) + device = BlackbirdZone(blackbird, sources, zone_id, extra[CONF_NAME]) + hass.data[DATA_BLACKBIRD][unique_id] = device + devices.append(device) - add_devices(hass.data[DATA_BLACKBIRD], True) + add_devices(devices, True) def service_handle(service): """Handle for services.""" entity_ids = service.data.get(ATTR_ENTITY_ID) source = service.data.get(ATTR_SOURCE) if entity_ids: - devices = [device for device in hass.data[DATA_BLACKBIRD] + devices = [device for device in hass.data[DATA_BLACKBIRD].values() if device.entity_id in entity_ids] else: - devices = hass.data[DATA_BLACKBIRD] + devices = hass.data[DATA_BLACKBIRD].values() for device in devices: if service.service == SERVICE_SETALLZONES: @@ -146,14 +144,13 @@ class BlackbirdZone(MediaPlayerDevice): """Retrieve latest state.""" state = self._blackbird.zone_status(self._zone_id) if not state: - return False + return self._state = STATE_ON if state.power else STATE_OFF idx = state.av if idx in self._source_id_name: self._source = self._source_id_name[idx] else: self._source = None - return True @property def name(self): @@ -187,7 +184,6 @@ class BlackbirdZone(MediaPlayerDevice): def set_all_zones(self, source): """Set all zones to one source.""" - _LOGGER.debug("Setting all zones") if source not in self._source_name_id: return idx = self._source_name_id[source] diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 58703165385..39c278ff95d 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) + SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -22,12 +23,15 @@ REQUIREMENTS = ['onkyo-eiscp==1.2.4'] _LOGGER = logging.getLogger(__name__) CONF_SOURCES = 'sources' +CONF_MAX_VOLUME = 'max_volume' CONF_ZONE2 = 'zone2' DEFAULT_NAME = 'Onkyo Receiver' +SUPPORTED_MAX_VOLUME = 80 SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY KNOWN_HOSTS = [] # type: List[str] DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', @@ -39,6 +43,8 @@ DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MAX_VOLUME, default=SUPPORTED_MAX_VOLUME): + vol.All(vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME)), vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, vol.Optional(CONF_ZONE2, default=False): cv.boolean, @@ -57,7 +63,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: hosts.append(OnkyoDevice( eiscp.eISCP(host), config.get(CONF_SOURCES), - name=config.get(CONF_NAME))) + name=config.get(CONF_NAME), + max_volume=config.get(CONF_MAX_VOLUME), + )) KNOWN_HOSTS.append(host) # Add Zone2 if configured @@ -80,7 +88,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OnkyoDevice(MediaPlayerDevice): """Representation of an Onkyo device.""" - def __init__(self, receiver, sources, name=None): + def __init__(self, receiver, sources, name=None, + max_volume=SUPPORTED_MAX_VOLUME): """Initialize the Onkyo Receiver.""" self._receiver = receiver self._muted = False @@ -88,6 +97,7 @@ class OnkyoDevice(MediaPlayerDevice): self._pwstate = STATE_OFF self._name = name or '{}_{}'.format( receiver.info['model_name'], receiver.info['identifier']) + self._max_volume = max_volume self._current_source = None self._source_list = list(sources.values()) self._source_mapping = sources @@ -141,7 +151,7 @@ class OnkyoDevice(MediaPlayerDevice): self._current_source = '_'.join( [i for i in current_source_tuples[1]]) self._muted = bool(mute_raw[1] == 'on') - self._volume = volume_raw[1] / 80.0 + self._volume = volume_raw[1] / self._max_volume @property def name(self): @@ -183,8 +193,21 @@ class OnkyoDevice(MediaPlayerDevice): self.command('system-power standby') def set_volume_level(self, volume): - """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command('volume {}'.format(int(volume*80))) + """ + Set volume level, input is range 0..1. + + Onkyo ranges from 1-80 however 80 is usually far too loud + so allow the user to specify the upper range with CONF_MAX_VOLUME + """ + self.command('volume {}'.format(int(volume * self._max_volume))) + + def volume_up(self): + """Increase volume by 1 step.""" + self.command('volume level-up') + + def volume_down(self): + """Decrease volume by 1 step.""" + self.command('volume level-down') def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" @@ -251,6 +274,14 @@ class OnkyoDeviceZone2(OnkyoDevice): """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" self.command('zone2.volume={}'.format(int(volume*80))) + def volume_up(self): + """Increase volume by 1 step.""" + self.command('zone2.volume=level-up') + + def volume_down(self): + """Decrease volume by 1 step.""" + self.command('zone2.volume=level-down') + def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 39e5f81b71d..db60de922d9 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -56,8 +56,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = discovery_info.get('name') host = discovery_info.get('host') port = discovery_info.get('port') + udn = discovery_info.get('udn') + if udn and udn.startswith('uuid:'): + uuid = udn[len('uuid:'):] + else: + uuid = None remote = RemoteControl(host, port) - add_devices([PanasonicVieraTVDevice(mac, name, remote)]) + add_devices([PanasonicVieraTVDevice(mac, name, remote, uuid)]) return True host = config.get(CONF_HOST) @@ -70,19 +75,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" - def __init__(self, mac, name, remote): + def __init__(self, mac, name, remote, uuid=None): """Initialize the Panasonic device.""" import wakeonlan # Save a reference to the imported class self._wol = wakeonlan self._mac = mac self._name = name + self._uuid = uuid self._muted = False self._playing = True self._state = STATE_UNKNOWN self._remote = remote self._volume = 0 + @property + def unique_id(self) -> str: + """Return the unique ID of this Viera TV.""" + return self._uuid + def update(self): """Retrieve the latest data.""" try: @@ -138,20 +149,20 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" if self._state != STATE_OFF: - self.send_key('NRC_POWER-ONOFF') + self._remote.turn_off() self._state = STATE_OFF def volume_up(self): """Volume up the media player.""" - self.send_key('NRC_VOLUP-ONOFF') + self._remote.volume_up() def volume_down(self): """Volume down media player.""" - self.send_key('NRC_VOLDOWN-ONOFF') + self._remote.volume_down() def mute_volume(self, mute): """Send mute command.""" - self.send_key('NRC_MUTE-ONOFF') + self._remote.set_mute(mute) def set_volume_level(self, volume): """Set volume level, range 0..1.""" @@ -172,20 +183,20 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def media_play(self): """Send play command.""" self._playing = True - self.send_key('NRC_PLAY-ONOFF') + self._remote.media_play() def media_pause(self): """Send media pause command to media player.""" self._playing = False - self.send_key('NRC_PAUSE-ONOFF') + self._remote.media_pause() def media_next_track(self): """Send next track command.""" - self.send_key('NRC_FF-ONOFF') + self._remote.media_next_track() def media_previous_track(self): """Send the previous track command.""" - self.send_key('NRC_REW-ONOFF') + self._remote.media_previous_track() def play_media(self, media_type, media_id, **kwargs): """Play media.""" diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index b10c761d532..cc10355abe8 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -67,7 +67,7 @@ ATTR_WITH_GROUP = 'with_group' ATTR_NIGHT_SOUND = 'night_sound' ATTR_SPEECH_ENHANCE = 'speech_enhance' -ATTR_IS_COORDINATOR = 'is_coordinator' +ATTR_SONOS_GROUP = 'sonos_group' UPNP_ERRORS_TO_IGNORE = ['701', '711'] @@ -340,6 +340,7 @@ class SonosDevice(MediaPlayerDevice): self._play_mode = None self._name = None self._coordinator = None + self._sonos_group = None self._status = None self._media_duration = None self._media_position = None @@ -688,7 +689,14 @@ class SonosDevice(MediaPlayerDevice): if p.uid != coordinator_uid] if self.unique_id == coordinator_uid: + sonos_group = [] + for uid in (coordinator_uid, *slave_uids): + entity = _get_entity_from_soco_uid(self.hass, uid) + if entity: + sonos_group.append(entity.entity_id) + self._coordinator = None + self._sonos_group = sonos_group self.schedule_update_ha_state() for slave_uid in slave_uids: @@ -696,6 +704,7 @@ class SonosDevice(MediaPlayerDevice): if slave: # pylint: disable=protected-access slave._coordinator = self + slave._sonos_group = sonos_group slave.schedule_update_ha_state() @property @@ -1038,7 +1047,7 @@ class SonosDevice(MediaPlayerDevice): @property def device_state_attributes(self): """Return device specific state attributes.""" - attributes = {ATTR_IS_COORDINATOR: self.is_coordinator} + attributes = {ATTR_SONOS_GROUP: self._sonos_group} if self._night_sound is not None: attributes[ATTR_NIGHT_SOUND] = self._night_sound diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 27a0714527d..fa4f03f1179 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -4,7 +4,6 @@ Combination of multiple media players into one for a universal controller. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.universal/ """ -import asyncio import logging # pylint: disable=import-error from copy import copy @@ -63,8 +62,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }, extra=vol.REMOVE_EXTRA) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the universal media players.""" player = UniversalMediaPlayer( hass, @@ -99,8 +98,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): if state_template is not None: self._state_template.hass = hass - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to children and template state changes. This method must be run in the event loop and returns a coroutine. @@ -144,15 +142,14 @@ class UniversalMediaPlayer(MediaPlayerDevice): active_child = self._child_state return active_child.attributes.get(attr_name) if active_child else None - @asyncio.coroutine - def _async_call_service(self, service_name, service_data=None, - allow_override=False): + async def _async_call_service(self, service_name, service_data=None, + allow_override=False): """Call either a specified or active child's service.""" if service_data is None: service_data = {} if allow_override and service_name in self._cmds: - yield from async_call_from_config( + await async_call_from_config( self.hass, self._cmds[service_name], variables=service_data, blocking=True, validate_config=False) @@ -165,7 +162,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): service_data[ATTR_ENTITY_ID] = active_child.entity_id - yield from self.hass.services.async_call( + await self.hass.services.async_call( DOMAIN, service_name, service_data, blocking=True) @property @@ -506,8 +503,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): return self._async_call_service( SERVICE_SHUFFLE_SET, data, allow_override=True) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update state in HA.""" for child_name in self._children: child_state = self.hass.states.get(child_name) diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 64d1f642e6e..381482a4839 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv import homeassistant.util as util -REQUIREMENTS = ['pyvizio==0.0.2'] +REQUIREMENTS = ['pyvizio==0.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 0a940c0aa9d..11ab1615617 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -8,6 +8,7 @@ Volumio rest API: https://volumio.github.io/docs/API/REST_API.html """ from datetime import timedelta import logging +import socket import asyncio import aiohttp @@ -31,6 +32,8 @@ DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'Volumio' DEFAULT_PORT = 3000 +DATA_VOLUMIO = 'volumio' + TIMEOUT = 10 SUPPORT_VOLUMIO = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -50,11 +53,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Volumio platform.""" - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME) + if DATA_VOLUMIO not in hass.data: + hass.data[DATA_VOLUMIO] = dict() - async_add_devices([Volumio(name, host, port, hass)]) + # This is a manual configuration? + if discovery_info is None: + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + else: + name = "{} ({})".format(DEFAULT_NAME, discovery_info.get('hostname')) + host = discovery_info.get('host') + port = discovery_info.get('port') + + # Only add a device once, so discovered devices do not override manual + # config. + ip_addr = socket.gethostbyname(host) + if ip_addr in hass.data[DATA_VOLUMIO]: + return + + entity = Volumio(name, host, port, hass) + + hass.data[DATA_VOLUMIO][ip_addr] = entity + async_add_devices([entity]) class Volumio(MediaPlayerDevice): diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index d7682a611b9..c3426e45404 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -352,33 +352,30 @@ class LgWebOSDevice(MediaPlayerDevice): if media_type == MEDIA_TYPE_CHANNEL: _LOGGER.debug("Searching channel...") partial_match_channel_id = None + perfect_match_channel_id = None for channel in self._client.get_channels(): - _LOGGER.debug( - "Checking channel number <%s>, name <%s>, id <%s>...", - channel['channelNumber'], - channel['channelName'], - channel['channelId']) if media_id == channel['channelNumber']: - _LOGGER.debug( - "Perfect match on channel number: switching!") - self._client.set_channel(channel['channelId']) - return + perfect_match_channel_id = channel['channelId'] + continue elif media_id.lower() == channel['channelName'].lower(): - _LOGGER.debug( - "Perfect match on channel name: switching!") - self._client.set_channel(channel['channelId']) - return + perfect_match_channel_id = channel['channelId'] + continue elif media_id.lower() in channel['channelName'].lower(): - _LOGGER.debug( - "Partial match on channel name: saving it...") partial_match_channel_id = channel['channelId'] - if partial_match_channel_id is not None: - _LOGGER.debug( - "Using partial match on channel name: switching!") + if perfect_match_channel_id is not None: + _LOGGER.info( + "Switching to channel <%s> with perfect match", + perfect_match_channel_id) + self._client.set_channel(perfect_match_channel_id) + elif partial_match_channel_id is not None: + _LOGGER.info( + "Switching to channel <%s> with partial match", + partial_match_channel_id) self._client.set_channel(partial_match_channel_id) - return + + return def media_play(self): """Send play command.""" diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index 5a0bf2af1c4..7c167f93142 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -18,7 +18,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -231,7 +230,7 @@ def async_setup(hass, config): p_id = face.store[g_id].get(service.data[ATTR_PERSON]) camera_entity = service.data[ATTR_CAMERA_ENTITY] - camera = get_component('camera') + camera = hass.components.camera try: image = yield from camera.async_get_image(hass, camera_entity) @@ -240,7 +239,7 @@ def async_setup(hass, config): 'post', "persongroups/{0}/persons/{1}/persistedFaces".format( g_id, p_id), - image, + image.content, binary=True ) except HomeAssistantError as err: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index b81a4fc16a7..55d99a0817e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -90,22 +90,52 @@ ATTR_RETAIN = CONF_RETAIN MAX_RECONNECT_WAIT = 300 # seconds -def valid_subscribe_topic(value: Any, invalid_chars='\0') -> str: - """Validate that we can subscribe using this MQTT topic.""" +def valid_topic(value: Any) -> str: + """Validate that this is a valid topic name/filter.""" value = cv.string(value) - if all(c not in value for c in invalid_chars): - return vol.Length(min=1, max=65535)(value) - raise vol.Invalid('Invalid MQTT topic name') + try: + raw_value = value.encode('utf-8') + except UnicodeError: + raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.") + if not raw_value: + raise vol.Invalid("MQTT topic name/filter must not be empty.") + if len(raw_value) > 65535: + raise vol.Invalid("MQTT topic name/filter must not be longer than " + "65535 encoded bytes.") + if '\0' in value: + raise vol.Invalid("MQTT topic name/filter must not contain null " + "character.") + return value + + +def valid_subscribe_topic(value: Any) -> str: + """Validate that we can subscribe using this MQTT topic.""" + value = valid_topic(value) + for i in (i for i, c in enumerate(value) if c == '+'): + if (i > 0 and value[i - 1] != '/') or \ + (i < len(value) - 1 and value[i + 1] != '/'): + raise vol.Invalid("Single-level wildcard must occupy an entire " + "level of the filter") + + index = value.find('#') + if index != -1: + if index != len(value) - 1: + # If there are multiple wildcards, this will also trigger + raise vol.Invalid("Multi-level wildcard must be the last " + "character in the topic filter.") + if len(value) > 1 and value[index - 1] != '/': + raise vol.Invalid("Multi-level wildcard must be after a topic " + "level separator.") + + return value def valid_publish_topic(value: Any) -> str: """Validate that we can publish using this MQTT topic.""" - return valid_subscribe_topic(value, invalid_chars='#+\0') - - -def valid_discovery_topic(value: Any) -> str: - """Validate a discovery topic.""" - return valid_subscribe_topic(value, invalid_chars='#+\0/') + value = valid_topic(value) + if '+' in value or '#' in value: + raise vol.Invalid("Wildcards can not be used in topic names") + return value _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) @@ -143,8 +173,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. vol.Optional(CONF_DISCOVERY_PREFIX, - default=DEFAULT_DISCOVERY_PREFIX): valid_discovery_topic, + default=DEFAULT_DISCOVERY_PREFIX): valid_publish_topic, }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index db251ab4180..8a012928792 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['hbmqtt==0.9.1'] +REQUIREMENTS = ['hbmqtt==0.9.2'] DEPENDENCIES = ['http'] # None allows custom config to be created through generate_config diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py index 6f6cb312f2b..aa670578172 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -10,7 +10,6 @@ import json import voluptuous as vol from homeassistant.core import callback -import homeassistant.loader as loader from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( @@ -42,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema({ @asyncio.coroutine def async_setup(hass, config): """Set up the MQTT eventstream component.""" - mqtt = loader.get_component('mqtt') + mqtt = hass.components.mqtt conf = config.get(DOMAIN, {}) pub_topic = conf.get(CONF_PUBLISH_TOPIC) sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) @@ -82,7 +81,7 @@ def async_setup(hass, config): event_info = {'event_type': event.event_type, 'event_data': event.data} msg = json.dumps(event_info, cls=JSONEncoder) - mqtt.async_publish(hass, pub_topic, msg) + mqtt.async_publish(pub_topic, msg) # Only listen for local events if you are going to publish them. if pub_topic: @@ -115,7 +114,7 @@ def async_setup(hass, config): # Only subscribe if you specified a topic. if sub_topic: - yield from mqtt.async_subscribe(hass, sub_topic, _event_receiver) + yield from mqtt.async_subscribe(sub_topic, _event_receiver) hass.states.async_set('{domain}.initialized'.format(domain=DOMAIN), True) return True diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index 4427870c294..205a638c574 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -88,10 +88,9 @@ def async_setup(hass, config): if publish_attributes: for key, val in new_state.attributes.items(): - if val: - encoded_val = json.dumps(val, cls=JSONEncoder) - hass.components.mqtt.async_publish(mybase + key, - encoded_val, 1, True) + encoded_val = json.dumps(val, cls=JSONEncoder) + hass.components.mqtt.async_publish(mybase + key, + encoded_val, 1, True) async_track_state_change(hass, MATCH_ALL, _state_publisher) return True diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 17c9129a31d..9b394457973 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -24,7 +24,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component from homeassistant.setup import setup_component REQUIREMENTS = ['pymysensors==0.11.1'] @@ -294,16 +293,16 @@ def setup(hass, config): if device == MQTT_COMPONENT: if not setup_component(hass, MQTT_COMPONENT, config): return - mqtt = get_component(MQTT_COMPONENT) + mqtt = hass.components.mqtt retain = config[DOMAIN].get(CONF_RETAIN) def pub_callback(topic, payload, qos, retain): """Call MQTT publish function.""" - mqtt.publish(hass, topic, payload, qos, retain) + mqtt.publish(topic, payload, qos, retain) def sub_callback(topic, sub_cb, qos): """Call MQTT subscribe function.""" - mqtt.subscribe(hass, topic, sub_cb, qos) + mqtt.subscribe(topic, sub_cb, qos) gateway = mysensors.MQTTGateway( pub_callback, sub_callback, event_callback=None, persistence=persistence, diff --git a/homeassistant/components/notify/matrix.py b/homeassistant/components/notify/matrix.py index 03bc53e204c..fc29ad91dc9 100644 --- a/homeassistant/components/notify/matrix.py +++ b/homeassistant/components/notify/matrix.py @@ -5,181 +5,46 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.matrix/ """ import logging -import os -from urllib.parse import urlparse import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, - BaseNotificationService) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL -from homeassistant.util.json import load_json, save_json - -REQUIREMENTS = ['matrix-client==0.0.6'] + BaseNotificationService, + ATTR_MESSAGE) _LOGGER = logging.getLogger(__name__) -SESSION_FILE = 'matrix.conf' - -CONF_HOMESERVER = 'homeserver' CONF_DEFAULT_ROOM = 'default_room' +DOMAIN = 'matrix' +DEPENDENCIES = [DOMAIN] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOMESERVER): cv.url, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_DEFAULT_ROOM): cv.string, }) def get_service(hass, config, discovery_info=None): """Get the Matrix notification service.""" - from matrix_client.client import MatrixRequestError - - try: - return MatrixNotificationService( - os.path.join(hass.config.path(), SESSION_FILE), - config.get(CONF_HOMESERVER), - config.get(CONF_DEFAULT_ROOM), - config.get(CONF_VERIFY_SSL), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) - - except MatrixRequestError: - return None + return MatrixNotificationService(config.get(CONF_DEFAULT_ROOM)) class MatrixNotificationService(BaseNotificationService): """Send Notifications to a Matrix Room.""" - def __init__(self, config_file, homeserver, default_room, verify_ssl, - username, password): - """Set up the client.""" - self.session_filepath = config_file - self.auth_tokens = self.get_auth_tokens() + def __init__(self, default_room): + """Set up the notification service.""" + self._default_room = default_room - self.homeserver = homeserver - self.default_room = default_room - self.verify_tls = verify_ssl - self.username = username - self.password = password - - self.mx_id = "{user}@{homeserver}".format( - user=username, homeserver=urlparse(homeserver).netloc) - - # Login, this will raise a MatrixRequestError if login is unsuccessful - self.client = self.login() - - def get_auth_tokens(self): - """ - Read sorted authentication tokens from disk. - - Returns the auth_tokens dictionary. - """ - if not os.path.exists(self.session_filepath): - return {} - - try: - data = load_json(self.session_filepath) - - auth_tokens = {} - for mx_id, token in data.items(): - auth_tokens[mx_id] = token - - return auth_tokens - - except (OSError, IOError, PermissionError) as ex: - _LOGGER.warning( - "Loading authentication tokens from file '%s' failed: %s", - self.session_filepath, str(ex)) - return {} - - def store_auth_token(self, token): - """Store authentication token to session and persistent storage.""" - self.auth_tokens[self.mx_id] = token - - save_json(self.session_filepath, self.auth_tokens) - - def login(self): - """Login to the matrix homeserver and return the client instance.""" - from matrix_client.client import MatrixRequestError - - # Attempt to generate a valid client using either of the two possible - # login methods: - client = None - - # If we have an authentication token - if self.mx_id in self.auth_tokens: - try: - client = self.login_by_token() - _LOGGER.debug("Logged in using stored token.") - - except MatrixRequestError as ex: - _LOGGER.warning( - "Login by token failed, falling back to password. " - "login_by_token raised: (%d) %s", - ex.code, ex.content) - - # If we still don't have a client try password. - if not client: - try: - client = self.login_by_password() - _LOGGER.debug("Logged in using password.") - - except MatrixRequestError as ex: - _LOGGER.error( - "Login failed, both token and username/password invalid " - "login_by_password raised: (%d) %s", - ex.code, ex.content) - - # re-raise the error so the constructor can catch it. - raise - - return client - - def login_by_token(self): - """Login using authentication token and return the client.""" - from matrix_client.client import MatrixClient - - return MatrixClient( - base_url=self.homeserver, - token=self.auth_tokens[self.mx_id], - user_id=self.username, - valid_cert_check=self.verify_tls) - - def login_by_password(self): - """Login using password authentication and return the client.""" - from matrix_client.client import MatrixClient - - _client = MatrixClient( - base_url=self.homeserver, - valid_cert_check=self.verify_tls) - - _client.login_with_password(self.username, self.password) - - self.store_auth_token(_client.token) - - return _client - - def send_message(self, message, **kwargs): + def send_message(self, message="", **kwargs): """Send the message to the matrix server.""" - from matrix_client.client import MatrixRequestError + target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room] - target_rooms = kwargs.get(ATTR_TARGET) or [self.default_room] + service_data = { + ATTR_TARGET: target_rooms, + ATTR_MESSAGE: message + } - rooms = self.client.get_rooms() - for target_room in target_rooms: - try: - if target_room in rooms: - room = rooms[target_room] - else: - room = self.client.join_room(target_room) - - _LOGGER.debug(room.send_text(message)) - - except MatrixRequestError as ex: - _LOGGER.error( - "Unable to deliver message to room '%s': (%d): %s", - target_room, ex.code, ex.content) + return self.hass.services.call( + DOMAIN, 'send_message', service_data=service_data) diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index dc1cbd945a7..96ed098567d 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -86,9 +86,16 @@ class Metrics(object): if hasattr(self, handler): getattr(self, handler)(state) + metric = self._metric( + 'state_change', + self.prometheus_client.Counter, + 'The number of state changes', + ) + metric.labels(**self._labels(state)).inc() + def _metric(self, metric, factory, documentation, labels=None): if labels is None: - labels = ['entity', 'friendly_name'] + labels = ['entity', 'friendly_name', 'domain'] try: return self._metrics[metric] @@ -100,6 +107,7 @@ class Metrics(object): def _labels(state): return { 'entity': state.entity_id, + 'domain': state.domain, 'friendly_name': state.attributes.get('friendly_name'), } diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py index dedc39ef3a2..1d33740d4a4 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.0b2'] +REQUIREMENTS = ['restrictedpython==4.0b3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py new file mode 100644 index 00000000000..99cec53c2ed --- /dev/null +++ b/homeassistant/components/rainmachine.py @@ -0,0 +1,89 @@ +""" +This component provides support for RainMachine sprinkler controllers. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/rainmachine/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.const import ( + CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_SWITCHES) + +REQUIREMENTS = ['regenmaschine==0.4.1'] + +_LOGGER = logging.getLogger(__name__) + +DATA_RAINMACHINE = 'data_rainmachine' +DOMAIN = 'rainmachine' + +NOTIFICATION_ID = 'rainmachine_notification' +NOTIFICATION_TITLE = 'RainMachine Component Setup' + +CONF_ZONE_RUN_TIME = 'zone_run_time' + +DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_PORT = 8080 +DEFAULT_SSL = True + +MIN_SCAN_TIME = timedelta(seconds=1) +MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) + +SWITCH_SCHEMA = vol.Schema({ + vol.Optional(CONF_ZONE_RUN_TIME): + cv.positive_int +}) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_SWITCHES): SWITCH_SCHEMA, + }) + }, + extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the RainMachine component.""" + from regenmaschine import Authenticator, Client + from regenmaschine.exceptions import HTTPError + from requests.exceptions import ConnectTimeout + + conf = config[DOMAIN] + ip_address = conf[CONF_IP_ADDRESS] + password = conf[CONF_PASSWORD] + port = conf[CONF_PORT] + ssl = conf[CONF_SSL] + + _LOGGER.debug('Setting up RainMachine client') + + try: + auth = Authenticator.create_local( + ip_address, password, port=port, https=ssl) + client = Client(auth) + mac = client.provision.wifi()['macAddress'] + hass.data[DATA_RAINMACHINE] = (client, mac) + except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: + _LOGGER.error('An error occurred: %s', str(exc_info)) + hass.components.persistent_notification.create( + 'Error: {0}
' + 'You will need to restart hass after fixing.' + ''.format(exc_info), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + _LOGGER.debug('Setting up switch platform') + switch_config = conf.get(CONF_SWITCHES, {}) + discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) + + _LOGGER.debug('Setup complete') + + return True diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 64e2b85f611..9b5bea043f4 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.6'] +REQUIREMENTS = ['sqlalchemy==1.2.7'] _LOGGER = logging.getLogger(__name__) @@ -111,8 +111,7 @@ def run_information(hass, point_in_time: Optional[datetime] = None): return res -@asyncio.coroutine -def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" conf = config.get(DOMAIN, {}) keep_days = conf.get(CONF_PURGE_KEEP_DAYS) @@ -131,8 +130,7 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instance.async_initialize() instance.start() - @asyncio.coroutine - def async_handle_purge_service(service): + async def async_handle_purge_service(service): """Handle calls to the purge service.""" instance.do_adhoc_purge(**service.data) @@ -140,7 +138,7 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA) - return (yield from instance.async_db_ready) + return await instance.async_db_ready PurgeTask = namedtuple('PurgeTask', ['keep_days', 'repack']) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index d6873a0bd91..2e96ec64d97 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyRFXtrx==0.22.0'] +REQUIREMENTS = ['pyRFXtrx==0.22.1'] DOMAIN = 'rfxtrx' diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 8f0b9d5c7ab..7b76836555c 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/scene/ """ import asyncio +import importlib import logging import voluptuous as vol @@ -16,7 +17,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import HASS_DOMAIN -from homeassistant.loader import get_platform DOMAIN = 'scene' STATE = 'scening' @@ -34,20 +34,24 @@ def _hass_domain_validator(config): def _platform_validator(config): """Validate it is a valid platform.""" - p_name = config[CONF_PLATFORM] - platform = get_platform(DOMAIN, p_name) + try: + platform = importlib.import_module( + 'homeassistant.components.scene.{}'.format( + config[CONF_PLATFORM])) + except ImportError: + raise vol.Invalid('Invalid platform specified') from None if not hasattr(platform, 'PLATFORM_SCHEMA'): return config - return getattr(platform, 'PLATFORM_SCHEMA')(config) + return platform.PLATFORM_SCHEMA(config) PLATFORM_SCHEMA = vol.Schema( vol.All( _hass_domain_validator, vol.Schema({ - vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) + vol.Required(CONF_PLATFORM): str }, extra=vol.ALLOW_EXTRA), _platform_validator ), extra=vol.ALLOW_EXTRA) @@ -71,7 +75,7 @@ def activate(hass, entity_id=None): async def async_setup(hass, config): """Set up the scenes.""" logger = logging.getLogger(__name__) - component = EntityComponent(logger, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent(logger, DOMAIN, hass) await component.async_setup(config) @@ -90,6 +94,16 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Setup 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 Scene(Entity): """A scene is a group of entities and the states we want them to be.""" diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index dffc7720776..3eb73736717 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -13,10 +13,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up scenes for deCONZ component.""" - if discovery_info is None: - return + """Old way of setting up deCONZ scenes.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up scenes for deCONZ component.""" scenes = hass.data[DATA_DECONZ].scenes entities = [] diff --git a/homeassistant/components/sensor/.translations/season.bg.json b/homeassistant/components/sensor/.translations/season.bg.json new file mode 100644 index 00000000000..e3865ca42e5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u0415\u0441\u0435\u043d", + "spring": "\u041f\u0440\u043e\u043b\u0435\u0442", + "summer": "\u041b\u044f\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.da.json b/homeassistant/components/sensor/.translations/season.da.json new file mode 100644 index 00000000000..9cded2f9c0f --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.da.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Efter\u00e5r", + "spring": "For\u00e5r", + "summer": "Sommer", + "winter": "Vinter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.hu.json b/homeassistant/components/sensor/.translations/season.hu.json new file mode 100644 index 00000000000..63596b09784 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.hu.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u0150sz", + "spring": "Tavasz", + "summer": "Ny\u00e1r", + "winter": "T\u00e9l" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.it.json b/homeassistant/components/sensor/.translations/season.it.json new file mode 100644 index 00000000000..d9138f6b16e --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.it.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Autunno", + "spring": "Primavera", + "summer": "Estate", + "winter": "Inverno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.lb.json b/homeassistant/components/sensor/.translations/season.lb.json new file mode 100644 index 00000000000..f33afde7a07 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.lb.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Hierscht", + "spring": "Fr\u00e9ijoer", + "summer": "Summer", + "winter": "Wanter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ru.json b/homeassistant/components/sensor/.translations/season.ru.json new file mode 100644 index 00000000000..2b04886b72d --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ru.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u041e\u0441\u0435\u043d\u044c", + "spring": "\u0412\u0435\u0441\u043d\u0430", + "summer": "\u041b\u0435\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2bc35a034f4..8550d175b63 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -12,6 +12,9 @@ import voluptuous as vol from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -21,9 +24,10 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=30) DEVICE_CLASSES = [ - 'battery', # % of battery that is left - 'humidity', # % of humidity in the air - 'temperature', # temperature (C/F) + DEVICE_CLASS_BATTERY, # % of battery that is left + DEVICE_CLASS_HUMIDITY, # % of humidity in the air + DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) + DEVICE_CLASS_TEMPERATURE, # temperature (C/F) ] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) @@ -31,8 +35,18 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): """Track states and offer events for sensors.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True + + +async def async_setup_entry(hass, entry): + """Setup 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) diff --git a/homeassistant/components/sensor/abode.py b/homeassistant/components/sensor/abode.py index 1a700e24de6..b51ab288c1a 100644 --- a/homeassistant/components/sensor/abode.py +++ b/homeassistant/components/sensor/abode.py @@ -7,6 +7,8 @@ https://home-assistant.io/components/sensor.abode/ import logging from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -14,9 +16,9 @@ DEPENDENCIES = ['abode'] # Sensor types: Name, icon SENSOR_TYPES = { - 'temp': ['Temperature', 'thermometer'], - 'humidity': ['Humidity', 'water-percent'], - 'lux': ['Lux', 'lightbulb'], + 'temp': ['Temperature', DEVICE_CLASS_TEMPERATURE], + 'humidity': ['Humidity', DEVICE_CLASS_HUMIDITY], + 'lux': ['Lux', DEVICE_CLASS_ILLUMINANCE], } @@ -46,20 +48,20 @@ class AbodeSensor(AbodeDevice): """Initialize a sensor for an Abode device.""" super().__init__(data, device) self._sensor_type = sensor_type - self._icon = 'mdi:{}'.format(SENSOR_TYPES[self._sensor_type][1]) self._name = '{0} {1}'.format( self._device.name, SENSOR_TYPES[self._sensor_type][0]) - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon + self._device_class = SENSOR_TYPES[self._sensor_type][1] @property def name(self): """Return the name of the sensor.""" return self._name + @property + def device_class(self): + """Return the device class.""" + return self._device_class + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/sensor/bh1750.py b/homeassistant/components/sensor/bh1750.py index 0c538a6cfcc..6d34d4ea9f8 100644 --- a/homeassistant/components/sensor/bh1750.py +++ b/homeassistant/components/sensor/bh1750.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE from homeassistant.helpers.entity import Entity REQUIREMENTS = ['i2csense==0.0.4', @@ -130,7 +130,7 @@ class BH1750Sensor(Entity): @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" - return 'light' + return DEVICE_CLASS_ILLUMINANCE @asyncio.coroutine def async_update(self): diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py index ce44abdb087..b460498c901 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available BloomSky weather sensors.""" - bloomsky = get_component('bloomsky') + bloomsky = hass.components.bloomsky # Default needed in case of discovery sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index 272d5d1e0b8..128f532e459 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -33,8 +33,7 @@ CONF_STATION = 'station' CONF_ZONE_ID = 'zone_id' CONF_WMO_ID = 'wmo_id' -MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=60) -LAST_UPDATE = 0 +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=35) # Sensor types are defined like: Name, units SENSOR_TYPES = { @@ -114,13 +113,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Could not get BOM weather station from lat/lon") return False - rest = BOMCurrentData(hass, station) + bom_data = BOMCurrentData(hass, station) try: - rest.update() + bom_data.update() except ValueError as err: _LOGGER.error("Received error from BOM_Current: %s", err) return False - add_devices([BOMCurrentSensor(rest, variable, config.get(CONF_NAME)) + add_devices([BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME)) for variable in config[CONF_MONITORED_CONDITIONS]]) return True @@ -128,9 +127,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class BOMCurrentSensor(Entity): """Implementation of a BOM current sensor.""" - def __init__(self, rest, condition, stationname): + def __init__(self, bom_data, condition, stationname): """Initialize the sensor.""" - self.rest = rest + self.bom_data = bom_data self._condition = condition self.stationname = stationname @@ -146,8 +145,8 @@ class BOMCurrentSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.rest.data and self._condition in self.rest.data: - return self.rest.data[self._condition] + if self.bom_data.data and self._condition in self.bom_data.data: + return self.bom_data.data[self._condition] return STATE_UNKNOWN @@ -156,11 +155,11 @@ class BOMCurrentSensor(Entity): """Return the state attributes of the device.""" attr = {} attr['Sensor Id'] = self._condition - attr['Zone Id'] = self.rest.data['history_product'] - attr['Station Id'] = self.rest.data['wmo'] - attr['Station Name'] = self.rest.data['name'] + attr['Zone Id'] = self.bom_data.data['history_product'] + attr['Station Id'] = self.bom_data.data['wmo'] + attr['Station Name'] = self.bom_data.data['name'] attr['Last Update'] = datetime.datetime.strptime(str( - self.rest.data['local_date_time_full']), '%Y%m%d%H%M%S') + self.bom_data.data['local_date_time_full']), '%Y%m%d%H%M%S') attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION return attr @@ -171,7 +170,7 @@ class BOMCurrentSensor(Entity): def update(self): """Update current conditions.""" - self.rest.update() + self.bom_data.update() class BOMCurrentData(object): @@ -182,7 +181,6 @@ class BOMCurrentData(object): self._hass = hass self._zone_id, self._wmo_id = station_id.split('.') self.data = None - self._lastupdate = LAST_UPDATE def _build_url(self): url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) @@ -192,20 +190,9 @@ class BOMCurrentData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from BOM.""" - if self._lastupdate != 0 and \ - ((datetime.datetime.now() - self._lastupdate) < - datetime.timedelta(minutes=35)): - _LOGGER.info( - "BOM was updated %s minutes ago, skipping update as" - " < 35 minutes", (datetime.datetime.now() - self._lastupdate)) - return self._lastupdate - try: result = requests.get(self._build_url(), timeout=10).json() self.data = result['observations']['data'][0] - self._lastupdate = datetime.datetime.strptime( - str(self.data['local_date_time_full']), '%Y%m%d%H%M%S') - return self._lastupdate except ValueError as err: _LOGGER.error("Check BOM %s", err.args) self.data = None diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 5d74f038eaa..6eb67f7cbd8 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -161,7 +161,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'))) + dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'), + coordinates)) async_add_devices(dev) data = BrData(hass, coordinates, timeframe, dev) @@ -172,9 +173,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class BrSensor(Entity): """Representation of an Buienradar sensor.""" - def __init__(self, sensor_type, client_name): + def __init__(self, sensor_type, client_name, coordinates): """Initialize the sensor.""" - from buienradar.buienradar import (PRECIPITATION_FORECAST) + from buienradar.buienradar import (PRECIPITATION_FORECAST, CONDITION) self.client_name = client_name self._name = SENSOR_TYPES[sensor_type][0] @@ -185,10 +186,22 @@ class BrSensor(Entity): self._attribution = None self._measured = None self._stationname = None + self._unique_id = self.uid(coordinates) + + # All continuous sensors should be forced to be updated + self._force_update = self.type != SYMBOL and \ + not self.type.startswith(CONDITION) if self.type.startswith(PRECIPITATION_FORECAST): self._timeframe = None + def uid(self, coordinates): + """Generate a unique id using coordinates and sensor type.""" + # The combination of the location, name an sensor type is unique + return "%2.6f%2.6f%s" % (coordinates[CONF_LATITUDE], + coordinates[CONF_LONGITUDE], + self.type) + def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor @@ -198,6 +211,11 @@ class BrSensor(Entity): PRECIPITATION_FORECAST, STATIONNAME, TIMEFRAME) + # Check if we have a new measurement, + # otherwise we do not have to update the sensor + if self._measured == data.get(MEASURED): + return False + self._attribution = data.get(ATTRIBUTION) self._stationname = data.get(STATIONNAME) self._measured = data.get(MEASURED) @@ -246,18 +264,12 @@ class BrSensor(Entity): return False else: try: - new_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) + return True except IndexError: _LOGGER.warning("No forecast for fcday=%s...", fcday) return False - if new_state != self._state: - self._state = new_state - return True - return False - - return False - if self.type == SYMBOL or self.type.startswith(CONDITION): # update weather symbol & status text condition = data.get(CONDITION, None) @@ -286,27 +298,26 @@ class BrSensor(Entity): if self.type.startswith(PRECIPITATION_FORECAST): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) - new_state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) self._timeframe = nested.get(TIMEFRAME) # pylint: disable=protected-access - if new_state != self._state: - self._state = new_state - return True - return False + self._state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) + return True # update all other sensors - new_state = data.get(self.type) # pylint: disable=protected-access - if new_state != self._state: - self._state = new_state - return True - return False + self._state = data.get(self.type) + return True @property def attribution(self): """Return the attribution.""" return self._attribution + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def name(self): """Return the name of the sensor.""" @@ -360,6 +371,11 @@ class BrSensor(Entity): """Return possible sensor specific icon.""" return SENSOR_TYPES[self.type][2] + @property + def force_update(self): + """Return true for continuous sensors, false for discrete sensors.""" + return self._force_update + class BrData(object): """Get the latest data and updates the states.""" diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 7d535c5f1d9..ac09de9c699 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -146,7 +146,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'Latitude and longitude must exist together'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.longitude, - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=120)): ( + vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=300)): ( vol.All(cv.time_period, cv.positive_timedelta)), vol.Optional(CONF_FORECAST): vol.All(cv.ensure_list, [vol.Range(min=1, max=7)]), diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index e569c5578ac..221cdf2129e 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -5,12 +5,12 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_EVENT, CONF_ID) -from homeassistant.core import EventOrigin, callback + ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify DEPENDENCIES = ['deconz'] @@ -22,23 +22,29 @@ ATTR_EVENT_ID = 'event_id' async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Old way of setting up deCONZ sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the deCONZ sensors.""" - if discovery_info is None: - return + @callback + def async_add_sensor(sensors): + """Add sensors from deCONZ.""" + from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE + entities = [] + for sensor in sensors: + if sensor.type in DECONZ_SENSOR: + if sensor.type in DECONZ_REMOTE: + if sensor.battery: + entities.append(DeconzBattery(sensor)) + else: + entities.append(DeconzSensor(sensor)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) - from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE - sensors = hass.data[DATA_DECONZ].sensors - entities = [] - - for sensor in sensors.values(): - if sensor and sensor.type in DECONZ_SENSOR: - if sensor.type in DECONZ_REMOTE: - DeconzEvent(hass, sensor) - if sensor.battery: - entities.append(DeconzBattery(sensor)) - else: - entities.append(DeconzSensor(sensor)) - async_add_devices(entities, True) + async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) class DeconzSensor(Entity): @@ -126,7 +132,6 @@ class DeconzBattery(Entity): """Register dispatcher callback for update of battery state.""" self._device = device self._name = '{} {}'.format(self._device.name, 'Battery Level') - self._device_class = 'battery' self._unit_of_measurement = "%" async def async_added_to_hass(self): @@ -158,12 +163,7 @@ class DeconzBattery(Entity): @property def device_class(self): """Return the class of the sensor.""" - return self._device_class - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return icon_for_battery_level(int(self.state)) + return DEVICE_CLASS_BATTERY @property def unit_of_measurement(self): @@ -182,26 +182,3 @@ class DeconzBattery(Entity): ATTR_EVENT_ID: slugify(self._device.name), } return attr - - -class DeconzEvent(object): - """When you want signals instead of entities. - - Stateless sensors such as remotes are expected to generate an event - instead of a sensor entity in hass. - """ - - def __init__(self, hass, device): - """Register callback that will be used for signals.""" - self._hass = hass - self._device = device - self._device.register_async_callback(self.async_update_callback) - self._event = 'deconz_{}'.format(CONF_EVENT) - self._id = slugify(self._device.name) - - @callback - def async_update_callback(self, reason): - """Fire the event if reason is that state is updated.""" - if reason['state']: - data = {CONF_ID: self._id, CONF_EVENT: self._device.state} - self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/homeassistant/components/sensor/deluge.py b/homeassistant/components/sensor/deluge.py index f4793867d4c..8acbda74d7d 100644 --- a/homeassistant/components/sensor/deluge.py +++ b/homeassistant/components/sensor/deluge.py @@ -14,8 +14,9 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES, STATE_IDLE) from homeassistant.helpers.entity import Entity +from homeassistant.exceptions import PlatformNotReady -REQUIREMENTS = ['deluge-client==1.0.5'] +REQUIREMENTS = ['deluge-client==1.4.0'] _LOGGER = logging.getLogger(__name__) _THROTTLED_REFRESH = None @@ -24,7 +25,6 @@ DEFAULT_NAME = 'Deluge' DEFAULT_PORT = 58846 DHT_UPLOAD = 1000 DHT_DOWNLOAD = 1000 - SENSOR_TYPES = { 'current_status': ['Status', None], 'download_speed': ['Down Speed', 'kB/s'], @@ -58,8 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): deluge_api.connect() except ConnectionRefusedError: _LOGGER.error("Connection to Deluge Daemon failed") - return - + raise PlatformNotReady dev = [] for variable in config[CONF_MONITORED_VARIABLES]: dev.append(DelugeSensor(variable, deluge_api, name)) @@ -79,6 +78,7 @@ class DelugeSensor(Entity): self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self.data = None + self._available = False @property def name(self): @@ -90,6 +90,11 @@ class DelugeSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return true if device is available.""" + return self._available + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" @@ -97,9 +102,17 @@ class DelugeSensor(Entity): def update(self): """Get the latest data from Deluge and updates the state.""" - self.data = self.client.call('core.get_session_status', - ['upload_rate', 'download_rate', - 'dht_upload_rate', 'dht_download_rate']) + from deluge_client import FailedToReconnectException + try: + self.data = self.client.call('core.get_session_status', + ['upload_rate', 'download_rate', + 'dht_upload_rate', + 'dht_download_rate']) + self._available = True + except FailedToReconnectException: + _LOGGER.error("Connection to Deluge Daemon Lost") + self._available = False + return upload = self.data[b'upload_rate'] - self.data[b'dht_upload_rate'] download = self.data[b'download_rate'] - self.data[ diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index ba7c93203df..325d3e0ae58 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -4,7 +4,9 @@ Demo platform that has a couple of fake sensors. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.const import ATTR_BATTERY_LEVEL, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE) from homeassistant.helpers.entity import Entity @@ -12,18 +14,21 @@ from homeassistant.helpers.entity import Entity def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo sensors.""" add_devices([ - DemoSensor('Outside Temperature', 15.6, TEMP_CELSIUS, 12), - DemoSensor('Outside Humidity', 54, '%', None), + DemoSensor('Outside Temperature', 15.6, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, 12), + DemoSensor('Outside Humidity', 54, DEVICE_CLASS_HUMIDITY, '%', None), ]) class DemoSensor(Entity): """Representation of a Demo sensor.""" - def __init__(self, name, state, unit_of_measurement, battery): + def __init__(self, name, state, device_class, + unit_of_measurement, battery): """Initialize the sensor.""" self._name = name self._state = state + self._device_class = device_class self._unit_of_measurement = unit_of_measurement self._battery = battery @@ -32,6 +37,11 @@ class DemoSensor(Entity): """No polling needed for a demo sensor.""" return False + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index cea29d437ae..d7982f1c9db 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -31,6 +31,8 @@ DOMAIN = 'dsmr' ICON_GAS = 'mdi:fire' ICON_POWER = 'mdi:flash' +ICON_POWER_FAILURE = 'mdi:flash-off' +ICON_SWELL_SAG = 'mdi:pulse' # Smart meter sends telegram every 10 seconds MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -61,13 +63,86 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # Define list of name,obis mappings to generate entities obis_mapping = [ - ['Power Consumption', obis_ref.CURRENT_ELECTRICITY_USAGE], - ['Power Production', obis_ref.CURRENT_ELECTRICITY_DELIVERY], - ['Power Tariff', obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ['Power Consumption (low)', obis_ref.ELECTRICITY_USED_TARIFF_1], - ['Power Consumption (normal)', obis_ref.ELECTRICITY_USED_TARIFF_2], - ['Power Production (low)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], - ['Power Production (normal)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], + [ + 'Power Consumption', + obis_ref.CURRENT_ELECTRICITY_USAGE + ], + [ + 'Power Production', + obis_ref.CURRENT_ELECTRICITY_DELIVERY + ], + [ + 'Power Tariff', + obis_ref.ELECTRICITY_ACTIVE_TARIFF + ], + [ + 'Power Consumption (low)', + obis_ref.ELECTRICITY_USED_TARIFF_1 + ], + [ + 'Power Consumption (normal)', + obis_ref.ELECTRICITY_USED_TARIFF_2 + ], + [ + 'Power Production (low)', + obis_ref.ELECTRICITY_DELIVERED_TARIFF_1 + ], + [ + 'Power Production (normal)', + obis_ref.ELECTRICITY_DELIVERED_TARIFF_2 + ], + [ + 'Power Consumption Phase L1', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE + ], + [ + 'Power Consumption Phase L2', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE + ], + [ + 'Power Consumption Phase L3', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE + ], + [ + 'Power Production Phase L1', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE + ], + [ + 'Power Production Phase L2', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE + ], + [ + 'Power Production Phase L3', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE + ], + [ + 'Long Power Failure Count', + obis_ref.LONG_POWER_FAILURE_COUNT + ], + [ + 'Voltage Sags Phase L1', + obis_ref.VOLTAGE_SAG_L1_COUNT + ], + [ + 'Voltage Sags Phase L2', + obis_ref.VOLTAGE_SAG_L2_COUNT + ], + [ + 'Voltage Sags Phase L3', + obis_ref.VOLTAGE_SAG_L3_COUNT + ], + [ + 'Voltage Swells Phase L1', + obis_ref.VOLTAGE_SWELL_L1_COUNT + ], + [ + 'Voltage Swells Phase L2', + obis_ref.VOLTAGE_SWELL_L2_COUNT + ], + [ + 'Voltage Swells Phase L3', + obis_ref.VOLTAGE_SWELL_L3_COUNT + ], ] # Generate device entities @@ -174,6 +249,10 @@ class DSMREntity(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" + if 'Sags' in self._name or 'Swells' in self.name: + return ICON_SWELL_SAG + if 'Failure' in self._name: + return ICON_POWER_FAILURE if 'Power' in self._name: return ICON_POWER elif 'Gas' in self._name: diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index 7274f421f15..a478f964f5a 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ecobee/ """ from homeassistant.components import ecobee -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity DEPENDENCIES = ['ecobee'] @@ -55,7 +56,7 @@ class EcobeeSensor(Entity): @property def device_class(self): """Return the device class of the sensor.""" - if self.type in ('temperature', 'humidity'): + if self.type in (DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE): return self.type return None diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 23c397053c5..6405c707536 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -14,8 +14,7 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -# pylint: disable=import-error, no-member -REQUIREMENTS = [] # ['eliqonline==1.0.13'] - package disappeared +REQUIREMENTS = ['eliqonline==1.0.14'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 5b28faf78ca..9c05028b394 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -89,6 +89,8 @@ FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 350f1e2eb59..bdbc207a79c 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -43,7 +43,7 @@ HM_UNIT_HA_CAST = { 'ENERGY_COUNTER': 'Wh', 'GAS_POWER': 'm3', 'GAS_ENERGY_COUNTER': 'm3', - 'LUX': 'lux', + 'LUX': 'lx', 'RAIN_COUNTER': 'mm', 'WIND_SPEED': 'km/h', 'WIND_DIRECTION': '°', diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index 1a37aa1ad4e..aa350f7be5d 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -7,13 +7,10 @@ https://home-assistant.io/components/sensor.homematicip_cloud/ import logging -from homeassistant.core import callback -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN, EVENT_HOME_CHANGED, - ATTR_HOME_LABEL, ATTR_HOME_ID, ATTR_LOW_BATTERY, ATTR_RSSI) -from homeassistant.const import TEMP_CELSIUS, STATE_OK + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) +from homeassistant.const import TEMP_CELSIUS _LOGGER = logging.getLogger(__name__) @@ -21,68 +18,49 @@ DEPENDENCIES = ['homematicip_cloud'] ATTR_VALVE_STATE = 'valve_state' ATTR_VALVE_POSITION = 'valve_position' +ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE_OFFSET = 'temperature_offset' +ATTR_HUMIDITY = 'humidity' HMIP_UPTODATE = 'up_to_date' HMIP_VALVE_DONE = 'adaption_done' HMIP_SABOTAGE = 'sabotage' +STATE_OK = 'ok' STATE_LOW_BATTERY = 'low_battery' STATE_SABOTAGE = 'sabotage' -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the HomematicIP sensors devices.""" - # pylint: disable=import-error, no-name-in-module from homematicip.device import ( HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, TemperatureHumiditySensorDisplay) - homeid = discovery_info['homeid'] - home = hass.data[DOMAIN][homeid] - devices = [HomematicipAccesspoint(home)] + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [HomematicipAccesspointStatus(home)] for device in home.devices: - devices.append(HomematicipDeviceStatus(home, device)) if isinstance(device, HeatingThermostat): devices.append(HomematicipHeatingThermostat(home, device)) - if isinstance(device, TemperatureHumiditySensorWithoutDisplay): - devices.append(HomematicipSensorThermometer(home, device)) - devices.append(HomematicipSensorHumidity(home, device)) - if isinstance(device, TemperatureHumiditySensorDisplay): - devices.append(HomematicipSensorThermometer(home, device)) - devices.append(HomematicipSensorHumidity(home, device)) + if isinstance(device, (TemperatureHumiditySensorDisplay, + TemperatureHumiditySensorWithoutDisplay)): + devices.append(HomematicipTemperatureSensor(home, device)) + devices.append(HomematicipHumiditySensor(home, device)) - if home.devices: - add_devices(devices) + if devices: + async_add_devices(devices) -class HomematicipAccesspoint(Entity): +class HomematicipAccesspointStatus(HomematicipGenericDevice): """Representation of an HomeMaticIP access point.""" def __init__(self, home): - """Initialize the access point sensor.""" - self._home = home - _LOGGER.debug('Setting up access point %s', home.label) - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, EVENT_HOME_CHANGED, self._home_changed) - - @callback - def _home_changed(self, deviceid): - """Handle device state changes.""" - if deviceid is None or deviceid == self._home.id: - _LOGGER.debug('Event home %s', self._home.label) - self.async_schedule_update_ha_state() - - @property - def name(self): - """Return the name of the access point device.""" - if self._home.label == '': - return 'Access Point Status' - return '{} Access Point Status'.format(self._home.label) + """Initialize access point device.""" + super().__init__(home, home) @property def icon(self): @@ -102,24 +80,15 @@ class HomematicipAccesspoint(Entity): @property def device_state_attributes(self): """Return the state attributes of the access point.""" - return { - ATTR_HOME_LABEL: self._home.label, - ATTR_HOME_ID: self._home.id, - } + return {} class HomematicipDeviceStatus(HomematicipGenericDevice): """Representation of an HomematicIP device status.""" def __init__(self, home, device): - """Initialize the device.""" - super().__init__(home, device) - _LOGGER.debug('Setting up sensor device status: %s', device.label) - - @property - def name(self): - """Return the name of the device.""" - return self._name('Status') + """Initialize generic status device.""" + super().__init__(home, device, 'Status') @property def icon(self): @@ -150,9 +119,8 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): """MomematicIP heating thermostat representation.""" def __init__(self, home, device): - """"Initialize heating thermostat.""" - super().__init__(home, device) - _LOGGER.debug('Setting up heating thermostat device: %s', device.label) + """Initialize heating thermostat device.""" + super().__init__(home, device, 'Heating') @property def icon(self): @@ -173,34 +141,18 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return '%' - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_VALVE_STATE: self._device.valveState.lower(), - ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue - } - -class HomematicipSensorHumidity(HomematicipGenericDevice): - """MomematicIP thermometer device.""" +class HomematicipHumiditySensor(HomematicipGenericDevice): + """MomematicIP humidity device.""" def __init__(self, home, device): - """"Initialize the thermometer device.""" - super().__init__(home, device) - _LOGGER.debug('Setting up humidity device: %s', device.label) - - @property - def name(self): - """Return the name of the device.""" - return self._name('Humidity') + """Initialize the thermometer device.""" + super().__init__(home, device, 'Humidity') @property def icon(self): """Return the icon.""" - return 'mdi:water' + return 'mdi:water-percent' @property def state(self): @@ -212,27 +164,13 @@ class HomematicipSensorHumidity(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return '%' - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue, - } - -class HomematicipSensorThermometer(HomematicipGenericDevice): - """MomematicIP thermometer device.""" +class HomematicipTemperatureSensor(HomematicipGenericDevice): + """MomematicIP the thermometer device.""" def __init__(self, home, device): - """"Initialize the thermometer device.""" - super().__init__(home, device) - _LOGGER.debug('Setting up thermometer device: %s', device.label) - - @property - def name(self): - """Return the name of the device.""" - return self._name('Temperature') + """Initialize the thermometer device.""" + super().__init__(home, device, 'Temperature') @property def icon(self): @@ -248,12 +186,3 @@ class HomematicipSensorThermometer(HomematicipGenericDevice): def unit_of_measurement(self): """Return the unit this state is expressed in.""" return TEMP_CELSIUS - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue, - } diff --git a/homeassistant/components/sensor/insteon_plm.py b/homeassistant/components/sensor/insteon_plm.py index a72b8efbc05..61f5877ed78 100644 --- a/homeassistant/components/sensor/insteon_plm.py +++ b/homeassistant/components/sensor/insteon_plm.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index c34a4a8fca7..ecf7bc0b8c2 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -49,7 +49,7 @@ UOM_FRIENDLY_NAME = { '33': 'kWH', '34': 'liedu', '35': 'l', - '36': 'lux', + '36': 'lx', '37': 'mercalli', '38': 'm', '39': 'm³/hr', diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index 9d305973ecf..9fec4b4b5e3 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pylast==2.1.0'] +REQUIREMENTS = ['pylast==2.2.0'] ATTR_LAST_PLAYED = 'last_played' ATTR_PLAY_COUNT = 'play_count' diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py index 1f0e3e89e5c..aad8c2f7a92 100644 --- a/homeassistant/components/sensor/linux_battery.py +++ b/homeassistant/components/sensor/linux_battery.py @@ -10,7 +10,7 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -48,8 +48,6 @@ DEFAULT_SYSTEM = 'linux' SYSTEMS = ['android', 'linux'] -ICON = 'mdi:battery' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_BATTERY, default=DEFAULT_BATTERY): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -97,7 +95,7 @@ class LinuxBatterySensor(Entity): @property def device_class(self): """Return the device class of the sensor.""" - return 'battery' + return DEVICE_CLASS_BATTERY @property def state(self): @@ -109,11 +107,6 @@ class LinuxBatterySensor(Entity): """Return the unit the value is expressed in.""" return self._unit_of_measurement - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - @property def device_state_attributes(self): """Return the state attributes of the sensor.""" diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 98cc7731d4d..f1f8adab062 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -38,7 +38,7 @@ DEFAULT_TIMEOUT = 10 # Sensor types are defined like: Name, units SENSOR_TYPES = { 'temperature': ['Temperature', '°C'], - 'light': ['Light intensity', 'lux'], + 'light': ['Light intensity', 'lx'], 'moisture': ['Moisture', '%'], 'conductivity': ['Conductivity', 'µS/cm'], 'battery': ['Battery', '%'], diff --git a/homeassistant/components/sensor/mitemp_bt.py b/homeassistant/components/sensor/mitemp_bt.py new file mode 100644 index 00000000000..3628765293b --- /dev/null +++ b/homeassistant/components/sensor/mitemp_bt.py @@ -0,0 +1,172 @@ +""" +Support for Xiaomi Mi Temp BLE environmental sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.mitemp_bt/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC +) + + +REQUIREMENTS = ['mitemp_bt==0.0.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ADAPTER = 'adapter' +CONF_CACHE = 'cache_value' +CONF_MEDIAN = 'median' +CONF_RETRIES = 'retries' +CONF_TIMEOUT = 'timeout' + +DEFAULT_ADAPTER = 'hci0' +DEFAULT_UPDATE_INTERVAL = 300 +DEFAULT_FORCE_UPDATE = False +DEFAULT_MEDIAN = 3 +DEFAULT_NAME = 'MiTemp BT' +DEFAULT_RETRIES = 2 +DEFAULT_TIMEOUT = 10 + + +# Sensor types are defined like: Name, units +SENSOR_TYPES = { + 'temperature': ['Temperature', '°C'], + 'humidity': ['Humidity', '%'], + 'battery': ['Battery', '%'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, + vol.Optional(CONF_CACHE, default=DEFAULT_UPDATE_INTERVAL): cv.positive_int, + vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the MiTempBt sensor.""" + from mitemp_bt import mitemp_bt_poller + try: + import bluepy.btle # noqa: F401 # pylint: disable=unused-variable + from btlewrap import BluepyBackend + backend = BluepyBackend + except ImportError: + from btlewrap import GatttoolBackend + backend = GatttoolBackend + _LOGGER.debug('MiTempBt is using %s backend.', backend.__name__) + + cache = config.get(CONF_CACHE) + poller = mitemp_bt_poller.MiTempBtPoller( + config.get(CONF_MAC), cache_timeout=cache, + adapter=config.get(CONF_ADAPTER), backend=backend) + force_update = config.get(CONF_FORCE_UPDATE) + median = config.get(CONF_MEDIAN) + poller.ble_timeout = config.get(CONF_TIMEOUT) + poller.retries = config.get(CONF_RETRIES) + + devs = [] + + for parameter in config[CONF_MONITORED_CONDITIONS]: + name = SENSOR_TYPES[parameter][0] + unit = SENSOR_TYPES[parameter][1] + + prefix = config.get(CONF_NAME) + if prefix: + name = "{} {}".format(prefix, name) + + devs.append(MiTempBtSensor( + poller, parameter, name, unit, force_update, median)) + + add_devices(devs) + + +class MiTempBtSensor(Entity): + """Implementing the MiTempBt sensor.""" + + def __init__(self, poller, parameter, name, unit, force_update, median): + """Initialize the sensor.""" + self.poller = poller + self.parameter = parameter + self._unit = unit + self._name = name + self._state = None + self.data = [] + self._force_update = force_update + # Median is used to filter out outliers. median of 3 will filter + # single outliers, while median of 5 will filter double outliers + # Use median_count = 1 if no filtering is required. + self.median_count = median + + @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 units of measurement.""" + return self._unit + + @property + def force_update(self): + """Force update.""" + return self._force_update + + def update(self): + """ + Update current conditions. + + This uses a rolling median over 3 values to filter out outliers. + """ + from btlewrap.base import BluetoothBackendException + try: + _LOGGER.debug("Polling data for %s", self.name) + data = self.poller.parameter_value(self.parameter) + except IOError as ioerr: + _LOGGER.warning("Polling error %s", ioerr) + return + except BluetoothBackendException as bterror: + _LOGGER.warning("Polling error %s", bterror) + return + + if data is not None: + _LOGGER.debug("%s = %s", self.name, data) + self.data.append(data) + else: + _LOGGER.warning("Did not receive any data from Mi Temp sensor %s", + self.name) + # Remove old data from median list or set sensor value to None + # if no data is available anymore + if self.data: + self.data = self.data[1:] + else: + self._state = None + return + + if len(self.data) > self.median_count: + self.data = self.data[1:] + + if len(self.data) == self.median_count: + median = sorted(self.data)[int((self.median_count - 1) / 2)] + _LOGGER.debug("Median is: %s", median) + self._state = median + else: + _LOGGER.debug("Not yet enough data for median calculation") diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index c4f64e9e015..997fd312a6a 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mqtt/ """ -import asyncio import logging import json from datetime import timedelta @@ -16,12 +15,14 @@ from homeassistant.core import callback from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, - CONF_UNIT_OF_MEASUREMENT, CONF_ICON) + CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS) from homeassistant.helpers.entity import Entity import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -39,6 +40,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -48,8 +50,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up MQTT Sensor.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -66,6 +68,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_FORCE_UPDATE), config.get(CONF_EXPIRE_AFTER), config.get(CONF_ICON), + config.get(CONF_DEVICE_CLASS), value_template, config.get(CONF_JSON_ATTRS), config.get(CONF_UNIQUE_ID), @@ -79,8 +82,8 @@ class MqttSensor(MqttAvailability, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, - force_update, expire_after, icon, value_template, - json_attributes, unique_id: Optional[str], + force_update, expire_after, icon, device_class: Optional[str], + value_template, json_attributes, unique_id: Optional[str], availability_topic, payload_available, payload_not_available): """Initialize the sensor.""" @@ -95,15 +98,15 @@ class MqttSensor(MqttAvailability, Entity): self._template = value_template self._expire_after = expire_after self._icon = icon + self._device_class = device_class self._expiration_trigger = None self._json_attributes = set(json_attributes) self._unique_id = unique_id self._attributes = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def message_received(topic, payload, qos): @@ -142,8 +145,8 @@ class MqttSensor(MqttAvailability, Entity): self._state = payload self.async_schedule_update_ha_state() - yield from mqtt.async_subscribe( - self.hass, self._state_topic, message_received, self._qos) + await mqtt.async_subscribe(self.hass, self._state_topic, + message_received, self._qos) @callback def value_is_expired(self, *_): @@ -191,3 +194,8 @@ class MqttSensor(MqttAvailability, Entity): def icon(self): """Return the icon.""" return self._icon + + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 669ef3998de..1add4157f0e 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -26,7 +26,7 @@ SENSORS = { 'V_PERCENTAGE': ['%', 'mdi:percent'], 'V_LEVEL': { 'S_SOUND': ['dB', 'mdi:volume-high'], 'S_VIBRATION': ['Hz', None], - 'S_LIGHT_LEVEL': ['lux', 'white-balance-sunny']}, + 'S_LIGHT_LEVEL': ['lx', 'white-balance-sunny']}, 'V_ORP': ['mV', None], 'V_EC': ['μS/cm', None], 'V_VAR': ['var', None], diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 5ee4f738051..9ce50dc61e5 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -9,8 +9,9 @@ import logging from homeassistant.components.nest import DATA_NEST from homeassistant.helpers.entity import Entity -from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_MONITORED_CONDITIONS) +from homeassistant.const import ( + TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_TEMPERATURE) DEPENDENCIES = ['nest'] SENSOR_TYPES = ['humidity', @@ -143,7 +144,7 @@ class NestTempSensor(NestSensor): @property def device_class(self): """Return the device class of the sensor.""" - return 'temperature' + return DEVICE_CLASS_TEMPERATURE def update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 4dddaf45aa4..4aeba082e55 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -64,7 +63,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available Netatmo weather sensors.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None)) dev = [] diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index b55c60f6e7c..1ef5a27cf3d 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -160,7 +160,7 @@ class BaseSensor(Entity): def __init__(self, data, data_params, name, icon, unique_id): """Initialize the sensor.""" - self._attrs = {} + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._icon = icon self._name = name self._data_params = data_params @@ -172,7 +172,6 @@ class BaseSensor(Entity): @property def device_state_attributes(self): """Return the device state attributes.""" - self._attrs.update({ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}) return self._attrs @property @@ -254,10 +253,25 @@ class AllergyIndexSensor(BaseSensor): i['label'] for i in RATING_MAPPING if i['minimum'] <= period['Index'] <= i['maximum'] ] - self._attrs[ATTR_ALLERGEN_GENUS] = period['Triggers'][0]['Genus'] - self._attrs[ATTR_ALLERGEN_NAME] = period['Triggers'][0]['Name'] - self._attrs[ATTR_ALLERGEN_TYPE] = period['Triggers'][0][ - 'PlantType'] + + for i in range(3): + index = i + 1 + try: + data = period['Triggers'][i] + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_GENUS, index)] = data['Genus'] + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_NAME, index)] = data['Name'] + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_TYPE, index)] = data['PlantType'] + except IndexError: + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_GENUS, index)] = None + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_NAME, index)] = None + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_TYPE, index)] = None + self._attrs[ATTR_RATING] = rating except KeyError: diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py new file mode 100644 index 00000000000..c38f58b7916 --- /dev/null +++ b/homeassistant/components/sensor/postnl.py @@ -0,0 +1,110 @@ +""" +Sensor for PostNL packages. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.postnl/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['postnl_api==1.0.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = 'Information provided by PostNL' + +DEFAULT_NAME = 'postnl' + +ICON = 'mdi:package-variant-closed' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the PostNL sensor platform.""" + from postnl_api import PostNL_API, UnauthorizedException + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + name = config.get(CONF_NAME) + + try: + api = PostNL_API(username, password) + + except UnauthorizedException: + _LOGGER.exception("Can't connect to the PostNL webservice") + return + + add_devices([PostNLSensor(api, name)], True) + + +class PostNLSensor(Entity): + """Representation of a PostNL sensor.""" + + def __init__(self, api, name): + """Initialize the PostNL sensor.""" + self._name = name + self._attributes = None + self._state = None + self._api = api + + @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 of this entity, if any.""" + return 'package(s)' + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @property + def icon(self): + """Icon to use in the frontend.""" + return ICON + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update device state.""" + shipments = self._api.get_relevant_shipments() + status_counts = {} + + for shipment in shipments: + status = shipment['status']['formatted']['short'] + status = self._api.parse_datetime(status, '%d-%m-%Y', '%H:%M') + + name = shipment['settings']['title'] + status_counts[name] = status + + self._attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + **status_counts + } + + self._state = len(status_counts) diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 629a5f6a0ee..b3ca054f88f 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['qnapstats==0.2.5'] +REQUIREMENTS = ['qnapstats==0.2.6'] _LOGGER = logging.getLogger(__name__) @@ -352,7 +352,7 @@ class QNAPDriveSensor(QNAPSensor): return data['health'] if self.var_id == 'drive_temp': - return int(data['temp_c']) + return int(data['temp_c']) if data['temp_c'] is not None else 0 @property def name(self): diff --git a/homeassistant/components/sensor/socialblade.py b/homeassistant/components/sensor/socialblade.py new file mode 100644 index 00000000000..1e0084e1404 --- /dev/null +++ b/homeassistant/components/sensor/socialblade.py @@ -0,0 +1,90 @@ +""" +Support for Social Blade. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.socialblade/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['socialbladeclient==0.2'] + +CHANNEL_ID = 'channel_id' + +DEFAULT_NAME = "Social Blade" + +MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2) + +SUBSCRIBERS = 'subscribers' + +TOTAL_VIEWS = 'total_views' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CHANNEL_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Social Blade sensor.""" + social_blade = SocialBladeSensor( + config[CHANNEL_ID], config[CONF_NAME]) + + social_blade.update() + if social_blade.valid_channel_id is False: + return + + add_devices([social_blade]) + + +class SocialBladeSensor(Entity): + """Representation of a Social Blade Sensor.""" + + def __init__(self, case, name): + """Initialize the Social Blade sensor.""" + self._state = None + self.channel_id = case + self._attributes = None + self.valid_channel_id = None + self._name = name + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._attributes: + return self._attributes + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from Social Blade.""" + import socialbladeclient + try: + data = socialbladeclient.get_data(self.channel_id) + self._attributes = {TOTAL_VIEWS: data[TOTAL_VIEWS]} + self._state = data[SUBSCRIBERS] + self.valid_channel_id = True + + except (ValueError, IndexError): + _LOGGER.error("Unable to find valid channel ID") + self.valid_channel_id = False + self._attributes = None diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index eeca31fa36b..b7ece1bdb87 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sql/ """ import decimal +import datetime import logging import voluptuous as vol @@ -19,11 +20,11 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.6'] +REQUIREMENTS = ['sqlalchemy==1.2.7'] +CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' CONF_QUERY = 'query' -CONF_COLUMN_NAME = 'column' def validate_sql_select(value): @@ -34,9 +35,9 @@ def validate_sql_select(value): _QUERY_SCHEME = vol.Schema({ + vol.Required(CONF_COLUMN_NAME): cv.string, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select), - vol.Required(CONF_COLUMN_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }) @@ -48,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" + """Set up the SQL sensor platform.""" db_url = config.get(CONF_DB_URL, None) if not db_url: db_url = DEFAULT_URL.format( @@ -90,10 +91,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SQLSensor(Entity): - """An SQL sensor.""" + """Representation of an SQL sensor.""" def __init__(self, name, sessmaker, query, column, unit, value_template): - """Initialize SQL sensor.""" + """Initialize the SQL sensor.""" self._name = name if "LIMIT" in query: self._query = query @@ -145,6 +146,8 @@ class SQLSensor(Entity): for key, value in res.items(): if isinstance(value, decimal.Decimal): value = float(value) + if isinstance(value, datetime.date): + value = str(value) self._attributes[key] = value except sqlalchemy.exc.SQLAlchemyError as err: _LOGGER.error("Error executing query %s: %s", self._query, err) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 2f970796fe1..0b85de8e4f2 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==5.4.3'] +REQUIREMENTS = ['psutil==5.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py index 39d1cbc75a3..aedecfe61e5 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -46,8 +46,10 @@ class TahomaSensor(TahomaDevice, Entity): """Return the unit of measurement of this entity, if any.""" if self.tahoma_device.type == 'Temperature Sensor': return None + elif self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': + return None elif self.tahoma_device.type == 'io:LightIOSystemSensor': - return 'lux' + return 'lx' elif self.tahoma_device.type == 'Humidity Sensor': return '%' @@ -57,3 +59,6 @@ class TahomaSensor(TahomaDevice, Entity): if self.tahoma_device.type == 'io:LightIOSystemSensor': self.current_value = self.tahoma_device.active_states[ 'core:LuminanceState'] + if self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': + self.current_value = self.tahoma_device.active_states[ + 'core:ContactState'] diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 61a084c6266..048ca988e3d 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -7,7 +7,9 @@ https://home-assistant.io/components/sensor.tellduslive/ import logging from homeassistant.components.tellduslive import TelldusLiveEntity -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS) _LOGGER = logging.getLogger(__name__) @@ -25,18 +27,20 @@ SENSOR_TYPE_DEW_POINT = 'dewp' SENSOR_TYPE_BAROMETRIC_PRESSURE = 'barpress' SENSOR_TYPES = { - SENSOR_TYPE_TEMPERATURE: ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - SENSOR_TYPE_HUMIDITY: ['Humidity', '%', 'mdi:water'], - SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm/h', 'mdi:water'], - SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', 'mdi:water'], - SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ''], - SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ''], - SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ''], - SENSOR_TYPE_UV: ['UV', 'UV', ''], - SENSOR_TYPE_WATT: ['Power', 'W', ''], - SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ''], - SENSOR_TYPE_DEW_POINT: ['Dew Point', TEMP_CELSIUS, 'mdi:thermometer'], - SENSOR_TYPE_BAROMETRIC_PRESSURE: ['Barometric Pressure', 'kPa', ''], + SENSOR_TYPE_TEMPERATURE: ['Temperature', TEMP_CELSIUS, None, + DEVICE_CLASS_TEMPERATURE], + SENSOR_TYPE_HUMIDITY: ['Humidity', '%', None, DEVICE_CLASS_HUMIDITY], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm/h', 'mdi:water', None], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', 'mdi:water', None], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', '', None], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', '', None], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', '', None], + SENSOR_TYPE_UV: ['UV', 'UV', '', None], + SENSOR_TYPE_WATT: ['Power', 'W', '', None], + SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', None, DEVICE_CLASS_ILLUMINANCE], + SENSOR_TYPE_DEW_POINT: + ['Dew Point', TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + SENSOR_TYPE_BAROMETRIC_PRESSURE: ['Barometric Pressure', 'kPa', '', None], } @@ -117,3 +121,9 @@ class TelldusLiveSensor(TelldusLiveEntity): """Return the icon.""" return SENSOR_TYPES[self._type][2] \ if self._type in SENSOR_TYPES else None + + @property + def device_class(self): + """Return the device class.""" + return SENSOR_TYPES[self._type][3] \ + if self._type in SENSOR_TYPES else None diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 1cd43262513..65f49998dbf 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -6,16 +6,18 @@ https://home-assistant.io/components/sensor.template/ """ import asyncio import logging +from typing import Optional import voluptuous as vol from homeassistant.core import callback -from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA +from homeassistant.components.sensor import ENTITY_ID_FORMAT, \ + PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, ATTR_ENTITY_ID, CONF_SENSORS, EVENT_HOMEASSISTANT_START, CONF_FRIENDLY_NAME_TEMPLATE, - MATCH_ALL) + MATCH_ALL, CONF_DEVICE_CLASS) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -30,6 +32,7 @@ SENSOR_SCHEMA = vol.Schema({ vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) @@ -52,6 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) + device_class = device_config.get(CONF_DEVICE_CLASS) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) @@ -86,7 +90,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): state_template, icon_template, entity_picture_template, - entity_ids) + entity_ids, + device_class) ) if not sensors: _LOGGER.error("No sensors added") @@ -101,7 +106,7 @@ class SensorTemplate(Entity): def __init__(self, hass, device_id, friendly_name, friendly_name_template, unit_of_measurement, state_template, icon_template, - entity_picture_template, entity_ids): + entity_picture_template, entity_ids, device_class): """Initialize the sensor.""" self.hass = hass self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, @@ -116,6 +121,7 @@ class SensorTemplate(Entity): self._icon = None self._entity_picture = None self._entities = entity_ids + self._device_class = device_class @asyncio.coroutine def async_added_to_hass(self): @@ -151,6 +157,11 @@ class SensorTemplate(Entity): """Return the icon to use in the frontend, if any.""" return self._icon + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + @property def entity_picture(self): """Return the entity_picture to use in the frontend, if any.""" diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/upnp.py index e0c57ca9ac6..07b63553fcb 100644 --- a/homeassistant/components/sensor/upnp.py +++ b/homeassistant/components/sensor/upnp.py @@ -11,6 +11,8 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['upnp'] + BYTES_RECEIVED = 1 BYTES_SENT = 2 PACKETS_RECEIVED = 3 @@ -25,12 +27,16 @@ SENSOR_TYPES = { } -async def async_setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the IGD sensors.""" + if discovery_info is None: + return + device = hass.data[DATA_UPNP] service = device.find_first_service(CIC_SERVICE) unit = discovery_info['unit'] - add_devices([ + async_add_devices([ IGDSensor(service, t, unit if SENSOR_TYPES[t][1] else '#') for t in SENSOR_TYPES], True) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index c81c208e33e..eb8ccae768e 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -52,7 +52,7 @@ class VeraSensor(VeraDevice, Entity): if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: return self._temperature_units elif self.vera_device.category == veraApi.CATEGORY_LIGHT_SENSOR: - return 'lux' + return 'lx' elif self.vera_device.category == veraApi.CATEGORY_UV_SENSOR: return 'level' elif self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR: diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 7938b17e4d6..7f2df4bcda9 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -15,13 +15,13 @@ import voluptuous as vol from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components import sensor -from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS, - LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE) + LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity import Entity from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -618,8 +618,6 @@ LANG_CODES = [ 'CY', 'SN', 'JI', 'YI', ] -DEFAULT_ENTITY_NAMESPACE = 'pws' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_PWS_ID): cv.string, @@ -629,34 +627,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Inclusive(CONF_LONGITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.longitude, vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_ENTITY_NAMESPACE, - default=DEFAULT_ENTITY_NAMESPACE): cv.string, + vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]) }) -# Stores a list of entity ids we added in order to support multiple stations -# at once. -ADDED_ENTITY_IDS_KEY = 'wunderground_added_entity_ids' - -@asyncio.coroutine -def async_setup_platform(hass: HomeAssistantType, config: ConfigType, - async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up the WUnderground sensor.""" - hass.data.setdefault(ADDED_ENTITY_IDS_KEY, set()) - latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - namespace = config.get(CONF_ENTITY_NAMESPACE) + pws_id = config.get(CONF_PWS_ID) rest = WUndergroundData( - hass, config.get(CONF_API_KEY), config.get(CONF_PWS_ID), + hass, config.get(CONF_API_KEY), pws_id, config.get(CONF_LANG), latitude, longitude) + + if pws_id is None: + unique_id_base = "@{:06f},{:06f}".format(longitude, latitude) + else: + # Manually specified weather station, use that for unique_id + unique_id_base = pws_id sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: - sensors.append(WUndergroundSensor(hass, rest, variable, namespace)) + sensors.append(WUndergroundSensor(hass, rest, variable, + unique_id_base)) - yield from rest.async_update() + await rest.async_update() if not rest.data: raise PlatformNotReady @@ -667,7 +663,7 @@ class WUndergroundSensor(Entity): """Implementing the WUnderground sensor.""" def __init__(self, hass: HomeAssistantType, rest, condition, - namespace: str): + unique_id_base: str): """Initialize the sensor.""" self.rest = rest self._condition = condition @@ -679,12 +675,10 @@ class WUndergroundSensor(Entity): self._entity_picture = None self._unit_of_measurement = self._cfg_expand("unit_of_measurement") self.rest.request_feature(SENSOR_TYPES[condition].feature) - current_ids = set(hass.states.async_entity_ids(sensor.DOMAIN)) - current_ids |= hass.data[ADDED_ENTITY_IDS_KEY] - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, "{} {}".format(namespace, condition), - current_ids=current_ids) - hass.data[ADDED_ENTITY_IDS_KEY].add(self.entity_id) + # This is only the suggested entity id, it might get changed by + # the entity registry later. + self.entity_id = sensor.ENTITY_ID_FORMAT.format('pws_' + condition) + self._unique_id = "{},{}".format(unique_id_base, condition) def _cfg_expand(self, what, default=None): """Parse and return sensor data.""" @@ -764,6 +758,11 @@ class WUndergroundSensor(Entity): self._entity_picture = re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE) + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + class WUndergroundData(object): """Get data from WUnderground.""" diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 33bbdc32308..3192d0d2f60 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -3,16 +3,18 @@ import logging from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS) _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'temperature': [TEMP_CELSIUS, 'mdi:thermometer'], - 'humidity': ['%', 'mdi:water-percent'], - 'illumination': ['lm', 'mdi:weather-sunset'], - 'lux': ['lx', 'mdi:weather-sunset'], - 'pressure': ['hPa', 'mdi:gauge'] + 'temperature': [TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + 'humidity': ['%', None, DEVICE_CLASS_HUMIDITY], + 'illumination': ['lm', None, DEVICE_CLASS_ILLUMINANCE], + 'lux': ['lx', None, DEVICE_CLASS_ILLUMINANCE], + 'pressure': ['hPa', 'mdi:gauge', None] } @@ -26,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'temperature', gateway)) devices.append(XiaomiSensor(device, 'Humidity', 'humidity', gateway)) - elif device['model'] == 'weather.v1': + elif device['model'] in ['weather', 'weather.v1']: devices.append(XiaomiSensor(device, 'Temperature', 'temperature', gateway)) devices.append(XiaomiSensor(device, 'Humidity', @@ -36,7 +38,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif device['model'] == 'sensor_motion.aq2': devices.append(XiaomiSensor(device, 'Illumination', 'lux', gateway)) - elif device['model'] == 'gateway': + elif device['model'] in ['gateway', 'gateway.v3', 'acpartner.v3']: devices.append(XiaomiSensor(device, 'Illumination', 'illumination', gateway)) add_devices(devices) @@ -66,6 +68,12 @@ class XiaomiSensor(XiaomiDevice): except TypeError: return None + @property + def device_class(self): + """Return the device class of this entity.""" + return SENSOR_TYPES.get(self._data_key)[2] \ + if self._data_key in SENSOR_TYPES else None + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index df18e086ddd..db66419e54a 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -131,9 +131,12 @@ class YahooWeatherSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - } + attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + + if self._code is not None and "weather" in self._type: + attrs['condition_code'] = self._code + + return attrs def update(self): """Get the latest data from Yahoo! and updates the states.""" diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 36cdca2e638..d856ed1a17e 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -71,7 +71,7 @@ class Sensor(zha.Entity): _LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value) if attribute == self.value_attribute: self._state = value - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() class TemperatureSensor(Sensor): diff --git a/homeassistant/components/switch/deluge.py b/homeassistant/components/switch/deluge.py index 30287a2669e..da0b3bf3228 100644 --- a/homeassistant/components/switch/deluge.py +++ b/homeassistant/components/switch/deluge.py @@ -9,15 +9,16 @@ import logging import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, CONF_PASSWORD, CONF_USERNAME, STATE_OFF, STATE_ON) from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['deluge-client==1.0.5'] +REQUIREMENTS = ['deluge-client==1.4.0'] -_LOGGING = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Deluge Switch' DEFAULT_PORT = 58846 @@ -46,8 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: deluge_api.connect() except ConnectionRefusedError: - _LOGGING.error("Connection to Deluge Daemon failed") - return + _LOGGER.error("Connection to Deluge Daemon failed") + raise PlatformNotReady add_devices([DelugeSwitch(deluge_api, name)]) @@ -60,6 +61,7 @@ class DelugeSwitch(ToggleEntity): self._name = name self.deluge_client = deluge_client self._state = STATE_OFF + self._available = False @property def name(self): @@ -76,18 +78,32 @@ class DelugeSwitch(ToggleEntity): """Return true if device is on.""" return self._state == STATE_ON + @property + def available(self): + """Return true if device is available.""" + return self._available + def turn_on(self, **kwargs): """Turn the device on.""" - self.deluge_client.call('core.resume_all_torrents') + torrent_ids = self.deluge_client.call('core.get_session_state') + self.deluge_client.call('core.resume_torrent', torrent_ids) def turn_off(self, **kwargs): """Turn the device off.""" - self.deluge_client.call('core.pause_all_torrents') + torrent_ids = self.deluge_client.call('core.get_session_state') + self.deluge_client.call('core.pause_torrent', torrent_ids) def update(self): """Get the latest data from deluge and updates the state.""" - torrent_list = self.deluge_client.call('core.get_torrents_status', {}, - ['paused']) + from deluge_client import FailedToReconnectException + try: + torrent_list = self.deluge_client.call('core.get_torrents_status', + {}, ['paused']) + self._available = True + except FailedToReconnectException: + _LOGGER.error("Connection to Deluge Daemon Lost") + self._available = False + return for torrent in torrent_list.values(): item = torrent.popitem() if not item[1]: diff --git a/homeassistant/components/switch/fritzbox.py b/homeassistant/components/switch/fritzbox.py index c8313b0dfef..65a1aa6aabc 100755 --- a/homeassistant/components/switch/fritzbox.py +++ b/homeassistant/components/switch/fritzbox.py @@ -87,7 +87,7 @@ class FritzboxSwitch(SwitchDevice): if self._device.has_powermeter: attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format( - (self._device.energy or 0.0) / 100000) + (self._device.energy or 0.0) / 1000) attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = \ ATTR_TOTAL_CONSUMPTION_UNIT_VALUE if self._device.has_temperature_sensor: diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py index 5f9482ce955..be562e9d909 100644 --- a/homeassistant/components/switch/insteon_plm.py +++ b/homeassistant/components/switch/insteon_plm.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index f3bd0bef012..69f12536c5f 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -17,9 +16,10 @@ from homeassistant.components.mqtt import ( from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON, CONF_ICON) + CONF_PAYLOAD_ON, CONF_ICON, STATE_ON) import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -39,8 +39,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the MQTT switch.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -88,10 +88,9 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._optimistic = optimistic self._template = value_template - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def state_message_received(topic, payload, qos): @@ -110,10 +109,16 @@ class MqttSwitch(MqttAvailability, SwitchDevice): # Force into optimistic mode. self._optimistic = True else: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, state_message_received, self._qos) + if self._optimistic: + last_state = await async_get_last_state(self.hass, + self.entity_id) + if last_state: + self._state = last_state.state == STATE_ON + @property def should_poll(self): """Return the polling state.""" @@ -139,8 +144,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): """Return the icon.""" return self._icon - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -153,8 +157,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._state = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 99d41bdd9c3..8306b323330 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -1,153 +1,80 @@ """Implements a RainMachine sprinkler controller for Home Assistant.""" -from datetime import timedelta from logging import getLogger -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv +from homeassistant.components.rainmachine import ( + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, + MIN_SCAN_TIME_FORCED) from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_EMAIL, CONF_IP_ADDRESS, - CONF_PASSWORD, CONF_PLATFORM, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL) +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.util import Throttle +DEPENDENCIES = ['rainmachine'] + _LOGGER = getLogger(__name__) -REQUIREMENTS = ['regenmaschine==0.4.1'] ATTR_CYCLES = 'cycles' ATTR_TOTAL_DURATION = 'total_duration' -CONF_ZONE_RUN_TIME = 'zone_run_time' - -DEFAULT_PORT = 8080 -DEFAULT_SSL = True -DEFAULT_ZONE_RUN_SECONDS = 60 * 10 - -MIN_SCAN_TIME_LOCAL = timedelta(seconds=1) -MIN_SCAN_TIME_REMOTE = timedelta(seconds=5) -MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) - -PLATFORM_SCHEMA = vol.Schema( - vol.All( - cv.has_at_least_one_key(CONF_IP_ADDRESS, CONF_EMAIL), - { - vol.Required(CONF_PLATFORM): cv.string, - vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, - vol.Exclusive(CONF_IP_ADDRESS, 'auth'): cv.string, - vol.Exclusive(CONF_EMAIL, 'auth'): - vol.Email(), # pylint: disable=no-value-for-parameter - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS): - cv.positive_int - }), - extra=vol.ALLOW_EXTRA) +DEFAULT_ZONE_RUN = 60 * 10 def setup_platform(hass, config, add_devices, discovery_info=None): """Set this component up under its platform.""" - import regenmaschine as rm + if discovery_info is None: + return - _LOGGER.debug('Config data: %s', config) + _LOGGER.debug('Config received: %s', discovery_info) - ip_address = config.get(CONF_IP_ADDRESS, None) - email_address = config.get(CONF_EMAIL, None) - password = config[CONF_PASSWORD] - zone_run_time = config[CONF_ZONE_RUN_TIME] + zone_run_time = discovery_info.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN) - try: - if ip_address: - _LOGGER.debug('Configuring local API') + client, device_mac = hass.data.get(DATA_RAINMACHINE) - port = config[CONF_PORT] - ssl = config[CONF_SSL] - auth = rm.Authenticator.create_local( - ip_address, password, port=port, https=ssl) - elif email_address: - _LOGGER.debug('Configuring remote API') - auth = rm.Authenticator.create_remote(email_address, password) + entities = [] + for program in client.programs.all().get('programs', {}): + if not program.get('active'): + continue - _LOGGER.debug('Querying against: %s', auth.url) + _LOGGER.debug('Adding program: %s', program) + entities.append( + RainMachineProgram(client, device_mac, program)) - client = rm.Client(auth) - device_name = client.provision.device_name()['name'] - device_mac = client.provision.wifi()['macAddress'] + for zone in client.zones.all().get('zones', {}): + if not zone.get('active'): + continue - entities = [] - for program in client.programs.all().get('programs', {}): - if not program.get('active'): - continue + _LOGGER.debug('Adding zone: %s', zone) + entities.append( + RainMachineZone(client, device_mac, zone, + zone_run_time)) - _LOGGER.debug('Adding program: %s', program) - entities.append( - RainMachineProgram(client, device_name, device_mac, program)) - - for zone in client.zones.all().get('zones', {}): - if not zone.get('active'): - continue - - _LOGGER.debug('Adding zone: %s', zone) - entities.append( - RainMachineZone(client, device_name, device_mac, zone, - zone_run_time)) - - add_devices(entities) - except rm.exceptions.HTTPError as exc_info: - _LOGGER.error('An HTTP error occurred while talking with RainMachine') - _LOGGER.debug(exc_info) - return False - except UnboundLocalError as exc_info: - _LOGGER.error('Could not authenticate against RainMachine') - _LOGGER.debug(exc_info) - return False - - -def aware_throttle(api_type): - """Create an API type-aware throttler.""" - _decorator = None - if api_type == 'local': - - @Throttle(MIN_SCAN_TIME_LOCAL, MIN_SCAN_TIME_FORCED) - def decorator(function): - """Create a local API throttler.""" - return function - - _decorator = decorator - else: - - @Throttle(MIN_SCAN_TIME_REMOTE, MIN_SCAN_TIME_FORCED) - def decorator(function): - """Create a remote API throttler.""" - return function - - _decorator = decorator - - return _decorator + add_devices(entities, True) class RainMachineEntity(SwitchDevice): """A class to represent a generic RainMachine entity.""" - def __init__(self, client, device_name, device_mac, entity_json): + def __init__(self, client, device_mac, entity_json): """Initialize a generic RainMachine entity.""" self._api_type = 'remote' if client.auth.using_remote_api else 'local' self._client = client self._entity_json = entity_json + self.device_mac = device_mac - self.device_name = device_name self._attrs = { - ATTR_ATTRIBUTION: '© RainMachine', - ATTR_DEVICE_CLASS: self.device_name + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION } @property def device_state_attributes(self) -> dict: """Return the state attributes.""" - if self._client: - return self._attrs + return self._attrs + + @property + def icon(self) -> str: + """Return the icon.""" + return 'mdi:water' @property def is_enabled(self) -> bool: @@ -159,27 +86,6 @@ class RainMachineEntity(SwitchDevice): """Return the RainMachine ID for this entity.""" return self._entity_json.get('uid') - @aware_throttle('local') - def _local_update(self) -> None: - """Call an update with scan times appropriate for the local API.""" - self._update() - - @aware_throttle('remote') - def _remote_update(self) -> None: - """Call an update with scan times appropriate for the remote API.""" - self._update() - - def _update(self) -> None: # pylint: disable=no-self-use - """Logic for update method, regardless of API type.""" - raise NotImplementedError() - - def update(self) -> None: - """Determine how the entity updates itself.""" - if self._api_type == 'remote': - self._remote_update() - else: - self._local_update() - class RainMachineProgram(RainMachineEntity): """A RainMachine program.""" @@ -192,7 +98,7 @@ class RainMachineProgram(RainMachineEntity): @property def name(self) -> str: """Return the name of the program.""" - return 'Program: {}'.format(self._entity_json.get('name')) + return 'Program: {0}'.format(self._entity_json.get('name')) @property def unique_id(self) -> str: @@ -224,7 +130,8 @@ class RainMachineProgram(RainMachineEntity): _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _LOGGER.debug(exc_info) - def _update(self) -> None: + @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) + def update(self) -> None: """Update info for the program.""" import regenmaschine.exceptions as exceptions @@ -240,10 +147,10 @@ class RainMachineProgram(RainMachineEntity): class RainMachineZone(RainMachineEntity): """A RainMachine zone.""" - def __init__(self, client, device_name, device_mac, zone_json, + def __init__(self, client, device_mac, zone_json, zone_run_time): """Initialize a RainMachine zone.""" - super().__init__(client, device_name, device_mac, zone_json) + super().__init__(client, device_mac, zone_json) self._run_time = zone_run_time self._attrs.update({ ATTR_CYCLES: self._entity_json.get('noOfCycles'), @@ -258,7 +165,7 @@ class RainMachineZone(RainMachineEntity): @property def name(self) -> str: """Return the name of the zone.""" - return 'Zone: {}'.format(self._entity_json.get('name')) + return 'Zone: {0}'.format(self._entity_json.get('name')) @property def unique_id(self) -> str: @@ -287,7 +194,8 @@ class RainMachineZone(RainMachineEntity): _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _LOGGER.debug(exc_info) - def _update(self) -> None: + @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) + def update(self) -> None: """Update info for the zone.""" import regenmaschine.exceptions as exceptions diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py index 939fc70660a..4c44d6b2592 100644 --- a/homeassistant/components/switch/xiaomi_aqara.py +++ b/homeassistant/components/switch/xiaomi_aqara.py @@ -26,7 +26,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device in gateway.devices['switch']: model = device['model'] if model == 'plug': - devices.append(XiaomiGenericSwitch(device, "Plug", 'status', + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'channel_0' + devices.append(XiaomiGenericSwitch(device, "Plug", data_key, True, gateway)) elif model in ['ctrl_neutral1', 'ctrl_neutral1.aq1']: devices.append(XiaomiGenericSwitch(device, 'Wall Switch', @@ -52,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'Wall Switch LN Right', 'channel_1', False, gateway)) - elif model in ['86plug', 'ctrl_86plug.aq1']: + elif model in ['86plug', 'ctrl_86plug', 'ctrl_86plug.aq1']: devices.append(XiaomiGenericSwitch(device, 'Wall Plug', 'status', True, gateway)) add_devices(devices) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 1dad1f3a1eb..5994184d815 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -19,12 +19,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP CONF_MAX_ENTRIES = 'max_entries' +CONF_FIRE_EVENT = 'fire_event' CONF_MESSAGE = 'message' CONF_LEVEL = 'level' CONF_LOGGER = 'logger' DATA_SYSTEM_LOG = 'system_log' DEFAULT_MAX_ENTRIES = 50 +DEFAULT_FIRE_EVENT = False DEPENDENCIES = ['http'] DOMAIN = 'system_log' @@ -37,6 +39,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): cv.positive_int, + vol.Optional(CONF_FIRE_EVENT, default=DEFAULT_FIRE_EVENT): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -97,11 +100,12 @@ def _exception_as_string(exc_info): class LogErrorHandler(logging.Handler): """Log handler for error messages.""" - def __init__(self, hass, maxlen): + def __init__(self, hass, maxlen, fire_event): """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass self.records = deque(maxlen=maxlen) + self.fire_event = fire_event def _create_entry(self, record, call_stack): return { @@ -130,7 +134,8 @@ class LogErrorHandler(logging.Handler): entry = self._create_entry(record, stack) self.records.appendleft(entry) - self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) + if self.fire_event: + self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) @asyncio.coroutine @@ -140,7 +145,8 @@ def async_setup(hass, config): if conf is None: conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - handler = LogErrorHandler(hass, conf.get(CONF_MAX_ENTRIES)) + handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES], + conf[CONF_FIRE_EVENT]) logging.getLogger().addHandler(handler) hass.http.register_view(AllErrorsView(handler)) diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 055e3f410ea..9848d20094c 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -38,6 +38,8 @@ TAHOMA_COMPONENTS = [ TAHOMA_TYPES = { 'rts:RollerShutterRTSComponent': 'cover', 'rts:CurtainRTSComponent': 'cover', + 'rts:BlindRTSComponent': 'cover', + 'rts:VenetianBlindRTSComponent': 'cover', 'io:RollerShutterWithLowSpeedManagementIOComponent': 'cover', 'io:RollerShutterVeluxIOComponent': 'cover', 'io:RollerShutterGenericIOComponent': 'cover', diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index e43640e4df2..af0fe5bd572 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==10.0.1'] +REQUIREMENTS = ['python-telegram-bot==10.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 84d2d3f349d..5a363e84d7b 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -122,8 +122,7 @@ def async_finish(hass, entity_id): DOMAIN, SERVICE_FINISH, {ATTR_ENTITY_ID: entity_id})) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up a timer.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -142,8 +141,7 @@ def async_setup(hass, config): if not entities: return False - @asyncio.coroutine - def async_handler_service(service): + async def async_handler_service(service): """Handle a call to the timer services.""" target_timers = component.async_extract_from_service(service) @@ -162,7 +160,7 @@ def async_setup(hass, config): timer.async_start(service.data.get(ATTR_DURATION)) ) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_START, async_handler_service, @@ -177,7 +175,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_FINISH, async_handler_service, schema=SERVICE_SCHEMA) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -224,19 +222,17 @@ class Timer(Entity): ATTR_REMAINING: str(self._remaining) } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is about to be added to Home Assistant.""" # If not None, we got an initial value. if self._state is not None: return restore_state = self._hass.helpers.restore_state - state = yield from restore_state.async_get_last_state(self.entity_id) + state = await restore_state.async_get_last_state(self.entity_id) self._state = state and state.state == state - @asyncio.coroutine - def async_start(self, duration): + async def async_start(self, duration): """Start a timer.""" if self._listener: self._listener() @@ -260,10 +256,9 @@ class Timer(Entity): self._listener = async_track_point_in_utc_time(self._hass, self.async_finished, self._end) - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_pause(self): + async def async_pause(self): """Pause a timer.""" if self._listener is None: return @@ -273,10 +268,9 @@ class Timer(Entity): self._remaining = self._end - dt_util.utcnow() self._state = STATUS_PAUSED self._end = None - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_cancel(self): + async def async_cancel(self): """Cancel a timer.""" if self._listener: self._listener() @@ -286,10 +280,9 @@ class Timer(Entity): self._remaining = timedelta() self._hass.bus.async_fire(EVENT_TIMER_CANCELLED, {"entity_id": self.entity_id}) - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_finish(self): + async def async_finish(self): """Reset and updates the states, fire finished event.""" if self._state != STATUS_ACTIVE: return @@ -299,10 +292,9 @@ class Timer(Entity): self._remaining = timedelta() self._hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_finished(self, time): + async def async_finished(self, time): """Reset and updates the states, fire finished event.""" if self._state != STATUS_ACTIVE: return @@ -312,4 +304,4 @@ class Timer(Entity): self._remaining = timedelta() self._hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index 084a7229212..bf03ec1adad 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -39,8 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_engine(hass, config): +async def async_get_engine(hass, config): """Set up Google speech component.""" return GoogleProvider(hass, config[CONF_LANG]) @@ -70,8 +69,7 @@ class GoogleProvider(Provider): """Return list of supported languages.""" return SUPPORT_LANGUAGES - @asyncio.coroutine - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from google.""" from gtts_token import gtts_token @@ -81,7 +79,7 @@ class GoogleProvider(Provider): data = b'' for idx, part in enumerate(message_parts): - part_token = yield from self.hass.async_add_job( + part_token = await self.hass.async_add_job( token.calculate_token, part) url_param = { @@ -97,7 +95,7 @@ class GoogleProvider(Provider): try: with async_timeout.timeout(10, loop=self.hass.loop): - request = yield from websession.get( + request = await websession.get( GOOGLE_SPEECH_URL, params=url_param, headers=self.headers ) @@ -106,7 +104,7 @@ class GoogleProvider(Provider): _LOGGER.error("Error %d on load url %s", request.status, request.url) return (None, None) - data += yield from request.read() + data += await request.read() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for google speech.") diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index f7bf9774e42..9ccf280ed04 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -72,8 +72,7 @@ def _load_uuid(hass, filename=UPDATER_UUID_FILE): return _create_uuid(hass, filename) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the updater component.""" if 'dev' in current_version: # This component only makes sense in release versions @@ -81,16 +80,15 @@ def async_setup(hass, config): config = config.get(DOMAIN, {}) if config.get(CONF_REPORTING): - huuid = yield from hass.async_add_job(_load_uuid, hass) + huuid = await hass.async_add_job(_load_uuid, hass) else: huuid = None include_components = config.get(CONF_COMPONENT_REPORTING) - @asyncio.coroutine - def check_new_version(now): + async def check_new_version(now): """Check if a new version is available and report if one is.""" - result = yield from get_newest_version(hass, huuid, include_components) + result = await get_newest_version(hass, huuid, include_components) if result is None: return @@ -125,8 +123,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def get_system_info(hass, include_components): +async def get_system_info(hass, include_components): """Return info about the system.""" info_object = { 'arch': platform.machine(), @@ -151,7 +148,7 @@ def get_system_info(hass, include_components): info_object['os_version'] = platform.release() elif platform.system() == 'Linux': import distro - linux_dist = yield from hass.async_add_job( + linux_dist = await hass.async_add_job( distro.linux_distribution, False) info_object['distribution'] = linux_dist[0] info_object['os_version'] = linux_dist[1] @@ -160,11 +157,10 @@ def get_system_info(hass, include_components): return info_object -@asyncio.coroutine -def get_newest_version(hass, huuid, include_components): +async def get_newest_version(hass, huuid, include_components): """Get the newest Home Assistant version.""" if huuid: - info_object = yield from get_system_info(hass, include_components) + info_object = await get_system_info(hass, include_components) info_object['huuid'] = huuid else: info_object = {} @@ -172,7 +168,7 @@ def get_newest_version(hass, huuid, include_components): session = async_get_clientsession(hass) try: with async_timeout.timeout(5, loop=hass.loop): - req = yield from session.post(UPDATER_URL, json=info_object) + req = await session.post(UPDATER_URL, json=info_object) _LOGGER.info(("Submitted analytics to Home Assistant servers. " "Information submitted includes %s"), info_object) except (asyncio.TimeoutError, aiohttp.ClientError): @@ -181,7 +177,7 @@ def get_newest_version(hass, huuid, include_components): return None try: - res = yield from req.json() + res = await req.json() except ValueError: _LOGGER.error("Received invalid JSON from Home Assistant Update") return None diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py index dd611090c22..8aeb93fed25 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.util import get_local_ip -REQUIREMENTS = ['pyupnp-async==0.1.0.1'] +REQUIREMENTS = ['pyupnp-async==0.1.0.2'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) @@ -50,7 +50,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_ENABLE_PORT_MAPPING, default=True): cv.boolean, vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), - vol.Optional(CONF_LOCAL_IP): ip_address, + vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), vol.Optional(CONF_PORTS): vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int}) }), @@ -62,9 +62,7 @@ async def async_setup(hass, config): config = config[DOMAIN] host = config.get(CONF_LOCAL_IP) - if host is not None: - host = str(host) - else: + if host is None: host = get_local_ip() if host == '127.0.0.1': @@ -90,10 +88,8 @@ async def async_setup(hass, config): service = device.find_first_service(IP_SERVICE) if _service['serviceType'] == CIC_SERVICE: unit = config.get(CONF_UNITS) - discovery.load_platform(hass, 'sensor', - DOMAIN, - {'unit': unit}, - config) + hass.async_add_job(discovery.async_load_platform( + hass, 'sensor', DOMAIN, {'unit': unit}, config)) except UpnpSoapError as error: _LOGGER.error(error) return False diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b200d634ba9..c36c960c4fc 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -22,7 +22,10 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' ATTR_CONDITION_CLASS = 'condition_class' ATTR_FORECAST = 'forecast' +ATTR_FORECAST_CONDITION = 'condition' +ATTR_FORECAST_PRECIPITATION = 'precipitation' ATTR_FORECAST_TEMP = 'temperature' +ATTR_FORECAST_TEMP_LOW = 'templow' ATTR_FORECAST_TIME = 'datetime' ATTR_WEATHER_ATTRIBUTION = 'attribution' ATTR_WEATHER_HUMIDITY = 'humidity' @@ -110,9 +113,12 @@ class WeatherEntity(Entity): ATTR_WEATHER_TEMPERATURE: show_temp( self.hass, self.temperature, self.temperature_unit, self.precision), - ATTR_WEATHER_HUMIDITY: round(self.humidity) } + humidity = self.humidity + if humidity is not None: + data[ATTR_WEATHER_HUMIDITY] = round(humidity) + ozone = self.ozone if ozone is not None: data[ATTR_WEATHER_OZONE] = ozone @@ -144,6 +150,10 @@ class WeatherEntity(Entity): forecast_entry[ATTR_FORECAST_TEMP] = show_temp( self.hass, forecast_entry[ATTR_FORECAST_TEMP], self.temperature_unit, self.precision) + if ATTR_FORECAST_TEMP_LOW in forecast_entry: + forecast_entry[ATTR_FORECAST_TEMP_LOW] = show_temp( + self.hass, forecast_entry[ATTR_FORECAST_TEMP_LOW], + self.temperature_unit, self.precision) forecast.append(forecast_entry) data[ATTR_FORECAST] = forecast diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index a49a1664eec..9b9707e87f6 100644 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -10,7 +10,8 @@ import asyncio import voluptuous as vol from homeassistant.components.weather import ( - WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) from homeassistant.const import \ CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers import config_validation as cv @@ -28,9 +29,6 @@ DEFAULT_TIMEFRAME = 60 CONF_FORECAST = 'forecast' -ATTR_FORECAST_CONDITION = 'condition' -ATTR_FORECAST_TEMP_LOW = 'templow' - CONDITION_CLASSES = { 'cloudy': ['c', 'p'], @@ -121,15 +119,6 @@ class BrWeather(WeatherEntity): if conditions: return conditions.get(ccode) - @property - def entity_picture(self): - """Return the entity picture to use in the frontend, if any.""" - from buienradar.buienradar import (IMAGE) - - if self._data and self._data.condition: - return self._data.condition.get(IMAGE, None) - return None - @property def temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 52aa8c46046..f0712542ea5 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -25,9 +25,6 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" -ATTR_DAILY_FORECAST_SUMMARY = 'daily_forecast_summary' -ATTR_HOURLY_FORECAST_SUMMARY = 'hourly_forecast_summary' - CONF_UNITS = 'units' DEFAULT_NAME = 'Dark Sky' @@ -122,25 +119,6 @@ class DarkSkyWeather(WeatherEntity): ATTR_FORECAST_TEMP: entry.d.get('temperature')} for entry in self._ds_hourly.data] - @property - def hourly_forecast_summary(self): - """Return a summary of the hourly forecast.""" - return self._ds_hourly.summary - - @property - def daily_forecast_summary(self): - """Return a summary of the daily forecast.""" - return self._ds_daily.summary - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = { - ATTR_DAILY_FORECAST_SUMMARY: self.daily_forecast_summary, - ATTR_HOURLY_FORECAST_SUMMARY: self.hourly_forecast_summary - } - return attrs - def update(self): """Get the latest data from Dark Sky.""" self._dark_sky.update() diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py index 02e07996213..fffdf03d07d 100644 --- a/homeassistant/components/weather/demo.py +++ b/homeassistant/components/weather/demo.py @@ -7,7 +7,8 @@ https://home-assistant.io/components/demo/ from datetime import datetime, timedelta from homeassistant.components.weather import ( - WeatherEntity, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) + WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT) CONDITION_CLASSES = { @@ -32,9 +33,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo weather.""" add_devices([ DemoWeather('South', 'Sunshine', 21.6414, 92, 1099, 0.5, TEMP_CELSIUS, - [22, 19, 15, 12, 14, 18, 21]), + [['rainy', 1, 22, 15], ['rainy', 5, 19, 8], + ['cloudy', 0, 15, 9], ['sunny', 0, 12, 6], + ['partlycloudy', 2, 14, 7], ['rainy', 15, 18, 7], + ['fog', 0.2, 21, 12]]), DemoWeather('North', 'Shower rain', -12, 54, 987, 4.8, TEMP_FAHRENHEIT, - [-10, -13, -18, -23, -19, -14, -9]) + [['snowy', 2, -10, -15], ['partlycloudy', 1, -13, -14], + ['sunny', 0, -18, -22], ['sunny', 0.1, -23, -23], + ['snowy', 4, -19, -20], ['sunny', 0.3, -14, -19], + ['sunny', 0, -9, -12]]) ]) @@ -108,7 +115,10 @@ class DemoWeather(WeatherEntity): for entry in self._forecast: data_dict = { ATTR_FORECAST_TIME: reftime.isoformat(), - ATTR_FORECAST_TEMP: entry + ATTR_FORECAST_CONDITION: entry[0], + ATTR_FORECAST_PRECIPITATION: entry[1], + ATTR_FORECAST_TEMP: entry[2], + ATTR_FORECAST_TEMP_LOW: entry[3] } reftime = reftime + timedelta(hours=4) forecast_data.append(data_dict) diff --git a/homeassistant/components/weather/ecobee.py b/homeassistant/components/weather/ecobee.py index 379f5c1211b..80ee4c29fbe 100644 --- a/homeassistant/components/weather/ecobee.py +++ b/homeassistant/components/weather/ecobee.py @@ -6,14 +6,13 @@ https://home-assistant.io/components/weather.ecobee/ """ from homeassistant.components import ecobee from homeassistant.components.weather import ( - WeatherEntity, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) + WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) from homeassistant.const import (TEMP_FAHRENHEIT) DEPENDENCIES = ['ecobee'] -ATTR_FORECAST_CONDITION = 'condition' -ATTR_FORECAST_TEMP_LOW = 'templow' ATTR_FORECAST_TEMP_HIGH = 'temphigh' ATTR_FORECAST_PRESSURE = 'pressure' ATTR_FORECAST_VISIBILITY = 'visibility' diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index c8a1bdf8f68..909f123b52c 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -10,7 +10,8 @@ import logging import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS) @@ -21,14 +22,12 @@ REQUIREMENTS = ['pyowm==2.8.0'] _LOGGER = logging.getLogger(__name__) -ATTR_FORECAST_CONDITION = 'condition' ATTRIBUTION = 'Data provided by OpenWeatherMap' DEFAULT_NAME = 'OpenWeatherMap' MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) -MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS = 3 CONDITION_CLASSES = { 'cloudy': [804], @@ -144,12 +143,12 @@ class OpenWeatherMapWeather(WeatherEntity): data.append({ ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000, ATTR_FORECAST_TEMP: - entry.get_temperature('celsius').get('temp') - }) - if (len(data) - 1) % MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS == 0: - data[len(data) - 1][ATTR_FORECAST_CONDITION] = \ + entry.get_temperature('celsius').get('temp'), + ATTR_FORECAST_PRECIPITATION: entry.get_rain().get('3h'), + ATTR_FORECAST_CONDITION: [k for k, v in CONDITION_CLASSES.items() if entry.get_weather_code() in v][0] + }) return data def update(self): diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 5987cf7621f..f9befece5a4 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -10,7 +10,8 @@ import logging import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv @@ -20,10 +21,8 @@ _LOGGER = logging.getLogger(__name__) DATA_CONDITION = 'yahoo_condition' -ATTR_FORECAST_CONDITION = 'condition' ATTRIBUTION = "Weather details provided by Yahoo! Inc." -ATTR_FORECAST_TEMP_LOW = 'templow' CONF_WOEID = 'woeid' diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 1e23ad19897..4989f4f0db2 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -18,8 +18,8 @@ from voluptuous.humanize import humanize_error from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, __version__) -from homeassistant.components import frontend from homeassistant.core import callback +from homeassistant.loader import bind_hass from homeassistant.remote import JSONEncoder from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_get_all_descriptions @@ -46,7 +46,6 @@ TYPE_AUTH_REQUIRED = 'auth_required' TYPE_CALL_SERVICE = 'call_service' TYPE_EVENT = 'event' TYPE_GET_CONFIG = 'get_config' -TYPE_GET_PANELS = 'get_panels' TYPE_GET_SERVICES = 'get_services' TYPE_GET_STATES = 'get_states' TYPE_PING = 'ping' @@ -64,62 +63,56 @@ AUTH_MESSAGE_SCHEMA = vol.Schema({ vol.Required('api_password'): str, }) -SUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({ +# Minimal requirements of a message +MINIMAL_MESSAGE_SCHEMA = vol.Schema({ vol.Required('id'): cv.positive_int, + vol.Required('type'): cv.string, +}, extra=vol.ALLOW_EXTRA) +# Base schema to extend by message handlers +BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ + vol.Required('id'): cv.positive_int, +}) + + +SCHEMA_SUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_SUBSCRIBE_EVENTS, vol.Optional('event_type', default=MATCH_ALL): str, }) -UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_UNSUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_UNSUBSCRIBE_EVENTS, vol.Required('subscription'): cv.positive_int, }) -CALL_SERVICE_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_CALL_SERVICE = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_CALL_SERVICE, vol.Required('domain'): str, vol.Required('service'): str, vol.Optional('service_data'): dict }) -GET_STATES_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_GET_STATES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_GET_STATES, }) -GET_SERVICES_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_GET_SERVICES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_GET_SERVICES, }) -GET_CONFIG_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_GET_CONFIG = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_GET_CONFIG, }) -GET_PANELS_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, - vol.Required('type'): TYPE_GET_PANELS, -}) -PING_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, +SCHEMA_PING = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_PING, }) -BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, - vol.Required('type'): vol.Any(TYPE_CALL_SERVICE, - TYPE_SUBSCRIBE_EVENTS, - TYPE_UNSUBSCRIBE_EVENTS, - TYPE_GET_STATES, - TYPE_GET_SERVICES, - TYPE_GET_CONFIG, - TYPE_GET_PANELS, - TYPE_PING) -}, extra=vol.ALLOW_EXTRA) # Define the possible errors that occur when connections are cancelled. # Originally, this was just asyncio.CancelledError, but issue #9546 showed @@ -191,9 +184,36 @@ def result_message(iden, result=None): } +@bind_hass +@callback +def async_register_command(hass, command, handler, schema): + """Register a websocket command.""" + handlers = hass.data.get(DOMAIN) + if handlers is None: + handlers = hass.data[DOMAIN] = {} + handlers[command] = (handler, schema) + + async def async_setup(hass, config): """Initialize the websocket API.""" hass.http.register_view(WebsocketAPIView) + + async_register_command(hass, TYPE_SUBSCRIBE_EVENTS, + handle_subscribe_events, SCHEMA_SUBSCRIBE_EVENTS) + async_register_command(hass, TYPE_UNSUBSCRIBE_EVENTS, + handle_unsubscribe_events, + SCHEMA_UNSUBSCRIBE_EVENTS) + async_register_command(hass, TYPE_CALL_SERVICE, + handle_call_service, SCHEMA_CALL_SERVICE) + async_register_command(hass, TYPE_GET_STATES, + handle_get_states, SCHEMA_GET_STATES) + async_register_command(hass, TYPE_GET_SERVICES, + handle_get_services, SCHEMA_GET_SERVICES) + async_register_command(hass, TYPE_GET_CONFIG, + handle_get_config, SCHEMA_GET_CONFIG) + async_register_command(hass, TYPE_PING, + handle_ping, SCHEMA_PING) + return True @@ -316,10 +336,11 @@ class ActiveConnection: msg = await wsock.receive_json() last_id = 0 + handlers = self.hass.data[DOMAIN] while msg: self.debug("Received", msg) - msg = BASE_COMMAND_MESSAGE_SCHEMA(msg) + msg = MINIMAL_MESSAGE_SCHEMA(msg) cur_id = msg['id'] if cur_id <= last_id: @@ -327,9 +348,13 @@ class ActiveConnection: cur_id, ERR_ID_REUSE, 'Identifier values have to increase.')) + elif msg['type'] not in handlers: + # Unknown command + break + else: - handler_name = 'handle_{}'.format(msg['type']) - getattr(self, handler_name)(msg) + handler, schema = handlers[msg['type']] + handler(self.hass, self, schema(msg)) last_id = cur_id msg = await wsock.receive_json() @@ -403,109 +428,96 @@ class ActiveConnection: return wsock - def handle_subscribe_events(self, msg): - """Handle subscribe events command. - Async friendly. - """ - msg = SUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg) +@callback +def handle_subscribe_events(hass, connection, msg): + """Handle subscribe events command. - async def forward_events(event): - """Forward events to websocket.""" - if event.event_type == EVENT_TIME_CHANGED: - return + Async friendly. + """ + async def forward_events(event): + """Forward events to websocket.""" + if event.event_type == EVENT_TIME_CHANGED: + return - self.send_message_outside(event_message(msg['id'], event)) + connection.send_message_outside(event_message(msg['id'], event)) - self.event_listeners[msg['id']] = self.hass.bus.async_listen( - msg['event_type'], forward_events) + connection.event_listeners[msg['id']] = hass.bus.async_listen( + msg['event_type'], forward_events) - self.to_write.put_nowait(result_message(msg['id'])) + connection.to_write.put_nowait(result_message(msg['id'])) - def handle_unsubscribe_events(self, msg): - """Handle unsubscribe events command. - Async friendly. - """ - msg = UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg) +@callback +def handle_unsubscribe_events(hass, connection, msg): + """Handle unsubscribe events command. - subscription = msg['subscription'] + Async friendly. + """ + subscription = msg['subscription'] - if subscription in self.event_listeners: - self.event_listeners.pop(subscription)() - self.to_write.put_nowait(result_message(msg['id'])) - else: - self.to_write.put_nowait(error_message( - msg['id'], ERR_NOT_FOUND, - 'Subscription not found.')) + if subscription in connection.event_listeners: + connection.event_listeners.pop(subscription)() + connection.to_write.put_nowait(result_message(msg['id'])) + else: + connection.to_write.put_nowait(error_message( + msg['id'], ERR_NOT_FOUND, 'Subscription not found.')) - def handle_call_service(self, msg): - """Handle call service command. - Async friendly. - """ - msg = CALL_SERVICE_MESSAGE_SCHEMA(msg) +@callback +def handle_call_service(hass, connection, msg): + """Handle call service command. - async def call_service_helper(msg): - """Call a service and fire complete message.""" - await self.hass.services.async_call( - msg['domain'], msg['service'], msg.get('service_data'), True) - self.send_message_outside(result_message(msg['id'])) + Async friendly. + """ + async def call_service_helper(msg): + """Call a service and fire complete message.""" + await hass.services.async_call( + msg['domain'], msg['service'], msg.get('service_data'), True) + connection.send_message_outside(result_message(msg['id'])) - self.hass.async_add_job(call_service_helper(msg)) + hass.async_add_job(call_service_helper(msg)) - def handle_get_states(self, msg): - """Handle get states command. - Async friendly. - """ - msg = GET_STATES_MESSAGE_SCHEMA(msg) +@callback +def handle_get_states(hass, connection, msg): + """Handle get states command. - self.to_write.put_nowait(result_message( - msg['id'], self.hass.states.async_all())) + Async friendly. + """ + connection.to_write.put_nowait(result_message( + msg['id'], hass.states.async_all())) - def handle_get_services(self, msg): - """Handle get services command. - Async friendly. - """ - msg = GET_SERVICES_MESSAGE_SCHEMA(msg) +@callback +def handle_get_services(hass, connection, msg): + """Handle get services command. - async def get_services_helper(msg): - """Get available services and fire complete message.""" - descriptions = await async_get_all_descriptions(self.hass) - self.send_message_outside(result_message(msg['id'], descriptions)) + 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)) - self.hass.async_add_job(get_services_helper(msg)) + hass.async_add_job(get_services_helper(msg)) - def handle_get_config(self, msg): - """Handle get config command. - Async friendly. - """ - msg = GET_CONFIG_MESSAGE_SCHEMA(msg) +@callback +def handle_get_config(hass, connection, msg): + """Handle get config command. - self.to_write.put_nowait(result_message( - msg['id'], self.hass.config.as_dict())) + Async friendly. + """ + connection.to_write.put_nowait(result_message( + msg['id'], hass.config.as_dict())) - def handle_get_panels(self, msg): - """Handle get panels command. - Async friendly. - """ - msg = GET_PANELS_MESSAGE_SCHEMA(msg) - panels = { - panel: - self.hass.data[frontend.DATA_PANELS][panel].to_response( - self.hass, self.request) - for panel in self.hass.data[frontend.DATA_PANELS]} +@callback +def handle_ping(hass, connection, msg): + """Handle ping command. - self.to_write.put_nowait(result_message( - msg['id'], panels)) - - def handle_ping(self, msg): - """Handle ping command. - - Async friendly. - """ - self.to_write.put_nowait(pong_message(msg['id'])) + Async friendly. + """ + connection.to_write.put_nowait(pong_message(msg['id'])) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 73c1fdf9075..9b66c4c6ded 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -221,39 +221,68 @@ class ApplicationListener: self._config, ) - for cluster_id, cluster in endpoint.in_clusters.items(): - cluster_type = type(cluster) - if cluster_id in profile_clusters[0]: - continue - if cluster_type not in zha_const.SINGLE_CLUSTER_DEVICE_CLASS: - continue + for cluster in endpoint.in_clusters.values(): + await self._attempt_single_cluster_device( + endpoint, + cluster, + profile_clusters[0], + device_key, + zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS, + 'in_clusters', + discovered_info, + join, + ) - component = zha_const.SINGLE_CLUSTER_DEVICE_CLASS[cluster_type] - cluster_key = "{}-{}".format(device_key, cluster_id) - discovery_info = { - 'application_listener': self, - 'endpoint': endpoint, - 'in_clusters': {cluster.cluster_id: cluster}, - 'out_clusters': {}, - 'new_join': join, - 'unique_id': cluster_key, - 'entity_suffix': '_{}'.format(cluster_id), - } - discovery_info.update(discovered_info) - self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info - - await discovery.async_load_platform( - self._hass, - component, - DOMAIN, - {'discovery_key': cluster_key}, - self._config, + for cluster in endpoint.out_clusters.values(): + await self._attempt_single_cluster_device( + endpoint, + cluster, + profile_clusters[1], + device_key, + zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, + 'out_clusters', + discovered_info, + join, ) def register_entity(self, ieee, entity_obj): """Record the creation of a hass entity associated with ieee.""" self._device_registry[ieee].append(entity_obj) + async def _attempt_single_cluster_device(self, endpoint, cluster, + profile_clusters, device_key, + device_classes, discovery_attr, + entity_info, is_new_join): + """Try to set up an entity from a "bare" cluster.""" + if cluster.cluster_id in profile_clusters: + return + # pylint: disable=unidiomatic-typecheck + if type(cluster) not in device_classes: + return + + component = device_classes[type(cluster)] + cluster_key = "{}-{}".format(device_key, cluster.cluster_id) + discovery_info = { + 'application_listener': self, + 'endpoint': endpoint, + 'in_clusters': {}, + 'out_clusters': {}, + '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( + self._hass, + component, + DOMAIN, + {'discovery_key': cluster_key}, + self._config, + ) + class Entity(entity.Entity): """A base class for ZHA entities.""" @@ -287,18 +316,30 @@ class Entity(entity.Entity): kwargs.get('entity_suffix', ''), ) - for cluster in in_clusters.values(): - cluster.add_listener(self) - for cluster in out_clusters.values(): - cluster.add_listener(self) self._endpoint = endpoint self._in_clusters = in_clusters self._out_clusters = out_clusters self._state = ha_const.STATE_UNKNOWN self._unique_id = unique_id + # Normally the entity itself is the listener. Sub-classes may set this + # to a dict of cluster ID -> listener to receive messages for specific + # clusters separately + self._in_listeners = {} + self._out_listeners = {} + application_listener.register_entity(ieee, self) + async def async_added_to_hass(self): + """Callback once the entity is added to hass. + + It is now safe to update the entity state + """ + for cluster_id, cluster in self._in_clusters.items(): + cluster.add_listener(self._in_listeners.get(cluster_id, self)) + for cluster_id, cluster in self._out_clusters.items(): + cluster.add_listener(self._out_listeners.get(cluster_id, self)) + @property def unique_id(self) -> str: """Return a unique ID.""" @@ -379,7 +420,7 @@ async def safe_read(cluster, attributes): try: result, _ = await cluster.read_attributes( attributes, - allow_cache=False, + allow_cache=True, ) return result except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 4fe3581d5b2..36eb4d55c97 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -1,7 +1,8 @@ """All constants related to the ZHA component.""" DEVICE_CLASS = {} -SINGLE_CLUSTER_DEVICE_CLASS = {} +SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} +SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} COMPONENT_CLUSTERS = {} @@ -15,11 +16,17 @@ def populate_data(): from zigpy.profiles import PROFILES, zha, zll DEVICE_CLASS[zha.PROFILE_ID] = { + zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', + zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor', + zha.DeviceType.REMOTE_CONTROL: 'binary_sensor', zha.DeviceType.SMART_PLUG: 'switch', zha.DeviceType.ON_OFF_LIGHT: 'light', zha.DeviceType.DIMMABLE_LIGHT: 'light', zha.DeviceType.COLOR_DIMMABLE_LIGHT: 'light', + zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'binary_sensor', + zha.DeviceType.DIMMER_SWITCH: 'binary_sensor', + zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor', } DEVICE_CLASS[zll.PROFILE_ID] = { zll.DeviceType.ON_OFF_LIGHT: 'light', @@ -29,15 +36,23 @@ def populate_data(): zll.DeviceType.COLOR_LIGHT: 'light', zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light', zll.DeviceType.COLOR_TEMPERATURE_LIGHT: 'light', + zll.DeviceType.COLOR_CONTROLLER: 'binary_sensor', + zll.DeviceType.COLOR_SCENE_CONTROLLER: 'binary_sensor', + zll.DeviceType.CONTROLLER: 'binary_sensor', + zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor', + zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor', } - SINGLE_CLUSTER_DEVICE_CLASS.update({ + SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({ zcl.clusters.general.OnOff: 'switch', zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ + zcl.clusters.general.OnOff: 'binary_sensor', + }) # A map of hass components to all Zigbee clusters it could use for profile_id, classes in DEVICE_CLASS.items(): diff --git a/homeassistant/components/zone/.translations/cy.json b/homeassistant/components/zone/.translations/cy.json new file mode 100644 index 00000000000..e34fae81b61 --- /dev/null +++ b/homeassistant/components/zone/.translations/cy.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Enw eisoes yn bodoli" + }, + "step": { + "init": { + "data": { + "icon": "Eicon", + "latitude": "Lledred", + "longitude": "Hydred", + "name": "Enw", + "passive": "Goddefol", + "radius": "Radiws" + }, + "title": "Ddiffinio paramedrau parth" + } + }, + "title": "Parth" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/de.json b/homeassistant/components/zone/.translations/de.json new file mode 100644 index 00000000000..fc1e3537f33 --- /dev/null +++ b/homeassistant/components/zone/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "init": { + "data": { + "icon": "Symbol", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definieren Sie die Zonenparameter" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/en.json b/homeassistant/components/zone/.translations/en.json new file mode 100644 index 00000000000..1faf0110a53 --- /dev/null +++ b/homeassistant/components/zone/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Name already exists" + }, + "step": { + "init": { + "data": { + "icon": "Icon", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name", + "passive": "Passive", + "radius": "Radius" + }, + "title": "Define zone parameters" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ko.json b/homeassistant/components/zone/.translations/ko.json new file mode 100644 index 00000000000..364f8f3cc77 --- /dev/null +++ b/homeassistant/components/zone/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "icon": "\uc544\uc774\ucf58", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984", + "passive": "\uc790\ub3d9\ud654 \uc804\uc6a9", + "radius": "\ubc18\uacbd" + }, + "title": "\uad6c\uc5ed \ub9e4\uac1c \ubcc0\uc218 \uc815\uc758" + } + }, + "title": "\uad6c\uc5ed" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/lb.json b/homeassistant/components/zone/.translations/lb.json new file mode 100644 index 00000000000..10b65bcca30 --- /dev/null +++ b/homeassistant/components/zone/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Numm g\u00ebtt et schonn" + }, + "step": { + "init": { + "data": { + "icon": "Ikone", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm", + "passive": "Passif", + "radius": "Radius" + }, + "title": "D\u00e9fin\u00e9iert Zone Parameter" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/nl.json b/homeassistant/components/zone/.translations/nl.json new file mode 100644 index 00000000000..6dcf565ada6 --- /dev/null +++ b/homeassistant/components/zone/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Naam bestaat al" + }, + "step": { + "init": { + "data": { + "icon": "Pictogram", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam", + "passive": "Passief", + "radius": "Straal" + }, + "title": "Definieer zone parameters" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/no.json b/homeassistant/components/zone/.translations/no.json new file mode 100644 index 00000000000..3c1a91976f0 --- /dev/null +++ b/homeassistant/components/zone/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Navnet eksisterer allerede" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definer sone parametere" + } + }, + "title": "Sone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pl.json b/homeassistant/components/zone/.translations/pl.json new file mode 100644 index 00000000000..e649de4c75e --- /dev/null +++ b/homeassistant/components/zone/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Nazwa ju\u017c istnieje" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa", + "passive": "Pasywnie", + "radius": "Promie\u0144" + }, + "title": "Zdefiniuj parametry strefy" + } + }, + "title": "Strefa" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt.json b/homeassistant/components/zone/.translations/pt.json new file mode 100644 index 00000000000..a4ced557805 --- /dev/null +++ b/homeassistant/components/zone/.translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "Nome j\u00e1 existente" + }, + "step": { + "init": { + "data": { + "icon": "\u00cdcone", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome", + "passive": "Passivo", + "radius": "Raio" + } + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json new file mode 100644 index 00000000000..f0619f2163c --- /dev/null +++ b/homeassistant/components/zone/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + }, + "step": { + "init": { + "data": { + "icon": "\u0417\u043d\u0430\u0447\u043e\u043a", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u0430\u044f", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0437\u043e\u043d\u044b" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hans.json b/homeassistant/components/zone/.translations/zh-Hans.json new file mode 100644 index 00000000000..6d06b68dad8 --- /dev/null +++ b/homeassistant/components/zone/.translations/zh-Hans.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + }, + "step": { + "init": { + "data": { + "icon": "\u56fe\u6807", + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0", + "passive": "\u88ab\u52a8", + "radius": "\u534a\u5f84" + }, + "title": "\u5b9a\u4e49\u533a\u57df\u76f8\u5173\u53d8\u91cf" + } + }, + "title": "\u533a\u57df" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py new file mode 100644 index 00000000000..d3628fd57f3 --- /dev/null +++ b/homeassistant/components/zone/__init__.py @@ -0,0 +1,93 @@ +""" +Support for the definition of zones. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zone/ +""" + +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.util import slugify + +from .config_flow import configured_zones +from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE +from .zone import Zone + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Unnamed zone' +DEFAULT_PASSIVE = False +DEFAULT_RADIUS = 100 + +ENTITY_ID_FORMAT = 'zone.{}' +ENTITY_ID_HOME = ENTITY_ID_FORMAT.format(HOME_ZONE) + +ICON_HOME = 'mdi:home' +ICON_IMPORT = 'mdi:import' + +# The config that zone accepts is the same as if it has platforms. +PLATFORM_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Setup configured zones as well as home assistant zone if necessary.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + zone_entries = configured_zones(hass) + for _, entry in config_per_platform(config, DOMAIN): + name = slugify(entry[CONF_NAME]) + if name not in zone_entries: + zone = Zone(hass, entry[CONF_NAME], entry[CONF_LATITUDE], + entry[CONF_LONGITUDE], entry.get(CONF_RADIUS), + entry.get(CONF_ICON), entry.get(CONF_PASSIVE)) + zone.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, entry[CONF_NAME], None, hass) + hass.async_add_job(zone.async_update_ha_state()) + hass.data[DOMAIN][name] = zone + + if HOME_ZONE not in hass.data[DOMAIN] and HOME_ZONE not in zone_entries: + name = hass.config.location_name + zone = Zone(hass, name, hass.config.latitude, hass.config.longitude, + DEFAULT_RADIUS, ICON_HOME, False) + zone.entity_id = ENTITY_ID_HOME + hass.async_add_job(zone.async_update_ha_state()) + hass.data[DOMAIN][slugify(name)] = zone + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up zone as config entry.""" + entry = config_entry.data + name = entry[CONF_NAME] + zone = Zone(hass, name, entry[CONF_LATITUDE], entry[CONF_LONGITUDE], + entry.get(CONF_RADIUS), entry.get(CONF_ICON), + entry.get(CONF_PASSIVE)) + zone.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, name, None, hass) + hass.async_add_job(zone.async_update_ha_state()) + hass.data[DOMAIN][slugify(name)] = zone + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + zones = hass.data[DOMAIN] + name = slugify(config_entry.data[CONF_NAME]) + zone = zones.pop(name) + await zone.async_remove() + return True diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py new file mode 100644 index 00000000000..5ec955a48d9 --- /dev/null +++ b/homeassistant/components/zone/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow to configure zone component.""" + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries, data_entry_flow +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) +from homeassistant.core import callback +from homeassistant.util import slugify + +from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE + + +@callback +def configured_zones(hass): + """Return a set of the configured hosts.""" + return set((slugify(entry.data[CONF_NAME])) for + entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class ZoneFlowHandler(data_entry_flow.FlowHandler): + """Zone config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize zone configuration flow.""" + pass + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + errors = {} + + if user_input is not None: + name = slugify(user_input[CONF_NAME]) + if name not in configured_zones(self.hass) and name != HOME_ZONE: + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + errors['base'] = 'name_exists' + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required(CONF_NAME): str, + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS): vol.Coerce(float), + vol.Optional(CONF_ICON): str, + vol.Optional(CONF_PASSIVE): bool, + }), + errors=errors, + ) diff --git a/homeassistant/components/zone/const.py b/homeassistant/components/zone/const.py new file mode 100644 index 00000000000..b69ba67302a --- /dev/null +++ b/homeassistant/components/zone/const.py @@ -0,0 +1,5 @@ +"""Constants for the zone component.""" + +CONF_PASSIVE = 'passive' +DOMAIN = 'zone' +HOME_ZONE = 'home' diff --git a/homeassistant/components/zone/strings.json b/homeassistant/components/zone/strings.json new file mode 100644 index 00000000000..ff2c7c07c14 --- /dev/null +++ b/homeassistant/components/zone/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Zone", + "step": { + "init": { + "title": "Define zone parameters", + "data": { + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Radius", + "passive": "Passive", + "icon": "Icon" + } + } + }, + "error": { + "name_exists": "Name already exists" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone/zone.py similarity index 57% rename from homeassistant/components/zone.py rename to homeassistant/components/zone/zone.py index b1a94f3809c..b7c2e9ee858 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone/zone.py @@ -1,54 +1,18 @@ -""" -Support for the definition of zones. +"""Component entity and functionality.""" -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zone/ -""" -import asyncio -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_LATITUDE, - CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) +from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass -from homeassistant.helpers import config_per_platform -from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.location import distance -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN ATTR_PASSIVE = 'passive' ATTR_RADIUS = 'radius' -CONF_PASSIVE = 'passive' - -DEFAULT_NAME = 'Unnamed zone' -DEFAULT_PASSIVE = False -DEFAULT_RADIUS = 100 -DOMAIN = 'zone' - -ENTITY_ID_FORMAT = 'zone.{}' -ENTITY_ID_HOME = ENTITY_ID_FORMAT.format('home') - -ICON_HOME = 'mdi:home' -ICON_IMPORT = 'mdi:import' - STATE = 'zoning' -# The config that zone accepts is the same as if it has platforms. -PLATFORM_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_LATITUDE): cv.latitude, - vol.Required(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), - vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, -}, extra=vol.ALLOW_EXTRA) - @bind_hass def active_zone(hass, latitude, longitude, radius=0): @@ -104,32 +68,6 @@ def in_zone(zone, latitude, longitude, radius=0): return zone_dist - radius < zone.attributes[ATTR_RADIUS] -@asyncio.coroutine -def async_setup(hass, config): - """Set up the zone.""" - entities = set() - tasks = [] - for _, entry in config_per_platform(config, DOMAIN): - name = entry.get(CONF_NAME) - zone = Zone(hass, name, entry[CONF_LATITUDE], entry[CONF_LONGITUDE], - entry.get(CONF_RADIUS), entry.get(CONF_ICON), - entry.get(CONF_PASSIVE)) - zone.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, name, entities) - tasks.append(zone.async_update_ha_state()) - entities.add(zone.entity_id) - - if ENTITY_ID_HOME not in entities: - zone = Zone(hass, hass.config.location_name, - hass.config.latitude, hass.config.longitude, - DEFAULT_RADIUS, ICON_HOME, False) - zone.entity_id = ENTITY_ID_HOME - tasks.append(zone.async_update_ha_state()) - - yield from asyncio.wait(tasks, loop=hass.loop) - return True - - class Zone(Entity): """Representation of a Zone.""" diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 02d2b574592..01b17023c12 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -297,15 +297,46 @@ def setup(hass, config): def node_added(node): """Handle a new node on the network.""" entity = ZWaveNodeEntity(node, network) - name = node_name(node) - generated_id = generate_entity_id(DOMAIN + '.{}', name, []) - node_config = device_config.get(generated_id) - if node_config.get(CONF_IGNORED): - _LOGGER.info( - "Ignoring node entity %s due to device settings", - generated_id) + + def _add_node_to_component(): + name = node_name(node) + generated_id = generate_entity_id(DOMAIN + '.{}', name, []) + node_config = device_config.get(generated_id) + if node_config.get(CONF_IGNORED): + _LOGGER.info( + "Ignoring node entity %s due to device settings", + generated_id) + return + component.add_entities([entity]) + + if entity.unique_id: + _add_node_to_component() return - component.add_entities([entity]) + + async def _check_node_ready(): + """Wait for node to be parsed.""" + start_time = dt_util.utcnow() + while True: + waited = int((dt_util.utcnow()-start_time).total_seconds()) + + if entity.unique_id: + _LOGGER.info("Z-Wave node %d ready after %d seconds", + entity.node_id, waited) + break + elif waited >= const.NODE_READY_WAIT_SECS: + # Wait up to NODE_READY_WAIT_SECS seconds for the Z-Wave + # node to be ready. + _LOGGER.warning( + "Z-Wave node %d not ready after %d seconds, " + "continuing anyway", + entity.node_id, waited) + break + else: + await asyncio.sleep(1, loop=hass.loop) + + hass.async_add_job(_add_node_to_component) + + hass.add_job(_check_node_ready) def network_ready(): """Handle the query of all awake nodes.""" @@ -359,6 +390,11 @@ def setup(hass, config): _LOGGER.info("Z-Wave soft_reset have been initialized") network.controller.soft_reset() + def update_config(service): + """Update the config from git.""" + _LOGGER.info("Configuration update has been initialized") + network.controller.update_ozw_config() + def test_network(service): """Test the network by sending commands to all the nodes.""" _LOGGER.info("Z-Wave test_network have been initialized") @@ -616,6 +652,8 @@ def setup(hass, config): hass.services.register(DOMAIN, const.SERVICE_HEAL_NETWORK, heal_network) hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset) + hass.services.register(DOMAIN, const.SERVICE_UPDATE_CONFIG, + update_config) hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, test_network) hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, @@ -788,7 +826,7 @@ class ZWaveDeviceEntityValues(): if polling_intensity: self.primary.enable_poll(polling_intensity) - platform = get_platform(component, DOMAIN) + platform = get_platform(self._hass, component, DOMAIN) device = platform.get_device( node=self._node, values=self, node_config=node_config, hass=self._hass) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 8e1a22047c1..3e503e4d9a4 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -20,6 +20,7 @@ ATTR_POLL_INTENSITY = "poll_intensity" ATTR_VALUE_INDEX = "value_index" ATTR_VALUE_INSTANCE = "value_instance" NETWORK_READY_WAIT_SECS = 300 +NODE_READY_WAIT_SECS = 30 DISCOVERY_DEVICE = 'device' @@ -51,6 +52,7 @@ SERVICE_RENAME_VALUE = "rename_value" SERVICE_REFRESH_ENTITY = "refresh_entity" SERVICE_REFRESH_NODE = "refresh_node" SERVICE_RESET_NODE_METERS = "reset_node_meters" +SERVICE_UPDATE_CONFIG = "update_config" EVENT_SCENE_ACTIVATED = "zwave.scene_activated" EVENT_NODE_EVENT = "zwave.node_event" diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 5a4b1b02504..bcddcb0b800 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -81,6 +81,7 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self._name = node_name(self.node) self._product_name = node.product_name self._manufacturer_name = node.manufacturer_name + self._unique_id = self._compute_unique_id() self._attributes = {} self.wakeup_interval = None self.location = None @@ -95,6 +96,11 @@ class ZWaveNodeEntity(ZWaveBaseEntity): dispatcher.connect( self.network_scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT) + @property + def unique_id(self): + """Unique ID of Z-wave node.""" + return self._unique_id + def network_node_changed(self, node=None, value=None, args=None): """Handle a changed node on the network.""" if node and node.node_id != self.node_id: @@ -138,8 +144,14 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self.wakeup_interval = None self.battery_level = self.node.get_battery_level() + self._product_name = self.node.product_name + self._manufacturer_name = self.node.manufacturer_name + self._name = node_name(self.node) self._attributes = attributes + if not self._unique_id: + self._unique_id = self._compute_unique_id() + self.maybe_schedule_update() def network_node_event(self, node, value): @@ -229,3 +241,8 @@ class ZWaveNodeEntity(ZWaveBaseEntity): attrs[ATTR_WAKEUP] = self.wakeup_interval return attrs + + def _compute_unique_id(self): + if self._manufacturer_name and self._product_name: + return 'node-{}'.format(self.node_id) + return None diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 61855143d59..1762c33237d 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -119,6 +119,9 @@ set_wakeup: value: description: Value of the interval to set. (integer) +update_config: + description: Attempt to update ozw configuration files from git to support newer devices. + start_network: description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. diff --git a/homeassistant/config.py b/homeassistant/config.py index 28936ae12e9..5c432490f6a 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -12,13 +12,14 @@ from typing import Any, List, Tuple # NOQA import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant import auth from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, __version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, - CONF_WHITELIST_EXTERNAL_DIRS) + CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS) from homeassistant.core import callback, DOMAIN as CONF_CORE from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import get_component, get_platform @@ -157,6 +158,8 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ # pylint: disable=no-value-for-parameter vol.All(cv.ensure_list, [vol.IsDir()]), vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, + vol.Optional(CONF_AUTH_PROVIDERS): + vol.All(cv.ensure_list, [auth.AUTH_PROVIDER_SCHEMA]) }) @@ -394,6 +397,12 @@ async def async_process_ha_core_config(hass, config): This method is a coroutine. """ config = CORE_CONFIG_SCHEMA(config) + + # Only load auth during startup. + if not hasattr(hass, 'auth'): + hass.auth = await auth.auth_manager_from_config( + hass, config.get(CONF_AUTH_PROVIDERS, [])) + hac = hass.config def set_time_zone(time_zone_str): @@ -539,7 +548,8 @@ def _identify_config_schema(module): return '', schema -def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): +def merge_packages_config(hass, config, packages, + _log_pkg_error=_log_pkg_error): """Merge packages into the top-level configuration. Mutate config.""" # pylint: disable=too-many-nested-blocks PACKAGES_CONFIG_SCHEMA(packages) @@ -547,7 +557,7 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): for comp_name, comp_conf in pack_conf.items(): if comp_name == CONF_CORE: continue - component = get_component(comp_name) + component = get_component(hass, comp_name) if component is None: _log_pkg_error(pack_name, comp_name, config, "does not exist") @@ -616,7 +626,7 @@ def async_process_component_config(hass, config, domain): This method must be run in the event loop. """ - component = get_component(domain) + component = get_component(hass, domain) if hasattr(component, 'CONFIG_SCHEMA'): try: @@ -642,7 +652,7 @@ def async_process_component_config(hass, config, domain): platforms.append(p_validated) continue - platform = get_platform(domain, p_name) + platform = get_platform(hass, domain, p_name) if platform is None: continue @@ -677,7 +687,7 @@ async def async_check_ha_config_file(hass): from homeassistant.scripts.check_config import check_ha_config_file res = await hass.async_add_job( - check_ha_config_file, hass.config.config_dir) + check_ha_config_file, hass) if not res.errors: return None diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 46bb2f7bfe2..1350cd7d76a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -129,6 +129,7 @@ HANDLERS = Registry() FLOWS = [ 'deconz', 'hue', + 'zone', ] @@ -141,6 +142,9 @@ ENTRY_STATE_SETUP_ERROR = 'setup_error' ENTRY_STATE_NOT_LOADED = 'not_loaded' ENTRY_STATE_FAILED_UNLOAD = 'failed_unload' +DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery' +DISCOVERY_SOURCES = (data_entry_flow.SOURCE_DISCOVERY,) + class ConfigEntry: """Hold a configuration entry.""" @@ -256,7 +260,7 @@ class ConfigEntries: """Initialize the entry manager.""" self.hass = hass self.flow = data_entry_flow.FlowManager( - hass, self._async_create_flow, self._async_save_entry) + hass, self._async_create_flow, self._async_finish_flow) self._hass_config = hass_config self._entries = None self._sched_save = None @@ -341,8 +345,8 @@ class ConfigEntries: return await entry.async_unload( self.hass, component=getattr(self.hass.components, component)) - async def _async_save_entry(self, result): - """Add an entry.""" + async def _async_finish_flow(self, result): + """Finish a config flow and add an entry.""" entry = ConfigEntry( version=result['version'], domain=result['handler'], @@ -362,9 +366,19 @@ class ConfigEntries: await async_setup_component( self.hass, entry.domain, self._hass_config) + # Return Entry if they not from a discovery request + if result['source'] not in DISCOVERY_SOURCES: + return entry + + # If no discovery config entries in progress, remove notification. + if not any(ent['source'] in DISCOVERY_SOURCES for ent + in self.hass.config_entries.flow.async_progress()): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID) + return entry - async def _async_create_flow(self, handler): + async def _async_create_flow(self, handler, *, source, data): """Create a flow for specified handler. Handler key is the domain of the component that we want to setup. @@ -379,6 +393,15 @@ class ConfigEntries: await async_process_deps_reqs( self.hass, self._hass_config, handler, component) + # Create notification. + if source in DISCOVERY_SOURCES: + self.hass.components.persistent_notification.async_create( + title='New devices discovered', + message=("We have discovered new devices on your network. " + "[Check it out](/config/integrations)"), + notification_id=DISCOVERY_NOTIFICATION_ID + ) + return handler() @callback diff --git a/homeassistant/const.py b/homeassistant/const.py index 4014a719912..0f319891649 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 = 68 -PATCH_VERSION = '1' +MINOR_VERSION = 69 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) @@ -30,6 +30,7 @@ CONF_API_KEY = 'api_key' CONF_API_VERSION = 'api_version' CONF_AT = 'at' CONF_AUTHENTICATION = 'authentication' +CONF_AUTH_PROVIDERS = 'auth_providers' CONF_BASE = 'base' CONF_BEFORE = 'before' CONF_BELOW = 'below' @@ -165,6 +166,12 @@ EVENT_SERVICE_REMOVED = 'service_removed' EVENT_LOGBOOK_ENTRY = 'logbook_entry' EVENT_THEMES_UPDATED = 'themes_updated' +# #### DEVICE CLASSES #### +DEVICE_CLASS_BATTERY = 'battery' +DEVICE_CLASS_HUMIDITY = 'humidity' +DEVICE_CLASS_ILLUMINANCE = 'illuminance' +DEVICE_CLASS_TEMPERATURE = 'temperature' + # #### STATES #### STATE_ON = 'on' STATE_OFF = 'off' diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index cadec3f3d69..e9580aba273 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -52,7 +52,7 @@ class FlowManager: async def async_init(self, handler, *, source=SOURCE_USER, data=None): """Start a configuration flow.""" - flow = await self._async_create_flow(handler) + flow = await self._async_create_flow(handler, source=source, data=data) flow.hass = self.hass flow.handler = handler flow.flow_id = uuid.uuid4().hex @@ -67,7 +67,7 @@ class FlowManager: return await self._async_handle_step(flow, step, data) async def async_configure(self, flow_id, user_input=None): - """Start or continue a configuration flow.""" + """Continue a configuration flow.""" flow = self._progress.get(flow_id) if flow is None: diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index f8f841cc449..cb577e8a9c7 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -393,8 +393,8 @@ def zone(hass, zone_ent, entity): if latitude is None or longitude is None: return False - return zone_cmp.in_zone(zone_ent, latitude, longitude, - entity.attributes.get(ATTR_GPS_ACCURACY, 0)) + return zone_cmp.zone.in_zone(zone_ent, latitude, longitude, + entity.attributes.get(ATTR_GPS_ACCURACY, 0)) def zone_from_config(config, config_validation=True): diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4b7c58f6e66..0bd490940a9 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -12,7 +12,6 @@ from typing import Any, Union, TypeVar, Callable, Sequence, Dict import voluptuous as vol -from homeassistant.loader import get_platform from homeassistant.const import ( CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, @@ -97,6 +96,36 @@ def isdevice(value): raise vol.Invalid('No device at {} found'.format(value)) +def matches_regex(regex): + """Validate that the value is a string that matches a regex.""" + regex = re.compile(regex) + + def validator(value: Any) -> str: + """Validate that value matches the given regex.""" + if not isinstance(value, str): + raise vol.Invalid('not a string value: {}'.format(value)) + + if not regex.match(value): + raise vol.Invalid('value {} does not match regular expression {}' + .format(regex.pattern, value)) + + return value + return validator + + +def is_regex(value): + """Validate that a string is a valid regular expression.""" + try: + r = re.compile(value) + return r + except TypeError: + raise vol.Invalid("value {} is of the wrong type for a regular " + "expression".format(value)) + except re.error: + raise vol.Invalid("value {} is not a valid regular expression".format( + value)) + + def isfile(value: Any) -> str: """Validate that the value is an existing file.""" if value is None: @@ -283,19 +312,6 @@ def match_all(value): return value -def platform_validator(domain): - """Validate if platform exists for given domain.""" - def validator(value): - """Test if platform exists.""" - if value is None: - raise vol.Invalid('platform cannot be None') - if get_platform(domain, str(value)): - return value - raise vol.Invalid( - 'platform {} does not exist for {}'.format(value, domain)) - return validator - - def positive_timedelta(value: timedelta) -> timedelta: """Validate timedelta is positive.""" if value < timedelta(0): diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index a8aca2fd2e9..913e90a859d 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -7,40 +7,40 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -def _prepare_json(result): - """Convert result for JSON.""" - if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - data = result.copy() - data.pop('result') - data.pop('data') - return data - - elif result['type'] != data_entry_flow.RESULT_TYPE_FORM: - return result - - import voluptuous_serialize - - data = result.copy() - - schema = data['data_schema'] - if schema is None: - data['data_schema'] = [] - else: - data['data_schema'] = voluptuous_serialize.convert(schema) - - return data - - -class FlowManagerIndexView(HomeAssistantView): - """View to create config flows.""" +class _BaseFlowManagerView(HomeAssistantView): + """Foundation for flow manager views.""" def __init__(self, flow_mgr): """Initialize the flow manager index view.""" self._flow_mgr = flow_mgr - async def get(self, request): - """List flows that are in progress.""" - return self.json(self._flow_mgr.async_progress()) + # pylint: disable=no-self-use + def _prepare_result_json(self, result): + """Convert result to JSON.""" + if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + data = result.copy() + data.pop('result') + data.pop('data') + return data + + elif result['type'] != data_entry_flow.RESULT_TYPE_FORM: + return result + + import voluptuous_serialize + + data = result.copy() + + schema = data['data_schema'] + if schema is None: + data['data_schema'] = [] + else: + data['data_schema'] = voluptuous_serialize.convert(schema) + + return data + + +class FlowManagerIndexView(_BaseFlowManagerView): + """View to create config flows.""" @RequestDataValidator(vol.Schema({ vol.Required('handler'): vol.Any(str, list), @@ -59,18 +59,14 @@ class FlowManagerIndexView(HomeAssistantView): except data_entry_flow.UnknownStep: return self.json_message('Handler does not support init', 400) - result = _prepare_json(result) + result = self._prepare_result_json(result) return self.json(result) -class FlowManagerResourceView(HomeAssistantView): +class FlowManagerResourceView(_BaseFlowManagerView): """View to interact with the flow manager.""" - def __init__(self, flow_mgr): - """Initialize the flow manager resource view.""" - self._flow_mgr = flow_mgr - async def get(self, request, flow_id): """Get the current state of a data_entry_flow.""" try: @@ -78,7 +74,7 @@ class FlowManagerResourceView(HomeAssistantView): except data_entry_flow.UnknownFlow: return self.json_message('Invalid flow specified', 404) - result = _prepare_json(result) + result = self._prepare_result_json(result) return self.json(result) @@ -92,7 +88,7 @@ class FlowManagerResourceView(HomeAssistantView): except vol.Invalid: return self.json_message('User input malformed', 400) - result = _prepare_json(result) + result = self._prepare_result_json(result) return self.json(result) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 3595b258f12..9114a4db941 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -92,7 +92,7 @@ def extract_entity_ids(hass, service_call, expand_group=True): if not (service_call.data and ATTR_ENTITY_ID in service_call.data): return [] - group = get_component('group') + group = hass.components.group # Entity ID attr can be a list or a string service_ent_id = service_call.data[ATTR_ENTITY_ID] @@ -100,10 +100,10 @@ def extract_entity_ids(hass, service_call, expand_group=True): if expand_group: if isinstance(service_ent_id, str): - return group.expand_entity_ids(hass, [service_ent_id]) + return group.expand_entity_ids([service_ent_id]) return [ent_id for ent_id in - group.expand_entity_ids(hass, service_ent_id)] + group.expand_entity_ids(service_ent_id)] else: @@ -128,7 +128,7 @@ async def async_get_all_descriptions(hass): import homeassistant.components as components component_path = path.dirname(components.__file__) else: - component_path = path.dirname(get_component(domain).__file__) + component_path = path.dirname(get_component(hass, domain).__file__) return path.join(component_path, 'services.yaml') def load_services_files(yaml_files): diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3a24de6b39c..f523726c388 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.core import State, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper -from homeassistant.loader import bind_hass, get_component +from homeassistant.loader import bind_hass from homeassistant.util import convert from homeassistant.util import dt as dt_util from homeassistant.util import location as loc_util @@ -349,10 +349,10 @@ class TemplateMethods(object): else: gr_entity_id = str(entities) - group = get_component('group') + group = self._hass.components.group states = [self._hass.states.get(entity_id) for entity_id - in group.expand_entity_ids(self._hass, [gr_entity_id])] + in group.expand_entity_ids([gr_entity_id])] return _wrap_state(loc_helper.closest(latitude, longitude, states)) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 26cb34ede8c..f1335f73346 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -30,14 +30,14 @@ def flatten(data): return recursive_flatten('', data) -def component_translation_file(component, language): +def component_translation_file(hass, component, language): """Return the translation json file location for a component.""" if '.' in component: name = component.split('.', 1)[1] else: name = component - module = get_component(component) + module = get_component(hass, component) component_path = path.dirname(module.__file__) # If loading translations for the package root, (__init__.py), the @@ -97,7 +97,7 @@ async def async_get_component_resources(hass, language): missing_files = {} for component in missing_components: missing_files[component] = component_translation_file( - component, language) + hass, component, language) # Load missing files if missing_files: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index a3ce2a13f56..67647a323c9 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -6,15 +6,13 @@ documentation as possible to keep it understandable. Components can be accessed via hass.components.switch from your code. If you want to retrieve a platform that is part of a component, you should -call get_component('switch.your_platform'). In both cases the config directory -is checked to see if it contains a user provided version. If not available it -will check the built-in components and platforms. +call get_component(hass, 'switch.your_platform'). In both cases the config +directory is checked to see if it contains a user provided version. If not +available it will check the built-in components and platforms. """ import functools as ft import importlib import logging -import os -import pkgutil import sys from types import ModuleType @@ -33,111 +31,57 @@ PREPARED = False DEPENDENCY_BLACKLIST = set(('config',)) -# List of available components -AVAILABLE_COMPONENTS = [] # type: List[str] - -# Dict of loaded components mapped name => module -_COMPONENT_CACHE = {} # type: Dict[str, ModuleType] - _LOGGER = logging.getLogger(__name__) -def prepare(hass: 'HomeAssistant'): - """Prepare the loading of components. - - This method needs to run in an executor. - """ - global PREPARED # pylint: disable=global-statement - - # Load the built-in components - import homeassistant.components as components - - AVAILABLE_COMPONENTS.clear() - - AVAILABLE_COMPONENTS.extend( - item[1] for item in - pkgutil.iter_modules(components.__path__, 'homeassistant.components.')) - - # Look for available custom components - custom_path = hass.config.path("custom_components") - - if os.path.isdir(custom_path): - # Ensure we can load custom components using Pythons import - sys.path.insert(0, hass.config.config_dir) - - # We cannot use the same approach as for built-in components because - # custom components might only contain a platform for a component. - # ie custom_components/switch/some_platform.py. Using pkgutil would - # not give us the switch component (and neither should it). - - # Assumption: the custom_components dir only contains directories or - # python components. If this assumption is not true, HA won't break, - # just might output more errors. - for fil in os.listdir(custom_path): - if fil == '__pycache__': - continue - elif os.path.isdir(os.path.join(custom_path, fil)): - AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil)) - else: - # For files we will strip out .py extension - AVAILABLE_COMPONENTS.append( - 'custom_components.{}'.format(fil[0:-3])) - - PREPARED = True +DATA_KEY = 'components' +PATH_CUSTOM_COMPONENTS = 'custom_components' +PACKAGE_COMPONENTS = 'homeassistant.components' -def set_component(comp_name: str, component: ModuleType) -> None: +def set_component(hass, comp_name: str, component: ModuleType) -> None: """Set a component in the cache. Async friendly. """ - _check_prepared() - - _COMPONENT_CACHE[comp_name] = component + cache = hass.data.get(DATA_KEY) + if cache is None: + cache = hass.data[DATA_KEY] = {} + cache[comp_name] = component -def get_platform(domain: str, platform: str) -> Optional[ModuleType]: +def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]: """Try to load specified platform. Async friendly. """ - return get_component(PLATFORM_FORMAT.format(domain, platform)) + return get_component(hass, PLATFORM_FORMAT.format(domain, platform)) -def get_component(comp_name) -> Optional[ModuleType]: +def get_component(hass, comp_or_platform) -> Optional[ModuleType]: """Try to load specified component. Looks in config dir first, then built-in components. Only returns it if also found to be valid. - Async friendly. """ - if comp_name in _COMPONENT_CACHE: - return _COMPONENT_CACHE[comp_name] + try: + return hass.data[DATA_KEY][comp_or_platform] + except KeyError: + pass - _check_prepared() - - # If we ie. try to load custom_components.switch.wemo but the parent - # custom_components.switch does not exist, importing it will trigger - # an exception because it will try to import the parent. - # Because of this behavior, we will approach loading sub components - # with caution: only load it if we can verify that the parent exists. - # We do not want to silent the ImportErrors as they provide valuable - # information to track down when debugging Home Assistant. + cache = hass.data.get(DATA_KEY) + if cache is None: + # Only insert if it's not there (happens during tests) + if sys.path[0] != hass.config.config_dir: + sys.path.insert(0, hass.config.config_dir) + cache = hass.data[DATA_KEY] = {} # First check custom, then built-in - potential_paths = ['custom_components.{}'.format(comp_name), - 'homeassistant.components.{}'.format(comp_name)] + potential_paths = ['custom_components.{}'.format(comp_or_platform), + 'homeassistant.components.{}'.format(comp_or_platform)] for path in potential_paths: - # Validate here that root component exists - # If path contains a '.' we are specifying a sub-component - # Using rsplit we get the parent component from sub-component - root_comp = path.rsplit(".", 1)[0] if '.' in comp_name else path - - if root_comp not in AVAILABLE_COMPONENTS: - continue - try: module = importlib.import_module(path) @@ -152,21 +96,30 @@ def get_component(comp_name) -> Optional[ModuleType]: if module.__spec__.origin == 'namespace': continue - _LOGGER.info("Loaded %s from %s", comp_name, path) + _LOGGER.info("Loaded %s from %s", comp_or_platform, path) - _COMPONENT_CACHE[comp_name] = module + cache[comp_or_platform] = module return module except ImportError as err: # This error happens if for example custom_components/switch # exists and we try to load switch.demo. - if str(err) != "No module named '{}'".format(path): + # Ignore errors for custom_components, custom_components.switch + # and custom_components.switch.demo. + white_listed_errors = [] + parts = [] + for part in path.split('.'): + parts.append(part) + white_listed_errors.append( + "No module named '{}'".format('.'.join(parts))) + + if str(err) not in white_listed_errors: _LOGGER.exception( ("Error loading %s. Make sure all " "dependencies are installed"), path) - _LOGGER.error("Unable to find component %s", comp_name) + _LOGGER.error("Unable to find component %s", comp_or_platform) return None @@ -180,7 +133,7 @@ class Components: def __getattr__(self, comp_name): """Fetch a component.""" - component = get_component(comp_name) + component = get_component(self._hass, comp_name) if component is None: raise ImportError('Unable to load {}'.format(comp_name)) wrapped = ModuleWrapper(self._hass, component) @@ -230,7 +183,7 @@ def bind_hass(func): return func -def load_order_component(comp_name: str) -> OrderedSet: +def load_order_component(hass, comp_name: str) -> OrderedSet: """Return an OrderedSet of components in the correct order of loading. Raises HomeAssistantError if a circular dependency is detected. @@ -238,16 +191,16 @@ def load_order_component(comp_name: str) -> OrderedSet: Async friendly. """ - return _load_order_component(comp_name, OrderedSet(), set()) + return _load_order_component(hass, comp_name, OrderedSet(), set()) -def _load_order_component(comp_name: str, load_order: OrderedSet, +def _load_order_component(hass, comp_name: str, load_order: OrderedSet, loading: Set) -> OrderedSet: """Recursive function to get load order of components. Async friendly. """ - component = get_component(comp_name) + component = get_component(hass, comp_name) # If None it does not exist, error already thrown by get_component. if component is None: @@ -266,7 +219,8 @@ def _load_order_component(comp_name: str, load_order: OrderedSet, comp_name, dependency) return OrderedSet() - dep_load_order = _load_order_component(dependency, load_order, loading) + dep_load_order = _load_order_component( + hass, dependency, load_order, loading) # length == 0 means error loading dependency or children if not dep_load_order: @@ -280,14 +234,3 @@ def _load_order_component(comp_name: str, load_order: OrderedSet, loading.remove(comp_name) return load_order - - -def _check_prepared() -> None: - """Issue a warning if loader.prepare() has never been called. - - Async friendly. - """ - if not PREPARED: - _LOGGER.warning(( - "You did not call loader.prepare() yet. " - "Certain functionality might not be working")) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6de885942fb..f6666c829e0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,9 +7,9 @@ voluptuous==0.11.1 typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 -astral==1.6 +astral==1.6.1 certifi>=2017.4.17 -attrs==17.4.0 +attrs==18.1.0 # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 8c78602f3d0..3a1ffa82d47 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -16,12 +16,12 @@ from homeassistant import bootstrap, core, loader from homeassistant.config import ( get_default_config_dir, CONF_CORE, CORE_CONFIG_SCHEMA, CONF_PACKAGES, merge_packages_config, _format_config_error, - find_config_file, load_yaml_config_file, get_component, - extract_domain_configs, config_per_platform, get_platform) + find_config_file, load_yaml_config_file, + extract_domain_configs, config_per_platform) import homeassistant.util.yaml as yaml from homeassistant.exceptions import HomeAssistantError -REQUIREMENTS = ('colorlog==3.1.2',) +REQUIREMENTS = ('colorlog==3.1.4',) if system() == 'Windows': # Ensure colorama installed for colorlog on Windows REQUIREMENTS += ('colorama<=1',) @@ -58,7 +58,7 @@ def color(the_color, *args, reset=None): def run(script_args: List) -> int: """Handle ensure config commandline script.""" parser = argparse.ArgumentParser( - description=("Check Home Assistant configuration.")) + description="Check Home Assistant configuration.") parser.add_argument( '--script', choices=['check_config']) parser.add_argument( @@ -201,18 +201,10 @@ def check(config_dir, secrets=False): yaml.yaml.SafeLoader.add_constructor('!secret', yaml._secret_yaml) try: - class HassConfig(): - """Hass object with config.""" - - def __init__(self, conf_dir): - """Init the config_dir.""" - self.config = core.Config() - self.config.config_dir = conf_dir - - loader.prepare(HassConfig(config_dir)) - - res['components'] = check_ha_config_file(config_dir) + hass = core.HomeAssistant() + hass.config.config_dir = config_dir + res['components'] = check_ha_config_file(hass) res['secret_cache'] = OrderedDict(yaml.__SECRET_CACHE) for err in res['components'].errors: @@ -222,6 +214,7 @@ def check(config_dir, secrets=False): res['except'].setdefault(domain, []).append(err.config) except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("BURB") print(color('red', 'Fatal error while loading config:'), str(err)) res['except'].setdefault(ERROR_STR, []).append(str(err)) finally: @@ -290,8 +283,9 @@ class HomeAssistantConfig(OrderedDict): return self -def check_ha_config_file(config_dir): +def check_ha_config_file(hass): """Check if Home Assistant configuration file is valid.""" + config_dir = hass.config.config_dir result = HomeAssistantConfig() def _pack_error(package, component, config, message): @@ -330,7 +324,7 @@ def check_ha_config_file(config_dir): # Merge packages merge_packages_config( - config, core_config.get(CONF_PACKAGES, {}), _pack_error) + hass, config, core_config.get(CONF_PACKAGES, {}), _pack_error) del core_config[CONF_PACKAGES] # Ensure we have no None values after merge @@ -343,7 +337,7 @@ def check_ha_config_file(config_dir): # Process and validate config for domain in components: - component = get_component(domain) + component = loader.get_component(hass, domain) if not component: result.add_error("Component not found: {}".format(domain)) continue @@ -375,7 +369,7 @@ def check_ha_config_file(config_dir): platforms.append(p_validated) continue - platform = get_platform(domain, p_name) + platform = loader.get_platform(hass, domain, p_name) if platform is None: result.add_error( diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 169a160af65..f26aa9b61f1 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -98,14 +98,14 @@ async def _async_setup_component(hass: core.HomeAssistant, _LOGGER.error("Setup failed for %s: %s", domain, msg) async_notify_setup_error(hass, domain, link) - component = loader.get_component(domain) + component = loader.get_component(hass, domain) if not component: log_error("Component not found.", False) return False # Validate no circular dependencies - components = loader.load_order_component(domain) + components = loader.load_order_component(hass, domain) # OrderedSet is empty if component or dependencies could not be resolved if not components: @@ -159,7 +159,7 @@ async def _async_setup_component(hass: core.HomeAssistant, elif result is not True: log_error("Component did not return boolean if setup was successful. " "Disabling component.") - loader.set_component(domain, None) + loader.set_component(hass, domain, None) return False for entry in hass.config_entries.async_entries(domain): @@ -193,7 +193,7 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, config, platform_path, msg) async_notify_setup_error(hass, platform_path) - platform = loader.get_platform(domain, platform_name) + platform = loader.get_platform(hass, domain, platform_name) # Not found if platform is None: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index f7306cae98b..10b43445184 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -49,17 +49,16 @@ class AsyncHandler(object): """Wrap close to handler.""" self.emit(None) - @asyncio.coroutine - def async_close(self, blocking=False): + async def async_close(self, blocking=False): """Close the handler. When blocking=True, will wait till closed. """ - yield from self._queue.put(None) + await self._queue.put(None) if blocking: while self._thread.is_alive(): - yield from asyncio.sleep(0, loop=self.loop) + await asyncio.sleep(0, loop=self.loop) def emit(self, record): """Process a record.""" diff --git a/pylintrc b/pylintrc index 85a44782af1..df839b379b5 100644 --- a/pylintrc +++ b/pylintrc @@ -41,3 +41,7 @@ disable= [EXCEPTIONS] overgeneral-exceptions=Exception,HomeAssistantError + +# For attrs +[typecheck] +ignored-classes=_CountingAttr diff --git a/requirements_all.txt b/requirements_all.txt index ff6e680051d..0887ff98996 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,9 +8,9 @@ voluptuous==0.11.1 typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 -astral==1.6 +astral==1.6.1 certifi>=2017.4.17 -attrs==17.4.0 +attrs==18.1.0 # homeassistant.components.nuimo_controller --only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 @@ -28,7 +28,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==1.1.9 +HAP-python==2.0.0 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 @@ -202,7 +202,7 @@ coinbase==2.1.0 coinmarketcap==4.2.1 # homeassistant.scripts.check_config -colorlog==3.1.2 +colorlog==3.1.4 # homeassistant.components.alarm_control_panel.concord232 # homeassistant.components.binary_sensor.concord232 @@ -243,7 +243,7 @@ defusedxml==0.5.0 # homeassistant.components.sensor.deluge # homeassistant.components.switch.deluge -deluge-client==1.0.5 +deluge-client==1.4.0 # homeassistant.components.media_player.denonavr denonavr==0.6.1 @@ -276,6 +276,9 @@ dsmr_parser==0.11 # homeassistant.components.sensor.dweet dweepy==0.3.0 +# homeassistant.components.sensor.eliqonline +eliqonline==1.0.14 + # homeassistant.components.enocean enocean==0.40 @@ -368,7 +371,7 @@ ha-philipsjs==0.0.3 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.climate.heatmiser heatmiserV3==0.9.1 @@ -380,16 +383,16 @@ hikvision==0.4 hipnotify==1.0.8 # homeassistant.components.binary_sensor.workday -holidays==0.9.4 +holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180426.0 +home-assistant-frontend==20180509.0 # homeassistant.components.homekit_controller # homekit==0.6 # homeassistant.components.homematicip_cloud -homematicip==0.8 +homematicip==0.9.2.4 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a @@ -443,7 +446,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.6 +insteonplm==0.9.1 # homeassistant.components.verisure jsonpath==0.75 @@ -500,7 +503,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==1.2.1 +locationsharinglib==1.2.2 # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 @@ -508,8 +511,8 @@ luftdaten==0.1.3 # homeassistant.components.sensor.lyft lyft_rides==0.2 -# homeassistant.components.notify.matrix -matrix-client==0.0.6 +# homeassistant.components.matrix +matrix-client==0.2.0 # homeassistant.components.maxcube maxcube-api==0.1.0 @@ -524,6 +527,9 @@ mficlient==0.3.0 # homeassistant.components.sensor.miflora miflora==0.4.0 +# homeassistant.components.sensor.mitemp_bt +mitemp_bt==0.0.1 + # homeassistant.components.sensor.mopar motorparts==1.0.2 @@ -547,7 +553,7 @@ nad_receiver==0.0.9 nanoleaf==0.4.1 # homeassistant.components.discovery -netdisco==1.3.1 +netdisco==1.4.1 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 @@ -560,7 +566,7 @@ nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.2 +numpy==1.14.3 # homeassistant.components.google oauth2client==4.0.0 @@ -629,6 +635,9 @@ pmsensor==0.4 # homeassistant.components.sensor.pocketcasts pocketcasts==0.1 +# homeassistant.components.sensor.postnl +postnl_api==1.0.1 + # homeassistant.components.climate.proliphix proliphix==0.4.1 @@ -636,7 +645,7 @@ proliphix==0.4.1 prometheus_client==0.1.0 # homeassistant.components.sensor.systemmonitor -psutil==5.4.3 +psutil==5.4.5 # homeassistant.components.wink pubnubsub-handler==1.0.2 @@ -674,7 +683,7 @@ pyCEC==0.4.13 pyHS100==0.3.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.22.0 +pyRFXtrx==0.22.1 # homeassistant.components.sensor.tibber pyTibber==0.4.1 @@ -689,7 +698,7 @@ pyads==2.2.6 pyairvisual==1.0.0 # homeassistant.components.alarm_control_panel.alarmdotcom -pyalarmdotcom==0.3.1 +pyalarmdotcom==0.3.2 # homeassistant.components.arlo pyarlo==0.1.2 @@ -736,7 +745,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==36 +pydeconz==37 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -751,7 +760,7 @@ pyeconet==0.0.5 pyedimax==0.1 # homeassistant.components.eight_sleep -pyeight==0.0.7 +pyeight==0.0.8 # homeassistant.components.media_player.emby pyemby==1.5 @@ -775,7 +784,7 @@ pyfritzhome==0.3.7 pyfttt==0.3 # homeassistant.components.cover.gogogate2 -pygogogate2==0.0.3 +pygogogate2==0.0.7 # homeassistant.components.remote.harmony pyharmony==1.0.20 @@ -787,7 +796,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.41 +pyhomematic==0.1.42 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 @@ -817,7 +826,7 @@ pykwb==0.0.8 pylacrosse==0.3.1 # homeassistant.components.sensor.lastfm -pylast==2.1.0 +pylast==2.2.0 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv @@ -866,7 +875,7 @@ pymysensors==0.11.1 pynello==1.5.1 # homeassistant.components.device_tracker.netgear -pynetgear==0.3.3 +pynetgear==0.4.0 # homeassistant.components.switch.netio pynetio==0.1.6 @@ -1021,7 +1030,7 @@ python-synology==0.1.0 python-tado==0.2.3 # homeassistant.components.telegram_bot -python-telegram-bot==10.0.1 +python-telegram-bot==10.0.2 # homeassistant.components.sensor.twitch python-twitch==1.3.0 @@ -1063,7 +1072,7 @@ pytradfri[async]==5.4.2 pyunifi==2.13 # homeassistant.components.upnp -pyupnp-async==0.1.0.1 +pyupnp-async==0.1.0.2 # homeassistant.components.keyboard # pyuserinput==0.1.11 @@ -1075,7 +1084,7 @@ pyvera==0.2.42 pyvesync==0.1.1 # homeassistant.components.media_player.vizio -pyvizio==0.0.2 +pyvizio==0.0.3 # homeassistant.components.velux pyvlx==0.1.3 @@ -1093,7 +1102,7 @@ pyxeoma==1.4.0 pyzabbix==0.7.4 # homeassistant.components.sensor.qnap -qnapstats==0.2.5 +qnapstats==0.2.6 # homeassistant.components.switch.rachio rachiopy==0.1.2 @@ -1107,11 +1116,11 @@ raincloudy==0.0.4 # homeassistant.components.raspihats # raspihats==2.2.3 -# homeassistant.components.switch.rainmachine +# homeassistant.components.rainmachine regenmaschine==0.4.1 # homeassistant.components.python_script -restrictedpython==4.0b2 +restrictedpython==4.0b3 # homeassistant.components.rflink rflink==0.0.37 @@ -1197,6 +1206,9 @@ smappy==0.2.15 # homeassistant.components.media_player.snapcast snapcast==2.0.8 +# homeassistant.components.sensor.socialblade +socialbladeclient==0.2 + # homeassistant.components.climate.honeywell somecomfort==0.5.2 @@ -1209,7 +1221,7 @@ spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.6 +sqlalchemy==1.2.7 # homeassistant.components.statsd statsd==3.2.1 @@ -1227,7 +1239,7 @@ tahoma-api==0.0.13 tank_utility==1.4.0 # homeassistant.components.binary_sensor.tapsaff -tapsaff==0.1.3 +tapsaff==0.2.0 # homeassistant.components.tellstick tellcore-net==0.4 @@ -1355,7 +1367,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.04.16 +youtube_dl==2018.04.25 # homeassistant.components.light.zengge zengge==0.2 diff --git a/requirements_test.txt b/requirements_test.txt index 38b716406fd..6d5f68615be 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.580 +mypy==0.590 pydocstyle==1.1.1 pylint==1.8.3 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 876aba4574d..a25f36a8195 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.580 +mypy==0.590 pydocstyle==1.1.1 pylint==1.8.3 pytest-aiohttp==0.3.0 @@ -19,7 +19,7 @@ requests_mock==1.4 # homeassistant.components.homekit -HAP-python==1.1.9 +HAP-python==2.0.0 # homeassistant.components.notify.html5 PyJWT==1.6.0 @@ -75,13 +75,13 @@ ha-ffmpeg==1.9 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.binary_sensor.workday -holidays==0.9.4 +holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180426.0 +home-assistant-frontend==20180509.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb @@ -99,7 +99,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.2 +numpy==1.14.3 # homeassistant.components.mqtt # homeassistant.components.shiftr @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==36 +pydeconz==37 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -162,13 +162,13 @@ pythonwhois==2.4.3 pyunifi==2.13 # homeassistant.components.upnp -pyupnp-async==0.1.0.1 +pyupnp-async==0.1.0.2 # homeassistant.components.notify.html5 pywebpush==1.6.0 # homeassistant.components.python_script -restrictedpython==4.0b2 +restrictedpython==4.0b3 # homeassistant.components.rflink rflink==0.0.37 @@ -188,7 +188,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.6 +sqlalchemy==1.2.7 # homeassistant.components.statsd statsd==3.2.1 diff --git a/setup.py b/setup.py index 8815b0227ad..8a68617afd9 100755 --- a/setup.py +++ b/setup.py @@ -51,9 +51,9 @@ REQUIRES = [ 'typing>=3,<4', 'aiohttp==3.1.3', 'async_timeout==2.0.1', - 'astral==1.6', + 'astral==1.6.1', 'certifi>=2017.4.17', - 'attrs==17.4.0', + 'attrs==18.1.0', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) diff --git a/tests/auth_providers/__init__.py b/tests/auth_providers/__init__.py new file mode 100644 index 00000000000..dd1b58639b1 --- /dev/null +++ b/tests/auth_providers/__init__.py @@ -0,0 +1 @@ +"""Tests for the auth providers.""" diff --git a/tests/auth_providers/test_insecure_example.py b/tests/auth_providers/test_insecure_example.py new file mode 100644 index 00000000000..92fc2974e27 --- /dev/null +++ b/tests/auth_providers/test_insecure_example.py @@ -0,0 +1,89 @@ +"""Tests for the insecure example auth provider.""" +from unittest.mock import Mock +import uuid + +import pytest + +from homeassistant import auth +from homeassistant.auth_providers import insecure_example + +from tests.common import mock_coro + + +@pytest.fixture +def store(): + """Mock store.""" + return auth.AuthStore(Mock()) + + +@pytest.fixture +def provider(store): + """Mock provider.""" + return insecure_example.ExampleAuthProvider(store, { + 'type': 'insecure_example', + 'users': [ + { + 'username': 'user-test', + 'password': 'password-test', + }, + { + 'username': '🎉', + 'password': '😎', + } + ] + }) + + +async def test_create_new_credential(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + 'username': 'user-test', + 'password': 'password-test', + }) + assert credentials.is_new is True + + +async def test_match_existing_credentials(store, provider): + """See if we match existing users.""" + existing = auth.Credentials( + id=uuid.uuid4(), + auth_provider_type='insecure_example', + auth_provider_id=None, + data={ + 'username': 'user-test' + }, + is_new=False, + ) + store.credentials_for_provider = Mock(return_value=mock_coro([existing])) + credentials = await provider.async_get_or_create_credentials({ + 'username': 'user-test', + 'password': 'password-test', + }) + assert credentials is existing + + +async def test_verify_username(provider): + """Test we raise if incorrect user specified.""" + with pytest.raises(auth.InvalidUser): + await provider.async_get_or_create_credentials({ + 'username': 'non-existing-user', + 'password': 'password-test', + }) + + +async def test_verify_password(provider): + """Test we raise if incorrect user specified.""" + with pytest.raises(auth.InvalidPassword): + await provider.async_get_or_create_credentials({ + 'username': 'user-test', + 'password': 'incorrect-password', + }) + + +async def test_utf_8_username_password(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + 'username': '🎉', + 'password': '😎', + }) + assert credentials.is_new is True diff --git a/tests/common.py b/tests/common.py index 67fd8bab23f..f53d1c2be2b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -10,7 +10,7 @@ import logging import threading from contextlib import contextmanager -from homeassistant import core as ha, loader, data_entry_flow, config_entries +from homeassistant import auth, core as ha, data_entry_flow, config_entries from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( @@ -113,6 +113,9 @@ def async_test_home_assistant(loop): hass.config_entries = config_entries.ConfigEntries(hass, {}) hass.config_entries._entries = [] hass.config.async_load = Mock() + store = auth.AuthStore(hass) + hass.auth = auth.AuthManager(hass, store, {}) + ensure_auth_manager_loaded(hass.auth) INSTANCES.append(hass) orig_async_add_job = hass.async_add_job @@ -134,9 +137,6 @@ def async_test_home_assistant(loop): hass.config.units = METRIC_SYSTEM hass.config.skip_pip = True - if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: - yield from loop.run_in_executor(None, loader.prepare, hass) - hass.state = ha.CoreState.running # Mock async_start @@ -303,6 +303,34 @@ def mock_registry(hass, mock_entries=None): return registry +class MockUser(auth.User): + """Mock a user in Home Assistant.""" + + def __init__(self, id='mock-id', is_owner=True, is_active=True, + name='Mock User'): + """Initialize mock user.""" + super().__init__(id, is_owner, is_active, name) + + def add_to_hass(self, hass): + """Test helper to add entry to hass.""" + return self.add_to_auth_manager(hass.auth) + + def add_to_auth_manager(self, auth_mgr): + """Test helper to add entry to hass.""" + auth_mgr._store.users[self.id] = self + return self + + +@ha.callback +def ensure_auth_manager_loaded(auth_mgr): + """Ensure an auth manager is considered loaded.""" + store = auth_mgr._store + if store.clients is None: + store.clients = {} + if store.users is None: + store.users = {} + + class MockModule(object): """Representation of a fake module.""" diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py new file mode 100644 index 00000000000..3e5a59e8386 --- /dev/null +++ b/tests/components/auth/__init__.py @@ -0,0 +1,38 @@ +"""Tests for the auth component.""" +from aiohttp.helpers import BasicAuth + +from homeassistant import auth +from homeassistant.setup import async_setup_component + +from tests.common import ensure_auth_manager_loaded + + +BASE_CONFIG = [{ + 'name': 'Example', + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] +}] +CLIENT_ID = 'test-id' +CLIENT_SECRET = 'test-secret' +CLIENT_AUTH = BasicAuth(CLIENT_ID, CLIENT_SECRET) + + +async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, + setup_api=False): + """Helper to setup authentication and create a HTTP client.""" + hass.auth = await auth.auth_manager_from_config(hass, provider_configs) + ensure_auth_manager_loaded(hass.auth) + await async_setup_component(hass, 'auth', { + 'http': { + 'api_password': 'bla' + } + }) + client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET) + hass.auth._store.clients[client.id] = client + if setup_api: + await async_setup_component(hass, 'api', {}) + return await aiohttp_client(hass.http.app) diff --git a/tests/components/auth/test_client.py b/tests/components/auth/test_client.py new file mode 100644 index 00000000000..2995a6ac81a --- /dev/null +++ b/tests/components/auth/test_client.py @@ -0,0 +1,70 @@ +"""Tests for the client validator.""" +from aiohttp.helpers import BasicAuth +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components.auth.client import verify_client +from homeassistant.components.http.view import HomeAssistantView + +from . import async_setup_auth + + +@pytest.fixture +def mock_view(hass): + """Register a view that verifies client id/secret.""" + hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) + + clients = [] + + class ClientView(HomeAssistantView): + url = '/' + name = 'bla' + + @verify_client + async def get(self, request, client_id): + """Handle GET request.""" + clients.append(client_id) + + hass.http.register_view(ClientView) + return clients + + +async def test_verify_client(hass, aiohttp_client, mock_view): + """Test that verify client can extract client auth from a request.""" + http_client = await async_setup_auth(hass, aiohttp_client) + client = await hass.auth.async_create_client('Hello') + + resp = await http_client.get('/', auth=BasicAuth(client.id, client.secret)) + assert resp.status == 200 + assert mock_view == [client.id] + + +async def test_verify_client_no_auth_header(hass, aiohttp_client, mock_view): + """Test that verify client will decline unknown client id.""" + http_client = await async_setup_auth(hass, aiohttp_client) + + resp = await http_client.get('/') + assert resp.status == 401 + assert mock_view == [] + + +async def test_verify_client_invalid_client_id(hass, aiohttp_client, + mock_view): + """Test that verify client will decline unknown client id.""" + http_client = await async_setup_auth(hass, aiohttp_client) + client = await hass.auth.async_create_client('Hello') + + resp = await http_client.get('/', auth=BasicAuth('invalid', client.secret)) + assert resp.status == 401 + assert mock_view == [] + + +async def test_verify_client_invalid_client_secret(hass, aiohttp_client, + mock_view): + """Test that verify client will decline incorrect client secret.""" + http_client = await async_setup_auth(hass, aiohttp_client) + client = await hass.auth.async_create_client('Hello') + + resp = await http_client.get('/', auth=BasicAuth(client.id, 'invalid')) + assert resp.status == 401 + assert mock_view == [] diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py new file mode 100644 index 00000000000..5d9bf6b98cc --- /dev/null +++ b/tests/components/auth/test_init.py @@ -0,0 +1,53 @@ +"""Integration tests for the auth component.""" +from . import async_setup_auth, CLIENT_AUTH + + +async def test_login_new_user_and_refresh_token(hass, aiohttp_client): + """Test logging in with new user and refreshing tokens.""" + client = await async_setup_auth(hass, aiohttp_client, setup_api=True) + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', None] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'test-user', + 'password': 'test-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + code = step['result'] + + # Exchange code for tokens + resp = await client.post('/auth/token', data={ + 'grant_type': 'authorization_code', + 'code': code + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + tokens = await resp.json() + + assert hass.auth.async_get_access_token(tokens['access_token']) is not None + + # Use refresh token to get more tokens. + resp = await client.post('/auth/token', data={ + 'grant_type': 'refresh_token', + 'refresh_token': tokens['refresh_token'] + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + tokens = await resp.json() + assert 'refresh_token' not in tokens + assert hass.auth.async_get_access_token(tokens['access_token']) is not None + + # Test using access token to hit API. + resp = await client.get('/api/') + assert resp.status == 401 + + resp = await client.get('/api/', headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + assert resp.status == 200 diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py new file mode 100644 index 00000000000..44695bce202 --- /dev/null +++ b/tests/components/auth/test_init_link_user.py @@ -0,0 +1,150 @@ +"""Tests for the link user flow.""" +from . import async_setup_auth, CLIENT_AUTH, CLIENT_ID + + +async def async_get_code(hass, aiohttp_client): + """Helper for link user tests that returns authorization code.""" + config = [{ + 'name': 'Example', + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }, { + 'name': 'Example', + 'id': '2nd auth', + 'type': 'insecure_example', + 'users': [{ + 'username': '2nd-user', + 'password': '2nd-pass', + 'name': '2nd Name' + }] + }] + client = await async_setup_auth(hass, aiohttp_client, config) + + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', None] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'test-user', + 'password': 'test-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + code = step['result'] + + # Exchange code for tokens + resp = await client.post('/auth/token', data={ + 'grant_type': 'authorization_code', + 'code': code + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + tokens = await resp.json() + + access_token = hass.auth.async_get_access_token(tokens['access_token']) + assert access_token is not None + user = access_token.refresh_token.user + assert len(user.credentials) == 1 + + # Now authenticate with the 2nd flow + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', '2nd auth'] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': '2nd-user', + 'password': '2nd-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + + return { + 'user': user, + 'code': step['result'], + 'client': client, + 'tokens': tokens, + } + + +async def test_link_user(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + code = info['code'] + tokens = info['tokens'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': CLIENT_ID, + 'code': code + }, headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + + assert resp.status == 200 + assert len(info['user'].credentials) == 2 + + +async def test_link_user_invalid_client_id(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + code = info['code'] + tokens = info['tokens'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': 'invalid', + 'code': code + }, headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + + assert resp.status == 400 + assert len(info['user'].credentials) == 1 + + +async def test_link_user_invalid_code(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + tokens = info['tokens'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': CLIENT_ID, + 'code': 'invalid' + }, headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + + assert resp.status == 400 + assert len(info['user'].credentials) == 1 + + +async def test_link_user_invalid_auth(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + code = info['code'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': CLIENT_ID, + 'code': code, + }, headers={'authorization': 'Bearer invalid'}) + + assert resp.status == 401 + assert len(info['user'].credentials) == 1 diff --git a/tests/components/auth/test_init_login_flow.py b/tests/components/auth/test_init_login_flow.py new file mode 100644 index 00000000000..96fece6506b --- /dev/null +++ b/tests/components/auth/test_init_login_flow.py @@ -0,0 +1,66 @@ +"""Tests for the login flow.""" +from aiohttp.helpers import BasicAuth + +from . import async_setup_auth, CLIENT_AUTH + + +async def test_fetch_auth_providers(hass, aiohttp_client): + """Test fetching auth providers.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.get('/auth/providers', auth=CLIENT_AUTH) + assert await resp.json() == [{ + 'name': 'Example', + 'type': 'insecure_example', + 'id': None + }] + + +async def test_fetch_auth_providers_require_valid_client(hass, aiohttp_client): + """Test fetching auth providers.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.get('/auth/providers', + auth=BasicAuth('invalid', 'bla')) + assert resp.status == 401 + + +async def test_cannot_get_flows_in_progress(hass, aiohttp_client): + """Test we cannot get flows in progress.""" + client = await async_setup_auth(hass, aiohttp_client, []) + resp = await client.get('/auth/login_flow') + assert resp.status == 405 + + +async def test_invalid_username_password(hass, aiohttp_client): + """Test we cannot get flows in progress.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', None] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + # Incorrect username + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'wrong-user', + 'password': 'test-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + + assert step['step_id'] == 'init' + assert step['errors']['base'] == 'invalid_auth' + + # Incorrect password + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'test-user', + 'password': 'wrong-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + + assert step['step_id'] == 'init' + assert step['errors']['base'] == 'invalid_auth' diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py new file mode 100644 index 00000000000..88dd0dae737 --- /dev/null +++ b/tests/components/binary_sensor/test_deconz.py @@ -0,0 +1,79 @@ +"""deCONZ binary sensor platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHATemperature", + "state": {"temperature": False}, + "config": {} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ binary sensor 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') + await hass.config_entries.async_forward_entry_setup( + config_entry, 'binary_sensor') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_binary_sensors(hass): + """Test that no sensors in deconz results in no sensor entities.""" + 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_binary_sensors(hass): + """Test successful creation of binary sensor entities.""" + data = {"sensors": SENSOR} + await setup_bridge(hass, data) + assert "binary_sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "binary_sensor.sensor_2_name" not in \ + hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 1 + + +async def test_add_new_sensor(hass): + """Test successful creation of sensor entities.""" + data = {} + await setup_bridge(hass, data) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHAPresence' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "binary_sensor.name" in hass.data[deconz.DATA_DECONZ_ID] diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 465d6276ad5..d0f1425a595 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,18 +1,19 @@ """The tests for the camera component.""" import asyncio +import base64 from unittest.mock import patch, mock_open import pytest from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import ATTR_ENTITY_PICTURE -import homeassistant.components.camera as camera -import homeassistant.components.http as http +from homeassistant.components import camera, http, websocket_api from homeassistant.exceptions import HomeAssistantError from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( - get_test_home_assistant, get_test_instance_port, assert_setup_component) + get_test_home_assistant, get_test_instance_port, assert_setup_component, + mock_coro) @pytest.fixture @@ -90,36 +91,32 @@ class TestGetImage(object): self.hass, 'camera.demo_camera'), self.hass.loop).result() assert mock_camera.called - assert image == b'Test' + assert image.content == b'Test' def test_get_image_without_exists_camera(self): """Try to get image without exists camera.""" - self.hass.states.remove('camera.demo_camera') - - with pytest.raises(HomeAssistantError): + with patch('homeassistant.helpers.entity_component.EntityComponent.' + 'get_entity', return_value=None), \ + pytest.raises(HomeAssistantError): run_coroutine_threadsafe(camera.async_get_image( self.hass, 'camera.demo_camera'), self.hass.loop).result() - def test_get_image_with_timeout(self, aioclient_mock): + def test_get_image_with_timeout(self): """Try to get image with timeout.""" - aioclient_mock.get(self.url, exc=asyncio.TimeoutError()) - - with pytest.raises(HomeAssistantError): + with patch('homeassistant.components.camera.Camera.async_camera_image', + side_effect=asyncio.TimeoutError), \ + pytest.raises(HomeAssistantError): run_coroutine_threadsafe(camera.async_get_image( self.hass, 'camera.demo_camera'), self.hass.loop).result() - assert len(aioclient_mock.mock_calls) == 1 - - def test_get_image_with_bad_http_state(self, aioclient_mock): - """Try to get image with bad http status.""" - aioclient_mock.get(self.url, status=400) - - with pytest.raises(HomeAssistantError): + def test_get_image_fails(self): + """Try to get image with timeout.""" + with patch('homeassistant.components.camera.Camera.async_camera_image', + return_value=mock_coro(None)), \ + pytest.raises(HomeAssistantError): run_coroutine_threadsafe(camera.async_get_image( self.hass, 'camera.demo_camera'), self.hass.loop).result() - assert len(aioclient_mock.mock_calls) == 1 - @asyncio.coroutine def test_snapshot_service(hass, mock_camera): @@ -136,3 +133,24 @@ def test_snapshot_service(hass, mock_camera): assert len(mock_write.mock_calls) == 1 assert mock_write.mock_calls[0][1][0] == b'Test' + + +async def test_webocket_camera_thumbnail(hass, hass_ws_client, mock_camera): + """Test camera_thumbnail websocket command.""" + await async_setup_component(hass, 'camera') + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'camera_thumbnail', + 'entity_id': 'camera.demo_camera', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['success'] + assert msg['result']['content_type'] == 'image/jpeg' + assert msg['result']['content'] == \ + base64.b64encode(b'Test').decode('utf-8') diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 1098c8c9233..40517ea1298 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -6,6 +6,9 @@ from unittest import mock # https://bugs.python.org/issue23004 from mock_open import MockOpen +from homeassistant.components.camera import DOMAIN +from homeassistant.components.camera.local_file import ( + SERVICE_UPDATE_FILE_PATH) from homeassistant.setup import async_setup_component from tests.common import mock_registry @@ -115,3 +118,37 @@ def test_camera_content_type(hass, aiohttp_client): assert resp_4.content_type == 'image/jpeg' body = yield from resp_4.text() assert body == image + + +async def test_update_file_path(hass): + """Test update_file_path service.""" + # Setup platform + + mock_registry(hass) + + with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ + mock.patch('os.access', mock.Mock(return_value=True)): + await async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'local_file', + 'file_path': 'mock/path.jpg' + } + }) + + # Fetch state and check motion detection attribute + state = hass.states.get('camera.local_file') + assert state.attributes.get('friendly_name') == 'Local File' + assert state.attributes.get('file_path') == 'mock/path.jpg' + + service_data = { + "entity_id": 'camera.local_file', + "file_path": 'new/path.jpg' + } + + await hass.services.async_call(DOMAIN, + SERVICE_UPDATE_FILE_PATH, + service_data) + await hass.async_block_till_done() + + state = hass.states.get('camera.local_file') + assert state.attributes.get('file_path') == 'new/path.jpg' diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index bd0b764c6fe..7bc0b0a18e7 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -116,7 +116,7 @@ class TestGenericThermostatHeaterSwitching(unittest.TestCase): def test_heater_switch(self): """Test heater switching test switch.""" - platform = loader.get_component('switch.test') + platform = loader.get_component(self.hass, 'switch.test') platform.init() self.switch_1 = platform.DEVICES[1] assert setup_component(self.hass, switch.DOMAIN, {'switch': { diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index f4ae81ad2f2..81b1e315085 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -318,7 +318,8 @@ def test_handler_google_actions(hass): 'entity_config': { 'switch.test': { 'name': 'Config name', - 'aliases': 'Config alias' + 'aliases': 'Config alias', + 'room': 'living room' } } } @@ -347,6 +348,7 @@ def test_handler_google_actions(hass): assert device['name']['name'] == 'Config name' assert device['name']['nicknames'] == ['Config alias'] assert device['type'] == 'action.devices.types.SWITCH' + assert device['roomHint'] == 'living room' async def test_refresh_token_expired(hass): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index f53be8818a3..84d15578e13 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -17,10 +17,10 @@ from homeassistant.loader import set_component from tests.common import MockConfigEntry, MockModule, mock_coro_func -@pytest.fixture(scope='session', autouse=True) -def mock_test_component(): +@pytest.fixture(autouse=True) +def mock_test_component(hass): """Ensure a component called 'test' exists.""" - set_component('test', MockModule('test')) + set_component(hass, 'test', MockModule('test')) @pytest.fixture @@ -172,7 +172,8 @@ def test_abort(hass, client): def test_create_account(hass, client): """Test a flow that creates an account.""" set_component( - 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(FlowHandler): VERSION = 1 @@ -204,7 +205,8 @@ def test_create_account(hass, client): def test_two_step_flow(hass, client): """Test we can finish a two step flow.""" set_component( - 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(FlowHandler): VERSION = 1 diff --git a/tests/components/conftest.py b/tests/components/conftest.py new file mode 100644 index 00000000000..53caeb80783 --- /dev/null +++ b/tests/components/conftest.py @@ -0,0 +1,22 @@ +"""Fixtures for component testing.""" +import pytest + +from homeassistant.setup import async_setup_component + + +@pytest.fixture +def hass_ws_client(aiohttp_client): + """Websocket client fixture connected to websocket server.""" + async def create_client(hass): + """Create a websocket client.""" + wapi = hass.components.websocket_api + assert await async_setup_component(hass, 'websocket_api') + + client = await aiohttp_client(hass.http.app) + websocket = await client.ws_connect(wapi.URL) + auth_ok = await websocket.receive_json() + assert auth_ok['type'] == wapi.TYPE_AUTH_OK + + return websocket + + return create_client diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index cbc8a373972..888094deea6 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,9 +1,12 @@ """Test deCONZ component setup process.""" -from unittest.mock import patch +from unittest.mock import Mock, patch +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component from homeassistant.components import deconz +from tests.common import mock_coro + async def test_config_with_host_passed_to_config_entry(hass): """Test that configured options for a host are loaded via config entry.""" @@ -67,3 +70,105 @@ async def test_config_discovery(hass): assert await async_setup_component(hass, deconz.DOMAIN, {}) is True # No flow started assert len(mock_config_entries.flow.mock_calls) == 0 + + +async def test_setup_entry_already_registered_bridge(hass): + """Test setup entry doesn't allow more than one instance of deCONZ.""" + hass.data[deconz.DOMAIN] = True + assert await deconz.async_setup_entry(hass, {}) is False + + +async def test_setup_entry_no_available_bridge(hass): + """Test setup entry fails if deCONZ is not available.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(False)): + assert await deconz.async_setup_entry(hass, entry) is False + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch.object(hass, 'async_add_job') as mock_add_job, \ + patch.object(hass, 'config_entries') as mock_config_entries, \ + patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + 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) == 4 + assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 4 + 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') + assert mock_config_entries.async_forward_entry_setup.mock_calls[2][1] == \ + (entry, 'scene') + assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \ + (entry, 'sensor') + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + assert deconz.DATA_DECONZ_EVENT in hass.data + hass.data[deconz.DATA_DECONZ_EVENT].append(Mock()) + hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'} + assert await deconz.async_unload_entry(hass, entry) + assert deconz.DOMAIN not in hass.data + assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 0 + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + + +async def test_add_new_device(hass): + """Test adding a new device generates a signal for platforms.""" + new_event = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": { + "config": { + "on": "True", + "reachable": "True" + }, + "name": "event", + "state": {}, + "type": "ZHASwitch" + } + } + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch.object(deconz, 'async_dispatcher_send') as mock_dispatch_send, \ + patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + hass.data[deconz.DOMAIN].async_event_handler(new_event) + await hass.async_block_till_done() + assert len(mock_dispatch_send.mock_calls) == 1 + assert len(mock_dispatch_send.mock_calls[0]) == 3 + + +async def test_add_new_remote(hass): + """Test new added device creates a new remote.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + remote = Mock() + remote.name = 'name' + remote.type = 'ZHASwitch' + remote.register_async_callback = Mock() + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + + async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 1 diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index d2ae8965668..0cbece6d1b0 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -3,9 +3,9 @@ import os from datetime import timedelta import unittest from unittest import mock +import socket import voluptuous as vol -from future.backports import socket from homeassistant.setup import setup_component from homeassistant.components import device_tracker diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 912bd315ecd..0b17b4e0ac8 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -189,7 +189,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): def test_update_stale(self): """Test stalled update.""" - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() scanner.come_home('DEV1') @@ -251,7 +251,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): hide_if_away=True) device_tracker.update_config(self.yaml_devices, dev_id, device) - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() with assert_setup_component(1, device_tracker.DOMAIN): @@ -270,7 +270,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): hide_if_away=True) device_tracker.update_config(self.yaml_devices, dev_id, device) - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() with assert_setup_component(1, device_tracker.DOMAIN): @@ -431,7 +431,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): 'zone': zone_info }) - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() scanner.come_home('dev1') @@ -547,7 +547,7 @@ def test_bad_platform(hass): async def test_adding_unknown_device_to_config(mock_device_tracker_conf, hass): """Test the adding of unknown devices to configuration file.""" - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(hass, 'device_tracker.test').SCANNER scanner.reset() scanner.come_home('DEV1') diff --git a/tests/components/fan/test_template.py b/tests/components/fan/test_template.py new file mode 100644 index 00000000000..719a3f96aed --- /dev/null +++ b/tests/components/fan/test_template.py @@ -0,0 +1,549 @@ +"""The tests for the Template fan platform.""" +import logging + +from homeassistant.core import callback +from homeassistant import setup +import homeassistant.components as components +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.fan import ( + ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) + +from tests.common import ( + get_test_home_assistant, assert_setup_component) +_LOGGER = logging.getLogger(__name__) + + +_TEST_FAN = 'fan.test_fan' +# Represent for fan's state +_STATE_INPUT_BOOLEAN = 'input_boolean.state' +# Represent for fan's speed +_SPEED_INPUT_SELECT = 'input_select.speed' +# Represent for fan's oscillating +_OSC_INPUT = 'input_select.osc' + + +class TestTemplateFan: + """Test the Template light.""" + + hass = None + calls = None + # pylint: disable=invalid-name + + def setup_method(self, method): + """Setup.""" + self.hass = get_test_home_assistant() + + self.calls = [] + + @callback + def record_call(service): + """Track function calls..""" + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + # Configuration tests # + def test_missing_optional_config(self): + """Test: missing optional template is ok.""" + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_ON, None, None) + + def test_missing_value_template_config(self): + """Test: missing 'value_template' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_missing_turn_on_config(self): + """Test: missing 'turn_on' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_missing_turn_off_config(self): + """Test: missing 'turn_off' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + 'turn_on': { + 'service': 'script.fan_on' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_invalid_config(self): + """Test: missing 'turn_off' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + 'turn_on': { + 'service': 'script.fan_on' + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + # End of configuration tests # + + # Template tests # + def test_templates_with_entities(self): + """Test tempalates with values from other entities.""" + value_template = """ + {% if is_state('input_boolean.state', 'True') %} + {{ 'on' }} + {% else %} + {{ 'off' }} + {% endif %} + """ + + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': value_template, + 'speed_template': + "{{ states('input_select.speed') }}", + 'oscillating_template': + "{{ states('input_select.osc') }}", + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_OFF, None, None) + + self.hass.states.set(_STATE_INPUT_BOOLEAN, True) + self.hass.states.set(_SPEED_INPUT_SELECT, SPEED_MEDIUM) + self.hass.states.set(_OSC_INPUT, 'True') + self.hass.block_till_done() + + self._verify(STATE_ON, SPEED_MEDIUM, True) + + def test_templates_with_valid_values(self): + """Test templates with valid values.""" + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': + "{{ 'on' }}", + 'speed_template': + "{{ 'medium' }}", + 'oscillating_template': + "{{ 1 == 1 }}", + + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_ON, SPEED_MEDIUM, True) + + def test_templates_invalid_values(self): + """Test templates with invalid values.""" + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': + "{{ 'abc' }}", + 'speed_template': + "{{ '0' }}", + 'oscillating_template': + "{{ 'xyz' }}", + + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_OFF, None, None) + + # End of template tests # + + # Function tests # + def test_on_off(self): + """Test turn on and turn off.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON + self._verify(STATE_ON, None, None) + + # Turn off fan + components.fan.turn_off(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_OFF + self._verify(STATE_OFF, None, None) + + def test_on_with_speed(self): + """Test turn on with speed.""" + self._register_components() + + # Turn on fan with high speed + components.fan.turn_on(self.hass, _TEST_FAN, SPEED_HIGH) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + def test_set_speed(self): + """Test set valid speed.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to high + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + # Set fan's speed to medium + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_MEDIUM + self._verify(STATE_ON, SPEED_MEDIUM, None) + + def test_set_invalid_speed_from_initial_stage(self): + """Test set invalid speed when fan is in initial state.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to 'invalid' + components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify speed is unchanged + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '' + self._verify(STATE_ON, None, None) + + def test_set_invalid_speed(self): + """Test set invalid speed when fan has valid speed.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to high + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + # Set fan's speed to 'invalid' + components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify speed is unchanged + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + def test_custom_speed_list(self): + """Test set custom speed list.""" + self._register_components(['1', '2', '3']) + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to '1' + components.fan.set_speed(self.hass, _TEST_FAN, '1') + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' + self._verify(STATE_ON, '1', None) + + # Set fan's speed to 'medium' which is invalid + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + self.hass.block_till_done() + + # verify that speed is unchanged + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' + self._verify(STATE_ON, '1', None) + + def test_set_osc(self): + """Test set oscillating.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's osc to True + components.fan.oscillate(self.hass, _TEST_FAN, True) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == 'True' + self._verify(STATE_ON, None, True) + + # Set fan's osc to False + components.fan.oscillate(self.hass, _TEST_FAN, False) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == 'False' + self._verify(STATE_ON, None, False) + + def test_set_invalid_osc_from_initial_state(self): + """Test set invalid oscillating when fan is in initial state.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's osc to 'invalid' + components.fan.oscillate(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == '' + self._verify(STATE_ON, None, None) + + def test_set_invalid_osc(self): + """Test set invalid oscillating when fan has valid osc.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's osc to True + components.fan.oscillate(self.hass, _TEST_FAN, True) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == 'True' + self._verify(STATE_ON, None, True) + + # Set fan's osc to False + components.fan.oscillate(self.hass, _TEST_FAN, None) + self.hass.block_till_done() + + # verify osc is unchanged + assert self.hass.states.get(_OSC_INPUT).state == 'True' + self._verify(STATE_ON, None, True) + + def _verify(self, expected_state, expected_speed, expected_oscillating): + """Verify fan's state, speed and osc.""" + state = self.hass.states.get(_TEST_FAN) + attributes = state.attributes + assert state.state == expected_state + assert attributes.get(ATTR_SPEED, None) == expected_speed + assert attributes.get(ATTR_OSCILLATING, None) == expected_oscillating + + def _register_components(self, speed_list=None): + """Register basic components for testing.""" + with assert_setup_component(1, 'input_boolean'): + assert setup.setup_component( + self.hass, + 'input_boolean', + {'input_boolean': {'state': None}} + ) + + with assert_setup_component(2, 'input_select'): + assert setup.setup_component(self.hass, 'input_select', { + 'input_select': { + 'speed': { + 'name': 'Speed', + 'options': ['', SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + '1', '2', '3'] + }, + + 'osc': { + 'name': 'oscillating', + 'options': ['', 'True', 'False'] + }, + } + }) + + with assert_setup_component(1, 'fan'): + value_template = """ + {% if is_state('input_boolean.state', 'on') %} + {{ 'on' }} + {% else %} + {{ 'off' }} + {% endif %} + """ + + test_fan_config = { + 'value_template': value_template, + 'speed_template': + "{{ states('input_select.speed') }}", + 'oscillating_template': + "{{ states('input_select.osc') }}", + + 'turn_on': { + 'service': 'input_boolean.turn_on', + 'entity_id': _STATE_INPUT_BOOLEAN + }, + 'turn_off': { + 'service': 'input_boolean.turn_off', + 'entity_id': _STATE_INPUT_BOOLEAN + }, + 'set_speed': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _SPEED_INPUT_SELECT, + 'option': '{{ speed }}' + } + }, + 'set_oscillating': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _OSC_INPUT, + 'option': '{{ oscillating }}' + } + } + } + + if speed_list: + test_fan_config['speeds'] = speed_list + + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': test_fan_config + } + } + }) + + self.hass.start() + self.hass.block_till_done() diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index f8e026483aa..faa982f62f3 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -7,12 +7,12 @@ import unittest from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( - add_preload_service, set_accessory_info, debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) -from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, SERV_ACCESSORY_INFO, + CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, + CHAR_SERIAL_NUMBER, MANUFACTURER) +from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant @@ -62,69 +62,25 @@ class TestAccessories(unittest.TestCase): hass.stop() - def test_add_preload_service(self): - """Test add_preload_service without additional characteristics.""" - acc = Mock() - serv = add_preload_service(acc, 'AirPurifier') - self.assertEqual(acc.mock_calls, [call.add_service(serv)]) - with self.assertRaises(ValueError): - serv.get_characteristic('Name') - - # Test with typo in service name - with self.assertRaises(KeyError): - add_preload_service(Mock(), 'AirPurifierTypo') - - # Test adding additional characteristic as string - serv = add_preload_service(Mock(), 'AirPurifier', 'Name') - serv.get_characteristic('Name') - - # Test adding additional characteristics as list - serv = add_preload_service(Mock(), 'AirPurifier', - ['Name', 'RotationSpeed']) - serv.get_characteristic('Name') - serv.get_characteristic('RotationSpeed') - - # Test adding additional characteristic with typo - with self.assertRaises(KeyError): - add_preload_service(Mock(), 'AirPurifier', 'NameTypo') - - def test_set_accessory_info(self): - """Test setting the basic accessory information.""" - # Test HomeAccessory - acc = HomeAccessory('HA', 'Home Accessory', 'homekit.accessory', 2, '') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') - - serv = acc.get_service(SERV_ACCESSORY_INFO) - self.assertEqual(serv.get_characteristic(CHAR_NAME).value, 'name') - self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') - self.assertEqual( - serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - - # Test HomeBridge - acc = HomeBridge('hass') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') - - serv = acc.get_service(SERV_ACCESSORY_INFO) - self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') - self.assertEqual( - serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - def test_home_accessory(self): """Test HomeAccessory class.""" hass = get_test_home_assistant() - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2, '') + acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) self.assertEqual(acc.hass, hass) self.assertEqual(acc.display_name, 'Home Accessory') self.assertEqual(acc.category, 1) # Category.OTHER self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO + self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'homekit.accessory') + serv.get_characteristic(CHAR_NAME).value, 'Home Accessory') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, 'Homekit') + self.assertEqual(serv.get_characteristic(CHAR_SERIAL_NUMBER).value, + 'homekit.accessory') hass.states.set('homekit.accessory', 'on') hass.block_till_done() @@ -132,13 +88,13 @@ class TestAccessories(unittest.TestCase): hass.states.set('homekit.accessory', 'off') hass.block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model', 2, '') + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) self.assertEqual(acc.display_name, 'test_name') self.assertEqual(acc.aid, 2) self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'test_model') + serv.get_characteristic(CHAR_MODEL).value, 'Test Model') hass.stop() @@ -151,8 +107,17 @@ class TestAccessories(unittest.TestCase): self.assertEqual(len(bridge.services), 1) serv = bridge.services[0] # SERV_ACCESSORY_INFO self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) + self.assertEqual( + serv.get_characteristic(CHAR_NAME).value, BRIDGE_NAME) + self.assertEqual( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, __version__) + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) + self.assertEqual( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value, + BRIDGE_SERIAL_NUMBER) bridge = HomeBridge('hass', 'test_name') self.assertEqual(bridge.display_name, 'test_name') diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index c26982e170b..cff52b2ff20 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -68,14 +68,8 @@ class TestGetAccessories(unittest.TestCase): """Test humidity sensor with device class humidity.""" with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): state = State('sensor.humidity', '20', - {ATTR_DEVICE_CLASS: 'humidity'}) - get_accessory(None, state, 2, {}) - - def test_sensor_humidity_unit(self): - """Test humidity sensor with % as unit.""" - with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): - state = State('sensor.humidity', '20', - {ATTR_UNIT_OF_MEASUREMENT: '%'}) + {ATTR_DEVICE_CLASS: 'humidity', + ATTR_UNIT_OF_MEASUREMENT: '%'}) get_accessory(None, state, 2, {}) def test_air_quality_sensor(self): @@ -105,10 +99,10 @@ class TestGetAccessories(unittest.TestCase): get_accessory(None, state, 2, {}) def test_light_sensor(self): - """Test light sensor with device class lux.""" + """Test light sensor with device class illuminance.""" with patch.dict(TYPES, {'LightSensor': self.mock_type}): state = State('sensor.light', '900', - {ATTR_DEVICE_CLASS: 'light'}) + {ATTR_DEVICE_CLASS: 'illuminance'}) get_accessory(None, state, 2, {}) def test_light_sensor_unit_lm(self): @@ -118,11 +112,11 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: 'lm'}) get_accessory(None, state, 2, {}) - def test_light_sensor_unit_lux(self): - """Test light sensor with lux as unit.""" + def test_light_sensor_unit_lx(self): + """Test light sensor with lx as unit.""" with patch.dict(TYPES, {'LightSensor': self.mock_type}): state = State('sensor.light', '900', - {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) + {ATTR_UNIT_OF_MEASUREMENT: 'lx'}) get_accessory(None, state, 2, {}) def test_binary_sensor(self): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 7ae37becbd5..082953038b5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -4,7 +4,9 @@ from unittest.mock import call, patch, ANY, Mock from homeassistant import setup from homeassistant.core import State -from homeassistant.components.homekit import HomeKit, generate_aid +from homeassistant.components.homekit import ( + HomeKit, generate_aid, + STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, @@ -79,24 +81,28 @@ class TestHomeKit(unittest.TestCase): CONF_IP_ADDRESS: '172.0.0.0'}} self.assertTrue(setup.setup_component( self.hass, DOMAIN, config)) - - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) self.hass.block_till_done() self.assertEqual(mock_homekit.mock_calls, [ call(self.hass, 11111, '172.0.0.0', ANY, {}), call().setup()]) - # Test start call with driver stopped. + # Test auto_start disabled homekit.reset_mock() - homekit.configure_mock(**{'started': False}) + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.block_till_done() + self.assertEqual(homekit.mock_calls, []) + + # Test start call with driver is ready + homekit.reset_mock() + homekit.status = STATUS_READY self.hass.services.call('homekit', 'start') self.assertEqual(homekit.mock_calls, [call.start()]) - # Test start call with driver started. + # Test start call with driver started homekit.reset_mock() - homekit.configure_mock(**{'started': True}) + homekit.status = STATUS_STOPPED self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START) self.assertEqual(homekit.mock_calls, []) @@ -180,34 +186,38 @@ class TestHomeKit(unittest.TestCase): state = self.hass.states.all()[0] homekit.start() + self.hass.block_till_done() self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) self.assertEqual(mock_show_setup_msg.mock_calls, [ call(self.hass, homekit.bridge)]) self.assertEqual(homekit.driver.mock_calls, [call.start()]) - self.assertTrue(homekit.started) + self.assertEqual(homekit.status, STATUS_RUNNING) # Test start() if already started homekit.driver.reset_mock() homekit.start() + self.hass.block_till_done() self.assertEqual(homekit.driver.mock_calls, []) def test_homekit_stop(self): """Test HomeKit stop method.""" - homekit = HomeKit(None, None, None, None, None) + homekit = HomeKit(self.hass, None, None, None, None) homekit.driver = Mock() - # Test if started = False + self.assertEqual(homekit.status, STATUS_READY) homekit.stop() - self.assertFalse(homekit.driver.stop.called) - - # Test if driver not started - homekit.started = True - homekit.driver.configure_mock(**{'run_sentinel': None}) + self.hass.block_till_done() + homekit.status = STATUS_WAIT homekit.stop() + self.hass.block_till_done() + homekit.status = STATUS_STOPPED + homekit.stop() + self.hass.block_till_done() self.assertFalse(homekit.driver.stop.called) # Test if driver is started - homekit.driver.configure_mock(**{'run_sentinel': 'sentinel'}) + homekit.status = STATUS_RUNNING homekit.stop() + self.hass.block_till_done() self.assertTrue(homekit.driver.stop.called) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 2dcb48a4d4c..313d58e78fd 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,19 +4,35 @@ import unittest from homeassistant.core import callback from homeassistant.components.cover import ( ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP) -from homeassistant.components.homekit.type_covers import ( - GarageDoorOpener, WindowCovering, WindowCoveringBasic) from homeassistant.const import ( STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, ATTR_SUPPORTED_FEATURES) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitSensors(unittest.TestCase): +class TestHomekitCovers(unittest.TestCase): """Test class for all accessory types regarding covers.""" + @classmethod + def setUpClass(cls): + """Setup Light class import and debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + _import = __import__('homeassistant.components.homekit.type_covers', + fromlist=['GarageDoorOpener', 'WindowCovering,', + 'WindowCoveringBasic']) + cls.garage_cls = _import.GarageDoorOpener + cls.window_cls = _import.WindowCovering + cls.window_basic_cls = _import.WindowCoveringBasic + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -37,7 +53,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" garage_door = 'cover.garage_door' - acc = GarageDoorOpener(self.hass, 'Cover', garage_door, 2, config=None) + acc = self.garage_cls(self.hass, 'Cover', garage_door, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -95,7 +111,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' - acc = WindowCovering(self.hass, 'Cover', window_cover, 2, config=None) + acc = self.window_cls(self.hass, 'Cover', window_cover, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -146,8 +162,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -214,8 +230,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() # Set from HomeKit diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 9c1ff0faf1a..baa461af772 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -7,7 +7,8 @@ from homeassistant.components.homekit.type_security_systems import ( from homeassistant.const import ( ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_UNKNOWN) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + STATE_UNKNOWN) from tests.common import get_test_home_assistant @@ -65,10 +66,15 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual(acc.char_target_state.value, 3) self.assertEqual(acc.char_current_state.value, 3) + self.hass.states.set(acp, STATE_ALARM_TRIGGERED) + self.hass.block_till_done() + self.assertEqual(acc.char_target_state.value, 3) + self.assertEqual(acc.char_current_state.value, 4) + self.hass.states.set(acp, STATE_UNKNOWN) self.hass.block_till_done() self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 3) + self.assertEqual(acc.char_current_state.value, 4) # Set from HomeKit acc.char_target_state.client_update_value(0) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index adc3fb018f8..fe2a7f6cd02 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -7,7 +7,7 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) from homeassistant.const import ( - ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, + ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, EVENT_CALL_SERVICE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -261,6 +261,65 @@ class TestHomekitThermostats(unittest.TestCase): 25.0) self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) + def test_power_state(self): + """Test if accessory and HA are updated accordingly.""" + climate = 'climate.test' + + # SUPPORT_ON_OFF = True + self.hass.states.set(climate, STATE_HEAT, + {ATTR_SUPPORTED_FEATURES: 4096, + ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + self.hass.block_till_done() + acc = self.thermostat_cls(self.hass, 'Climate', climate, + 2, config=None) + acc.run() + self.assertTrue(acc.support_power_state) + + self.assertEqual(acc.char_current_heat_cool.value, 1) + self.assertEqual(acc.char_target_heat_cool.value, 1) + + self.hass.states.set(climate, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + self.hass.block_till_done() + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 0) + + self.hass.states.set(climate, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_OFF, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + self.hass.block_till_done() + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 0) + + # Set from HomeKit + acc.char_target_heat_cool.client_update_value(1) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'turn_on') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], + climate) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], 'set_operation_mode') + self.assertEqual( + self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE], + STATE_HEAT) + self.assertEqual(acc.char_target_heat_cool.value, 1) + + acc.char_target_heat_cool.client_update_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[2].data[ATTR_SERVICE], 'turn_off') + self.assertEqual( + self.events[2].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], + climate) + self.assertEqual(acc.char_target_heat_cool.value, 0) + def test_thermostat_fahrenheit(self): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index c02e203444f..d5368032a37 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,4 +1,6 @@ """The tests for the Home Assistant HTTP component.""" +import logging + from homeassistant.setup import async_setup_component import homeassistant.components.http as http @@ -76,14 +78,13 @@ async def test_api_no_base_url(hass): async def test_not_log_password(hass, aiohttp_client, caplog): """Test access with password doesn't get logged.""" - result = await async_setup_component(hass, 'api', { + assert await async_setup_component(hass, 'api', { 'http': { http.CONF_API_PASSWORD: 'some-pass' } }) - assert result - client = await aiohttp_client(hass.http.app) + logging.getLogger('aiohttp.access').setLevel(logging.INFO) resp = await client.get('/api/', params={ 'api_password': 'some-pass' diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index e840bce54f7..50060e08a4b 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -3,14 +3,13 @@ import asyncio from unittest.mock import patch, PropertyMock from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.setup import setup_component -import homeassistant.components.image_processing as ip +from homeassistant.components import camera, image_processing as ip from homeassistant.components.image_processing.openalpr_cloud import ( OPENALPR_API_URL) from tests.common import ( - get_test_home_assistant, assert_setup_component, load_fixture) + get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) class TestOpenAlprCloudSetup(object): @@ -131,11 +130,6 @@ class TestOpenAlprCloud(object): new_callable=PropertyMock(return_value=False)): setup_component(self.hass, ip.DOMAIN, config) - state = self.hass.states.get('camera.demo_camera') - self.url = "{0}{1}".format( - self.hass.config.api.base_url, - state.attributes.get(ATTR_ENTITY_PICTURE)) - self.alpr_events = [] @callback @@ -158,18 +152,20 @@ class TestOpenAlprCloud(object): def test_openalpr_process_image(self, aioclient_mock): """Setup and scan a picture and test plates from event.""" - aioclient_mock.get(self.url, content=b'image') aioclient_mock.post( OPENALPR_API_URL, params=self.params, text=load_fixture('alpr_cloud.json'), status=200 ) - ip.scan(self.hass, entity_id='image_processing.test_local') - self.hass.block_till_done() + with patch('homeassistant.components.camera.async_get_image', + return_value=mock_coro( + camera.Image('image/jpeg', b'image'))): + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') - assert len(aioclient_mock.mock_calls) == 2 + assert len(aioclient_mock.mock_calls) == 1 assert len(self.alpr_events) == 5 assert state.attributes.get('vehicles') == 1 assert state.state == 'H786P0J' @@ -184,28 +180,32 @@ class TestOpenAlprCloud(object): def test_openalpr_process_image_api_error(self, aioclient_mock): """Setup and scan a picture and test api error.""" - aioclient_mock.get(self.url, content=b'image') aioclient_mock.post( OPENALPR_API_URL, params=self.params, text="{'error': 'error message'}", status=400 ) - ip.scan(self.hass, entity_id='image_processing.test_local') - self.hass.block_till_done() + with patch('homeassistant.components.camera.async_get_image', + return_value=mock_coro( + camera.Image('image/jpeg', b'image'))): + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 2 + assert len(aioclient_mock.mock_calls) == 1 assert len(self.alpr_events) == 0 def test_openalpr_process_image_api_timeout(self, aioclient_mock): """Setup and scan a picture and test api error.""" - aioclient_mock.get(self.url, content=b'image') aioclient_mock.post( OPENALPR_API_URL, params=self.params, exc=asyncio.TimeoutError() ) - ip.scan(self.hass, entity_id='image_processing.test_local') - self.hass.block_till_done() + with patch('homeassistant.components.camera.async_get_image', + return_value=mock_coro( + camera.Image('image/jpeg', b'image'))): + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 2 + assert len(aioclient_mock.mock_calls) == 1 assert len(self.alpr_events) == 0 diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py new file mode 100644 index 00000000000..2608d77ce2a --- /dev/null +++ b/tests/components/light/test_deconz.py @@ -0,0 +1,100 @@ +"""deCONZ light platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from tests.common import mock_coro + + +LIGHT = { + "1": { + "id": "Light 1 id", + "name": "Light 1 name", + "state": {} + } +} + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "state": {}, + "action": {}, + "scenes": [], + "lights": [ + "1", + "2" + ] + }, + "2": { + "id": "Group 2 id", + "name": "Group 2 name", + "state": {}, + "action": {}, + "scenes": [] + }, +} + + +async def setup_bridge(hass, data): + """Load the deCONZ light 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') + 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() + + +async def test_no_lights_or_groups(hass): + """Test that no lights or groups 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_lights_and_groups(hass): + """Test that lights or groups entities are created.""" + await setup_bridge(hass, {"lights": LIGHT, "groups": GROUP}) + assert "light.light_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "light.group_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "light.group_2_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 3 + + +async def test_add_new_light(hass): + """Test successful creation of light entities.""" + data = {} + await setup_bridge(hass, data) + light = Mock() + light.name = 'name' + light.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_light', [light]) + await hass.async_block_till_done() + assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_add_new_group(hass): + """Test successful creation of group entities.""" + data = {} + await setup_bridge(hass, data) + group = Mock() + group.name = 'name' + group.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_group', [group]) + await hass.async_block_till_done() + assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 8f5b52ea6de..a1e3867f9c3 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -650,6 +650,19 @@ def test_hs_color(): assert light.hs_color is None + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'hs', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color is None + light = hue_light.HueLight( light=Mock(state={ 'colormode': 'xy', diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 4e8fad261bd..634e3774b8a 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -118,7 +118,7 @@ class TestLight(unittest.TestCase): def test_services(self): """Test the provided services.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( @@ -267,7 +267,7 @@ class TestLight(unittest.TestCase): def test_broken_light_profiles(self): """Test light profiles.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) @@ -282,7 +282,7 @@ class TestLight(unittest.TestCase): def test_light_profiles(self): """Test light profiles.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index d6835b00be0..5bae1061b7f 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -146,6 +146,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) + self.assertIsNone(state.attributes.get('hs_color')) fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON"}') self.hass.block_till_done() @@ -158,6 +159,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) + self.assertIsNone(state.attributes.get('hs_color')) def test_controlling_state_via_topic(self): \ # pylint: disable=invalid-name @@ -174,6 +176,7 @@ class TestLightMQTTJSON(unittest.TestCase): 'rgb': True, 'white_value': True, 'xy': True, + 'hs': True, 'qos': '0' } }) @@ -187,6 +190,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) + self.assertIsNone(state.attributes.get('hs_color')) self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) # Turn on the light, full white @@ -207,6 +211,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual('colorloop', state.attributes.get('effect')) self.assertEqual(150, state.attributes.get('white_value')) self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) + self.assertEqual((0.0, 0.0), state.attributes.get('hs_color')) # Turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') @@ -243,6 +248,15 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual((0.141, 0.14), light_state.attributes.get('xy_color')) + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"h":180,"s":50}}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual((180.0, 50.0), + light_state.attributes.get('hs_color')) + fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON",' '"color_temp":155}') @@ -361,6 +375,28 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(50, state.attributes['brightness']) self.assertEqual((125, 100), state.attributes['hs_color']) + def test_sending_hs_color(self): + """Test light.turn_on with hs color sends hs color parameters.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'hs': True, + } + }) + + light.turn_on(self.hass, 'light.test', hs_color=(180.0, 50.0)) + self.hass.block_till_done() + + message_json = json.loads( + self.mock_publish.async_publish.mock_calls[0][1][1]) + self.assertEqual("ON", message_json["state"]) + self.assertEqual({ + 'h': 180.0, + 's': 50.0, + }, message_json["color"]) + def test_flash_short_and_long(self): \ # pylint: disable=invalid-name """Test for flash length being sent when included.""" diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py index 86bfdfb52c4..eea6295b79e 100644 --- a/tests/components/media_player/test_blackbird.py +++ b/tests/components/media_player/test_blackbird.py @@ -59,7 +59,6 @@ class TestBlackbirdSchema(unittest.TestCase): """Test valid schema.""" valid_schema = { 'platform': 'blackbird', - 'type': 'serial', 'port': '/dev/ttyUSB0', 'zones': {1: {'name': 'a'}, 2: {'name': 'a'}, @@ -87,8 +86,7 @@ class TestBlackbirdSchema(unittest.TestCase): """Test valid schema.""" valid_schema = { 'platform': 'blackbird', - 'type': 'socket', - 'port': '192.168.1.50', + 'host': '192.168.1.50', 'zones': {1: {'name': 'a'}, 2: {'name': 'a'}, 3: {'name': 'a'}, @@ -109,10 +107,18 @@ class TestBlackbirdSchema(unittest.TestCase): schemas = ( {}, # Empty None, # None - # Missing type + # Port and host used concurrently + { + 'platform': 'blackbird', + 'port': '/dev/ttyUSB0', + 'host': '192.168.1.50', + 'name': 'Name', + 'zones': {1: {'name': 'a'}}, + 'sources': {1: {'name': 'b'}}, + }, + # Port or host missing { 'platform': 'blackbird', - 'port': 'aaa', 'name': 'Name', 'zones': {1: {'name': 'a'}}, 'sources': {1: {'name': 'b'}}, @@ -120,8 +126,7 @@ class TestBlackbirdSchema(unittest.TestCase): # Invalid zone number { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {11: {'name': 'a'}}, 'sources': {1: {'name': 'b'}}, @@ -129,8 +134,7 @@ class TestBlackbirdSchema(unittest.TestCase): # Invalid source number { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {1: {'name': 'a'}}, 'sources': {9: {'name': 'b'}}, @@ -138,8 +142,7 @@ class TestBlackbirdSchema(unittest.TestCase): # Zone missing name { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {1: {}}, 'sources': {1: {'name': 'b'}}, @@ -147,21 +150,11 @@ class TestBlackbirdSchema(unittest.TestCase): # Source missing name { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {1: {'name': 'a'}}, 'sources': {1: {}}, }, - # Invalid type - { - 'platform': 'blackbird', - 'type': 'aaa', - 'port': 'aaa', - 'name': 'Name', - 'zones': {1: {'name': 'a'}}, - 'sources': {1: {'name': 'b'}}, - }, ) for value in schemas: with self.assertRaises(vol.MultipleInvalid): @@ -181,7 +174,6 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): new=lambda *a: self.blackbird): setup_platform(self.hass, { 'platform': 'blackbird', - 'type': 'serial', 'port': '/dev/ttyUSB0', 'zones': {3: {'name': 'Zone name'}}, 'sources': {1: {'name': 'one'}, @@ -189,7 +181,7 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): 2: {'name': 'two'}}, }, lambda *args, **kwargs: None, {}) self.hass.block_till_done() - self.media_player = self.hass.data[DATA_BLACKBIRD][0] + self.media_player = self.hass.data[DATA_BLACKBIRD]['/dev/ttyUSB0-3'] self.media_player.hass = self.hass self.media_player.entity_id = 'media_player.zone_3' @@ -203,7 +195,8 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): self.assertTrue(self.hass.services.has_service(DOMAIN, SERVICE_SETALLZONES)) self.assertEqual(len(self.hass.data[DATA_BLACKBIRD]), 1) - self.assertEqual(self.hass.data[DATA_BLACKBIRD][0].name, 'Zone name') + self.assertEqual(self.hass.data[DATA_BLACKBIRD]['/dev/ttyUSB0-3'].name, + 'Zone name') def test_setallzones_service_call_with_entity_id(self): """Test set all zone source service call with entity id.""" diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py new file mode 100644 index 00000000000..5d632d4de0b --- /dev/null +++ b/tests/components/media_player/test_init.py @@ -0,0 +1,37 @@ +"""Test the base functions of the media player.""" +import base64 +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +from homeassistant.components import websocket_api + +from tests.common import mock_coro + + +async def test_get_panels(hass, hass_ws_client): + """Test get_panels command.""" + await async_setup_component(hass, 'media_player', { + 'media_player': { + 'platform': 'demo' + } + }) + + client = await hass_ws_client(hass) + + with patch('homeassistant.components.media_player.MediaPlayerDevice.' + 'async_get_media_image', return_value=mock_coro( + (b'image', 'image/jpeg'))): + await client.send_json({ + 'id': 5, + 'type': 'media_player_thumbnail', + 'entity_id': 'media_player.bedroom', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['success'] + assert msg['result']['content_type'] == 'image/jpeg' + assert msg['result']['content'] == \ + base64.b64encode(b'image').decode('utf-8') diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b25479bb75a..05c5de71b8c 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -131,10 +131,56 @@ class TestMQTTComponent(unittest.TestCase): self.hass.data['mqtt'].async_publish.call_args[0][2], 2) self.assertFalse(self.hass.data['mqtt'].async_publish.call_args[0][3]) - def test_invalid_mqtt_topics(self): - """Test invalid topics.""" + def test_validate_topic(self): + """Test topic name/filter validation.""" + # Invalid UTF-8, must not contain U+D800 to U+DFFF. + self.assertRaises(vol.Invalid, mqtt.valid_topic, '\ud800') + self.assertRaises(vol.Invalid, mqtt.valid_topic, '\udfff') + # Topic MUST NOT be empty + self.assertRaises(vol.Invalid, mqtt.valid_topic, '') + # Topic MUST NOT be longer than 65535 encoded bytes. + self.assertRaises(vol.Invalid, mqtt.valid_topic, 'ü' * 32768) + # UTF-8 MUST NOT include null character + self.assertRaises(vol.Invalid, mqtt.valid_topic, 'bad\0one') + + # Topics "SHOULD NOT" include these special characters + # (not MUST NOT, RFC2119). The receiver MAY close the connection. + mqtt.valid_topic('\u0001') + mqtt.valid_topic('\u001F') + mqtt.valid_topic('\u009F') + mqtt.valid_topic('\u009F') + mqtt.valid_topic('\uffff') + + def test_validate_subscribe_topic(self): + """Test invalid subscribe topics.""" + mqtt.valid_subscribe_topic('#') + mqtt.valid_subscribe_topic('sport/#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport/#/') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'foo/bar#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'foo/#/bar') + + mqtt.valid_subscribe_topic('+') + mqtt.valid_subscribe_topic('+/tennis/#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport+') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport+/') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport/+1') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport/+#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad+topic') + mqtt.valid_subscribe_topic('sport/+/player1') + mqtt.valid_subscribe_topic('/finance') + mqtt.valid_subscribe_topic('+/+') + mqtt.valid_subscribe_topic('$SYS/#') + + def test_validate_publish_topic(self): + """Test invalid publish topics.""" + self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'pub+') + self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'pub/+') + self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, '1#') self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'bad+topic') - self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad\0one') + mqtt.valid_publish_topic('//') + + # Topic names beginning with $ SHOULD NOT be used, but can + mqtt.valid_publish_topic('$SYS/') # pylint: disable=invalid-name diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index 42b9eb9d82d..c5064fca851 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -35,28 +35,30 @@ class TestNotifyFile(unittest.TestCase): assert setup_component(self.hass, notify.DOMAIN, config) assert not handle_config[notify.DOMAIN] - def _test_notify_file(self, timestamp, mock_utcnow, mock_stat): + def _test_notify_file(self, timestamp): """Test the notify file output.""" - mock_utcnow.return_value = dt_util.as_utc(dt_util.now()) - mock_stat.return_value.st_size = 0 + filename = 'mock_file' + message = 'one, two, testing, testing' + with assert_setup_component(1) as handle_config: + self.assertTrue(setup_component(self.hass, notify.DOMAIN, { + 'notify': { + 'name': 'test', + 'platform': 'file', + 'filename': filename, + 'timestamp': timestamp, + } + })) + assert handle_config[notify.DOMAIN] m_open = mock_open() with patch( 'homeassistant.components.notify.file.open', m_open, create=True - ): - filename = 'mock_file' - message = 'one, two, testing, testing' - with assert_setup_component(1) as handle_config: - self.assertTrue(setup_component(self.hass, notify.DOMAIN, { - 'notify': { - 'name': 'test', - 'platform': 'file', - 'filename': filename, - 'timestamp': timestamp, - } - })) - assert handle_config[notify.DOMAIN] + ), patch('homeassistant.components.notify.file.os.stat') as mock_st, \ + patch('homeassistant.util.dt.utcnow', + return_value=dt_util.utcnow()): + + mock_st.return_value.st_size = 0 title = '{} notifications (Log started: {})\n{}\n'.format( ATTR_TITLE_DEFAULT, dt_util.utcnow().isoformat(), @@ -82,14 +84,10 @@ class TestNotifyFile(unittest.TestCase): dt_util.utcnow().isoformat(), message))] ) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file(self, mock_utcnow, mock_stat): + def test_notify_file(self): """Test the notify file output without timestamp.""" - self._test_notify_file(False, mock_utcnow, mock_stat) + self._test_notify_file(False) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file_timestamp(self, mock_utcnow, mock_stat): + def test_notify_file_timestamp(self): """Test the notify file output with timestamp.""" - self._test_notify_file(True, mock_utcnow, mock_stat) + self._test_notify_file(True) diff --git a/tests/components/scene/test_deconz.py b/tests/components/scene/test_deconz.py new file mode 100644 index 00000000000..53f25808be2 --- /dev/null +++ b/tests/components/scene/test_deconz.py @@ -0,0 +1,57 @@ +"""deCONZ scenes platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "state": {}, + "action": {}, + "scenes": [{ + "id": "1", + "name": "Scene 1" + }], + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ scene 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_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + 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() + + +async def test_no_scenes(hass): + """Test the update_lights function with some lights.""" + 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_scenes(hass): + """Test the update_lights function with some lights.""" + data = {"groups": GROUP} + await setup_bridge(hass, data) + assert "scene.group_1_name_scene_1" in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 1 diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 25ea818c774..a832e249832 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -16,7 +16,7 @@ class TestScene(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - test_light = loader.get_component('light.test') + test_light = loader.get_component(self.hass, 'light.test') test_light.init() self.assertTrue(setup_component(self.hass, light.DOMAIN, { diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py new file mode 100644 index 00000000000..8f6a53e6e65 --- /dev/null +++ b/tests/components/sensor/test_deconz.py @@ -0,0 +1,99 @@ +"""deCONZ sensor platform tests.""" +from unittest.mock import Mock, patch + + +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHATemperature", + "state": {"temperature": False}, + "config": {} + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + }, + "3": { + "id": "Sensor 3 id", + "name": "Sensor 3 name", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {} + }, + "4": { + "id": "Sensor 4 id", + "name": "Sensor 4 name", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {"battery": 100} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ sensor 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_EVENT] = [] + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + 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() + + +async def test_no_sensors(hass): + """Test that no sensors in deconz results in no sensor entities.""" + 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_sensors(hass): + """Test successful creation of sensor entities.""" + data = {"sensors": SENSOR} + await setup_bridge(hass, data) + assert "sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_2_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_3_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_3_name_battery_level" not in \ + hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_4_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_4_name_battery_level" in \ + hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 2 + + +async def test_add_new_sensor(hass): + """Test successful creation of sensor entities.""" + data = {} + await setup_bridge(hass, data) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHATemperature' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "sensor.name" in hass.data[deconz.DATA_DECONZ_ID] diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 43432f3304c..8e79306fe13 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -67,6 +67,9 @@ class TestFilterSensor(unittest.TestCase): 'filter': 'lowpass', 'time_constant': 10, 'precision': 2 + }, { + 'filter': 'throttle', + 'window_size': 1 }] } } diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 88e74e11008..2583f52b3d2 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -10,7 +10,8 @@ import homeassistant.components.sensor as sensor from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util -from tests.common import mock_mqtt_component, fire_mqtt_message +from tests.common import mock_mqtt_component, fire_mqtt_message, \ + assert_setup_component from tests.common import get_test_home_assistant, mock_component @@ -350,3 +351,36 @@ class TestSensorMQTT(unittest.TestCase): self.hass.block_till_done() assert len(self.hass.states.all()) == 1 + + def test_invalid_device_class(self): + """Test device_class option with invalid value.""" + with assert_setup_component(0): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device_class': 'foobarnotreal' + } + }) + + def test_valid_device_class(self): + """Test device_class option with valid values.""" + assert setup_component(self.hass, 'sensor', { + 'sensor': [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device_class': 'temperature' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + }] + }) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_1') + assert state.attributes['device_class'] == 'temperature' + state = self.hass.states.get('sensor.test_2') + assert 'device_class' not in state.attributes diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index b05fc90bfe4..f8d912f24dd 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -267,3 +267,40 @@ class TestTemplateSensor: self.hass.block_till_done() assert self.hass.states.all() == [] + + def test_setup_invalid_device_class(self): + """"Test setup with invalid device_class.""" + with assert_setup_component(0): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test': { + 'value_template': '{{ foo }}', + 'device_class': 'foobarnotreal', + }, + }, + } + }) + + def test_setup_valid_device_class(self): + """"Test setup with valid device_class.""" + with assert_setup_component(1): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test1': { + 'value_template': '{{ foo }}', + 'device_class': 'temperature', + }, + 'test2': {'value_template': '{{ foo }}'}, + } + } + }) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test1') + assert state.attributes['device_class'] == 'temperature' + state = self.hass.states.get('sensor.test2') + assert 'device_class' not in state.attributes diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 65526e2d938..3f490b4ab12 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -148,10 +148,11 @@ def test_invalid_data(hass, aioclient_mock): async def test_entity_id_with_multiple_stations(hass, aioclient_mock): """Test not generating duplicate entity ids with multiple stations.""" aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) + aioclient_mock.get(PWS_URL, text=load_fixture('wunderground-valid.json')) config = [ VALID_CONFIG, - {**VALID_CONFIG, 'entity_namespace': 'hi'} + {**VALID_CONFIG_PWS, 'entity_namespace': 'hi'} ] await async_setup_component(hass, 'sensor', {'sensor': config}) await hass.async_block_till_done() @@ -160,6 +161,25 @@ async def test_entity_id_with_multiple_stations(hass, aioclient_mock): assert state is not None assert state.state == 'Clear' - state = hass.states.get('sensor.hi_weather') + state = hass.states.get('sensor.hi_pws_weather') assert state is not None assert state.state == 'Clear' + + +async def test_fails_because_of_unique_id(hass, aioclient_mock): + """Test same config twice fails because of unique_id.""" + aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) + aioclient_mock.get(PWS_URL, text=load_fixture('wunderground-valid.json')) + + config = [ + VALID_CONFIG, + {**VALID_CONFIG, 'entity_namespace': 'hi'}, + VALID_CONFIG_PWS + ] + await async_setup_component(hass, 'sensor', {'sensor': config}) + await hass.async_block_till_done() + + states = hass.states.async_all() + expected = len(VALID_CONFIG['monitored_conditions']) + \ + len(VALID_CONFIG_PWS['monitored_conditions']) + assert len(states) == expected diff --git a/tests/components/sensor/test_yweather.py b/tests/components/sensor/test_yweather.py index 88b94906a35..aeee47bfa80 100644 --- a/tests/components/sensor/test_yweather.py +++ b/tests/components/sensor/test_yweather.py @@ -162,6 +162,8 @@ class TestWeather(unittest.TestCase): state = self.hass.states.get('sensor.yweather_condition') assert state is not None self.assertEqual(state.state, 'Mostly Cloudy') + self.assertEqual(state.attributes.get('condition_code'), + '28') self.assertEqual(state.attributes.get('friendly_name'), 'Yweather Condition') diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index c42061db958..61e665f265c 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -71,7 +71,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_when_switch_is_off(self): """Test the flux switch when it is off.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -113,7 +113,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_before_sunrise(self): """Test the flux switch before sunrise.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -160,7 +160,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_after_sunrise_before_sunset(self): """Test the flux switch after sunrise and before sunset.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -207,7 +207,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_after_sunset_before_stop(self): """Test the flux switch after sunset and before stop.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -255,7 +255,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_after_stop_before_sunrise(self): """Test the flux switch after stop and before sunrise.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -302,7 +302,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_with_custom_start_stop_times(self): """Test the flux with custom start and stop times.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -353,7 +353,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -405,7 +405,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -456,7 +456,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -507,7 +507,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -558,7 +558,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -606,7 +606,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_with_custom_colortemps(self): """Test the flux with custom start and stop colortemps.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -656,7 +656,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_with_custom_brightness(self): """Test the flux with custom start and stop colortemps.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -704,7 +704,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_with_multiple_lights(self): """Test the flux switch with multiple light entities.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -773,7 +773,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_with_mired(self): """Test the flux switch´s mode mired.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -818,7 +818,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_with_rgb(self): """Test the flux switch´s mode rgb.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 090e3c74bf1..d679aa2c827 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -17,7 +17,7 @@ class TestSwitch(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - platform = loader.get_component('switch.test') + platform = loader.get_component(self.hass, 'switch.test') platform.init() # Switch 1 is ON, switch 2 is OFF self.switch_1, self.switch_2, self.switch_3 = \ @@ -79,10 +79,10 @@ class TestSwitch(unittest.TestCase): def test_setup_two_platforms(self): """Test with bad configuration.""" # Test if switch component returns 0 switches - test_platform = loader.get_component('switch.test') + test_platform = loader.get_component(self.hass, 'switch.test') test_platform.init(True) - loader.set_component('switch.test2', test_platform) + loader.set_component(self.hass, 'switch.test2', test_platform) test_platform.init(False) self.assertTrue(setup_component( diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index f79d0706321..b5e2a0b0395 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -1,12 +1,14 @@ """The tests for the MQTT switch platform.""" import unittest +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE,\ ATTR_ASSUMED_STATE +import homeassistant.core as ha import homeassistant.components.switch as switch from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) + mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro) class TestSwitchMQTT(unittest.TestCase): @@ -52,19 +54,23 @@ class TestSwitchMQTT(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): """Test the sending MQTT commands in optimistic mode.""" - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'command_topic': 'command-topic', - 'payload_on': 'beer on', - 'payload_off': 'beer off', - 'qos': '2' - } - }) + fake_state = ha.State('switch.test', 'on') + + with patch('homeassistant.components.switch.mqtt.async_get_last_state', + return_value=mock_coro(fake_state)): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'command-topic', + 'payload_on': 'beer on', + 'payload_off': 'beer off', + 'qos': '2' + } + }) state = self.hass.states.get('switch.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) switch.turn_on(self.hass, 'switch.test') diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 3c73e85c4e5..a8b8a201217 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -22,12 +22,12 @@ class TestDeviceSunLightTrigger(unittest.TestCase): self.hass = get_test_home_assistant() self.scanner = loader.get_component( - 'device_tracker.test').get_scanner(None, None) + self.hass, 'device_tracker.test').get_scanner(None, None) self.scanner.reset() self.scanner.come_home('DEV1') - loader.get_component('light.test').init() + loader.get_component(self.hass, 'light.test').init() with patch( 'homeassistant.components.device_tracker.load_yaml_config_file', diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index c742e215738..973544495d7 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -9,6 +9,7 @@ from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, CONF_EXTRA_HTML_URL_ES5, DATA_PANELS) +from homeassistant.components import websocket_api as wapi @pytest.fixture @@ -189,3 +190,26 @@ def test_panel_without_path(hass): 'test_component', 'nonexistant_file') yield from async_setup_component(hass, 'frontend', {}) assert 'test_component' not in hass.data[DATA_PANELS] + + +async def test_get_panels(hass, hass_ws_client): + """Test get_panels command.""" + await async_setup_component(hass, 'frontend') + await hass.components.frontend.async_register_built_in_panel( + 'map', 'Map', 'mdi:account-location') + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'get_panels', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result']['map']['component_name'] == 'map' + assert msg['result']['map']['url_path'] == 'map' + assert msg['result']['map']['icon'] == 'mdi:account-location' + assert msg['result']['map']['title'] == 'Map' diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index c909a8488be..e2323aca855 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -217,7 +217,7 @@ class TestInfluxDB(unittest.TestCase): """Test the event listener for missing units.""" self._setup() - attrs = {'bignumstring': "9" * 999} + attrs = {'bignumstring': '9' * 999, 'nonumstring': 'nan'} state = mock.MagicMock( state=8, domain='fake', entity_id='fake.entity-id', object_id='entity', attributes=attrs) diff --git a/tests/components/test_microsoft_face.py b/tests/components/test_microsoft_face.py index 7a047a73f47..370059a0a09 100644 --- a/tests/components/test_microsoft_face.py +++ b/tests/components/test_microsoft_face.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import patch -import homeassistant.components.microsoft_face as mf +from homeassistant.components import camera, microsoft_face as mf from homeassistant.setup import setup_component from tests.common import ( @@ -190,7 +190,7 @@ class TestMicrosoftFaceSetup(object): assert len(aioclient_mock.mock_calls) == 1 @patch('homeassistant.components.camera.async_get_image', - return_value=mock_coro(b'Test')) + return_value=mock_coro(camera.Image('image/jpeg', b'Test'))) def test_service_face(self, camera_mock, aioclient_mock): """Setup component, test person face services.""" aioclient_mock.get( diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/test_mqtt_statestream.py index 76d8e48d03a..e120c3a7dd2 100644 --- a/tests/components/test_mqtt_statestream.py +++ b/tests/components/test_mqtt_statestream.py @@ -134,7 +134,7 @@ class TestMqttStateStream(object): test_attributes = { "testing": "YES", "list": ["a", "b", "c"], - "bool": True + "bool": False } # Set a state of an entity @@ -150,7 +150,7 @@ class TestMqttStateStream(object): 1, True), call.async_publish(self.hass, 'pub/fake/entity/list', '["a", "b", "c"]', 1, True), - call.async_publish(self.hass, 'pub/fake/entity/bool', "true", + call.async_publish(self.hass, 'pub/fake/entity/bool', "false", 1, True) ] diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index c440ef9c30c..59e99e5c1b5 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -1,33 +1,26 @@ """Test system log component.""" -import asyncio import logging from unittest.mock import MagicMock, patch -import pytest - from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log _LOGGER = logging.getLogger('test_logger') +BASIC_CONFIG = { + 'system_log': { + 'max_entries': 2, + } +} -@pytest.fixture(autouse=True) -@asyncio.coroutine -def setup_test_case(hass, aiohttp_client): - """Setup system_log component before test case.""" - config = {'system_log': {'max_entries': 2}} - yield from async_setup_component(hass, system_log.DOMAIN, config) - - -@asyncio.coroutine -def get_error_log(hass, aiohttp_client, expected_count): +async def get_error_log(hass, aiohttp_client, expected_count): """Fetch all entries from system_log via the API.""" - client = yield from aiohttp_client(hass.http.app) - resp = yield from client.get('/api/error/all') + client = await aiohttp_client(hass.http.app) + resp = await client.get('/api/error/all') assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert len(data) == expected_count return data @@ -52,43 +45,43 @@ def get_frame(name): return (name, None, None, None) -@asyncio.coroutine -def test_normal_logs(hass, aiohttp_client): +async def test_normal_logs(hass, aiohttp_client): """Test that debug and info are not logged.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.debug('debug') _LOGGER.info('info') # Assert done by get_error_log - yield from get_error_log(hass, aiohttp_client, 0) + await get_error_log(hass, aiohttp_client, 0) -@asyncio.coroutine -def test_exception(hass, aiohttp_client): +async def test_exception(hass, aiohttp_client): """Test that exceptions are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _generate_and_log_exception('exception message', 'log message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, 'exception message', 'log message', 'ERROR') -@asyncio.coroutine -def test_warning(hass, aiohttp_client): +async def test_warning(hass, aiohttp_client): """Test that warning are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.warning('warning message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'warning message', 'WARNING') -@asyncio.coroutine -def test_error(hass, aiohttp_client): +async def test_error(hass, aiohttp_client): """Test that errors are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'error message', 'ERROR') -@asyncio.coroutine -def test_error_posted_as_event(hass, aiohttp_client): - """Test that error are posted as events.""" +async def test_config_not_fire_event(hass): + """Test that errors are not posted as events with default config.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) events = [] @callback @@ -99,77 +92,100 @@ def test_error_posted_as_event(hass, aiohttp_client): hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) _LOGGER.error('error message') - yield from hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(events) == 0 + + +async def test_error_posted_as_event(hass): + """Test that error are posted as events.""" + await async_setup_component(hass, system_log.DOMAIN, { + 'system_log': { + 'max_entries': 2, + 'fire_event': True, + } + }) + events = [] + + @callback + def event_listener(event): + """Listen to events of type system_log_event.""" + events.append(event) + + hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) + + _LOGGER.error('error message') + await hass.async_block_till_done() assert len(events) == 1 assert_log(events[0].data, '', 'error message', 'ERROR') -@asyncio.coroutine -def test_critical(hass, aiohttp_client): +async def test_critical(hass, aiohttp_client): """Test that critical are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.critical('critical message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'critical message', 'CRITICAL') -@asyncio.coroutine -def test_remove_older_logs(hass, aiohttp_client): +async def test_remove_older_logs(hass, aiohttp_client): """Test that older logs are rotated out.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message 1') _LOGGER.error('error message 2') _LOGGER.error('error message 3') - log = yield from get_error_log(hass, aiohttp_client, 2) + log = await get_error_log(hass, aiohttp_client, 2) assert_log(log[0], '', 'error message 3', 'ERROR') assert_log(log[1], '', 'error message 2', 'ERROR') -@asyncio.coroutine -def test_clear_logs(hass, aiohttp_client): +async def test_clear_logs(hass, aiohttp_client): """Test that the log can be cleared via a service call.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message') hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_CLEAR, {})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() # Assert done by get_error_log - yield from get_error_log(hass, aiohttp_client, 0) + await get_error_log(hass, aiohttp_client, 0) -@asyncio.coroutine -def test_write_log(hass): +async def test_write_log(hass): """Test that error propagates to logger.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) logger = MagicMock() with patch('logging.getLogger', return_value=logger) as mock_logging: hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_logging.assert_called_once_with( 'homeassistant.components.system_log.external') assert logger.method_calls[0] == ('error', ('test_message',)) -@asyncio.coroutine -def test_write_choose_logger(hass): +async def test_write_choose_logger(hass): """Test that correct logger is chosen.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch('logging.getLogger') as mock_logging: hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message', 'logger': 'myLogger'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_logging.assert_called_once_with( 'myLogger') -@asyncio.coroutine -def test_write_choose_level(hass): +async def test_write_choose_level(hass): """Test that correct logger is chosen.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) logger = MagicMock() with patch('logging.getLogger', return_value=logger): hass.async_add_job( @@ -177,17 +193,17 @@ def test_write_choose_level(hass): system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message', 'level': 'debug'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert logger.method_calls[0] == ('debug', ('test_message',)) -@asyncio.coroutine -def test_unknown_path(hass, aiohttp_client): +async def test_unknown_path(hass, aiohttp_client): """Test error logged from unknown path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.findCaller = MagicMock( return_value=('unknown_path', 0, None, None)) _LOGGER.error('error message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'unknown_path' @@ -206,31 +222,31 @@ def log_error_from_test_path(path): _LOGGER.error('error message') -@asyncio.coroutine -def test_homeassistant_path(hass, aiohttp_client): +async def test_homeassistant_path(hass, aiohttp_client): """Test error logged from homeassistant path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', new=['venv_path/homeassistant']): log_error_from_test_path( 'venv_path/homeassistant/component/component.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'component/component.py' -@asyncio.coroutine -def test_config_path(hass, aiohttp_client): +async def test_config_path(hass, aiohttp_client): """Test error logged from config path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.object(hass.config, 'config_dir', new='config'): log_error_from_test_path('config/custom_component/test.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'custom_component/test.py' -@asyncio.coroutine -def test_netdisco_path(hass, aiohttp_client): +async def test_netdisco_path(hass, aiohttp_client): """Test error logged from netdisco path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.dict('sys.modules', netdisco=MagicMock(__path__=['venv_path/netdisco'])): log_error_from_test_path('venv_path/netdisco/disco_component.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'disco_component.py' diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 4deccf65209..0a130e507d4 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -7,7 +7,7 @@ from async_timeout import timeout import pytest from homeassistant.core import callback -from homeassistant.components import websocket_api as wapi, frontend +from homeassistant.components import websocket_api as wapi from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -16,20 +16,9 @@ API_PASSWORD = 'test1234' @pytest.fixture -def websocket_client(loop, hass, aiohttp_client): - """Websocket client fixture connected to websocket server.""" - assert loop.run_until_complete( - async_setup_component(hass, 'websocket_api')) - - client = loop.run_until_complete(aiohttp_client(hass.http.app)) - ws = loop.run_until_complete(client.ws_connect(wapi.URL)) - auth_ok = loop.run_until_complete(ws.receive_json()) - assert auth_ok['type'] == wapi.TYPE_AUTH_OK - - yield ws - - if not ws.closed: - loop.run_until_complete(ws.close()) +def websocket_client(hass, hass_ws_client): + """Create a websocket client.""" + return hass.loop.run_until_complete(hass_ws_client(hass)) @pytest.fixture @@ -289,31 +278,6 @@ def test_get_config(hass, websocket_client): assert msg['result'] == hass.config.as_dict() -@asyncio.coroutine -def test_get_panels(hass, websocket_client): - """Test get_panels command.""" - yield from hass.components.frontend.async_register_built_in_panel( - 'map', 'Map', 'mdi:account-location') - hass.data[frontend.DATA_JS_VERSION] = 'es5' - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_GET_PANELS, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - assert msg['result'] == {'map': { - 'component_name': 'map', - 'url_path': 'map', - 'config': None, - 'url': None, - 'icon': 'mdi:account-location', - 'title': 'Map', - }} - - @asyncio.coroutine def test_ping(websocket_client): """Test get_panels command.""" @@ -337,3 +301,15 @@ def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): }) msg = yield from websocket_client.receive() assert msg.type == WSMsgType.close + + +@asyncio.coroutine +def test_unknown_command(websocket_client): + """Test get_panels command.""" + yield from websocket_client.send_json({ + 'id': 5, + 'type': 'unknown_command', + }) + + msg = yield from websocket_client.receive() + assert msg.type == WSMsgType.close diff --git a/tests/components/weather/test_darksky.py b/tests/components/weather/test_darksky.py index 787aca2ca17..7faa033e0a8 100644 --- a/tests/components/weather/test_darksky.py +++ b/tests/components/weather/test_darksky.py @@ -49,6 +49,3 @@ class TestDarkSky(unittest.TestCase): state = self.hass.states.get('weather.test') self.assertEqual(state.state, 'Clear') - self.assertEqual(state.attributes['daily_forecast_summary'], - 'No precipitation throughout the week, with ' - 'temperatures falling to 66°F on Thursday.') diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index 9d22b1ad0ae..a88e9979551 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -5,7 +5,8 @@ from homeassistant.components import weather from homeassistant.components.weather import ( ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, ATTR_FORECAST, ATTR_FORECAST_TEMP) + ATTR_WEATHER_WIND_SPEED, ATTR_FORECAST, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW) from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component @@ -45,8 +46,17 @@ class TestWeather(unittest.TestCase): assert data.get(ATTR_WEATHER_OZONE) is None assert data.get(ATTR_WEATHER_ATTRIBUTION) == \ 'Powered by Home Assistant' + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_CONDITION) == \ + 'rainy' + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION) == 1 assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP) == 22 + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP_LOW) == 15 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_CONDITION) == \ + 'fog' + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION) \ + == 0.2 assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP) == 21 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP_LOW) == 12 assert len(data.get(ATTR_FORECAST)) == 7 def test_temperature_convert(self): diff --git a/tests/components/zone/__init__.py b/tests/components/zone/__init__.py new file mode 100644 index 00000000000..2ba325fce81 --- /dev/null +++ b/tests/components/zone/__init__.py @@ -0,0 +1 @@ +"""Tests for the zone component.""" diff --git a/tests/components/zone/test_config_flow.py b/tests/components/zone/test_config_flow.py new file mode 100644 index 00000000000..d8ee6f7c5c0 --- /dev/null +++ b/tests/components/zone/test_config_flow.py @@ -0,0 +1,55 @@ +"""Tests for zone config flow.""" + +from homeassistant.components.zone import config_flow +from homeassistant.components.zone.const import CONF_PASSIVE, DOMAIN, HOME_ZONE +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) + +from tests.common import MockConfigEntry + + +async def test_flow_works(hass): + """Test that config flow works.""" + flow = config_flow.ZoneFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input={ + CONF_NAME: 'Name', + CONF_LATITUDE: '1.1', + CONF_LONGITUDE: '2.2', + CONF_RADIUS: '100', + CONF_ICON: 'mdi:home', + CONF_PASSIVE: True + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'Name' + assert result['data'] == { + CONF_NAME: 'Name', + CONF_LATITUDE: '1.1', + CONF_LONGITUDE: '2.2', + CONF_RADIUS: '100', + CONF_ICON: 'mdi:home', + CONF_PASSIVE: True + } + + +async def test_flow_requires_unique_name(hass): + """Test that config flow verifies that each zones name is unique.""" + MockConfigEntry(domain=DOMAIN, data={ + CONF_NAME: 'Name' + }).add_to_hass(hass) + flow = config_flow.ZoneFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input={CONF_NAME: 'Name'}) + assert result['errors'] == {'base': 'name_exists'} + + +async def test_flow_requires_name_different_from_home(hass): + """Test that config flow verifies that each zones name is unique.""" + flow = config_flow.ZoneFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input={CONF_NAME: HOME_ZONE}) + assert result['errors'] == {'base': 'name_exists'} diff --git a/tests/components/test_zone.py b/tests/components/zone/test_init.py similarity index 55% rename from tests/components/test_zone.py rename to tests/components/zone/test_init.py index 0ea84324362..1c698438f2c 100644 --- a/tests/components/test_zone.py +++ b/tests/components/zone/test_init.py @@ -1,10 +1,42 @@ """Test zone component.""" + import unittest +from unittest.mock import Mock from homeassistant import setup from homeassistant.components import zone from tests.common import get_test_home_assistant +from tests.common import MockConfigEntry + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = Mock() + entry.data = { + zone.CONF_NAME: 'Test Zone', + zone.CONF_LATITUDE: 1.1, + zone.CONF_LONGITUDE: -2.2, + zone.CONF_RADIUS: 250, + zone.CONF_RADIUS: True + } + hass.data[zone.DOMAIN] = {} + assert await zone.async_setup_entry(hass, entry) is True + assert 'test_zone' in hass.data[zone.DOMAIN] + + +async def test_unload_entry_successful(hass): + """Test unload entry is successful.""" + entry = Mock() + entry.data = { + zone.CONF_NAME: 'Test Zone', + zone.CONF_LATITUDE: 1.1, + zone.CONF_LONGITUDE: -2.2 + } + hass.data[zone.DOMAIN] = {} + assert await zone.async_setup_entry(hass, entry) is True + assert await zone.async_unload_entry(hass, entry) is True + assert not hass.data[zone.DOMAIN] class TestComponentZone(unittest.TestCase): @@ -20,18 +52,17 @@ class TestComponentZone(unittest.TestCase): def test_setup_no_zones_still_adds_home_zone(self): """Test if no config is passed in we still get the home zone.""" - assert setup.setup_component(self.hass, zone.DOMAIN, - {'zone': None}) - + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': None}) assert len(self.hass.states.entity_ids('zone')) == 1 state = self.hass.states.get('zone.home') assert self.hass.config.location_name == state.name assert self.hass.config.latitude == state.attributes['latitude'] assert self.hass.config.longitude == state.attributes['longitude'] assert not state.attributes.get('passive', False) + assert 'test_home' in self.hass.data[zone.DOMAIN] def test_setup(self): - """Test setup.""" + """Test a successful setup.""" info = { 'name': 'Test Zone', 'latitude': 32.880837, @@ -39,16 +70,61 @@ class TestComponentZone(unittest.TestCase): 'radius': 250, 'passive': True } - assert setup.setup_component(self.hass, zone.DOMAIN, { - 'zone': info - }) + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': info}) + assert len(self.hass.states.entity_ids('zone')) == 2 state = self.hass.states.get('zone.test_zone') assert info['name'] == state.name assert info['latitude'] == state.attributes['latitude'] assert info['longitude'] == state.attributes['longitude'] assert info['radius'] == state.attributes['radius'] assert info['passive'] == state.attributes['passive'] + assert 'test_zone' in self.hass.data[zone.DOMAIN] + assert 'test_home' in self.hass.data[zone.DOMAIN] + + def test_setup_zone_skips_home_zone(self): + """Test that zone named Home should override hass home zone.""" + info = { + 'name': 'Home', + 'latitude': 1.1, + 'longitude': -2.2, + } + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': info}) + + assert len(self.hass.states.entity_ids('zone')) == 1 + state = self.hass.states.get('zone.home') + assert info['name'] == state.name + assert 'home' in self.hass.data[zone.DOMAIN] + assert 'test_home' not in self.hass.data[zone.DOMAIN] + + def test_setup_registered_zone_skips_home_zone(self): + """Test that config entry named home should override hass home zone.""" + entry = MockConfigEntry(domain=zone.DOMAIN, data={ + zone.CONF_NAME: 'home' + }) + entry.add_to_hass(self.hass) + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': None}) + assert len(self.hass.states.entity_ids('zone')) == 0 + assert not self.hass.data[zone.DOMAIN] + + def test_setup_registered_zone_skips_configured_zone(self): + """Test if config entry will override configured zone.""" + entry = MockConfigEntry(domain=zone.DOMAIN, data={ + zone.CONF_NAME: 'Test Zone' + }) + entry.add_to_hass(self.hass) + info = { + 'name': 'Test Zone', + 'latitude': 1.1, + 'longitude': -2.2, + } + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': info}) + + assert len(self.hass.states.entity_ids('zone')) == 1 + state = self.hass.states.get('zone.test_zone') + assert not state + assert 'test_zone' not in self.hass.data[zone.DOMAIN] + assert 'test_home' in self.hass.data[zone.DOMAIN] def test_active_zone_skips_passive_zones(self): """Test active and passive zones.""" @@ -64,7 +140,7 @@ class TestComponentZone(unittest.TestCase): ] }) self.hass.block_till_done() - active = zone.active_zone(self.hass, 32.880600, -117.237561) + active = zone.zone.active_zone(self.hass, 32.880600, -117.237561) assert active is None def test_active_zone_skips_passive_zones_2(self): @@ -80,7 +156,7 @@ class TestComponentZone(unittest.TestCase): ] }) self.hass.block_till_done() - active = zone.active_zone(self.hass, 32.880700, -117.237561) + active = zone.zone.active_zone(self.hass, 32.880700, -117.237561) assert 'zone.active_zone' == active.entity_id def test_active_zone_prefers_smaller_zone_if_same_distance(self): @@ -104,7 +180,7 @@ class TestComponentZone(unittest.TestCase): ] }) - active = zone.active_zone(self.hass, latitude, longitude) + active = zone.zone.active_zone(self.hass, latitude, longitude) assert 'zone.small_zone' == active.entity_id def test_active_zone_prefers_smaller_zone_if_same_distance_2(self): @@ -122,7 +198,7 @@ class TestComponentZone(unittest.TestCase): ] }) - active = zone.active_zone(self.hass, latitude, longitude) + active = zone.zone.active_zone(self.hass, latitude, longitude) assert 'zone.smallest_zone' == active.entity_id def test_in_zone_works_for_passive_zones(self): @@ -141,5 +217,5 @@ class TestComponentZone(unittest.TestCase): ] }) - assert zone.in_zone(self.hass.states.get('zone.passive_zone'), - latitude, longitude) + assert zone.zone.in_zone(self.hass.states.get('zone.passive_zone'), + latitude, longitude) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 004e5e95ca0..faa7357bd8a 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -224,6 +224,47 @@ def test_node_discovery(hass, mock_openzwave): assert hass.states.get('zwave.mock_node').state is 'unknown' +async def test_unparsed_node_discovery(hass, mock_openzwave): + """Test discovery of a node.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_NODE_ADDED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + await async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + node = MockNode(node_id=14, manufacturer_name=None) + + sleeps = [] + + def utcnow(): + return datetime.fromtimestamp(len(sleeps)) + + asyncio_sleep = asyncio.sleep + + async def sleep(duration, loop): + if duration > 0: + sleeps.append(duration) + await asyncio_sleep(0, loop=loop) + + with patch('homeassistant.components.zwave.dt_util.utcnow', new=utcnow): + with patch('asyncio.sleep', new=sleep): + with patch.object(zwave, '_LOGGER') as mock_logger: + hass.async_add_job(mock_receivers[0], node) + await hass.async_block_till_done() + + assert len(sleeps) == const.NODE_READY_WAIT_SECS + assert mock_logger.warning.called + assert len(mock_logger.warning.mock_calls) == 1 + assert mock_logger.warning.mock_calls[0][1][1:] == \ + (14, const.NODE_READY_WAIT_SECS) + assert hass.states.get('zwave.mock_node').state is 'unknown' + + @asyncio.coroutine def test_node_ignored(hass, mock_openzwave): """Test discovery of a node.""" diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 299821d3685..f4d9b3ef0e8 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -182,8 +182,6 @@ class TestZWaveNodeEntity(unittest.TestCase): query_stage='Dynamic', is_awake=True, is_ready=False, is_failed=False, is_info_received=True, max_baud_rate=40000, is_zwave_plus=False, capabilities=[], neighbors=[], location=None) - self.node.manufacturer_name = 'Test Manufacturer' - self.node.product_name = 'Test Product' self.entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) @@ -357,3 +355,14 @@ class TestZWaveNodeEntity(unittest.TestCase): def test_not_polled(self): """Test should_poll property.""" self.assertFalse(self.entity.should_poll) + + def test_unique_id(self): + """Test unique_id.""" + self.assertEqual('node-567', self.entity.unique_id) + + def test_unique_id_missing_data(self): + """Test unique_id.""" + self.node.manufacturer_name = None + entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) + + self.assertIsNone(entity.unique_id) diff --git a/tests/conftest.py b/tests/conftest.py index 269d460ebb6..73e69605eae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ if os.environ.get('UVLOOP') == '1': import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -logging.basicConfig() +logging.basicConfig(level=logging.INFO) logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 90be56bbc7c..28efcb3e868 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -10,8 +10,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from tests.common import get_test_home_assistant - def test_boolean(): """Test boolean validation.""" @@ -256,24 +254,6 @@ def test_event_schema(): cv.EVENT_SCHEMA(value) -def test_platform_validator(): - """Test platform validation.""" - hass = None - - try: - hass = get_test_home_assistant() - - schema = vol.Schema(cv.platform_validator('light')) - - with pytest.raises(vol.MultipleInvalid): - schema('platform_that_does_not_exist') - - schema('hue') - finally: - if hass is not None: - hass.stop() - - def test_icon(): """Test icon validation.""" schema = vol.Schema(cv.icon) @@ -585,3 +565,31 @@ def test_socket_timeout(): # pylint: disable=invalid-name assert _GLOBAL_DEFAULT_TIMEOUT == schema(None) assert schema(1) == 1.0 + + +def test_matches_regex(): + """Test matches_regex validator.""" + schema = vol.Schema(cv.matches_regex('.*uiae.*')) + + with pytest.raises(vol.Invalid): + schema(1.0) + + with pytest.raises(vol.Invalid): + schema(" nrtd ") + + test_str = "This is a test including uiae." + assert(schema(test_str) == test_str) + + +def test_is_regex(): + """Test the is_regex validator.""" + schema = vol.Schema(cv.is_regex) + + with pytest.raises(vol.Invalid): + schema("(") + + with pytest.raises(vol.Invalid): + schema({"a dict": "is not a regex"}) + + valid_re = ".*" + schema(valid_re) diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index b345400ba17..c7b39954d85 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -129,11 +129,11 @@ class TestHelpersDiscovery: platform_calls.append('disc' if discovery_info else 'component') loader.set_component( - 'test_component', + self.hass, 'test_component', MockModule('test_component', setup=component_setup)) loader.set_component( - 'switch.test_circular', + self.hass, 'switch.test_circular', MockPlatform(setup_platform, dependencies=['test_component'])) @@ -177,11 +177,11 @@ class TestHelpersDiscovery: return True loader.set_component( - 'test_component1', + self.hass, 'test_component1', MockModule('test_component1', setup=component1_setup)) loader.set_component( - 'test_component2', + self.hass, 'test_component2', MockModule('test_component2', setup=component2_setup)) @callback diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0bc6a7601dc..504f31cc987 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -75,9 +75,9 @@ class TestHelpersEntityComponent(unittest.TestCase): component_setup = Mock(return_value=True) platform_setup = Mock(return_value=None) loader.set_component( - 'test_component', + self.hass, 'test_component', MockModule('test_component', setup=component_setup)) - loader.set_component('test_domain.mod2', + loader.set_component(self.hass, 'test_domain.mod2', MockPlatform(platform_setup, ['test_component'])) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -100,8 +100,10 @@ class TestHelpersEntityComponent(unittest.TestCase): platform1_setup = Mock(side_effect=Exception('Broken')) platform2_setup = Mock(return_value=None) - loader.set_component('test_domain.mod1', MockPlatform(platform1_setup)) - loader.set_component('test_domain.mod2', MockPlatform(platform2_setup)) + loader.set_component(self.hass, 'test_domain.mod1', + MockPlatform(platform1_setup)) + loader.set_component(self.hass, 'test_domain.mod2', + MockPlatform(platform2_setup)) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -145,7 +147,7 @@ class TestHelpersEntityComponent(unittest.TestCase): """Test the platform setup.""" add_devices([MockEntity(should_poll=True)]) - loader.set_component('test_domain.platform', + loader.set_component(self.hass, 'test_domain.platform', MockPlatform(platform_setup)) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -172,7 +174,7 @@ class TestHelpersEntityComponent(unittest.TestCase): platform = MockPlatform(platform_setup) - loader.set_component('test_domain.platform', platform) + loader.set_component(self.hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -220,7 +222,8 @@ def test_platform_not_ready(hass): """Test that we retry when platform not ready.""" platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None]) - loader.set_component('test_domain.mod1', MockPlatform(platform1_setup)) + loader.set_component(hass, 'test_domain.mod1', + MockPlatform(platform1_setup)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -316,10 +319,11 @@ def test_setup_dependencies_platform(hass): We're explictely testing that we process dependencies even if a component with the same name has already been loaded. """ - loader.set_component('test_component', MockModule('test_component')) - loader.set_component('test_component2', MockModule('test_component2')) + loader.set_component(hass, 'test_component', MockModule('test_component')) + loader.set_component(hass, 'test_component2', + MockModule('test_component2')) loader.set_component( - 'test_domain.test_component', + hass, 'test_domain.test_component', MockPlatform(dependencies=['test_component', 'test_component2'])) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -341,7 +345,7 @@ async def test_setup_entry(hass): """Test setup entry calls async_setup_entry on platform.""" mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( - 'test_domain.entry_domain', + hass, 'test_domain.entry_domain', MockPlatform(async_setup_entry=mock_setup_entry)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -366,7 +370,7 @@ async def test_setup_entry_fails_duplicate(hass): """Test we don't allow setting up a config entry twice.""" mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( - 'test_domain.entry_domain', + hass, 'test_domain.entry_domain', MockPlatform(async_setup_entry=mock_setup_entry)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -382,7 +386,7 @@ async def test_unload_entry_resets_platform(hass): """Test unloading an entry removes all entities.""" mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( - 'test_domain.entry_domain', + hass, 'test_domain.entry_domain', MockPlatform(async_setup_entry=mock_setup_entry)) component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 2018cb27541..4e09f9576f2 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -147,7 +147,7 @@ class TestHelpersEntityPlatform(unittest.TestCase): platform = MockPlatform(platform_setup) platform.SCAN_INTERVAL = timedelta(seconds=30) - loader.set_component('test_domain.platform', platform) + loader.set_component(self.hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -184,7 +184,7 @@ def test_platform_warn_slow_setup(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform() - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -218,7 +218,7 @@ def test_platform_error_slow_setup(hass, caplog): platform = MockPlatform(async_setup_platform=setup_platform) component = EntityComponent(_LOGGER, DOMAIN, hass) - loader.set_component('test_domain.test_platform', platform) + loader.set_component(hass, 'test_domain.test_platform', platform) yield from component.async_setup({ DOMAIN: { 'platform': 'test_platform', @@ -260,7 +260,7 @@ def test_parallel_updates_async_platform(hass): platform.async_setup_platform = mock_update - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) component._platforms = {} @@ -288,7 +288,7 @@ def test_parallel_updates_async_platform_with_constant(hass): platform.async_setup_platform = mock_update platform.PARALLEL_UPDATES = 1 - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) component._platforms = {} @@ -309,7 +309,7 @@ def test_parallel_updates_sync_platform(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform(setup_platform=lambda *args: None) - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) component._platforms = {} diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index a987f5130f1..79054726c03 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -138,7 +138,7 @@ class TestServiceHelpers(unittest.TestCase): self.hass.states.set('light.Ceiling', STATE_OFF) self.hass.states.set('light.Kitchen', STATE_OFF) - loader.get_component('group').Group.create_group( + loader.get_component(self.hass, 'group').Group.create_group( self.hass, 'test', ['light.Ceiling', 'light.Kitchen']) call = ha.ServiceCall('light', 'turn_on', @@ -160,7 +160,7 @@ class TestServiceHelpers(unittest.TestCase): @asyncio.coroutine def test_async_get_all_descriptions(hass): """Test async_get_all_descriptions.""" - group = loader.get_component('group') + group = loader.get_component(hass, 'group') group_config = {group.DOMAIN: {}} yield from async_setup_component(hass, group.DOMAIN, group_config) descriptions = yield from service.async_get_all_descriptions(hass) @@ -170,7 +170,7 @@ def test_async_get_all_descriptions(hass): assert 'description' in descriptions['group']['reload'] assert 'fields' in descriptions['group']['reload'] - logger = loader.get_component('logger') + logger = loader.get_component(hass, 'logger') logger_config = {logger.DOMAIN: {}} yield from async_setup_component(hass, logger.DOMAIN, logger_config) descriptions = yield from service.async_get_all_descriptions(hass) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index c72efca8c29..99c6f7dddf1 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -50,15 +50,15 @@ async def test_component_translation_file(hass): }) assert path.normpath(translation.component_translation_file( - 'switch.test', 'en')) == path.normpath(hass.config.path( + hass, 'switch.test', 'en')) == path.normpath(hass.config.path( 'custom_components', 'switch', '.translations', 'test.en.json')) assert path.normpath(translation.component_translation_file( - 'test_standalone', 'en')) == path.normpath(hass.config.path( + hass, 'test_standalone', 'en')) == path.normpath(hass.config.path( 'custom_components', '.translations', 'test_standalone.en.json')) assert path.normpath(translation.component_translation_file( - 'test_package', 'en')) == path.normpath(hass.config.path( + hass, 'test_package', 'en')) == path.normpath(hass.config.path( 'custom_components', 'test_package', '.translations', 'en.json')) diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 672cc884904..67bfb590c3f 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -119,6 +119,8 @@ class MockNode(MagicMock): product_type='678', command_classes=None, can_wake_up_value=True, + manufacturer_name='Test Manufacturer', + product_name='Test Product', network=None, **kwargs): """Initialize a Z-Wave mock node.""" @@ -128,6 +130,8 @@ class MockNode(MagicMock): self.manufacturer_id = manufacturer_id self.product_id = product_id self.product_type = product_type + self.manufacturer_name = manufacturer_name + self.product_name = product_name self.can_wake_up_value = can_wake_up_value self._command_classes = command_classes or [] if network is not None: diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 28a3f2ebdc8..8dfc5db90e0 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -7,7 +7,6 @@ from unittest.mock import patch import homeassistant.scripts.check_config as check_config from homeassistant.config import YAML_CONFIG_FILE -from homeassistant.loader import set_component from tests.common import patch_yaml_files, get_test_config_dir _LOGGER = logging.getLogger(__name__) @@ -106,7 +105,6 @@ class TestCheckConfig(unittest.TestCase): def test_component_platform_not_found(self, isfile_patch): """Test errors if component or platform not found.""" # Make sure they don't exist - set_component('beer', None) files = { YAML_CONFIG_FILE: BASE_CONFIG + 'beer:', } @@ -119,7 +117,6 @@ class TestCheckConfig(unittest.TestCase): assert res['secrets'] == {} assert len(res['yaml_files']) == 1 - set_component('light.beer', None) files = { YAML_CONFIG_FILE: BASE_CONFIG + 'light:\n platform: beer', } diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 00000000000..4bbf218fd23 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,159 @@ +"""Tests for the Home Assistant auth module.""" +from unittest.mock import Mock + +import pytest + +from homeassistant import auth, data_entry_flow +from tests.common import MockUser, ensure_auth_manager_loaded + + +@pytest.fixture +def mock_hass(): + """Hass mock with minimum amount of data set to make it work with auth.""" + hass = Mock() + hass.config.skip_pip = True + return hass + + +async def test_auth_manager_from_config_validates_config_and_id(mock_hass): + """Test get auth providers.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'name': 'Test Name', + 'type': 'insecure_example', + 'users': [], + }, { + 'name': 'Invalid config because no users', + 'type': 'insecure_example', + 'id': 'invalid_config', + }, { + 'name': 'Test Name 2', + 'type': 'insecure_example', + 'id': 'another', + 'users': [], + }, { + 'name': 'Wrong because duplicate ID', + 'type': 'insecure_example', + 'id': 'another', + 'users': [], + }]) + + providers = [{ + 'name': provider.name, + 'id': provider.id, + 'type': provider.type, + } for provider in manager.async_auth_providers] + assert providers == [{ + 'name': 'Test Name', + 'type': 'insecure_example', + 'id': None, + }, { + 'name': 'Test Name 2', + 'type': 'insecure_example', + 'id': 'another', + }] + + +async def test_create_new_user(mock_hass): + """Test creating new user.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }]) + + step = await manager.login_flow.async_init(('insecure_example', None)) + assert step['type'] == data_entry_flow.RESULT_TYPE_FORM + + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + assert step['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + credentials = step['result'] + user = await manager.async_get_or_create_user(credentials) + assert user is not None + assert user.is_owner is True + assert user.name == 'Test Name' + + +async def test_login_as_existing_user(mock_hass): + """Test login as existing user.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }]) + ensure_auth_manager_loaded(manager) + + # Add fake user with credentials for example auth provider. + user = MockUser( + id='mock-user', + is_owner=False, + is_active=False, + name='Paulus', + ).add_to_auth_manager(manager) + user.credentials.append(auth.Credentials( + id='mock-id', + auth_provider_type='insecure_example', + auth_provider_id=None, + data={'username': 'test-user'}, + is_new=False, + )) + + step = await manager.login_flow.async_init(('insecure_example', None)) + assert step['type'] == data_entry_flow.RESULT_TYPE_FORM + + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + assert step['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + credentials = step['result'] + + user = await manager.async_get_or_create_user(credentials) + assert user is not None + assert user.id == 'mock-user' + assert user.is_owner is False + assert user.is_active is False + assert user.name == 'Paulus' + + +async def test_linking_user_to_two_auth_providers(mock_hass): + """Test linking user to two auth providers.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + }] + }, { + 'type': 'insecure_example', + 'id': 'another-provider', + 'users': [{ + 'username': 'another-user', + 'password': 'another-password', + }] + }]) + + step = await manager.login_flow.async_init(('insecure_example', None)) + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + user = await manager.async_get_or_create_user(step['result']) + assert user is not None + + step = await manager.login_flow.async_init(('insecure_example', + 'another-provider')) + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'another-user', + 'password': 'another-password', + }) + await manager.async_link_user(user, step['result']) + assert len(user.credentials) == 2 diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c109ae30aad..3e4d4739779 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -40,9 +40,9 @@ def test_from_config_file(hass): assert components == hass.config.components -@asyncio.coroutine @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.""" # Extensive HA conf validation testing is done diff --git a/tests/test_config.py b/tests/test_config.py index 652b931366a..4b1115c3814 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -568,7 +568,7 @@ def merge_log_err(hass): yield logerr -def test_merge(merge_log_err): +def test_merge(merge_log_err, hass): """Test if we can merge packages.""" packages = { 'pack_dict': {'input_boolean': {'ib1': None}}, @@ -582,7 +582,7 @@ def test_merge(merge_log_err): 'input_boolean': {'ib2': None}, 'light': {'platform': 'test'} } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 assert len(config) == 5 @@ -592,7 +592,7 @@ def test_merge(merge_log_err): assert config['wake_on_lan'] is None -def test_merge_try_falsy(merge_log_err): +def test_merge_try_falsy(merge_log_err, hass): """Ensure we dont add falsy items like empty OrderedDict() to list.""" packages = { 'pack_falsy_to_lst': {'automation': OrderedDict()}, @@ -603,7 +603,7 @@ def test_merge_try_falsy(merge_log_err): 'automation': {'do': 'something'}, 'light': {'some': 'light'}, } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 assert len(config) == 3 @@ -611,7 +611,7 @@ def test_merge_try_falsy(merge_log_err): assert len(config['light']) == 1 -def test_merge_new(merge_log_err): +def test_merge_new(merge_log_err, hass): """Test adding new components to outer scope.""" packages = { 'pack_1': {'light': [{'platform': 'one'}]}, @@ -624,7 +624,7 @@ def test_merge_new(merge_log_err): config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 assert 'api' in config @@ -633,7 +633,7 @@ def test_merge_new(merge_log_err): assert len(config['panel_custom']) == 1 -def test_merge_type_mismatch(merge_log_err): +def test_merge_type_mismatch(merge_log_err, hass): """Test if we have a type mismatch for packages.""" packages = { 'pack_1': {'input_boolean': [{'ib1': None}]}, @@ -646,7 +646,7 @@ def test_merge_type_mismatch(merge_log_err): 'input_select': [{'ib2': None}], 'light': [{'platform': 'two'}] } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 2 assert len(config) == 4 @@ -654,7 +654,7 @@ def test_merge_type_mismatch(merge_log_err): assert len(config['light']) == 2 -def test_merge_once_only(merge_log_err): +def test_merge_once_only(merge_log_err, hass): """Test if we have a merge for a comp that may occur only once.""" packages = { 'pack_2': { @@ -666,7 +666,7 @@ def test_merge_once_only(merge_log_err): config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, 'mqtt': {}, 'api': {} } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 assert len(config) == 3 @@ -682,13 +682,13 @@ def test_merge_id_schema(hass): 'qwikswitch': 'dict', } for name, expected_type in types.items(): - module = config_util.get_component(name) + module = config_util.get_component(hass, name) typ, _ = config_util._identify_config_schema(module) assert typ == expected_type, "{} expected {}, got {}".format( name, expected_type, typ) -def test_merge_duplicate_keys(merge_log_err): +def test_merge_duplicate_keys(merge_log_err, hass): """Test if keys in dicts are duplicates.""" packages = { 'pack_1': {'input_select': {'ib1': None}}, @@ -697,7 +697,7 @@ def test_merge_duplicate_keys(merge_log_err): config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, 'input_select': {'ib1': None}, } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 assert len(config) == 2 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 94b1dcb47da..1518706db55 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -27,7 +27,7 @@ def test_call_setup_entry(hass): mock_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'comp', + hass, 'comp', MockModule('comp', async_setup_entry=mock_setup_entry)) result = yield from async_setup_component(hass, 'comp', {}) @@ -36,12 +36,12 @@ def test_call_setup_entry(hass): @asyncio.coroutine -def test_remove_entry(manager): +def test_remove_entry(hass, manager): """Test that we can remove an entry.""" mock_unload_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'test', + hass, 'test', MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) @@ -63,7 +63,7 @@ def test_remove_entry(manager): @asyncio.coroutine -def test_remove_entry_raises(manager): +def test_remove_entry_raises(hass, manager): """Test if a component raises while removing entry.""" @asyncio.coroutine def mock_unload_entry(hass, entry): @@ -71,7 +71,7 @@ def test_remove_entry_raises(manager): raise Exception("BROKEN") loader.set_component( - 'test', + hass, 'test', MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) @@ -96,7 +96,7 @@ def test_add_entry_calls_setup_entry(hass, manager): mock_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'comp', + hass, 'comp', MockModule('comp', async_setup_entry=mock_setup_entry)) class TestFlow(data_entry_flow.FlowHandler): @@ -151,6 +151,8 @@ def test_domains_gets_uniques(manager): @asyncio.coroutine def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" + loader.set_component(hass, 'test', MockModule('test')) + class TestFlow(data_entry_flow.FlowHandler): VERSION = 5 @@ -217,12 +219,12 @@ async def test_forward_entry_sets_up_component(hass): mock_original_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'original', + hass, 'original', MockModule('original', async_setup_entry=mock_original_setup_entry)) mock_forwarded_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'forwarded', + hass, 'forwarded', MockModule('forwarded', async_setup_entry=mock_forwarded_setup_entry)) await hass.config_entries.async_forward_entry_setup(entry, 'forwarded') @@ -236,7 +238,7 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): mock_setup = MagicMock(return_value=mock_coro(False)) mock_setup_entry = MagicMock() - loader.set_component('forwarded', MockModule( + hass, loader.set_component(hass, 'forwarded', MockModule( 'forwarded', async_setup=mock_setup, async_setup_entry=mock_setup_entry, @@ -245,3 +247,40 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): await hass.config_entries.async_forward_entry_setup(entry, 'forwarded') assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_discovery_notification(hass): + """Test that we create/dismiss a notification when source is discovery.""" + loader.set_component(hass, 'test', MockModule('test')) + await async_setup_component(hass, 'persistent_notification', {}) + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + if user_input is not None: + return self.async_create_entry( + title='Test Title', + data={ + 'token': 'abcd' + } + ) + return self.async_show_form( + step_id='discovery', + ) + + with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): + result = await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY) + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is not None + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], {}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is None diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 2767e206c30..6d3e41436c5 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -12,7 +12,7 @@ def manager(): handlers = Registry() entries = [] - async def async_create_flow(handler_name): + async def async_create_flow(handler_name, *, source, data): handler = handlers.get(handler_name) if handler is None: diff --git a/tests/test_loader.py b/tests/test_loader.py index 7fc33df57bb..c97e94a7ce1 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -27,37 +27,39 @@ class TestLoader(unittest.TestCase): def test_set_component(self): """Test if set_component works.""" - loader.set_component('switch.test_set', http) + comp = object() + loader.set_component(self.hass, 'switch.test_set', comp) - self.assertEqual(http, loader.get_component('switch.test_set')) + assert loader.get_component(self.hass, 'switch.test_set') is comp def test_get_component(self): """Test if get_component works.""" - self.assertEqual(http, loader.get_component('http')) - - self.assertIsNotNone(loader.get_component('switch.test')) + self.assertEqual(http, loader.get_component(self.hass, 'http')) + self.assertIsNotNone(loader.get_component(self.hass, 'light.hue')) def test_load_order_component(self): """Test if we can get the proper load order of components.""" - loader.set_component('mod1', MockModule('mod1')) - loader.set_component('mod2', MockModule('mod2', ['mod1'])) - loader.set_component('mod3', MockModule('mod3', ['mod2'])) + loader.set_component(self.hass, 'mod1', MockModule('mod1')) + loader.set_component(self.hass, 'mod2', MockModule('mod2', ['mod1'])) + loader.set_component(self.hass, 'mod3', MockModule('mod3', ['mod2'])) self.assertEqual( - ['mod1', 'mod2', 'mod3'], loader.load_order_component('mod3')) + ['mod1', 'mod2', 'mod3'], + loader.load_order_component(self.hass, 'mod3')) # Create circular dependency - loader.set_component('mod1', MockModule('mod1', ['mod3'])) + loader.set_component(self.hass, 'mod1', MockModule('mod1', ['mod3'])) - self.assertEqual([], loader.load_order_component('mod3')) + self.assertEqual([], loader.load_order_component(self.hass, 'mod3')) # Depend on non-existing component - loader.set_component('mod1', MockModule('mod1', ['nonexisting'])) + loader.set_component(self.hass, 'mod1', + MockModule('mod1', ['nonexisting'])) - self.assertEqual([], loader.load_order_component('mod1')) + self.assertEqual([], loader.load_order_component(self.hass, 'mod1')) # Try to get load order for non-existing component - self.assertEqual([], loader.load_order_component('mod1')) + self.assertEqual([], loader.load_order_component(self.hass, 'mod1')) def test_component_loader(hass): @@ -103,3 +105,22 @@ def test_helpers_wrapper(hass): yield from hass.async_block_till_done() assert result == ['hello'] + + +async def test_custom_component_name(hass): + """Test the name attribte of custom components.""" + comp = loader.get_component(hass, 'test_standalone') + assert comp.__name__ == 'custom_components.test_standalone' + assert comp.__package__ == 'custom_components' + + comp = loader.get_component(hass, 'test_package') + assert comp.__name__ == 'custom_components.test_package' + assert comp.__package__ == 'custom_components.test_package' + + comp = loader.get_component(hass, 'light.test') + assert comp.__name__ == 'custom_components.light.test' + assert comp.__package__ == 'custom_components.light' + + # Test custom components is mounted + from custom_components.test_package import TEST + assert TEST == 5 diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 5f09e0bd83e..8ae0f6c11de 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -35,7 +35,8 @@ class TestRequirements: mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) + self.hass, 'comp', + MockModule('comp', requirements=['package==0.0.1'])) assert setup.setup_component(self.hass, 'comp') assert 'comp' in self.hass.config.components assert mock_install.call_args == mock.call( @@ -53,7 +54,8 @@ class TestRequirements: mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) + self.hass, 'comp', + MockModule('comp', requirements=['package==0.0.1'])) assert setup.setup_component(self.hass, 'comp') assert 'comp' in self.hass.config.components assert mock_install.call_args == mock.call( diff --git a/tests/test_setup.py b/tests/test_setup.py index 6a94310793c..6f0c282e016 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -49,6 +49,7 @@ class TestSetup: } }, required=True) loader.set_component( + self.hass, 'comp_conf', MockModule('comp_conf', config_schema=config_schema)) with assert_setup_component(0): @@ -93,10 +94,12 @@ class TestSetup: 'hello': str, }) loader.set_component( + self.hass, 'platform_conf', MockModule('platform_conf', platform_schema=platform_schema)) loader.set_component( + self.hass, 'platform_conf.whatever', MockPlatform('whatever')) with assert_setup_component(0): @@ -179,7 +182,8 @@ class TestSetup: """Test we do not setup a component twice.""" mock_setup = mock.MagicMock(return_value=True) - loader.set_component('comp', MockModule('comp', setup=mock_setup)) + loader.set_component( + self.hass, 'comp', MockModule('comp', setup=mock_setup)) assert setup.setup_component(self.hass, 'comp') assert mock_setup.called @@ -195,6 +199,7 @@ class TestSetup: """Component setup should fail if requirement can't install.""" self.hass.config.skip_pip = False loader.set_component( + self.hass, 'comp', MockModule('comp', requirements=['package==0.0.1'])) assert not setup.setup_component(self.hass, 'comp') @@ -210,6 +215,7 @@ class TestSetup: result.append(1) loader.set_component( + self.hass, 'comp', MockModule('comp', async_setup=async_setup)) def setup_component(): @@ -227,20 +233,23 @@ class TestSetup: def test_component_not_setup_missing_dependencies(self): """Test we do not setup a component if not all dependencies loaded.""" deps = ['non_existing'] - loader.set_component('comp', MockModule('comp', dependencies=deps)) + loader.set_component( + self.hass, 'comp', MockModule('comp', dependencies=deps)) assert not setup.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components self.hass.data.pop(setup.DATA_SETUP) - loader.set_component('non_existing', MockModule('non_existing')) + loader.set_component( + self.hass, 'non_existing', MockModule('non_existing')) assert setup.setup_component(self.hass, 'comp', {}) def test_component_failing_setup(self): """Test component that fails setup.""" loader.set_component( - 'comp', MockModule('comp', setup=lambda hass, config: False)) + self.hass, 'comp', + MockModule('comp', setup=lambda hass, config: False)) assert not setup.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components @@ -251,7 +260,8 @@ class TestSetup: """Setup that raises exception.""" raise Exception('fail!') - loader.set_component('comp', MockModule('comp', setup=exception_setup)) + loader.set_component( + self.hass, 'comp', MockModule('comp', setup=exception_setup)) assert not setup.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components @@ -264,11 +274,12 @@ class TestSetup: return True raise Exception('Config not passed in: {}'.format(config)) - loader.set_component('comp_a', - MockModule('comp_a', setup=config_check_setup)) + loader.set_component( + self.hass, 'comp_a', + MockModule('comp_a', setup=config_check_setup)) - loader.set_component('switch.platform_a', MockPlatform('comp_b', - ['comp_a'])) + loader.set_component( + self.hass, 'switch.platform_a', MockPlatform('comp_b', ['comp_a'])) setup.setup_component(self.hass, 'switch', { 'comp_a': { @@ -289,6 +300,7 @@ class TestSetup: mock_setup = mock.MagicMock(spec_set=True) loader.set_component( + self.hass, 'switch.platform_a', MockPlatform(platform_schema=platform_schema, setup_platform=mock_setup)) @@ -330,29 +342,34 @@ class TestSetup: def test_disable_component_if_invalid_return(self): """Test disabling component if invalid return.""" loader.set_component( + self.hass, 'disabled_component', MockModule('disabled_component', setup=lambda hass, config: None)) assert not setup.setup_component(self.hass, 'disabled_component') - assert loader.get_component('disabled_component') is None + assert loader.get_component(self.hass, 'disabled_component') is None assert 'disabled_component' not in self.hass.config.components self.hass.data.pop(setup.DATA_SETUP) loader.set_component( + self.hass, 'disabled_component', MockModule('disabled_component', setup=lambda hass, config: False)) assert not setup.setup_component(self.hass, 'disabled_component') - assert loader.get_component('disabled_component') is not None + assert loader.get_component( + self.hass, 'disabled_component') is not None assert 'disabled_component' not in self.hass.config.components self.hass.data.pop(setup.DATA_SETUP) loader.set_component( + self.hass, 'disabled_component', MockModule('disabled_component', setup=lambda hass, config: True)) assert setup.setup_component(self.hass, 'disabled_component') - assert loader.get_component('disabled_component') is not None + assert loader.get_component( + self.hass, 'disabled_component') is not None assert 'disabled_component' in self.hass.config.components def test_all_work_done_before_start(self): @@ -373,14 +390,17 @@ class TestSetup: return True loader.set_component( + self.hass, 'test_component1', MockModule('test_component1', setup=component1_setup)) loader.set_component( + self.hass, 'test_component2', MockModule('test_component2', setup=component_track_setup)) loader.set_component( + self.hass, 'test_component3', MockModule('test_component3', setup=component_track_setup)) @@ -409,7 +429,8 @@ def test_component_cannot_depend_config(hass): @asyncio.coroutine def test_component_warn_slow_setup(hass): """Warn we log when a component setup takes a long time.""" - loader.set_component('test_component1', MockModule('test_component1')) + loader.set_component( + hass, 'test_component1', MockModule('test_component1')) with mock.patch.object(hass.loop, 'call_later', mock.MagicMock()) \ as mock_call: result = yield from setup.async_setup_component( @@ -430,7 +451,7 @@ def test_component_warn_slow_setup(hass): def test_platform_no_warn_slow(hass): """Do not warn for long entity setup time.""" loader.set_component( - 'test_component1', + hass, 'test_component1', MockModule('test_component1', platform_schema=PLATFORM_SCHEMA)) with mock.patch.object(hass.loop, 'call_later', mock.MagicMock()) \ as mock_call: diff --git a/tests/testing_config/custom_components/image_processing/test.py b/tests/testing_config/custom_components/image_processing/test.py index 29d362699f5..b50050ed68e 100644 --- a/tests/testing_config/custom_components/image_processing/test.py +++ b/tests/testing_config/custom_components/image_processing/test.py @@ -3,9 +3,11 @@ from homeassistant.components.image_processing import ImageProcessingEntity -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Set up the test image_processing platform.""" - add_devices([TestImageProcessing('camera.demo_camera', "Test")]) + async_add_devices_callback([ + TestImageProcessing('camera.demo_camera', "Test")]) class TestImageProcessing(ImageProcessingEntity): diff --git a/tests/testing_config/custom_components/light/test.py b/tests/testing_config/custom_components/light/test.py index 71625dfdf93..fbf79f9e770 100644 --- a/tests/testing_config/custom_components/light/test.py +++ b/tests/testing_config/custom_components/light/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Return mock devices.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/switch/test.py b/tests/testing_config/custom_components/switch/test.py index 2819f2f2951..79126b7b52a 100644 --- a/tests/testing_config/custom_components/switch/test.py +++ b/tests/testing_config/custom_components/switch/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Find and return test switches.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index 528f056948b..85e78a7f9d6 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,7 +1,10 @@ """Provide a mock package component.""" +from .const import TEST # noqa + + DOMAIN = 'test_package' -def setup(hass, config): +async def async_setup(hass, config): """Mock a successful setup.""" return True diff --git a/tests/testing_config/custom_components/test_package/const.py b/tests/testing_config/custom_components/test_package/const.py new file mode 100644 index 00000000000..7e13e04cb47 --- /dev/null +++ b/tests/testing_config/custom_components/test_package/const.py @@ -0,0 +1,2 @@ +"""Constants for test_package custom component.""" +TEST = 5 diff --git a/tests/testing_config/custom_components/test_standalone.py b/tests/testing_config/custom_components/test_standalone.py index f0d4ba7982b..de3a360a4da 100644 --- a/tests/testing_config/custom_components/test_standalone.py +++ b/tests/testing_config/custom_components/test_standalone.py @@ -2,6 +2,6 @@ DOMAIN = 'test_standalone' -def setup(hass, config): +async def async_setup(hass, config): """Mock a successful setup.""" return True diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index bd70af28dce..302dfba2e1d 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -25,6 +25,8 @@ PACKAGES=( libsodium13 # homeassistant.components.zwave libudev-dev + # homeassistant.components.homekit_controller + libmpc-dev libmpfr-dev libgmp-dev ) # Required debian packages for building dependencies