diff --git a/.coveragerc b/.coveragerc index d059d62b5f31a9..a100e2c0a4958d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -192,7 +192,7 @@ omit = homeassistant/components/mychevy.py homeassistant/components/*/mychevy.py - homeassistant/components/mysensors.py + homeassistant/components/mysensors/* homeassistant/components/*/mysensors.py homeassistant/components/neato.py @@ -612,6 +612,7 @@ omit = homeassistant/components/sensor/domain_expiry.py homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dublin_bus_transport.py + homeassistant/components/sensor/duke_energy.py homeassistant/components/sensor/dwd_weather_warnings.py homeassistant/components/sensor/ebox.py homeassistant/components/sensor/eddystone_temperature.py diff --git a/.gitignore b/.gitignore index bf49a1b61c1fea..c2b0d964a6225a 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,6 @@ desktop.ini # Secrets .lokalise_token + +# monkeytype +monkeytype.sqlite3 diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000000000..79a65508287448 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +multi_line_output=4 diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 5e434b74ca82db..e6760cd9096bc2 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -1,26 +1,27 @@ """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 os import uuid +from collections import OrderedDict +from datetime import datetime, timedelta 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.util.decorator import Registry +from homeassistant.core import callback from homeassistant.util import dt as dt_util - +from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) +STORAGE_VERSION = 1 +STORAGE_KEY = 'auth' AUTH_PROVIDERS = Registry() @@ -78,7 +79,14 @@ def name(self): async def async_credentials(self): """Return all credentials of this provider.""" - return await self.store.credentials_for_provider(self.type, self.id) + users = await self.store.async_get_users() + return [ + credentials + for user in users + for credentials in user.credentials + if (credentials.auth_provider_type == self.type and + credentials.auth_provider_id == self.id) + ] @callback def async_create_credentials(self, data): @@ -117,27 +125,17 @@ async def async_user_meta_for_credentials(self, credentials): class User: """A user.""" + name = attr.ib(type=str) 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) + system_generated = attr.ib(type=bool, default=False) # List of credentials of a user. - credentials = attr.ib(type=list, default=attr.Factory(list)) + credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False) # 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, - } + refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False) @attr.s(slots=True) @@ -152,7 +150,7 @@ class RefreshToken: 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)) + access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False) @attr.s(slots=True) @@ -168,9 +166,10 @@ class AccessToken: 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 + def expired(self): + """Return if this token has expired.""" + expires = self.created_at + self.refresh_token.access_token_expiration + return dt_util.utcnow() > expires @attr.s(slots=True) @@ -281,7 +280,24 @@ def __init__(self, hass, store, providers): self.login_flow = data_entry_flow.FlowManager( hass, self._async_create_login_flow, self._async_finish_login_flow) - self.access_tokens = {} + self._access_tokens = {} + + @property + def active(self): + """Return if any auth providers are registered.""" + return bool(self._providers) + + @property + def support_legacy(self): + """ + Return if legacy_api_password auth providers are registered. + + Should be removed when we removed legacy_api_password auth providers. + """ + for provider_type, _ in self._providers: + if provider_type == 'legacy_api_password': + return True + return False @property def async_auth_providers(self): @@ -292,10 +308,45 @@ async def async_get_user(self, user_id): """Retrieve a user.""" return await self._store.async_get_user(user_id) + async def async_create_system_user(self, name): + """Create a system user.""" + return await self._store.async_create_user( + name=name, + system_generated=True, + is_active=True, + ) + 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)) + if not credentials.is_new: + for user in await self._store.async_get_users(): + 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('Unable to find the user.') + + auth_provider = self._async_get_auth_provider(credentials) + info = await auth_provider.async_user_meta_for_credentials( + credentials) + + kwargs = { + 'credentials': credentials, + 'name': info.get('name') + } + + # Make owner and activate user if it's the first user. + if await self._store.async_get_users(): + kwargs['is_owner'] = False + kwargs['is_active'] = False + else: + kwargs['is_owner'] = True + kwargs['is_active'] = True + + return await self._store.async_create_user(**kwargs) async def async_link_user(self, user, credentials): """Link credentials to an existing user.""" @@ -305,9 +356,20 @@ 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): + async def async_create_refresh_token(self, user, client=None): """Create a new refresh token for a user.""" - return await self._store.async_create_refresh_token(user, client_id) + if not user.is_active: + raise ValueError('User is not active') + + if user.system_generated and client is not None: + raise ValueError( + 'System generated users cannot have refresh tokens connected ' + 'to a client.') + + if not user.system_generated and client is None: + raise ValueError('Client is required to generate a refresh token.') + + return await self._store.async_create_refresh_token(user, client) async def async_get_refresh_token(self, token): """Get refresh token by token.""" @@ -316,14 +378,23 @@ async def async_get_refresh_token(self, 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 + access_token = AccessToken(refresh_token=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) + tkn = self._access_tokens.get(token) + + if tkn is None: + return None + + if tkn.expired: + self._access_tokens.pop(token) + return None + + return tkn async def async_create_client(self, name, *, redirect_uris=None, no_secret=False): @@ -331,6 +402,16 @@ async def async_create_client(self, name, *, redirect_uris=None, return await self._store.async_create_client( name, redirect_uris, no_secret) + async def async_get_or_create_client(self, name, *, redirect_uris=None, + no_secret=False): + """Find a client, if not exists, create a new one.""" + for client in await self._store.async_get_clients(): + if client.name == name: + return client + + return await self._store.async_create_client( + name, redirect_uris, no_secret) + async def async_get_client(self, client_id): """Get a client.""" return await self._store.async_get_client(client_id) @@ -374,68 +455,54 @@ class AuthStore: 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) + self._users = None + self._clients = None + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - async def credentials_for_provider(self, provider_type, provider_id): - """Return credentials for specific auth provider type and id.""" - if self.users is None: + async def async_get_users(self): + """Retrieve all users.""" + 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) - ] + return list(self._users.values()) async def async_get_user(self, user_id): - """Retrieve a user.""" - if self.users is None: + """Retrieve a user by id.""" + if self._users is None: await self.async_load() - return self.users.get(user_id) + 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: + async def async_create_user(self, name, is_owner=None, is_active=None, + system_generated=None, credentials=None): + """Create a new user.""" + 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 + kwargs = { + 'name': name + } + + if is_owner is not None: + kwargs['is_owner'] = is_owner + + if is_active is not None: + kwargs['is_active'] = is_active - 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 + if system_generated is not None: + kwargs['system_generated'] = system_generated - raise ValueError('We got credentials with ID but found no user') + new_user = User(**kwargs) + + self._users[new_user.id] = new_user + + if credentials is None: + await self.async_save() + return new_user + + # Saving is done inside the link. + await self.async_link_user(new_user, credentials) + return new_user async def async_link_user(self, user, credentials): """Add credentials to an existing user.""" @@ -445,22 +512,23 @@ async def async_link_user(self, user, credentials): async def async_remove_user(self, user): """Remove a user.""" - self.users.pop(user.id) + self._users.pop(user.id) await self.async_save() - async def async_create_refresh_token(self, user, client_id): + async def async_create_refresh_token(self, user, client=None): """Create a new token for a user.""" - refresh_token = RefreshToken(user, client_id) + client_id = client.id if client is not None else None + refresh_token = RefreshToken(user=user, client_id=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: + if self._users is None: await self.async_load() - for user in self.users.values(): + for user in self._users.values(): refresh_token = user.refresh_tokens.get(token) if refresh_token is not None: return refresh_token @@ -469,7 +537,7 @@ async def async_get_refresh_token(self, token): async def async_create_client(self, name, redirect_uris, no_secret): """Create a new client.""" - if self.clients is None: + if self._clients is None: await self.async_load() kwargs = { @@ -481,23 +549,149 @@ async def async_create_client(self, name, redirect_uris, no_secret): kwargs['secret'] = None client = Client(**kwargs) - self.clients[client.id] = client + self._clients[client.id] = client await self.async_save() return client + async def async_get_clients(self): + """Return all clients.""" + if self._clients is None: + await self.async_load() + + return list(self._clients.values()) + async def async_get_client(self, client_id): """Get a client.""" - if self.clients is None: + if self._clients is None: await self.async_load() - return self.clients.get(client_id) + return self._clients.get(client_id) async def async_load(self): """Load the users.""" - async with self._load_lock: - self.users = {} - self.clients = {} + data = await self._store.async_load() + + # Make sure that we're not overriding data if 2 loads happened at the + # same time + if self._users is not None: + return + + if data is None: + self._users = {} + self._clients = {} + return + + users = { + user_dict['id']: User(**user_dict) for user_dict in data['users'] + } + + for cred_dict in data['credentials']: + users[cred_dict['user_id']].credentials.append(Credentials( + id=cred_dict['id'], + is_new=False, + auth_provider_type=cred_dict['auth_provider_type'], + auth_provider_id=cred_dict['auth_provider_id'], + data=cred_dict['data'], + )) + + refresh_tokens = {} + + for rt_dict in data['refresh_tokens']: + token = RefreshToken( + id=rt_dict['id'], + user=users[rt_dict['user_id']], + client_id=rt_dict['client_id'], + created_at=dt_util.parse_datetime(rt_dict['created_at']), + access_token_expiration=timedelta( + seconds=rt_dict['access_token_expiration']), + token=rt_dict['token'], + ) + refresh_tokens[token.id] = token + users[rt_dict['user_id']].refresh_tokens[token.token] = token + + for ac_dict in data['access_tokens']: + refresh_token = refresh_tokens[ac_dict['refresh_token_id']] + token = AccessToken( + refresh_token=refresh_token, + created_at=dt_util.parse_datetime(ac_dict['created_at']), + token=ac_dict['token'], + ) + refresh_token.access_tokens.append(token) + + clients = { + cl_dict['id']: Client(**cl_dict) for cl_dict in data['clients'] + } + + self._users = users + self._clients = clients async def async_save(self): """Save users.""" - pass + users = [ + { + 'id': user.id, + 'is_owner': user.is_owner, + 'is_active': user.is_active, + 'name': user.name, + 'system_generated': user.system_generated, + } + for user in self._users.values() + ] + + credentials = [ + { + 'id': credential.id, + 'user_id': user.id, + 'auth_provider_type': credential.auth_provider_type, + 'auth_provider_id': credential.auth_provider_id, + 'data': credential.data, + } + for user in self._users.values() + for credential in user.credentials + ] + + refresh_tokens = [ + { + 'id': refresh_token.id, + 'user_id': user.id, + 'client_id': refresh_token.client_id, + 'created_at': refresh_token.created_at.isoformat(), + 'access_token_expiration': + refresh_token.access_token_expiration.total_seconds(), + 'token': refresh_token.token, + } + for user in self._users.values() + for refresh_token in user.refresh_tokens.values() + ] + + access_tokens = [ + { + 'id': user.id, + 'refresh_token_id': refresh_token.id, + 'created_at': access_token.created_at.isoformat(), + 'token': access_token.token, + } + for user in self._users.values() + for refresh_token in user.refresh_tokens.values() + for access_token in refresh_token.access_tokens + ] + + clients = [ + { + 'id': client.id, + 'name': client.name, + 'secret': client.secret, + 'redirect_uris': client.redirect_uris, + } + for client in self._clients.values() + ] + + data = { + 'users': users, + 'clients': clients, + 'credentials': credentials, + 'access_tokens': access_tokens, + 'refresh_tokens': refresh_tokens, + } + + await self._store.async_save(data, delay=1) diff --git a/homeassistant/auth_providers/homeassistant.py b/homeassistant/auth_providers/homeassistant.py index c2db193ce1a1a2..c4d2021f6ce054 100644 --- a/homeassistant/auth_providers/homeassistant.py +++ b/homeassistant/auth_providers/homeassistant.py @@ -8,10 +8,10 @@ from homeassistant import auth, data_entry_flow from homeassistant.exceptions import HomeAssistantError -from homeassistant.util import json -PATH_DATA = '.users.json' +STORAGE_VERSION = 1 +STORAGE_KEY = 'auth_provider.homeassistant' CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA) @@ -31,14 +31,22 @@ class InvalidUser(HomeAssistantError): class Data: """Hold the user data.""" - def __init__(self, path, data): + def __init__(self, hass): """Initialize the user data store.""" - self.path = path + self.hass = hass + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._data = None + + async def async_load(self): + """Load stored data.""" + data = await self._store.async_load() + if data is None: data = { 'salt': auth.generate_secret(), 'users': [] } + self._data = data @property @@ -99,14 +107,9 @@ def change_password(self, username, new_password): else: raise InvalidUser - def save(self): + async def async_save(self): """Save data.""" - json.save_json(self.path, self._data) - - -def load_data(path): - """Load auth data.""" - return Data(path, json.load_json(path, None)) + await self._store.async_save(self._data) @auth.AUTH_PROVIDERS.register('homeassistant') @@ -121,12 +124,10 @@ async def async_credential_flow(self): async def async_validate_login(self, username, password): """Helper to validate a username and password.""" - def validate(): - """Validate creds.""" - data = self._auth_data() - data.validate_login(username, password) - - await self.hass.async_add_job(validate) + data = Data(self.hass) + await data.async_load() + await self.hass.async_add_executor_job( + data.validate_login, username, password) async def async_get_or_create_credentials(self, flow_result): """Get credentials based on the flow result.""" @@ -141,10 +142,6 @@ async def async_get_or_create_credentials(self, flow_result): 'username': username }) - def _auth_data(self): - """Return the auth provider data.""" - return load_data(self.hass.config.path(PATH_DATA)) - class LoginFlow(data_entry_flow.FlowHandler): """Handler for the login flow.""" diff --git a/homeassistant/auth_providers/legacy_api_password.py b/homeassistant/auth_providers/legacy_api_password.py new file mode 100644 index 00000000000000..510cc4d02792fe --- /dev/null +++ b/homeassistant/auth_providers/legacy_api_password.py @@ -0,0 +1,104 @@ +""" +Support Legacy API password auth provider. + +It will be removed when auth system production ready +""" +from collections import OrderedDict +import hmac + +import voluptuous as vol + +from homeassistant.exceptions import HomeAssistantError +from homeassistant import auth, data_entry_flow +from homeassistant.core import callback + +USER_SCHEMA = vol.Schema({ + vol.Required('username'): str, +}) + + +CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ +}, extra=vol.PREVENT_EXTRA) + +LEGACY_USER = 'homeassistant' + + +class InvalidAuthError(HomeAssistantError): + """Raised when submitting invalid authentication.""" + + +@auth.AUTH_PROVIDERS.register('legacy_api_password') +class LegacyApiPasswordAuthProvider(auth.AuthProvider): + """Example auth provider based on hardcoded usernames and passwords.""" + + DEFAULT_TITLE = 'Legacy API Password' + + async def async_credential_flow(self): + """Return a flow to login.""" + return LoginFlow(self) + + @callback + def async_validate_login(self, password): + """Helper to validate a username and password.""" + if not hasattr(self.hass, 'http'): + raise ValueError('http component is not loaded') + + if self.hass.http.api_password is None: + raise ValueError('http component is not configured using' + ' api_password') + + if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'), + password.encode('utf-8')): + raise InvalidAuthError + + async def async_get_or_create_credentials(self, flow_result): + """Return LEGACY_USER always.""" + for credential in await self.async_credentials(): + if credential.data['username'] == LEGACY_USER: + return credential + + return self.async_create_credentials({ + 'username': LEGACY_USER + }) + + async def async_user_meta_for_credentials(self, credentials): + """ + Set name as LEGACY_USER always. + + Will be used to populate info when creating a new user. + """ + return {'name': LEGACY_USER} + + +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['password']) + except InvalidAuthError: + errors['base'] = 'invalid_auth' + + if not errors: + return self.async_create_entry( + title=self._auth_provider.name, + data={} + ) + + schema = OrderedDict() + 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 b108ac805e9f67..0a71c2887b13d8 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -123,7 +123,6 @@ async def async_from_config_dict(config: Dict[str, Any], components.update(hass.config_entries.async_domains()) # setup components - # pylint: disable=not-an-iterable res = await core_components.async_setup(hass, config) if not res: _LOGGER.error("Home Assistant core failed to initialize. " diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 25e303cbe853c3..f81d2ef1037cd6 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -154,7 +154,6 @@ def async_alarm_service_handler(service): return True -# pylint: disable=no-self-use class AlarmControlPanel(Entity): """An abstract class for alarm control devices.""" diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 8a0dfefdc70849..9f2a4176ed8862 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -20,7 +20,7 @@ from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - MqttAvailability) + CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -54,6 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), config.get(CONF_QOS), + config.get(CONF_RETAIN), config.get(CONF_PAYLOAD_DISARM), config.get(CONF_PAYLOAD_ARM_HOME), config.get(CONF_PAYLOAD_ARM_AWAY), @@ -66,9 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" - def __init__(self, name, state_topic, command_topic, qos, payload_disarm, - payload_arm_home, payload_arm_away, code, availability_topic, - payload_available, payload_not_available): + def __init__(self, name, state_topic, command_topic, qos, retain, + payload_disarm, payload_arm_home, payload_arm_away, code, + availability_topic, payload_available, payload_not_available): """Init the MQTT Alarm Control Panel.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -77,6 +78,7 @@ def __init__(self, name, state_topic, command_topic, qos, payload_disarm, self._state_topic = state_topic self._command_topic = command_topic self._qos = qos + self._retain = retain self._payload_disarm = payload_disarm self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away @@ -134,7 +136,8 @@ def async_alarm_disarm(self, code=None): if not self._validate_code(code, 'disarming'): return mqtt.async_publish( - self.hass, self._command_topic, self._payload_disarm, self._qos) + self.hass, self._command_topic, self._payload_disarm, self._qos, + self._retain) @asyncio.coroutine def async_alarm_arm_home(self, code=None): @@ -145,7 +148,8 @@ def async_alarm_arm_home(self, code=None): if not self._validate_code(code, 'arming home'): return mqtt.async_publish( - self.hass, self._command_topic, self._payload_arm_home, self._qos) + self.hass, self._command_topic, self._payload_arm_home, self._qos, + self._retain) @asyncio.coroutine def async_alarm_arm_away(self, code=None): @@ -156,7 +160,8 @@ def async_alarm_arm_away(self, code=None): if not self._validate_code(code, 'arming away'): return mqtt.async_publish( - self.hass, self._command_topic, self._payload_arm_away, self._qos) + self.hass, self._command_topic, self._payload_arm_away, self._qos, + self._retain) def _validate_code(self, code, state): """Validate given code.""" diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index c5c68f1af40fa7..ff2d4adf30dc38 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -107,7 +107,6 @@ class _DisplayCategory(object): THERMOSTAT = "THERMOSTAT" # Indicates the endpoint is a television. - # pylint: disable=invalid-name TV = "TV" @@ -1474,9 +1473,6 @@ async def async_api_set_thermostat_mode(hass, config, request, entity): mode = mode if isinstance(mode, str) else mode['value'] operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST) - # Work around a pylint false positive due to - # https://github.com/PyCQA/pylint/issues/1830 - # pylint: disable=stop-iteration-return ha_mode = next( (k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ae89e2fc3b62c1..b80a571606161e 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -81,7 +81,6 @@ class APIEventStream(HomeAssistantView): async def get(self, request): """Provide a streaming interface for the event bus.""" - # pylint: disable=no-self-use hass = request.app['hass'] stop_obj = object() to_write = asyncio.Queue(loop=hass.loop) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index fa58c9b0baa897..475e43e55a4023 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -16,7 +16,7 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send -REQUIREMENTS = ['pyarlo==0.1.8'] +REQUIREMENTS = ['pyarlo==0.1.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 0f7295a41e0938..511999c52abaff 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -236,18 +236,16 @@ async def post(self, request, client): grant_type = data.get('grant_type') if grant_type == 'authorization_code': - return await self._async_handle_auth_code( - hass, client.id, data) + return await self._async_handle_auth_code(hass, client, data) elif grant_type == 'refresh_token': - return await self._async_handle_refresh_token( - hass, client.id, data) + return await self._async_handle_refresh_token(hass, client, data) return self.json({ 'error': 'unsupported_grant_type', }, status_code=400) - async def _async_handle_auth_code(self, hass, client_id, data): + async def _async_handle_auth_code(self, hass, client, data): """Handle authorization code request.""" code = data.get('code') @@ -256,7 +254,7 @@ async def _async_handle_auth_code(self, hass, client_id, data): 'error': 'invalid_request', }, status_code=400) - credentials = self._retrieve_credentials(client_id, code) + credentials = self._retrieve_credentials(client.id, code) if credentials is None: return self.json({ @@ -265,7 +263,7 @@ async def _async_handle_auth_code(self, hass, client_id, data): user = await hass.auth.async_get_or_create_user(credentials) refresh_token = await hass.auth.async_create_refresh_token(user, - client_id) + client) access_token = hass.auth.async_create_access_token(refresh_token) return self.json({ @@ -276,7 +274,7 @@ async def _async_handle_auth_code(self, hass, client_id, data): int(refresh_token.access_token_expiration.total_seconds()), }) - async def _async_handle_refresh_token(self, hass, client_id, data): + async def _async_handle_refresh_token(self, hass, client, data): """Handle authorization code request.""" token = data.get('refresh_token') @@ -287,7 +285,7 @@ async def _async_handle_refresh_token(self, hass, client_id, data): refresh_token = await hass.auth.async_get_refresh_token(token) - if refresh_token is None or refresh_token.client_id != client_id: + if refresh_token is None or refresh_token.client_id != client.id: return self.json({ 'error': 'invalid_grant', }, status_code=400) diff --git a/homeassistant/components/bbb_gpio.py b/homeassistant/components/bbb_gpio.py index 5d3954b4c87e21..f932f239969167 100644 --- a/homeassistant/components/bbb_gpio.py +++ b/homeassistant/components/bbb_gpio.py @@ -16,7 +16,6 @@ DOMAIN = 'bbb_gpio' -# pylint: disable=no-member def setup(hass, config): """Set up the BeagleBone Black GPIO component.""" # pylint: disable=import-error @@ -34,41 +33,39 @@ def prepare_gpio(event): return True -# noqa: F821 - def setup_output(pin): """Set up a GPIO as output.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO GPIO.setup(pin, GPIO.OUT) def setup_input(pin, pull_mode): """Set up a GPIO as input.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO - GPIO.setup(pin, GPIO.IN, # noqa: F821 - GPIO.PUD_DOWN if pull_mode == 'DOWN' # noqa: F821 - else GPIO.PUD_UP) # noqa: F821 + GPIO.setup(pin, GPIO.IN, + GPIO.PUD_DOWN if pull_mode == 'DOWN' + else GPIO.PUD_UP) def write_output(pin, value): """Write a value to a GPIO.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO GPIO.output(pin, value) def read_input(pin): """Read a value from a GPIO.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO return GPIO.input(pin) is GPIO.HIGH def edge_detect(pin, event_callback, bounce): """Add detection for RISING and FALLING events.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO GPIO.add_event_detect( pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index d72211d5ad1e1e..26878044fe28ad 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -67,7 +67,6 @@ async def async_unload_entry(hass, 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/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index e214610f46dfe7..308298d1bcd103 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -124,11 +124,11 @@ def device_state_attributes(self): result['check_control_messages'] = check_control_messages elif self._attribute == 'charging_status': result['charging_status'] = vehicle_state.charging_status.value - # pylint: disable=W0212 + # pylint: disable=protected-access result['last_charging_end_result'] = \ vehicle_state._attributes['lastChargingEndResult'] if self._attribute == 'connection_status': - # pylint: disable=W0212 + # pylint: disable=protected-access result['connection_status'] = \ vehicle_state._attributes['connectionStatus'] @@ -166,7 +166,7 @@ def update(self): # device class plug: On means device is plugged in, # Off means device is unplugged if self._attribute == 'connection_status': - # pylint: disable=W0212 + # pylint: disable=protected-access self._state = (vehicle_state._attributes['connectionStatus'] == 'CONNECTED') diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 6f59da0755ae37..0a370d754eea4d 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -5,9 +5,9 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.deconz import ( - CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, - DATA_DECONZ_UNSUB) +from homeassistant.components.deconz.const import ( + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, 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 @@ -62,7 +62,8 @@ def async_update_callback(self, reason): """ if reason['state'] or \ 'reachable' in reason['attr'] or \ - 'battery' in reason['attr']: + 'battery' in reason['attr'] or \ + 'on' in reason['attr']: self.async_schedule_update_ha_state() @property @@ -107,6 +108,8 @@ def device_state_attributes(self): attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.on is not None: + attr[ATTR_ON] = self._sensor.on if self._sensor.type in PRESENCE and self._sensor.dark is not None: - attr['dark'] = self._sensor.dark + attr[ATTR_DARK] = self._sensor.dark return attr diff --git a/homeassistant/components/binary_sensor/digital_ocean.py b/homeassistant/components/binary_sensor/digital_ocean.py index 140c84358c79d3..1eb86d4eb82bad 100644 --- a/homeassistant/components/binary_sensor/digital_ocean.py +++ b/homeassistant/components/binary_sensor/digital_ocean.py @@ -14,7 +14,8 @@ from homeassistant.components.digital_ocean import ( CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) +from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -75,6 +76,7 @@ def device_class(self): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, diff --git a/homeassistant/components/binary_sensor/gc100.py b/homeassistant/components/binary_sensor/gc100.py index 767be2874e6ab8..515d7e7123d4ad 100644 --- a/homeassistant/components/binary_sensor/gc100.py +++ b/homeassistant/components/binary_sensor/gc100.py @@ -39,7 +39,6 @@ class GC100BinarySensor(BinarySensorDevice): def __init__(self, name, port_addr, gc100): """Initialize the GC100 binary sensor.""" - # pylint: disable=no-member self._name = name or DEVICE_DEFAULT_NAME self._port_addr = port_addr self._gc100 = gc100 diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py index 40ffe4984020c9..72a7db1ac7a8ec 100644 --- a/homeassistant/components/binary_sensor/homematicip_cloud.py +++ b/homeassistant/components/binary_sensor/homematicip_cloud.py @@ -9,8 +9,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) DEPENDENCIES = ['homematicip_cloud'] @@ -26,12 +26,15 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the HomematicIP binary sensor devices.""" + """Set up the binary sensor devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP binary sensor from a config entry.""" from homematicip.device import (ShutterContact, MotionDetectorIndoor) - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: if isinstance(device, ShutterContact): diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index a80e4db747d505..deaa118f51cf43 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -8,7 +8,7 @@ import asyncio import logging from datetime import timedelta -from typing import Callable # noqa +from typing import Callable from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 214430211932e5..abb19129d5205d 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -29,7 +29,8 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): +class MySensorsBinarySensor( + mysensors.device.MySensorsEntity, BinarySensorDevice): """Representation of a MySensors Binary Sensor child node.""" @property diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 9da352e1268bff..31460c1eedca0d 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -31,12 +31,10 @@ STRUCTURE_BINARY_TYPES = { 'away': None, - # 'security_state', # pending python-nest update } STRUCTURE_BINARY_STATE_MAP = { 'away': {'away': True, 'home': False}, - 'security_state': {'deter': True, 'ok': False}, } _BINARY_TYPES_DEPRECATED = [ @@ -135,7 +133,7 @@ def update(self): value = getattr(self.device, self.variable) if self.variable in STRUCTURE_BINARY_TYPES: self._state = bool(STRUCTURE_BINARY_STATE_MAP - [self.variable][value]) + [self.variable].get(value)) else: self._state = bool(value) diff --git a/homeassistant/components/binary_sensor/rachio.py b/homeassistant/components/binary_sensor/rachio.py new file mode 100644 index 00000000000000..cc3079c6e53025 --- /dev/null +++ b/homeassistant/components/binary_sensor/rachio.py @@ -0,0 +1,127 @@ +""" +Integration with the Rachio Iro sprinkler system controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rachio/ +""" +from abc import abstractmethod +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, + KEY_DEVICE_ID, + KEY_STATUS, + KEY_SUBTYPE, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + STATUS_OFFLINE, + STATUS_ONLINE, + SUBTYPE_OFFLINE, + SUBTYPE_ONLINE,) +from homeassistant.helpers.dispatcher import dispatcher_connect + +DEPENDENCIES = ['rachio'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Rachio binary sensors.""" + devices = [] + for controller in hass.data[DOMAIN_RACHIO].controllers: + devices.append(RachioControllerOnlineBinarySensor(hass, controller)) + + add_devices(devices) + _LOGGER.info("%d Rachio binary sensor(s) added", len(devices)) + + +class RachioControllerBinarySensor(BinarySensorDevice): + """Represent a binary sensor that reflects a Rachio state.""" + + def __init__(self, hass, controller, poll=True): + """Set up a new Rachio controller binary sensor.""" + self._controller = controller + + if poll: + self._state = self._poll_update() + else: + self._state = None + + dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, + self._handle_any_update) + + @property + def should_poll(self) -> bool: + """Declare that this entity pushes its state to HA.""" + return False + + @property + def is_on(self) -> bool: + """Return whether the sensor has a 'true' value.""" + return self._state + + def _handle_any_update(self, *args, **kwargs) -> None: + """Determine whether an update event applies to this device.""" + if args[0][KEY_DEVICE_ID] != self._controller.controller_id: + # For another device + return + + # For this device + self._handle_update() + + @abstractmethod + def _poll_update(self, data=None) -> bool: + """Request the state from the API.""" + pass + + @abstractmethod + def _handle_update(self, *args, **kwargs) -> None: + """Handle an update to the state of this sensor.""" + pass + + +class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): + """Represent a binary sensor that reflects if the controller is online.""" + + def __init__(self, hass, controller): + """Set up a new Rachio controller online binary sensor.""" + super().__init__(hass, controller, poll=False) + self._state = self._poll_update(controller.init_data) + + @property + def name(self) -> str: + """Return the name of this sensor including the controller name.""" + return "{} online".format(self._controller.name) + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'connectivity' + + @property + def icon(self) -> str: + """Return the name of an icon for this sensor.""" + return 'mdi:wifi-strength-4' if self.is_on\ + else 'mdi:wifi-strength-off-outline' + + def _poll_update(self, data=None) -> bool: + """Request the state from the API.""" + if data is None: + data = self._controller.rachio.device.get( + self._controller.controller_id)[1] + + if data[KEY_STATUS] == STATUS_ONLINE: + return True + elif data[KEY_STATUS] == STATUS_OFFLINE: + return False + else: + _LOGGER.warning('"%s" reported in unknown state "%s"', self.name, + data[KEY_STATUS]) + + def _handle_update(self, *args, **kwargs) -> None: + """Handle an update to the state of this sensor.""" + if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE: + self._state = True + elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: + self._state = False + + self.schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index e1e06ce57b9612..4072f4ae23490e 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -58,7 +58,6 @@ class RPiGPIOBinarySensor(BinarySensorDevice): def __init__(self, name, port, pull_mode, bouncetime, invert_logic): """Initialize the RPi binary sensor.""" - # pylint: disable=no-member self._name = name or DEVICE_DEFAULT_NAME self._port = port self._pull_mode = pull_mode diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index d3c78597c70bc7..e6eff0d9bb5bca 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -13,7 +13,6 @@ _LOGGER = logging.getLogger(__name__) -# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Register discovered WeMo binary sensors.""" import pywemo.discovery as discovery diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 9716e46bc032af..35566b0cbed9f6 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -41,8 +41,9 @@ async def async_setup(hass, config): hass.http.register_view(CalendarListView(component)) hass.http.register_view(CalendarEventView(component)) - await hass.components.frontend.async_register_built_in_panel( - 'calendar', 'calendar', 'hass:calendar') + # Doesn't work in prod builds of the frontend: home-assistant-polymer#1289 + # await hass.components.frontend.async_register_built_in_panel( + # 'calendar', 'calendar', 'hass:calendar') await component.async_setup(config) return True diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index da76530a36d634..87893125e6f74a 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -89,9 +89,7 @@ async def async_get_events(self, hass, start_date, end_date): params['timeMin'] = start_date.isoformat('T') params['timeMax'] = end_date.isoformat('T') - # pylint: disable=no-member events = await hass.async_add_job(service.events) - # pylint: enable=no-member result = await hass.async_add_job(events.list(**params).execute) items = result.get('items', []) @@ -111,7 +109,7 @@ def update(self): service, params = self._prepare_query() params['timeMin'] = dt.now().isoformat('T') - events = service.events() # pylint: disable=no-member + events = service.events() result = events.list(**params).execute() items = result.get('items', []) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ebda09de20cd35..14550dab899d34 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -322,6 +322,7 @@ async def write_to_mjpeg_stream(img_bytes): except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") response = None + raise finally: if response is not None: diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py index 689129e1067ff9..3a8a137c1fe28e 100644 --- a/homeassistant/components/camera/neato.py +++ b/homeassistant/components/camera/neato.py @@ -10,12 +10,13 @@ from homeassistant.components.camera import Camera from homeassistant.components.neato import ( NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN) -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] +SCAN_INTERVAL = timedelta(minutes=10) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Neato Camera.""" @@ -45,7 +46,6 @@ def camera_image(self): self.update() return self._image - @Throttle(timedelta(seconds=60)) def update(self): """Check the contents of the map list.""" self.neato.update_robots() diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index 3ae47ba5dee9df..32f8e15748d7b1 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -25,9 +25,7 @@ REQUIREMENTS = ['onvif-py3==0.1.3', 'suds-py3==1.3.3.0', - 'http://github.com/tgaugry/suds-passworddigest-py3' - '/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip' - '#suds-passworddigest-py3==0.1.2a'] + 'suds-passworddigest-homeassistant==0.1.2a0.dev0'] DEPENDENCIES = ['ffmpeg'] DEFAULT_NAME = 'ONVIF Camera' DEFAULT_PORT = 5000 diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 1984c21fadbb77..447f4e1e56a7b0 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -233,6 +233,7 @@ async def write(img_bytes): _LOGGER.debug("Stream closed by frontend.") req.close() response = None + raise finally: if response is not None: diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py new file mode 100644 index 00000000000000..fc4b18e26e40d2 --- /dev/null +++ b/homeassistant/components/camera/push.py @@ -0,0 +1,162 @@ +""" +Camera platform that receives images through HTTP POST. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/camera.push/ +""" +import logging + +from collections import deque +from datetime import timedelta +import voluptuous as vol + +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ + STATE_IDLE, STATE_RECORDING +from homeassistant.core import callback +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_track_point_in_utc_time +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +CONF_BUFFER_SIZE = 'buffer' +CONF_IMAGE_FIELD = 'field' + +DEFAULT_NAME = "Push Camera" + +ATTR_FILENAME = 'filename' +ATTR_LAST_TRIP = 'last_trip' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_BUFFER_SIZE, default=1): cv.positive_int, + vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All( + cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Push Camera platform.""" + cameras = [PushCamera(config[CONF_NAME], + config[CONF_BUFFER_SIZE], + config[CONF_TIMEOUT])] + + hass.http.register_view(CameraPushReceiver(cameras, + config[CONF_IMAGE_FIELD])) + + async_add_devices(cameras) + + +class CameraPushReceiver(HomeAssistantView): + """Handle pushes from remote camera.""" + + url = "/api/camera_push/{entity_id}" + name = 'api:camera_push:camera_entity' + + def __init__(self, cameras, image_field): + """Initialize CameraPushReceiver with camera entity.""" + self._cameras = cameras + self._image = image_field + + async def post(self, request, entity_id): + """Accept the POST from Camera.""" + try: + (_camera,) = [camera for camera in self._cameras + if camera.entity_id == entity_id] + except ValueError: + _LOGGER.error("Unknown push camera %s", entity_id) + return self.json_message('Unknown Push Camera', + HTTP_BAD_REQUEST) + + try: + data = await request.post() + _LOGGER.debug("Received Camera push: %s", data[self._image]) + await _camera.update_image(data[self._image].file.read(), + data[self._image].filename) + except ValueError as value_error: + _LOGGER.error("Unknown value %s", value_error) + return self.json_message('Invalid POST', HTTP_BAD_REQUEST) + except KeyError as key_error: + _LOGGER.error('In your POST message %s', key_error) + return self.json_message('{} missing'.format(self._image), + HTTP_BAD_REQUEST) + + +class PushCamera(Camera): + """The representation of a Push camera.""" + + def __init__(self, name, buffer_size, timeout): + """Initialize push camera component.""" + super().__init__() + self._name = name + self._last_trip = None + self._filename = None + self._expired_listener = None + self._state = STATE_IDLE + self._timeout = timeout + self.queue = deque([], buffer_size) + self._current_image = None + + @property + def state(self): + """Current state of the camera.""" + return self._state + + async def update_image(self, image, filename): + """Update the camera image.""" + if self._state == STATE_IDLE: + self._state = STATE_RECORDING + self._last_trip = dt_util.utcnow() + self.queue.clear() + + self._filename = filename + self.queue.appendleft(image) + + @callback + def reset_state(now): + """Set state to idle after no new images for a period of time.""" + self._state = STATE_IDLE + self._expired_listener = None + _LOGGER.debug("Reset state") + self.async_schedule_update_ha_state() + + if self._expired_listener: + self._expired_listener() + + self._expired_listener = async_track_point_in_utc_time( + self.hass, reset_state, dt_util.utcnow() + self._timeout) + + self.async_schedule_update_ha_state() + + async def async_camera_image(self): + """Return a still image response.""" + if self.queue: + if self._state == STATE_IDLE: + self.queue.rotate(1) + self._current_image = self.queue[0] + + return self._current_image + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def motion_detection_enabled(self): + """Camera Motion Detection Status.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + name: value for name, value in ( + (ATTR_LAST_TRIP, self._last_trip), + (ATTR_FILENAME, self._filename), + ) if value is not None + } diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py index cec04b52047b8e..2a4d15268180c5 100644 --- a/homeassistant/components/camera/xeoma.py +++ b/homeassistant/components/camera/xeoma.py @@ -67,8 +67,6 @@ async def async_setup_platform(hass, config, async_add_devices, ] for cam in config.get(CONF_CAMERAS, []): - # https://github.com/PyCQA/pylint/issues/1830 - # pylint: disable=stop-iteration-return camera = next( (dc for dc in discovered_cameras if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None) diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py index c18a3649e7bbaa..e80f4b7532acff 100644 --- a/homeassistant/components/camera/xiaomi.py +++ b/homeassistant/components/camera/xiaomi.py @@ -104,27 +104,25 @@ def get_latest_video_url(self): dirs = [d for d in ftp.nlst() if '.' not in d] if not dirs: - if self._model == MODEL_YI: - _LOGGER.warning("There don't appear to be any uploaded videos") - return False - elif self._model == MODEL_XIAOFANG: - _LOGGER.warning("There don't appear to be any folders") - return False + _LOGGER.warning("There don't appear to be any folders") + return False - first_dir = dirs[-1] - try: - ftp.cwd(first_dir) - except error_perm as exc: - _LOGGER.error('Unable to find path: %s - %s', first_dir, exc) - return False + first_dir = dirs[-1] + try: + ftp.cwd(first_dir) + except error_perm as exc: + _LOGGER.error('Unable to find path: %s - %s', first_dir, exc) + return False + if self._model == MODEL_XIAOFANG: dirs = [d for d in ftp.nlst() if '.' not in d] if not dirs: _LOGGER.warning("There don't appear to be any uploaded videos") return False - latest_dir = dirs[-1] - ftp.cwd(latest_dir) + latest_dir = dirs[-1] + ftp.cwd(latest_dir) + videos = [v for v in ftp.nlst() if '.tmp' not in v] if not videos: _LOGGER.info('Video folder "%s" is empty; delaying', latest_dir) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 868c5afb4473c5..b575a705f98bdf 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -53,7 +53,6 @@ def __init__(self, hass, config): """Initialize.""" super().__init__() self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) - self._ftp = None self._last_image = None self._last_url = None self._manager = hass.data[DATA_FFMPEG] @@ -64,8 +63,6 @@ def __init__(self, hass, config): self.user = config[CONF_USERNAME] self.passwd = config[CONF_PASSWORD] - hass.async_add_job(self._connect_to_client) - @property def brand(self): """Camera brand.""" @@ -76,38 +73,35 @@ def name(self): """Return the name of this camera.""" return self._name - async def _connect_to_client(self): - """Attempt to establish a connection via FTP.""" + async def _get_latest_video_url(self): + """Retrieve the latest video file from the customized Yi FTP server.""" from aioftp import Client, StatusCodeError - ftp = Client() + ftp = Client(loop=self.hass.loop) try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) - self._ftp = ftp except StatusCodeError as err: raise PlatformNotReady(err) - async def _get_latest_video_url(self): - """Retrieve the latest video file from the customized Yi FTP server.""" - from aioftp import StatusCodeError - try: - await self._ftp.change_directory(self.path) + await ftp.change_directory(self.path) dirs = [] - for path, attrs in await self._ftp.list(): + for path, attrs in await ftp.list(): if attrs['type'] == 'dir' and '.' not in str(path): dirs.append(path) latest_dir = dirs[-1] - await self._ftp.change_directory(latest_dir) + await ftp.change_directory(latest_dir) videos = [] - for path, _ in await self._ftp.list(): + for path, _ in await ftp.list(): videos.append(path) if not videos: _LOGGER.info('Video folder "%s" empty; delaying', latest_dir) return None + await ftp.quit() + return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format( self.user, self.passwd, self.host, self.port, self.path, latest_dir, videos[-1]) diff --git a/homeassistant/components/cast/.translations/cs.json b/homeassistant/components/cast/.translations/cs.json new file mode 100644 index 00000000000000..82f063b365f1ec --- /dev/null +++ b/homeassistant/components/cast/.translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyly nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed Google Cast.", + "single_instance_allowed": "Pouze jedin\u00e1 konfigurace Google Cast je nezbytn\u00e1." + }, + "step": { + "confirm": { + "description": "Chcete nastavit Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/de.json b/homeassistant/components/cast/.translations/de.json new file mode 100644 index 00000000000000..2572c3344ebac2 --- /dev/null +++ b/homeassistant/components/cast/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie Google Cast einrichten?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/hu.json b/homeassistant/components/cast/.translations/hu.json new file mode 100644 index 00000000000000..f59a1b43ef1b1f --- /dev/null +++ b/homeassistant/components/cast/.translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Google Cast szolg\u00e1ltat\u00e1st?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/it.json b/homeassistant/components/cast/.translations/it.json new file mode 100644 index 00000000000000..21c8e60518e2ad --- /dev/null +++ b/homeassistant/components/cast/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo Google Cast trovato in rete.", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Google Cast." + }, + "step": { + "confirm": { + "description": "Vuoi configurare Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/lb.json b/homeassistant/components/cast/.translations/lb.json new file mode 100644 index 00000000000000..f1daff8306955c --- /dev/null +++ b/homeassistant/components/cast/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Google Cast Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Google Cast ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll Google Cast konfigur\u00e9iert ginn?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/nl.json b/homeassistant/components/cast/.translations/nl.json new file mode 100644 index 00000000000000..91c428770f5fc8 --- /dev/null +++ b/homeassistant/components/cast/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen Google Cast-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Google Cast nodig." + }, + "step": { + "confirm": { + "description": "Wilt u Google Cast instellen?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/sl.json b/homeassistant/components/cast/.translations/sl.json new file mode 100644 index 00000000000000..24a7215574dbd9 --- /dev/null +++ b/homeassistant/components/cast/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju niso najdene naprave Google Cast.", + "single_instance_allowed": "Potrebna je samo ena konfiguracija Google Cast-a." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/zh-Hant.json b/homeassistant/components/cast/.translations/zh-Hant.json new file mode 100644 index 00000000000000..711ac3203978c6 --- /dev/null +++ b/homeassistant/components/cast/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Google Cast\uff1f", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index a47edc5af42632..9584422e2b41c1 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -470,7 +470,6 @@ async def async_unload_entry(hass, entry): class ClimateDevice(Entity): """Representation of a climate device.""" - # pylint: disable=no-self-use @property def state(self): """Return the current state.""" diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 820e715b00d118..10fd879e386296 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -# pylint: disable=import-error, no-name-in-module +# pylint: disable=import-error class EQ3BTSmartThermostat(ClimateDevice): """Representation of an eQ-3 Bluetooth Smart thermostat.""" diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 030a76626c6e2a..3f1d9a208ac5fd 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -263,7 +263,6 @@ def async_set_temperature(self, **kwargs): @property def min_temp(self): """Return the minimum temperature.""" - # pylint: disable=no-member if self._min_temp: return self._min_temp @@ -273,7 +272,6 @@ def min_temp(self): @property def max_temp(self): """Return the maximum temperature.""" - # pylint: disable=no-member if self._max_temp: return self._max_temp diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index 19c033a319f5bf..92e363228a8b84 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -34,7 +34,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the heatmiser thermostat.""" from heatmiserV3 import heatmiser, connection diff --git a/homeassistant/components/climate/homekit_controller.py b/homeassistant/components/climate/homekit_controller.py new file mode 100644 index 00000000000000..f9178c2e0d55af --- /dev/null +++ b/homeassistant/components/climate/homekit_controller.py @@ -0,0 +1,130 @@ +""" +Support for Homekit climate devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.homekit_controller/ +""" +import logging + +from homeassistant.components.homekit_controller import ( + HomeKitEntity, KNOWN_ACCESSORIES) +from homeassistant.components.climate import ( + ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) +from homeassistant.const import TEMP_CELSIUS, STATE_OFF, ATTR_TEMPERATURE + +DEPENDENCIES = ['homekit_controller'] + +_LOGGER = logging.getLogger(__name__) + +# Map of Homekit operation modes to hass modes +MODE_HOMEKIT_TO_HASS = { + 0: STATE_OFF, + 1: STATE_HEAT, + 2: STATE_COOL, +} + +# Map of hass operation modes to homekit modes +MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Homekit climate.""" + if discovery_info is not None: + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + add_devices([HomeKitClimateDevice(accessory, discovery_info)], True) + + +class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): + """Representation of a Homekit climate device.""" + + def __init__(self, *args): + """Initialise the device.""" + super().__init__(*args) + self._state = None + self._current_mode = None + self._valid_modes = [] + self._current_temp = None + self._target_temp = None + + def update_characteristics(self, characteristics): + """Synchronise device state with Home Assistant.""" + # pylint: disable=import-error + from homekit import CharacteristicsTypes as ctypes + + for characteristic in characteristics: + ctype = characteristic['type'] + if ctype == ctypes.HEATING_COOLING_CURRENT: + self._state = MODE_HOMEKIT_TO_HASS.get( + characteristic['value']) + if ctype == ctypes.HEATING_COOLING_TARGET: + self._chars['target_mode'] = characteristic['iid'] + self._features |= SUPPORT_OPERATION_MODE + self._current_mode = MODE_HOMEKIT_TO_HASS.get( + characteristic['value']) + self._valid_modes = [MODE_HOMEKIT_TO_HASS.get( + mode) for mode in characteristic['valid-values']] + elif ctype == ctypes.TEMPERATURE_CURRENT: + self._current_temp = characteristic['value'] + elif ctype == ctypes.TEMPERATURE_TARGET: + self._chars['target_temp'] = characteristic['iid'] + self._features |= SUPPORT_TARGET_TEMPERATURE + self._target_temp = characteristic['value'] + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + + characteristics = [{'aid': self._aid, + 'iid': self._chars['target_temp'], + 'value': temp}] + self.put_characteristics(characteristics) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + characteristics = [{'aid': self._aid, + 'iid': self._chars['target_mode'], + 'value': MODE_HASS_TO_HOMEKIT[operation_mode]}] + self.put_characteristics(characteristics) + + @property + def state(self): + """Return the current state.""" + # If the device reports its operating mode as off, it sometimes doesn't + # report a new state. + if self._current_mode == STATE_OFF: + return STATE_OFF + + if self._state == STATE_OFF and self._current_mode != STATE_OFF: + return STATE_IDLE + return self._state + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temp + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temp + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_mode + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return self._valid_modes + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._features + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS diff --git a/homeassistant/components/climate/homematicip_cloud.py b/homeassistant/components/climate/homematicip_cloud.py index bf96f1f746d8c2..8cf47159c103fd 100644 --- a/homeassistant/components/climate/homematicip_cloud.py +++ b/homeassistant/components/climate/homematicip_cloud.py @@ -12,8 +12,8 @@ STATE_AUTO, STATE_MANUAL) from homeassistant.const import TEMP_CELSIUS from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) _LOGGER = logging.getLogger(__name__) @@ -30,12 +30,14 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the HomematicIP climate devices.""" - from homematicip.group import HeatingGroup + pass + - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP climate from a config entry.""" + from homematicip.group import HeatingGroup + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.groups: if isinstance(device, HeatingGroup): diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 5397daeb784cfd..fbe5460979b099 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -129,6 +129,9 @@ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT climate devices.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + template_keys = ( CONF_POWER_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, @@ -635,11 +638,9 @@ def supported_features(self): @property def min_temp(self): """Return the minimum temperature.""" - # pylint: disable=no-member return self._min_temp @property def max_temp(self): """Return the maximum temperature.""" - # pylint: disable=no-member return self._max_temp diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 9fab56c61ac56c..a2043c2434bfbb 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -26,9 +26,8 @@ 'Off': STATE_OFF, } -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE) +FAN_LIST = ['Auto', 'Min', 'Normal', 'Max'] +OPERATION_LIST = [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] async def async_setup_platform( @@ -39,13 +38,24 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): +class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): """Representation of a MySensors HVAC.""" @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + features = SUPPORT_OPERATION_MODE + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SPEED in self._values: + features = features | SUPPORT_FAN_MODE + if (set_req.V_HVAC_SETPOINT_COOL in self._values and + set_req.V_HVAC_SETPOINT_HEAT in self._values): + features = ( + features | SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_TARGET_TEMPERATURE_LOW) + else: + features = features | SUPPORT_TARGET_TEMPERATURE + return features @property def assumed_state(self): @@ -103,7 +113,7 @@ def current_operation(self): @property def operation_list(self): """List of available operation modes.""" - return [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] + return OPERATION_LIST @property def current_fan_mode(self): @@ -113,7 +123,7 @@ def current_fan_mode(self): @property def fan_list(self): """List of available fan modes.""" - return ['Auto', 'Min', 'Normal', 'Max'] + return FAN_LIST async def async_set_temperature(self, **kwargs): """Set new target temperature.""" diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 1eec9c82f3ca9a..52c544256b6768 100644 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -5,15 +5,15 @@ https://home-assistant.io/components/climate.zwave/ """ # Because we do not compile openzwave on CI -# pylint: disable=import-error import logging from homeassistant.components.climate import ( - DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, + DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -32,6 +32,15 @@ REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 } +STATE_MAPPINGS = { + 'Off': STATE_OFF, + 'Heat': STATE_HEAT, + 'Heat Mode': STATE_HEAT, + 'Heat (Default)': STATE_HEAT, + 'Cool': STATE_COOL, + 'Auto': STATE_AUTO, +} + def get_device(hass, values, **kwargs): """Create Z-Wave entity device.""" @@ -49,6 +58,7 @@ def __init__(self, values, temp_unit): self._current_temperature = None self._current_operation = None self._operation_list = None + self._operation_mapping = None self._operating_state = None self._current_fan_mode = None self._fan_list = None @@ -87,10 +97,21 @@ def update_properties(self): """Handle the data changes for node values.""" # Operation Mode if self.values.mode: - self._current_operation = self.values.mode.data + self._operation_list = [] + self._operation_mapping = {} operation_list = self.values.mode.data_items if operation_list: - self._operation_list = list(operation_list) + for mode in operation_list: + ha_mode = STATE_MAPPINGS.get(mode) + if ha_mode and ha_mode not in self._operation_mapping: + self._operation_mapping[ha_mode] = mode + self._operation_list.append(ha_mode) + continue + self._operation_list.append(mode) + current_mode = self.values.mode.data + self._current_operation = next( + (key for key, value in self._operation_mapping.items() + if value == current_mode), current_mode) _LOGGER.debug("self._operation_list=%s", self._operation_list) _LOGGER.debug("self._current_operation=%s", self._current_operation) @@ -206,7 +227,8 @@ def set_fan_mode(self, fan_mode): def set_operation_mode(self, operation_mode): """Set new target operation mode.""" if self.values.mode: - self.values.mode.data = operation_mode + self.values.mode.data = self._operation_mapping.get( + operation_mode, operation_mode) def set_swing_mode(self, swing_mode): """Set new target swing mode.""" diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index e4c8f5634cf4a9..f5d3d798e2eb6b 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -198,7 +198,6 @@ async def async_handle_cover_service(service): class CoverDevice(Entity): """Representation a cover.""" - # pylint: disable=no-self-use @property def current_cover_position(self): """Return current position of cover. diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py index 70e681f11207fb..b1533bd68c8a09 100644 --- a/homeassistant/components/cover/demo.py +++ b/homeassistant/components/cover/demo.py @@ -24,7 +24,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoCover(CoverDevice): """Representation of a demo cover.""" - # pylint: disable=no-self-use def __init__(self, hass, name, position=None, tilt_position=None, device_class=None, supported_features=None): """Initialize the cover.""" diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py index c19aa69c8f04bb..70f6956810984f 100644 --- a/homeassistant/components/cover/garadget.py +++ b/homeassistant/components/cover/garadget.py @@ -73,7 +73,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class GaradgetCover(CoverDevice): """Representation of a Garadget cover.""" - # pylint: disable=no-self-use def __init__(self, hass, args): """Initialize the cover.""" self.particle_url = 'https://api.particle.io' diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index 743a36d41d5084..0ccfe267989e71 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -5,7 +5,7 @@ https://home-assistant.io/components/cover.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.cover import CoverDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, diff --git a/homeassistant/components/cover/lutron_caseta.py b/homeassistant/components/cover/lutron_caseta.py index 1ed502e0f7f8af..87821b802ba6b9 100644 --- a/homeassistant/components/cover/lutron_caseta.py +++ b/homeassistant/components/cover/lutron_caseta.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.lutron_caseta/ """ -import asyncio import logging from homeassistant.components.cover import ( @@ -18,8 +17,8 @@ DEPENDENCIES = ['lutron_caseta'] -@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 Lutron Caseta shades as a cover device.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] @@ -49,25 +48,21 @@ def current_cover_position(self): """Return the current position of cover.""" return self._state['current_state'] - @asyncio.coroutine - def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close the cover.""" self._smartbridge.set_value(self._device_id, 0) - @asyncio.coroutine - def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the cover.""" self._smartbridge.set_value(self._device_id, 100) - @asyncio.coroutine - def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the shade to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] self._smartbridge.set_value(self._device_id, position) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Call when forcing a refresh of the device.""" self._state = self._smartbridge.get_device_by_id(self._device_id) _LOGGER.debug(self._state) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 235ff5799cc810..62e1069e18bb32 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -93,8 +92,8 @@ }).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 Cover.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -174,10 +173,9 @@ def __init__(self, name, state_topic, command_topic, availability_topic, self._position_topic = position_topic self._set_position_template = set_position_template - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def tilt_updated(topic, payload, qos): @@ -218,7 +216,7 @@ def state_message_received(topic, payload, qos): # 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) @@ -227,7 +225,7 @@ def state_message_received(topic, payload, qos): else: self._tilt_optimistic = False self._tilt_value = STATE_UNKNOWN - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._tilt_status_topic, tilt_updated, self._qos) @property @@ -278,8 +276,7 @@ def supported_features(self): return supported_features - @asyncio.coroutine - def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Move the cover up. This method is a coroutine. @@ -292,8 +289,7 @@ def async_open_cover(self, **kwargs): self._state = False self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Move the cover down. This method is a coroutine. @@ -306,8 +302,7 @@ def async_close_cover(self, **kwargs): self._state = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the device. This method is a coroutine. @@ -316,8 +311,7 @@ def async_stop_cover(self, **kwargs): self.hass, self._command_topic, self._payload_stop, self._qos, self._retain) - @asyncio.coroutine - def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" mqtt.async_publish(self.hass, self._tilt_command_topic, self._tilt_open_position, self._qos, @@ -326,8 +320,7 @@ def async_open_cover_tilt(self, **kwargs): self._tilt_value = self._tilt_open_position self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" mqtt.async_publish(self.hass, self._tilt_command_topic, self._tilt_closed_position, self._qos, @@ -336,8 +329,7 @@ def async_close_cover_tilt(self, **kwargs): self._tilt_value = self._tilt_closed_position self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" if ATTR_TILT_POSITION not in kwargs: return @@ -350,8 +342,7 @@ def async_set_cover_tilt_position(self, **kwargs): mqtt.async_publish(self.hass, self._tilt_command_topic, level, self._qos, self._retain) - @asyncio.coroutine - def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 3f8eb054710377..c815cf44df2d02 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -17,7 +17,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): +class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): """Representation of the value of a MySensors Cover child node.""" @property diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py index 028a7a0c9fc8ad..fe6c7763cc7779 100644 --- a/homeassistant/components/cover/opengarage.py +++ b/homeassistant/components/cover/opengarage.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OpenGarageCover(CoverDevice): """Representation of a OpenGarage cover.""" - # pylint: disable=no-self-use def __init__(self, hass, args): """Initialize the cover.""" self.opengarage_url = 'http://{}:{}'.format( diff --git a/homeassistant/components/cover/rflink.py b/homeassistant/components/cover/rflink.py index a9b7598159f967..3357bf2d204fb0 100644 --- a/homeassistant/components/cover/rflink.py +++ b/homeassistant/components/cover/rflink.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.rflink/ """ -import asyncio import logging import voluptuous as vol @@ -79,8 +78,8 @@ def devices_from_config(domain_config, hass=None): return devices -@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 Rflink cover platform.""" async_add_devices(devices_from_config(config, hass)) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index cf8b7dfad48c0e..824e330d6a0712 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -81,7 +81,11 @@ def stop_cover(self, **kwargs): self.apply_action('setPosition', 'secured') elif self.tahoma_device.type in \ ('rts:BlindRTSComponent', - 'io:ExteriorVenetianBlindIOComponent'): + 'io:ExteriorVenetianBlindIOComponent', + 'rts:VenetianBlindRTSComponent', + 'rts:DualCurtainRTSComponent', + 'rts:ExteriorVenetianBlindRTSComponent', + 'rts:BlindRTSComponent'): self.apply_action('my') else: self.apply_action('stopIdentify') diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index 4e197365a7098e..d9d0d61c77a8f0 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.template/ """ -import asyncio import logging import voluptuous as vol @@ -72,8 +71,8 @@ }) -@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 Template cover.""" covers = [] @@ -199,8 +198,7 @@ def __init__(self, hass, device_id, friendly_name, state_template, if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_cover_state_listener(entity, old_state, new_state): @@ -277,70 +275,62 @@ def should_poll(self): """Return the polling state.""" return False - @asyncio.coroutine - def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: - yield from self._open_script.async_run() + await self._open_script.async_run() elif self._position_script: - yield from self._position_script.async_run({"position": 100}) + await self._position_script.async_run({"position": 100}) if self._optimistic: self._position = 100 self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Move the cover down.""" if self._close_script: - yield from self._close_script.async_run() + await self._close_script.async_run() elif self._position_script: - yield from self._position_script.async_run({"position": 0}) + await self._position_script.async_run({"position": 0}) if self._optimistic: self._position = 0 self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Fire the stop action.""" if self._stop_script: - yield from self._stop_script.async_run() + await self._stop_script.async_run() - @asyncio.coroutine - def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Set cover position.""" self._position = kwargs[ATTR_POSITION] - yield from self._position_script.async_run( + await self._position_script.async_run( {"position": self._position}) if self._optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" self._tilt_value = 100 - yield from self._tilt_script.async_run({"tilt": self._tilt_value}) + await self._tilt_script.async_run({"tilt": self._tilt_value}) if self._tilt_optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" self._tilt_value = 0 - yield from self._tilt_script.async_run( + await self._tilt_script.async_run( {"tilt": self._tilt_value}) if self._tilt_optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] - yield from self._tilt_script.async_run({"tilt": self._tilt_value}) + await self._tilt_script.async_run({"tilt": self._tilt_value}) if self._tilt_optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state from the template.""" if self._template is not None: try: diff --git a/homeassistant/components/cover/velbus.py b/homeassistant/components/cover/velbus.py index ab5d6e8ef7947a..fd060e7a7e1c07 100644 --- a/homeassistant/components/cover/velbus.py +++ b/homeassistant/components/cover/velbus.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/cover.velbus/ """ import logging -import asyncio import time import voluptuous as vol @@ -70,15 +69,14 @@ def __init__(self, velbus, name, module, open_channel, close_channel): self._open_channel = open_channel self._close_channel = close_channel - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add listener for Velbus messages on bus.""" def _init_velbus(): """Initialize Velbus on startup.""" self._velbus.subscribe(self._on_message) self.get_status() - yield from self.hass.async_add_job(_init_velbus) + await self.hass.async_add_job(_init_velbus) def _on_message(self, message): import velbus diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index 7f7a3a116443a7..2206de05041435 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -4,8 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.wink/ """ -import asyncio - from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN, \ ATTR_POSITION from homeassistant.components.wink import WinkDevice, DOMAIN @@ -34,8 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WinkCoverDevice(WinkDevice, CoverDevice): """Representation of a Wink cover device.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['cover'].append(self) diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 6f4a11684bde61..c29c11c5b6bffb 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -42,7 +42,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def __init__(self, hass, values, invert_buttons): """Initialize the Z-Wave rollershutter.""" ZWaveDeviceEntity.__init__(self, values, DOMAIN) - # pylint: disable=no-member self._network = hass.data[zwave.const.DATA_NETWORK] self._open_id = None self._close_id = None diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index 0721cac3321bfc..1588766e406c78 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel" + "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel", + "allow_deconz_groups": "Povolit import skupin deCONZ " }, "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" } diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 9d3dc9e6e62f41..b09b7e15b31a61 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -19,8 +19,14 @@ "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" + }, + "options": { + "data": { + "allow_clip_sensor": "Import virtueller Sensoren zulassen", + "allow_deconz_groups": "Import von deCONZ-Gruppen zulassen" + } } }, - "title": "deCONZ" + "title": "deCONZ Zigbee Gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 46190d23926b8c..3de7de9ddb3e6e 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren" + "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren", + "allow_deconz_groups": "Erlaabt den Import vun deCONZ Gruppen" }, "title": "Extra Konfiguratiouns Optiounen fir deCONZ" } diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 90d13bb39b470d..6f3fa2ec9a4c01 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -19,6 +19,13 @@ "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" + }, + "options": { + "data": { + "allow_clip_sensor": "Sta het importeren van virtuele sensoren toe", + "allow_deconz_groups": "Sta de import van deCONZ-groepen toe" + }, + "title": "Extra configuratieopties voor deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 59c5577c96b5a8..bc7a2cbd861583 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev" + "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev", + "allow_deconz_groups": "Dovoli uvoz deCONZ skupin" }, "title": "Dodatne mo\u017enosti konfiguracije za deCONZ" } diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 17cbe87f1e8f9b..5cd1a14d499bd3 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668" + "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668", + "allow_deconz_groups": "\u5141\u8a31\u532f\u5165 deCONZ \u7fa4\u7d44" }, "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 850645225d0018..88174b9d61297b 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==38'] +REQUIREMENTS = ['pydeconz==42'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 27fb6987f8c24c..b67d32508be9e9 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -163,9 +163,6 @@ async def async_step_import(self, import_config): if CONF_API_KEY not in import_config: return await self.async_step_link() - self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True - self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = True - return self.async_create_entry( - title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], - data=self.deconz_config - ) + user_input = {CONF_ALLOW_CLIP_SENSOR: True, + CONF_ALLOW_DECONZ_GROUPS: True} + return await self.async_step_options(user_input=user_input) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index f7aa4c7a43057d..6deee322a15e31 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -11,3 +11,6 @@ CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' + +ATTR_DARK = 'dark' +ATTR_ON = 'on' diff --git a/homeassistant/components/device_tracker/cisco_ios.py b/homeassistant/components/device_tracker/cisco_ios.py index 0978ba99593e65..c13f622c5bf101 100644 --- a/homeassistant/components/device_tracker/cisco_ios.py +++ b/homeassistant/components/device_tracker/cisco_ios.py @@ -50,7 +50,6 @@ def __init__(self, config): self.success_init = self._update_info() _LOGGER.info('cisco_ios scanner initialized') - # pylint: disable=no-self-use def get_device_name(self, device): """Get the firmware doesn't save the name of the wireless device.""" return None diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py index 67957ca99b9f68..b278c4219254f7 100644 --- a/homeassistant/components/device_tracker/freebox.py +++ b/homeassistant/components/device_tracker/freebox.py @@ -22,7 +22,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT) -REQUIREMENTS = ['aiofreepybox==0.0.3'] +REQUIREMENTS = ['aiofreepybox==0.0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 68ea9ac88ae819..6336ba51d23dd8 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -7,7 +7,7 @@ import logging from hmac import compare_digest -from aiohttp.web import Request, HTTPUnauthorized # NOQA +from aiohttp.web import Request, HTTPUnauthorized import voluptuous as vol import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/device_tracker/linksys_ap.py b/homeassistant/components/device_tracker/linksys_ap.py index 8837b628b32650..bf3916f3abe0ed 100644 --- a/homeassistant/components/device_tracker/linksys_ap.py +++ b/homeassistant/components/device_tracker/linksys_ap.py @@ -61,7 +61,6 @@ def scan_devices(self): return self.last_results - # pylint: disable=no-self-use def get_device_name(self, device): """ Return the name (if known) of the device. diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py index b0d29bf0566757..49d3f3207ba5ca 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/device_tracker/mysensors.py @@ -23,13 +23,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): id(device.gateway), device.node_id, device.child_id, device.value_type) async_dispatcher_connect( - hass, mysensors.SIGNAL_CALLBACK.format(*dev_id), + hass, mysensors.const.SIGNAL_CALLBACK.format(*dev_id), device.async_update_callback) return True -class MySensorsDeviceScanner(mysensors.MySensorsDevice): +class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" def __init__(self, async_see, *args): diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 3d57cb108e243c..6a849d0b05abfd 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -74,8 +74,6 @@ def scan_devices(self): return [client['mac'] for client in self.last_results if client.get('mac')] - # Suppressing no-self-use warning - # pylint: disable=R0201 def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" # We have no names @@ -106,7 +104,6 @@ def get_snmp_data(self): if errindication: _LOGGER.error("SNMPLIB error: %s", errindication) return - # pylint: disable=no-member if errstatus: _LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(), errindex and restable[int(errindex) - 1][0] or '?') diff --git a/homeassistant/components/device_tracker/tile.py b/homeassistant/components/device_tracker/tile.py index 377686b69054f3..6df9f3c9974caa 100644 --- a/homeassistant/components/device_tracker/tile.py +++ b/homeassistant/components/device_tracker/tile.py @@ -5,24 +5,22 @@ https://home-assistant.io/components/device_tracker.tile/ """ import logging +from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.const import ( CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD) -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify from homeassistant.util.json import load_json, save_json _LOGGER = logging.getLogger(__name__) - -REQUIREMENTS = ['pytile==1.1.0'] +REQUIREMENTS = ['pytile==2.0.2'] CLIENT_UUID_CONFIG_FILE = '.tile.conf' -DEFAULT_ICON = 'mdi:bluetooth' DEVICE_TYPES = ['PHONE', 'TILE'] ATTR_ALTITUDE = 'altitude' @@ -34,89 +32,111 @@ CONF_SHOW_INACTIVE = 'show_inactive' +DEFAULT_ICON = 'mdi:bluetooth' +DEFAULT_SCAN_INTERVAL = timedelta(minutes=2) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean, - vol.Optional(CONF_MONITORED_VARIABLES): + vol.Optional(CONF_MONITORED_VARIABLES, default=DEVICE_TYPES): vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]), }) -def setup_scanner(hass, config: dict, see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Tile scanner.""" - TileDeviceScanner(hass, config, see) - return True - - -class TileDeviceScanner(DeviceScanner): - """Define a device scanner for Tiles.""" - - def __init__(self, hass, config, see): + from pytile import Client + + websession = aiohttp_client.async_get_clientsession(hass) + + config_data = await hass.async_add_job( + load_json, hass.config.path(CLIENT_UUID_CONFIG_FILE)) + if config_data: + client = Client( + config[CONF_USERNAME], + config[CONF_PASSWORD], + websession, + client_uuid=config_data['client_uuid']) + else: + client = Client( + config[CONF_USERNAME], config[CONF_PASSWORD], websession) + + config_data = {'client_uuid': client.client_uuid} + config_saved = await hass.async_add_job( + save_json, hass.config.path(CLIENT_UUID_CONFIG_FILE), config_data) + if not config_saved: + _LOGGER.error('Failed to save the client UUID') + + scanner = TileScanner( + client, hass, async_see, config[CONF_MONITORED_VARIABLES], + config[CONF_SHOW_INACTIVE]) + return await scanner.async_init() + + +class TileScanner(object): + """Define an object to retrieve Tile data.""" + + def __init__(self, client, hass, async_see, types, show_inactive): """Initialize.""" - from pytile import Client - - _LOGGER.debug('Received configuration data: %s', config) + self._async_see = async_see + self._client = client + self._hass = hass + self._show_inactive = show_inactive + self._types = types - # Load the client UUID (if it exists): - config_data = load_json(hass.config.path(CLIENT_UUID_CONFIG_FILE)) - if config_data: - _LOGGER.debug('Using existing client UUID') - self._client = Client( - config[CONF_USERNAME], - config[CONF_PASSWORD], - config_data['client_uuid']) - else: - _LOGGER.debug('Generating new client UUID') - self._client = Client( - config[CONF_USERNAME], - config[CONF_PASSWORD]) + async def async_init(self): + """Further initialize connection to the Tile servers.""" + from pytile.errors import TileError - if not save_json( - hass.config.path(CLIENT_UUID_CONFIG_FILE), - {'client_uuid': self._client.client_uuid}): - _LOGGER.error("Failed to save configuration file") + try: + await self._client.async_init() + except TileError as err: + _LOGGER.error('Unable to set up Tile scanner: %s', err) + return False - _LOGGER.debug('Client UUID: %s', self._client.client_uuid) - _LOGGER.debug('User UUID: %s', self._client.user_uuid) + await self._async_update() - self._show_inactive = config.get(CONF_SHOW_INACTIVE) - self._types = config.get(CONF_MONITORED_VARIABLES) + async_track_time_interval( + self._hass, self._async_update, DEFAULT_SCAN_INTERVAL) - self.devices = {} - self.see = see + return True - track_utc_time_change( - hass, self._update_info, second=range(0, 60, 30)) + async def _async_update(self, now=None): + """Update info from Tile.""" + from pytile.errors import SessionExpiredError, TileError - self._update_info() + _LOGGER.debug('Updating Tile data') - def _update_info(self, now=None) -> None: - """Update the device info.""" - self.devices = self._client.get_tiles( - type_whitelist=self._types, show_inactive=self._show_inactive) + try: + await self._client.asayn_init() + tiles = await self._client.tiles.all( + whitelist=self._types, show_inactive=self._show_inactive) + except SessionExpiredError: + _LOGGER.info('Session expired; trying again shortly') + return + except TileError as err: + _LOGGER.error('There was an error while updating: %s', err) + return - if not self.devices: + if not tiles: _LOGGER.warning('No Tiles found') return - for dev in self.devices: - dev_id = 'tile_{0}'.format(slugify(dev['name'])) - lat = dev['tileState']['latitude'] - lon = dev['tileState']['longitude'] - - attrs = { - ATTR_ALTITUDE: dev['tileState']['altitude'], - ATTR_CONNECTION_STATE: dev['tileState']['connection_state'], - ATTR_IS_DEAD: dev['is_dead'], - ATTR_IS_LOST: dev['tileState']['is_lost'], - ATTR_RING_STATE: dev['tileState']['ring_state'], - ATTR_VOIP_STATE: dev['tileState']['voip_state'], - } - - self.see( - dev_id=dev_id, - gps=(lat, lon), - attributes=attrs, - icon=DEFAULT_ICON - ) + for tile in tiles: + await self._async_see( + dev_id='tile_{0}'.format(slugify(tile['name'])), + gps=( + tile['tileState']['latitude'], + tile['tileState']['longitude'] + ), + attributes={ + ATTR_ALTITUDE: tile['tileState']['altitude'], + ATTR_CONNECTION_STATE: + tile['tileState']['connection_state'], + ATTR_IS_DEAD: tile['is_dead'], + ATTR_IS_LOST: tile['tileState']['is_lost'], + ATTR_RING_STATE: tile['tileState']['ring_state'], + ATTR_VOIP_STATE: tile['tileState']['voip_state'], + }, + icon=DEFAULT_ICON) diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 6c5fb697c072da..5266b9c6f574b3 100644 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -68,7 +68,6 @@ def scan_devices(self): self._update_info() return self.last_results - # pylint: disable=no-self-use def get_device_name(self, device): """Get firmware doesn't save the name of the wireless device.""" return None @@ -103,7 +102,6 @@ def scan_devices(self): self._update_info() return self.last_results.keys() - # pylint: disable=no-self-use def get_device_name(self, device): """Get firmware doesn't save the name of the wireless device.""" return self.last_results.get(device) @@ -164,7 +162,6 @@ def scan_devices(self): self._log_out() return self.last_results.keys() - # pylint: disable=no-self-use def get_device_name(self, device): """Get the firmware doesn't save the name of the wireless device. @@ -273,7 +270,6 @@ def scan_devices(self): self._update_info() return self.last_results - # pylint: disable=no-self-use def get_device_name(self, device): """Get the name of the wireless device.""" return None @@ -349,7 +345,6 @@ def scan_devices(self): self._update_info() return self.last_results.keys() - # pylint: disable=no-self-use def get_device_name(self, device): """Get firmware doesn't save the name of the wireless device.""" return None diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py index 5d6e1453124c09..074d6a1054ee51 100644 --- a/homeassistant/components/device_tracker/xiaomi_miio.py +++ b/homeassistant/components/device_tracker/xiaomi_miio.py @@ -64,7 +64,7 @@ async def async_scan_devices(self): station_info = await self.hass.async_add_job(self.device.status) _LOGGER.debug("Got new station info: %s", station_info) - for device in station_info['mat']: + for device in station_info.associated_stations: devices.append(device['mac']) except DeviceException as ex: diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean.py index bd03fb019759f9..a0f50842649446 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean.py @@ -27,6 +27,7 @@ ATTR_REGION = 'region' ATTR_VCPUS = 'vcpus' +CONF_ATTRIBUTION = 'Data provided by Digital Ocean' CONF_DROPLETS = 'droplets' DATA_DIGITAL_OCEAN = 'data_do' diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 22348dcc297abb..96f094b527dfdf 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -105,7 +105,6 @@ def setup(hass, config): Will automatically load thermostat and sensor components to support devices discovered on the network. """ - # pylint: disable=import-error global NETWORK if 'ecobee' in _CONFIGURING: diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index fd7f7147fdba01..6988e20fb5f0dd 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -91,9 +91,11 @@ def setup(hass, yaml_config): server_port=config.listen_port, api_password=None, ssl_certificate=None, + ssl_peer_certificate=None, ssl_key=None, cors_origins=None, use_x_forwarded_for=False, + trusted_proxies=[], trusted_networks=[], login_threshold=0, is_ban_enabled=False diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b2cac55bd77421..0b9c8edd4117b5 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180620.0'] +REQUIREMENTS = ['home-assistant-frontend==20180704.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -200,8 +200,8 @@ def add_manifest_json_key(key, val): async def async_setup(hass, config): """Set up the serving of the frontend.""" - if list(hass.auth.async_auth_providers): - client = await hass.auth.async_create_client( + if hass.auth.active: + client = await hass.auth.async_get_or_create_client( 'Home Assistant Frontend', redirect_uris=['/'], no_secret=True, diff --git a/homeassistant/components/gc100.py b/homeassistant/components/gc100.py index bc627d4441796d..25bcb5b0f79f55 100644 --- a/homeassistant/components/gc100.py +++ b/homeassistant/components/gc100.py @@ -31,7 +31,7 @@ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=no-member, import-self +# pylint: disable=no-member def setup(hass, base_config): """Set up the gc100 component.""" import gc100 diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index b41d4ea33a20b1..203b1a94b7f9e8 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -197,7 +197,7 @@ def _found_calendar(call): def _scan_for_calendars(service): """Scan for new calendars.""" service = calendar_service.get() - cal_list = service.calendarList() # pylint: disable=no-member + cal_list = service.calendarList() calendars = cal_list.list().execute()['items'] for calendar in calendars: calendar['track'] = track_new_found_calendars diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 1c6d11a7c99216..567a6d842339ca 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -13,9 +13,8 @@ import voluptuous as vol # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports -from homeassistant.core import HomeAssistant # NOQA -from typing import Dict, Any # NOQA +from homeassistant.core import HomeAssistant +from typing import Dict, Any from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py index a21dd0e673859d..e80b2282066b73 100644 --- a/homeassistant/components/google_assistant/auth.py +++ b/homeassistant/components/google_assistant/auth.py @@ -3,12 +3,11 @@ import logging # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports # if False: -from aiohttp.web import Request, Response # NOQA -from typing import Dict, Any # NOQA +from aiohttp.web import Request, Response +from typing import Dict, Any -from homeassistant.core import HomeAssistant # NOQA +from homeassistant.core import HomeAssistant from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( HTTP_BAD_REQUEST, diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 0ea5f7d9fa4379..05bc3cbd01c0a4 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -7,13 +7,11 @@ import logging from aiohttp.hdrs import AUTHORIZATION -from aiohttp.web import Request, Response # NOQA +from aiohttp.web import Request, Response # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports from homeassistant.components.http import HomeAssistantView -from homeassistant.core import HomeAssistant, callback # NOQA -from homeassistant.helpers.entity import Entity # NOQA +from homeassistant.core import callback from .const import ( GOOGLE_ASSISTANT_API_ENDPOINT, diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 27d993aee76abd..927139a483e0a8 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -3,14 +3,6 @@ from itertools import product import logging -# Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports -# if False: -from aiohttp.web import Request, Response # NOQA -from typing import Dict, Tuple, Any, Optional # NOQA -from homeassistant.helpers.entity import Entity # NOQA -from homeassistant.core import HomeAssistant # NOQA -from homeassistant.util.unit_system import UnitSystem # NOQA from homeassistant.util.decorator import Registry from homeassistant.core import callback diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 0883c5a3cc85b0..34fdcb2c035e86 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -23,6 +23,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { 'lightbulb': 'light', 'outlet': 'switch', + 'thermostat': 'climate', } KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN) @@ -219,8 +220,12 @@ def update_characteristics(self, characteristics): """Synchronise a HomeKit device state with Home Assistant.""" raise NotImplementedError + def put_characteristics(self, characteristics): + """Control a HomeKit device state from Home Assistant.""" + body = json.dumps({'characteristics': characteristics}) + self._securecon.put('/characteristics', body) + -# pylint: too-many-function-args def setup(hass, config): """Set up for Homekit devices.""" def discovery_dispatch(service, discovery_info): diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 2e05f638afc37c..1428bbd3e563f7 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -148,6 +148,7 @@ CONF_CALLBACK_IP = 'callback_ip' CONF_CALLBACK_PORT = 'callback_port' CONF_RESOLVENAMES = 'resolvenames' +CONF_JSONPORT = 'jsonport' CONF_VARIABLES = 'variables' CONF_DEVICES = 'devices' CONF_PRIMARY = 'primary' @@ -155,6 +156,7 @@ DEFAULT_LOCAL_IP = '0.0.0.0' DEFAULT_LOCAL_PORT = 0 DEFAULT_RESOLVENAMES = False +DEFAULT_JSONPORT = 80 DEFAULT_PORT = 2001 DEFAULT_PATH = '' DEFAULT_USERNAME = 'Admin' @@ -178,6 +180,7 @@ vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): vol.In(CONF_RESOLVENAMES_OPTIONS), + vol.Optional(CONF_JSONPORT, default=DEFAULT_JSONPORT): cv.port, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_CALLBACK_IP): cv.string, @@ -299,6 +302,7 @@ def setup(hass, config): 'port': rconfig.get(CONF_PORT), 'path': rconfig.get(CONF_PATH), 'resolvenames': rconfig.get(CONF_RESOLVENAMES), + 'jsonport': rconfig.get(CONF_JSONPORT), 'username': rconfig.get(CONF_USERNAME), 'password': rconfig.get(CONF_PASSWORD), 'callbackip': rconfig.get(CONF_CALLBACK_IP), diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py deleted file mode 100644 index 859841dfca64bc..00000000000000 --- a/homeassistant/components/homematicip_cloud.py +++ /dev/null @@ -1,262 +0,0 @@ -""" -Support for HomematicIP components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematicip_cloud/ -""" - -import asyncio -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -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.9.4'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'homematicip_cloud' - -COMPONENTS = [ - 'sensor', - 'binary_sensor', - 'switch', - 'light', - 'climate', -] - -CONF_NAME = 'name' -CONF_ACCESSPOINT = 'accesspoint' -CONF_AUTHTOKEN = 'authtoken' - -CONFIG_SCHEMA = vol.Schema({ - 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) - -HMIP_ACCESS_POINT = 'Access Point' -HMIP_HUB = 'HmIP-HUB' - -ATTR_HOME_ID = 'home_id' -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_OPERATION_LOCK = 'operation_lock' - - -async def async_setup(hass, config): - """Set up the HomematicIP component.""" - from homematicip.base.base_connection import HmipConnectionError - - hass.data.setdefault(DOMAIN, {}) - accesspoints = config.get(DOMAIN, []) - for conf in accesspoints: - _websession = async_get_clientsession(hass) - _hmip = HomematicipConnector(hass, conf, _websession) - try: - await _hmip.init() - except HmipConnectionError: - _LOGGER.error('Failed to connect to the HomematicIP server, %s.', - conf.get(CONF_ACCESSPOINT)) - return False - - 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, 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.""" - self._device.on_update(self._device_changed) - - def _device_changed(self, json, **kwargs): - """Handle device state changes.""" - _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.""" - 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): - """No polling needed.""" - return False - - @property - def available(self): - """Device available.""" - return not self._device.unreach - - @property - def device_state_attributes(self): - """Return the state attributes of the generic device.""" - return { - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_MODEL_TYPE: self._device.modelType - } diff --git a/homeassistant/components/homematicip_cloud/.translations/en.json b/homeassistant/components/homematicip_cloud/.translations/en.json new file mode 100644 index 00000000000000..887a3a5780b0eb --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "title": "HomematicIP Cloud", + "step": { + "init": { + "title": "Pick HomematicIP Accesspoint", + "data": { + "hapid": "Accesspoint ID (SGTIN)", + "pin": "Pin Code (optional)", + "name": "Name (optional, used as name prefix for all devices)" + } + }, + "link": { + "title": "Link Accesspoint", + "description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)" + } + }, + "error": { + "register_failed": "Failed to register, please try again.", + "invalid_pin": "Invalid PIN, please try again.", + "press_the_button": "Please press the blue button.", + "timeout_button": "Blue button press timeout, please try again." + }, + "abort": { + "unknown": "Unknown error occurred.", + "conection_aborted": "Could not connect to HMIP server", + "already_configured": "Accesspoint is already configured" + } + } +} diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py new file mode 100644 index 00000000000000..3ff4e438f53c7f --- /dev/null +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -0,0 +1,65 @@ +""" +Support for HomematicIP components. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematicip_cloud/ +""" + +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +from .const import ( + DOMAIN, HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_NAME, + CONF_ACCESSPOINT, CONF_AUTHTOKEN, CONF_NAME) +# Loading the config flow file will register the flow +from .config_flow import configured_haps +from .hap import HomematicipHAP, HomematicipAuth # noqa: F401 +from .device import HomematicipGenericDevice # noqa: F401 + +REQUIREMENTS = ['homematicip==0.9.6'] + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ + vol.Optional(CONF_NAME, default=''): vol.Any(cv.string), + vol.Required(CONF_ACCESSPOINT): cv.string, + vol.Required(CONF_AUTHTOKEN): cv.string, + })]), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the HomematicIP component.""" + hass.data[DOMAIN] = {} + + accesspoints = config.get(DOMAIN, []) + + for conf in accesspoints: + if conf[CONF_ACCESSPOINT] not in configured_haps(hass): + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, source='import', data={ + HMIPC_HAPID: conf[CONF_ACCESSPOINT], + HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN], + HMIPC_NAME: conf[CONF_NAME], + } + )) + + return True + + +async def async_setup_entry(hass, entry): + """Set up an accsspoint from a config entry.""" + hap = HomematicipHAP(hass, entry) + hapid = entry.data[HMIPC_HAPID].replace('-', '').upper() + hass.data[DOMAIN][hapid] = hap + return await hap.async_setup() + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hap = hass.data[DOMAIN].pop(entry.data[HMIPC_HAPID]) + return await hap.async_reset() diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py new file mode 100644 index 00000000000000..9e5356d914a5a6 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -0,0 +1,97 @@ +"""Config flow to configure HomematicIP Cloud.""" +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.core import callback + +from .const import ( + DOMAIN as HMIPC_DOMAIN, _LOGGER, + HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME) +from .hap import HomematicipAuth + + +@callback +def configured_haps(hass): + """Return a set of the configured accesspoints.""" + return set(entry.data[HMIPC_HAPID] for entry + in hass.config_entries.async_entries(HMIPC_DOMAIN)) + + +@config_entries.HANDLERS.register(HMIPC_DOMAIN) +class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler): + """Config flow HomematicIP Cloud.""" + + VERSION = 1 + + def __init__(self): + """Initialize HomematicIP Cloud config flow.""" + self.auth = None + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + errors = {} + + if user_input is not None: + user_input[HMIPC_HAPID] = \ + user_input[HMIPC_HAPID].replace('-', '').upper() + if user_input[HMIPC_HAPID] in configured_haps(self.hass): + return self.async_abort(reason='already_configured') + + self.auth = HomematicipAuth(self.hass, user_input) + connected = await self.auth.async_setup() + if connected: + _LOGGER.info("Connection established") + return await self.async_step_link() + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required(HMIPC_HAPID): str, + vol.Optional(HMIPC_PIN): str, + vol.Optional(HMIPC_NAME): str, + }), + errors=errors + ) + + async def async_step_link(self, user_input=None): + """Attempt to link with the HomematicIP Cloud accesspoint.""" + errors = {} + + pressed = await self.auth.async_checkbutton() + if pressed: + authtoken = await self.auth.async_register() + if authtoken: + _LOGGER.info("Write config entry") + return self.async_create_entry( + title=self.auth.config.get(HMIPC_HAPID), + data={ + HMIPC_HAPID: self.auth.config.get(HMIPC_HAPID), + HMIPC_AUTHTOKEN: authtoken, + HMIPC_NAME: self.auth.config.get(HMIPC_NAME) + }) + return self.async_abort(reason='conection_aborted') + else: + errors['base'] = 'press_the_button' + + return self.async_show_form(step_id='link', errors=errors) + + async def async_step_import(self, import_info): + """Import a new bridge as a config entry.""" + hapid = import_info[HMIPC_HAPID] + authtoken = import_info[HMIPC_AUTHTOKEN] + name = import_info[HMIPC_NAME] + + hapid = hapid.replace('-', '').upper() + if hapid in configured_haps(self.hass): + return self.async_abort(reason='already_configured') + + _LOGGER.info('Imported authentication for %s', hapid) + + return self.async_create_entry( + title=hapid, + data={ + HMIPC_HAPID: hapid, + HMIPC_AUTHTOKEN: authtoken, + HMIPC_NAME: name + } + ) diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py new file mode 100644 index 00000000000000..c40e577ae4a570 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/const.py @@ -0,0 +1,23 @@ +"""Constants for the HomematicIP Cloud component.""" +import logging + +_LOGGER = logging.getLogger('homeassistant.components.homematicip_cloud') + +DOMAIN = 'homematicip_cloud' + +COMPONENTS = [ + 'binary_sensor', + 'climate', + 'light', + 'sensor', + 'switch', +] + +CONF_NAME = 'name' +CONF_ACCESSPOINT = 'accesspoint' +CONF_AUTHTOKEN = 'authtoken' + +HMIPC_NAME = 'name' +HMIPC_HAPID = 'hapid' +HMIPC_AUTHTOKEN = 'authtoken' +HMIPC_PIN = 'pin' diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py new file mode 100644 index 00000000000000..94fe5f40be8db5 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/device.py @@ -0,0 +1,71 @@ +"""GenericDevice for the HomematicIP Cloud component.""" +import logging + +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +ATTR_HOME_ID = 'home_id' +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_OPERATION_LOCK = 'operation_lock' + + +class HomematicipGenericDevice(Entity): + """Representation of an HomematicIP generic 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.""" + self._device.on_update(self._device_changed) + + def _device_changed(self, json, **kwargs): + """Handle device state changes.""" + _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.""" + name = self._device.label + if (self._home.name is not None and self._home.name != ''): + name = "{} {}".format(self._home.name, name) + if (self.post is not None and self.post != ''): + name = "{} {}".format(name, self.post) + return name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Device available.""" + return not self._device.unreach + + @property + def device_state_attributes(self): + """Return the state attributes of the generic device.""" + return { + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_MODEL_TYPE: self._device.modelType + } diff --git a/homeassistant/components/homematicip_cloud/errors.py b/homeassistant/components/homematicip_cloud/errors.py new file mode 100644 index 00000000000000..cb2925d1a70450 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/errors.py @@ -0,0 +1,22 @@ +"""Errors for the HomematicIP component.""" +from homeassistant.exceptions import HomeAssistantError + + +class HmipcException(HomeAssistantError): + """Base class for HomematicIP exceptions.""" + + +class HmipcConnectionError(HmipcException): + """Unable to connect to the HomematicIP cloud server.""" + + +class HmipcConnectionWait(HmipcException): + """Wait for registration to the HomematicIP cloud server.""" + + +class HmipcRegistrationFailed(HmipcException): + """Registration on HomematicIP cloud failed.""" + + +class HmipcPressButton(HmipcException): + """User needs to press the blue button.""" diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py new file mode 100644 index 00000000000000..a4e3e78e860805 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -0,0 +1,256 @@ +"""Accesspoint for the HomematicIP Cloud component.""" +import asyncio +import logging + +from homeassistant import config_entries +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.core import callback + +from .const import ( + HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME, + COMPONENTS) +from .errors import HmipcConnectionError + +_LOGGER = logging.getLogger(__name__) + + +class HomematicipAuth(object): + """Manages HomematicIP client registration.""" + + def __init__(self, hass, config): + """Initialize HomematicIP Cloud client registration.""" + self.hass = hass + self.config = config + self.auth = None + + async def async_setup(self): + """Connect to HomematicIP for registration.""" + try: + self.auth = await self.get_auth( + self.hass, + self.config.get(HMIPC_HAPID), + self.config.get(HMIPC_PIN) + ) + return True + except HmipcConnectionError: + return False + + async def async_checkbutton(self): + """Check blue butten has been pressed.""" + from homematicip.base.base_connection import HmipConnectionError + + try: + await self.auth.isRequestAcknowledged() + return True + except HmipConnectionError: + return False + + async def async_register(self): + """Register client at HomematicIP.""" + from homematicip.base.base_connection import HmipConnectionError + + try: + authtoken = await self.auth.requestAuthToken() + await self.auth.confirmAuthToken(authtoken) + return authtoken + except HmipConnectionError: + return False + + async def get_auth(self, hass, hapid, pin): + """Create a HomematicIP access point object.""" + from homematicip.aio.auth import AsyncAuth + from homematicip.base.base_connection import HmipConnectionError + + auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) + print(auth) + try: + await auth.init(hapid) + if pin: + auth.pin = pin + await auth.connectionRequest('HomeAssistant') + except HmipConnectionError: + return False + return auth + + +class HomematicipHAP(object): + """Manages HomematicIP http and websocket connection.""" + + def __init__(self, hass, config_entry): + """Initialize HomematicIP cloud connection.""" + self.hass = hass + self.config_entry = config_entry + self.home = None + + self._ws_close_requested = False + self._retry_task = None + self._tries = 0 + self._accesspoint_connected = True + self._retry_setup = None + + async def async_setup(self, tries=0): + """Initialize connection.""" + try: + self.home = await self.get_hap( + self.hass, + self.config_entry.data.get(HMIPC_HAPID), + self.config_entry.data.get(HMIPC_AUTHTOKEN), + self.config_entry.data.get(HMIPC_NAME) + ) + except HmipcConnectionError: + retry_delay = 2 ** min(tries + 1, 6) + _LOGGER.error("Error connecting to HomematicIP with HAP %s. " + "Retrying in %d seconds.", + self.config_entry.data.get(HMIPC_HAPID), retry_delay) + + async def retry_setup(_now): + """Retry setup.""" + if await self.async_setup(tries + 1): + self.config_entry.state = config_entries.ENTRY_STATE_LOADED + + self._retry_setup = self.hass.helpers.event.async_call_later( + retry_delay, retry_setup) + + return False + + _LOGGER.info('Connected to HomematicIP with HAP %s.', + self.config_entry.data.get(HMIPC_HAPID)) + + for component in COMPONENTS: + self.hass.async_add_job( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, component) + ) + return True + + @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 + + try: + await self.home.get_current_state() + except HmipConnectionError: + return + hmip_events = await self.home.enable_events() + try: + await hmip_events + except HmipConnectionError: + return + + async def async_connect(self): + """Start websocket connection.""" + from homematicip.base.base_connection import HmipConnectionError + + tries = 0 + while True: + try: + await self.home.get_current_state() + hmip_events = await self.home.enable_events() + tries = 0 + await hmip_events + except HmipConnectionError: + pass + + if self._ws_close_requested: + break + self._ws_close_requested = False + + tries += 1 + retry_delay = 2 ** min(tries + 1, 6) + _LOGGER.error("Error connecting to HomematicIP with HAP %s. " + "Retrying in %d seconds.", + self.config_entry.data.get(HMIPC_HAPID), retry_delay) + try: + self._retry_task = self.hass.async_add_job(asyncio.sleep( + retry_delay, loop=self.hass.loop)) + await self._retry_task + except asyncio.CancelledError: + break + + async def async_reset(self): + """Close the websocket connection.""" + self._ws_close_requested = True + if self._retry_setup is not None: + self._retry_setup.cancel() + if self._retry_task is not None: + self._retry_task.cancel() + self.home.disable_events() + _LOGGER.info("Closed connection to HomematicIP cloud server.") + for component in COMPONENTS: + await self.hass.config_entries.async_forward_entry_unload( + self.config_entry, component) + return True + + async def get_hap(self, hass, hapid, authtoken, name): + """Create a HomematicIP access point object.""" + from homematicip.aio.home import AsyncHome + from homematicip.base.base_connection import HmipConnectionError + + home = AsyncHome(hass.loop, async_get_clientsession(hass)) + + home.name = name + home.label = 'Access Point' + home.modelType = 'HmIP-HAP' + + home.set_auth_token(authtoken) + try: + await home.init(hapid) + await home.get_current_state() + except HmipConnectionError: + raise HmipcConnectionError + home.on_update(self.async_update) + hass.loop.create_task(self.async_connect()) + + return home diff --git a/homeassistant/components/homematicip_cloud/strings.json b/homeassistant/components/homematicip_cloud/strings.json new file mode 100644 index 00000000000000..887a3a5780b0eb --- /dev/null +++ b/homeassistant/components/homematicip_cloud/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "title": "HomematicIP Cloud", + "step": { + "init": { + "title": "Pick HomematicIP Accesspoint", + "data": { + "hapid": "Accesspoint ID (SGTIN)", + "pin": "Pin Code (optional)", + "name": "Name (optional, used as name prefix for all devices)" + } + }, + "link": { + "title": "Link Accesspoint", + "description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)" + } + }, + "error": { + "register_failed": "Failed to register, please try again.", + "invalid_pin": "Invalid PIN, please try again.", + "press_the_button": "Please press the blue button.", + "timeout_button": "Blue button press timeout, please try again." + }, + "abort": { + "unknown": "Unknown error occurred.", + "conection_aborted": "Could not connect to HMIP server", + "already_configured": "Accesspoint is already configured" + } + } +} diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 17906157a6e797..37a6805dfb58b8 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -40,33 +40,29 @@ CONF_SERVER_PORT = 'server_port' CONF_BASE_URL = 'base_url' CONF_SSL_CERTIFICATE = 'ssl_certificate' +CONF_SSL_PEER_CERTIFICATE = 'ssl_peer_certificate' CONF_SSL_KEY = 'ssl_key' CONF_CORS_ORIGINS = 'cors_allowed_origins' CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' +CONF_TRUSTED_PROXIES = 'trusted_proxies' CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_IP_BAN_ENABLED = 'ip_ban_enabled' # TLS configuration follows the best-practice guidelines specified here: # https://wiki.mozilla.org/Security/Server_Side_TLS -# Intermediate guidelines are followed. -SSL_VERSION = ssl.PROTOCOL_SSLv23 -SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 +# Modern guidelines are followed. +SSL_VERSION = ssl.PROTOCOL_TLS # pylint: disable=no-member +SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | \ + ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | \ + ssl.OP_CIPHER_SERVER_PREFERENCE if hasattr(ssl, 'OP_NO_COMPRESSION'): SSL_OPTS |= ssl.OP_NO_COMPRESSION -CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ +CIPHERS = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ + "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ - "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ - "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \ - "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \ - "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \ - "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \ - "ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \ - "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \ - "DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \ - "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \ - "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \ - "AES256-SHA:DES-CBC3-SHA:!DSS" + "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" \ + "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" _LOGGER = logging.getLogger(__name__) @@ -80,10 +76,13 @@ vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_BASE_URL): cv.string, vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, + vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, + vol.Optional(CONF_TRUSTED_PROXIES, default=[]): + vol.All(cv.ensure_list, [ip_network]), vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All(cv.ensure_list, [ip_network]), vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, @@ -108,9 +107,11 @@ async def async_setup(hass, config): server_host = conf[CONF_SERVER_HOST] server_port = conf[CONF_SERVER_PORT] ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) + ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE) ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf[CONF_CORS_ORIGINS] use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR] + trusted_proxies = conf[CONF_TRUSTED_PROXIES] trusted_networks = conf[CONF_TRUSTED_NETWORKS] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] @@ -125,9 +126,11 @@ async def async_setup(hass, config): server_port=server_port, api_password=api_password, ssl_certificate=ssl_certificate, + ssl_peer_certificate=ssl_peer_certificate, ssl_key=ssl_key, cors_origins=cors_origins, use_x_forwarded_for=use_x_forwarded_for, + trusted_proxies=trusted_proxies, trusted_networks=trusted_networks, login_threshold=login_threshold, is_ban_enabled=is_ban_enabled @@ -166,21 +169,37 @@ async def start_server(event): class HomeAssistantHTTP(object): """HTTP server for Home Assistant.""" - def __init__(self, hass, api_password, ssl_certificate, + def __init__(self, hass, api_password, + ssl_certificate, ssl_peer_certificate, ssl_key, server_host, server_port, cors_origins, - use_x_forwarded_for, trusted_networks, + use_x_forwarded_for, trusted_proxies, trusted_networks, login_threshold, is_ban_enabled): """Initialize the HTTP Home Assistant server.""" app = self.app = web.Application( middlewares=[staticresource_middleware]) # This order matters - setup_real_ip(app, use_x_forwarded_for) + setup_real_ip(app, use_x_forwarded_for, trusted_proxies) if is_ban_enabled: setup_bans(hass, app, login_threshold) - setup_auth(app, trusted_networks, api_password) + if hass.auth.active: + if hass.auth.support_legacy: + _LOGGER.warning("Experimental auth api enabled and " + "legacy_api_password support enabled. Please " + "use access_token instead api_password, " + "although you can still use legacy " + "api_password") + else: + _LOGGER.warning("Experimental auth api enabled. Please use " + "access_token instead api_password.") + elif api_password is None: + _LOGGER.warning("You have been advised to set http.api_password.") + + setup_auth(app, trusted_networks, hass.auth.active, + support_legacy=hass.auth.support_legacy, + api_password=api_password) if cors_origins: setup_cors(app, cors_origins) @@ -190,6 +209,7 @@ def __init__(self, hass, api_password, ssl_certificate, self.hass = hass self.api_password = api_password self.ssl_certificate = ssl_certificate + self.ssl_peer_certificate = ssl_peer_certificate self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port @@ -287,8 +307,12 @@ async def start(self): except OSError as error: _LOGGER.error("Could not read SSL certificate from %s: %s", self.ssl_certificate, error) - context = None return + + if self.ssl_peer_certificate: + context.verify_mode = ssl.CERT_REQUIRED + context.load_verify_locations(cafile=self.ssl_peer_certificate) + else: context = None diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index c4723abccee348..a232d9295a4d7f 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -17,37 +17,44 @@ @callback -def setup_auth(app, trusted_networks, api_password): +def setup_auth(app, trusted_networks, use_auth, + support_legacy=False, api_password=None): """Create auth middleware for the app.""" @middleware async def auth_middleware(request, handler): """Authenticate as middleware.""" - # If no password set, just always set authenticated=True - if api_password is None: - request[KEY_AUTHENTICATED] = True - return await handler(request) - - # Check authentication authenticated = False - if (HTTP_HEADER_HA_AUTH in request.headers and - hmac.compare_digest( - api_password.encode('utf-8'), - request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): + if use_auth and (HTTP_HEADER_HA_AUTH in request.headers or + DATA_API_PASSWORD in request.query): + _LOGGER.warning('Please use access_token instead api_password.') + + legacy_auth = (not use_auth or support_legacy) and api_password + if (hdrs.AUTHORIZATION in request.headers and + await async_validate_auth_header( + request, api_password if legacy_auth else None)): + # it included both use_auth and api_password Basic auth + authenticated = True + + elif (legacy_auth and HTTP_HEADER_HA_AUTH in request.headers and + hmac.compare_digest( + 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 + elif (legacy_auth and DATA_API_PASSWORD in request.query and 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 - await async_validate_auth_header(api_password, request)): + elif _is_trusted_ip(request, trusted_networks): authenticated = True - elif _is_trusted_ip(request, trusted_networks): + elif not use_auth and api_password is None: + # If neither password nor auth_providers set, + # just always set authenticated=True authenticated = True request[KEY_AUTHENTICATED] = authenticated @@ -76,8 +83,12 @@ def validate_password(request, api_password): request.app['hass'].http.api_password.encode('utf-8')) -async def async_validate_auth_header(api_password, request): - """Test an authorization header if valid password.""" +async def async_validate_auth_header(request, api_password=None): + """ + Test authorization header against access token. + + Basic auth_type is legacy code, should be removed with api_password. + """ if hdrs.AUTHORIZATION not in request.headers: return False @@ -88,7 +99,16 @@ async def async_validate_auth_header(api_password, request): # If no space in authorization header return False - if auth_type == 'Basic': + if auth_type == 'Bearer': + hass = request.app['hass'] + access_token = hass.auth.async_get_access_token(auth_val) + if access_token is None: + return False + + request['hass_user'] = access_token.refresh_token.user + return True + + elif auth_type == 'Basic' and api_password is not None: decoded = base64.b64decode(auth_val).decode('utf-8') try: username, password = decoded.split(':', 1) @@ -102,13 +122,5 @@ async def async_validate_auth_header(api_password, request): return hmac.compare_digest(api_password.encode('utf-8'), password.encode('utf-8')) - if auth_type != 'Bearer': + else: return False - - hass = request.app['hass'] - access_token = hass.auth.async_get_access_token(auth_val) - if access_token is None: - return False - - request['hass_user'] = access_token.refresh_token.user - return True diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index c394016a683c43..f8adc815fdef25 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -11,18 +11,25 @@ @callback -def setup_real_ip(app, use_x_forwarded_for): +def setup_real_ip(app, use_x_forwarded_for, trusted_proxies): """Create IP Ban middleware for the app.""" @middleware async def real_ip_middleware(request, handler): """Real IP middleware.""" - if (use_x_forwarded_for and - X_FORWARDED_FOR in request.headers): - request[KEY_REAL_IP] = ip_address( - request.headers.get(X_FORWARDED_FOR).split(',')[0]) - else: - request[KEY_REAL_IP] = \ - ip_address(request.transport.get_extra_info('peername')[0]) + connected_ip = ip_address( + request.transport.get_extra_info('peername')[0]) + request[KEY_REAL_IP] = connected_ip + + # Only use the XFF header if enabled, present, and from a trusted proxy + try: + if (use_x_forwarded_for and + X_FORWARDED_FOR in request.headers and + any(connected_ip in trusted_proxy + for trusted_proxy in trusted_proxies)): + request[KEY_REAL_IP] = ip_address( + request.headers.get(X_FORWARDED_FOR).split(', ')[-1]) + except ValueError: + pass return await handler(request) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 3fbaf703d06798..cd07ab6df6916c 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -18,7 +18,6 @@ async def _handle(self, request): filename = URL(request.match_info['filename']).path try: # PyLint is wrong about resolve not being a member. - # pylint: disable=no-member filepath = self._directory.joinpath(filename).resolve() if not self._follow_symlinks: filepath.relative_to(self._directory) diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index d466488e9fcd85..dc0968dc88acbb 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -24,6 +24,6 @@ "title": "Hub verbinden" } }, - "title": "Philips Hue Bridge" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index ea1e4fff1bf915..b471dd1a0cd59e 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -24,6 +24,6 @@ "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" } }, - "title": "\u0428\u043b\u044e\u0437 Philips Hue" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index d7a8dc7f7300b3..8710b2561b0580 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -124,24 +124,16 @@ async def hue_activate_scene(self, call, updated=False): (group for group in self.api.groups.values() if group.name == group_name), None) - # The same scene name can exist in multiple groups. - # In this case, activate first scene that contains the - # the exact same light IDs as the group - scenes = [] - for scene in self.api.scenes.values(): - if scene.name == scene_name: - scenes.append(scene) - if len(scenes) == 1: - scene_id = scenes[0].id - else: - group_lights = sorted(group.lights) - for scene in scenes: - if group_lights == scene.lights: - scene_id = scene.id - break + # Additional scene logic to handle duplicate scene names across groups + scene = next( + (scene for scene in self.api.scenes.values() + if scene.name == scene_name + and group is not None + and sorted(scene.lights) == sorted(group.lights)), + None) # If we can't find it, fetch latest info. - if not updated and (group is None or scene_id is None): + if not updated and (group is None or scene is None): await self.api.groups.update() await self.api.scenes.update() await self.hue_activate_scene(call, updated=True) @@ -151,11 +143,11 @@ async def hue_activate_scene(self, call, updated=False): LOGGER.warning('Unable to find group %s', group_name) return - if scene_id is None: + if scene is None: LOGGER.warning('Unable to find scene %s', scene_name) return - await group.set_action(scene=scene_id) + await group.set_action(scene=scene.id) async def get_bridge(hass, host, username=None): diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 29f26cc84e6c0e..480ec31da7d127 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -69,27 +69,32 @@ @bind_hass def scan(hass, entity_id=None): - """Force process an image.""" + """Force process of all cameras or given entity.""" + hass.add_job(async_scan, hass, entity_id) + + +@callback +@bind_hass +def async_scan(hass, entity_id=None): + """Force process of all cameras or given entity.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_SCAN, data) + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) - yield from component.async_setup(config) + await component.async_setup(config) - @asyncio.coroutine - def async_scan_service(service): + async def async_scan_service(service): """Service handler for scan.""" image_entities = component.async_extract_from_service(service) update_task = [entity.async_update_ha_state(True) for entity in image_entities] if update_task: - yield from asyncio.wait(update_task, loop=hass.loop) + await asyncio.wait(update_task, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SCAN, async_scan_service, @@ -124,8 +129,7 @@ def async_process_image(self, image): """ return self.hass.async_add_job(self.process_image, image) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update image and process it. This method is a coroutine. @@ -134,7 +138,7 @@ def async_update(self): image = None try: - image = yield from camera.async_get_image( + image = await camera.async_get_image( self.camera_entity, timeout=self.timeout) except HomeAssistantError as err: @@ -142,7 +146,7 @@ def async_update(self): return # process image data - yield from self.async_process_image(image.content) + await self.async_process_image(image.content) class ImageProcessingFaceEntity(ImageProcessingEntity): diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index e01131c7d1b3f7..ca0f3527f73491 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -152,7 +152,6 @@ def process_image(self, image): import cv2 # pylint: disable=import-error import numpy - # pylint: disable=no-member cv_image = cv2.imdecode( numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) @@ -168,7 +167,6 @@ def process_image(self, image): else: path = classifier - # pylint: disable=no-member cascade = cv2.CascadeClassifier(path) detections = cascade.detectMultiScale( diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index b2f7c8b66551bd..82fc6b0226621a 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.10.0'] +REQUIREMENTS = ['insteonplm==0.11.3'] _LOGGER = logging.getLogger(__name__) @@ -300,7 +300,8 @@ def __init__(self): OpenClosedRelay) from insteonplm.states.dimmable import (DimmableSwitch, - DimmableSwitch_Fan) + DimmableSwitch_Fan, + DimmableRemote) from insteonplm.states.sensor import (VariableSensor, OnOffSensor, @@ -328,6 +329,7 @@ def __init__(self): State(DimmableSwitch_Fan, 'fan'), State(DimmableSwitch, 'light'), + State(DimmableRemote, 'binary_sensor'), State(X10DimmableSwitch, 'light'), State(X10OnOffSwitch, 'switch'), diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index 249f147847c08b..7f7377469fd3cb 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -181,7 +181,6 @@ def devices_with_push(): def enabled_push_ids(): """Return a list of push enabled target push IDs.""" push_ids = list() - # pylint: disable=unused-variable for device in CONFIG_FILE[ATTR_DEVICES].values(): if device.get(ATTR_PUSH_ID) is not None: push_ids.append(device.get(ATTR_PUSH_ID)) @@ -203,7 +202,6 @@ def device_name_for_push_id(push_id): def setup(hass, config): """Set up the iOS component.""" - # pylint: disable=import-error global CONFIG_FILE global CONFIG_FILE_PATH diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 90ab41cf98b7e0..d8afb7be5dae13 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -11,12 +11,12 @@ import voluptuous as vol -from homeassistant.core import HomeAssistant # noqa +from homeassistant.core import HomeAssistant from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery, config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, Dict # noqa +from homeassistant.helpers.typing import ConfigType, Dict REQUIREMENTS = ['PyISY==1.1.0'] @@ -268,7 +268,6 @@ def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str, sensor_identifier: str)-> None: """Sort the nodes to their proper domains.""" - # pylint: disable=no-member for (path, node) in nodes: ignored = ignore_identifier in path or ignore_identifier in node.name if ignored: diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index af45bd3d4f983d..bbd7bc4408235f 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -151,7 +151,6 @@ def run(self): if not event: continue - # pylint: disable=no-member if event.type is ecodes.EV_KEY and event.value is self.key_value: _LOGGER.debug(categorize(event)) self.hass.bus.fire( diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 5b28b7b0999740..26fe356d77247f 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -10,7 +10,7 @@ import voluptuous as vol from aiohttp.hdrs import AUTHORIZATION -from aiohttp.web import Request, Response # NOQA +from aiohttp.web import Request, Response from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.components.discovery import SERVICE_KONNECTED diff --git a/homeassistant/components/lametric.py b/homeassistant/components/lametric.py index 49b4f73ea17e3f..96ea3781566cd1 100644 --- a/homeassistant/components/lametric.py +++ b/homeassistant/components/lametric.py @@ -31,7 +31,6 @@ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=broad-except def setup(hass, config): """Set up the LaMetricManager.""" _LOGGER.debug("Setting up LaMetric platform") diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 30a1a800a44988..b8a97607215566 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -446,8 +446,6 @@ def get(cls, name): class Light(ToggleEntity): """Representation of a light.""" - # pylint: disable=no-self-use - @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py index b4b9f4e777567c..be608ea477668b 100644 --- a/homeassistant/components/light/avion.py +++ b/homeassistant/components/light/avion.py @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up an Avion switch.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import avion lights = [] @@ -70,7 +70,7 @@ class AvionLight(Light): def __init__(self, device): """Initialize the light.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import avion self._name = device['name'] @@ -117,7 +117,7 @@ def assumed_state(self): def set_state(self, brightness): """Set the state of this lamp to the provided brightness.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import avion # Bluetooth LE is unreliable, and the connection may drop at any diff --git a/homeassistant/components/light/blinkt.py b/homeassistant/components/light/blinkt.py index 97edd7c54d254e..7035320945a0ef 100644 --- a/homeassistant/components/light/blinkt.py +++ b/homeassistant/components/light/blinkt.py @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Blinkt Light platform.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import blinkt # ensure that the lights are off when exiting diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index a4593a72617bbf..08d7f5773f7579 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -101,9 +101,11 @@ def color_temp(self): return self._light.ct @property - def xy_color(self): - """Return the XY color value.""" - return self._light.xy + def hs_color(self): + """Return the hs color value.""" + if self._light.colormode in ('xy', 'hs') and self._light.xy: + return color_util.color_xy_to_hs(*self._light.xy) + return None @property def is_on(self): @@ -172,7 +174,7 @@ async def async_turn_off(self, **kwargs): data = {'on': False} if ATTR_TRANSITION in kwargs: - data = {'bri': 0} + data['bri'] = 0 data['transitiontime'] = int(kwargs[ATTR_TRANSITION]) * 10 if ATTR_FLASH in kwargs: diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py index c7478b435ee3e0..85d9180c59bcae 100644 --- a/homeassistant/components/light/decora.py +++ b/homeassistant/components/light/decora.py @@ -75,7 +75,7 @@ class DecoraLight(Light): def __init__(self, device): """Initialize the light.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import decora self._name = device['name'] diff --git a/homeassistant/components/light/decora_wifi.py b/homeassistant/components/light/decora_wifi.py index 111d39f20190ac..17003d51610c13 100644 --- a/homeassistant/components/light/decora_wifi.py +++ b/homeassistant/components/light/decora_wifi.py @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Decora WiFi platform.""" - # pylint: disable=import-error, no-member, no-name-in-module + # pylint: disable=import-error, no-name-in-module from decora_wifi import DecoraWiFiSession from decora_wifi.models.person import Person from decora_wifi.models.residential_account import ResidentialAccount diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index fc85e05238f9b6..b9db9d4f99b64d 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -33,6 +33,10 @@ MODE_RGB = 'rgb' MODE_RGBW = 'rgbw' +# This mode enables white value to be controlled by brightness. +# RGB value is ignored when this mode is specified. +MODE_WHITE = 'w' + # List of supported effects which aren't already declared in LIGHT EFFECT_RED_FADE = 'red_fade' EFFECT_GREEN_FADE = 'green_fade' @@ -84,7 +88,7 @@ DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Optional(ATTR_MODE, default=MODE_RGBW): - vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB])), + vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB, MODE_WHITE])), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(['ledenet'])), }) @@ -181,6 +185,9 @@ def is_on(self): @property def brightness(self): """Return the brightness of this light between 0..255.""" + if self._mode == MODE_WHITE: + return self.white_value + return self._bulb.brightness @property @@ -191,9 +198,12 @@ def hs_color(self): @property def supported_features(self): """Flag supported features.""" - if self._mode is MODE_RGBW: + if self._mode == MODE_RGBW: return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE + if self._mode == MODE_WHITE: + return SUPPORT_BRIGHTNESS + return SUPPORT_FLUX_LED @property @@ -208,9 +218,6 @@ def effect_list(self): def turn_on(self, **kwargs): """Turn the specified or all lights on.""" - if not self.is_on: - self._bulb.turnOn() - hs_color = kwargs.get(ATTR_HS_COLOR) if hs_color: @@ -247,10 +254,23 @@ def turn_on(self, **kwargs): if rgb is None: rgb = self._bulb.getRgb() - self._bulb.setRgb(*tuple(rgb), brightness=brightness) + if white is None and self._mode == MODE_RGBW: + white = self.white_value - if white is not None: - self._bulb.setWarmWhite255(white) + # handle W only mode (use brightness instead of white value) + if self._mode == MODE_WHITE: + self._bulb.setRgbw(0, 0, 0, w=brightness) + + # handle RGBW mode + elif self._mode == MODE_RGBW: + self._bulb.setRgbw(*tuple(rgb), w=white, brightness=brightness) + + # handle RGB mode + else: + self._bulb.setRgb(*tuple(rgb), brightness=brightness) + + if not self.is_on: + self._bulb.turnOn() def turn_off(self, **kwargs): """Turn the specified or all lights off.""" diff --git a/homeassistant/components/light/homekit_controller.py b/homeassistant/components/light/homekit_controller.py index e6dc09e455cb27..8d77cb0523668e 100644 --- a/homeassistant/components/light/homekit_controller.py +++ b/homeassistant/components/light/homekit_controller.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.homekit_controller/ """ -import json import logging from homeassistant.components.homekit_controller import ( @@ -122,13 +121,11 @@ def turn_on(self, **kwargs): characteristics.append({'aid': self._aid, 'iid': self._chars['on'], 'value': True}) - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) def turn_off(self, **kwargs): """Turn the specified light off.""" characteristics = [{'aid': self._aid, 'iid': self._chars['on'], 'value': False}] - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) diff --git a/homeassistant/components/light/homematicip_cloud.py b/homeassistant/components/light/homematicip_cloud.py index e433da44ae768b..5984fb0365792e 100644 --- a/homeassistant/components/light/homematicip_cloud.py +++ b/homeassistant/components/light/homematicip_cloud.py @@ -9,8 +9,8 @@ from homeassistant.components.light import Light from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) DEPENDENCIES = ['homematicip_cloud'] @@ -23,13 +23,16 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the HomematicIP light devices.""" + """Old way of setting up HomematicIP lights.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP lights from a config entry.""" from homematicip.device import ( BrandSwitchMeasuring) - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: if isinstance(device, BrandSwitchMeasuring): diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 421356f07bc632..9b2c183c1d1a87 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -446,7 +446,9 @@ def brightness(self): @property def color_temp(self): """Return the color temperature.""" - kelvin = self.device.color[3] + _, sat, _, kelvin = self.device.color + if sat: + return None return color_util.color_temperature_kelvin_to_mired(kelvin) @property @@ -601,7 +603,7 @@ def hs_color(self): hue, sat, _, _ = self.device.color hue = hue / 65535 * 360 sat = sat / 65535 * 100 - return (hue, sat) + return (hue, sat) if sat else None class LIFXStrip(LIFXColor): diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index bd4fece89e339c..71d3f9d95d7177 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -136,7 +136,7 @@ def state(new_state): """ def decorator(function): """Set up the decorator function.""" - # pylint: disable=no-member,protected-access + # pylint: disable=protected-access def wrapper(self, **kwargs): """Wrap a group state change.""" from limitlessled.pipeline import Pipeline diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 97a4cc8c137ea4..c0e363f85d6d40 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -442,8 +442,15 @@ async def async_turn_on(self, **kwargs): self._topic[CONF_RGB_COMMAND_TOPIC] is not None: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 14f5ee7a9b9142..705e106fdff8be 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -345,9 +345,14 @@ async def async_turn_on(self, **kwargs): hs_color = kwargs[ATTR_HS_COLOR] message['color'] = {} if self._rgb: - brightness = kwargs.get( - ATTR_BRIGHTNESS, - self._brightness if self._brightness else 255) + # If there's a brightness topic set, we don't want to scale the + # RGB values given using the brightness. + if self._brightness is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, + self._brightness if self._brightness else 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) message['color']['r'] = rgb[0] diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index e32c13fc5b6eff..f6b3fbe8b70799 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -317,8 +317,15 @@ async def async_turn_on(self, **kwargs): if ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) values['red'] = rgb[0] diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 55387288d7f233..4139abd40fa287 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -28,7 +28,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsLight(mysensors.MySensorsEntity, Light): +class MySensorsLight(mysensors.device.MySensorsEntity, Light): """Representation of a MySensors Light child node.""" def __init__(self, *args): diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 202c6ac594d807..791de291b4803d 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -310,7 +310,7 @@ def update(self) -> None: bright = self._properties.get('bright', None) if bright: - self._brightness = 255 * (int(bright) / 100) + self._brightness = round(255 * (int(bright) / 100)) temp_in_k = self._properties.get('ct', None) if temp_in_k: diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 04216780c80252..3bfa167f8eca20 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -6,8 +6,6 @@ """ import logging -# Because we do not compile openzwave on CI -# pylint: disable=import-error from threading import Timer from homeassistant.components.light import ( ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, diff --git a/homeassistant/components/lirc.py b/homeassistant/components/lirc.py index 0cd49ab6c9a322..d7ec49e00968fb 100644 --- a/homeassistant/components/lirc.py +++ b/homeassistant/components/lirc.py @@ -4,7 +4,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/lirc/ """ -# pylint: disable=import-error,no-member +# pylint: disable=no-member import threading import time import logging diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index b3e4ac8f0ff6a7..f03d028a38f1ee 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -145,7 +145,6 @@ def changed_by(self): """Last change triggered by.""" return None - # pylint: disable=no-self-use @property def code_format(self): """Regex for code format or None if no code is required.""" diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py index 79e4308dbda114..9bcf5a86d08ecc 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -5,7 +5,7 @@ https://home-assistant.io/components/lock.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.lock import LockDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, diff --git a/homeassistant/components/lock/sesame.py b/homeassistant/components/lock/sesame.py index 09f7266d15c6a1..8d9c05e3f26d71 100644 --- a/homeassistant/components/lock/sesame.py +++ b/homeassistant/components/lock/sesame.py @@ -4,7 +4,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/lock.sesame/ """ -from typing import Callable # noqa +from typing import Callable import voluptuous as vol import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 8f39d440caed8e..b7bc9f15e19953 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -4,8 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.zwave/ """ -# Because we do not compile openzwave on CI -# pylint: disable=import-error import asyncio import logging diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index daaffd0174c7ac..0baca2f341c8c0 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -55,7 +55,6 @@ def set_level(hass, logs): class HomeAssistantLogFilter(logging.Filter): """A log filter.""" - # pylint: disable=no-init def __init__(self, logfilter): """Initialize the filter.""" super().__init__() diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 497b6f995bd18b..85895fdd7516e9 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.06.14'] +REQUIREMENTS = ['youtube_dl==2018.06.25'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d963deba7b55e7..d314dec65ea967 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -471,7 +471,6 @@ class MediaPlayerDevice(Entity): _access_token = None - # pylint: disable=no-self-use # Implement these for your media player @property def state(self): diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index eced0dbbe25bb2..4e24d5f2f713b8 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -4,7 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.cast/ """ -# pylint: disable=import-error +import asyncio import logging import threading from typing import Optional, Tuple @@ -200,9 +200,13 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up Cast from a config entry.""" - await _async_setup_platform( - hass, hass.data[CAST_DOMAIN].get('media_player', {}), - async_add_devices, None) + config = hass.data[CAST_DOMAIN].get('media_player', {}) + if not isinstance(config, list): + config = [config] + + await asyncio.wait([ + _async_setup_platform(hass, cfg, async_add_devices, None) + for cfg in config]) async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 405c220c8770a3..9edf69cd9c69ef 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -295,7 +295,6 @@ def media_artist(self): @property def media_album_name(self): """Return the album of current playing media (Music track only).""" - # pylint: disable=no-self-use return "Bounzz" @property diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 8cd47476058f35..ff0e4d907b11e5 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -61,7 +61,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Denon platform.""" - # pylint: disable=import-error import denonavr # Initialize list with receivers to be started diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index 8c98844cf9358a..df1ee662124bd0 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -20,8 +20,7 @@ STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) import homeassistant.util as util -REQUIREMENTS = ['https://github.com/wokar/pylgnetcast/archive/' - 'v0.2.0.zip#pylgnetcast==0.2.0'] +REQUIREMENTS = ['pylgnetcast-homeassistant==0.2.0.dev0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 6690382846fd15..ca6b9722a496a0 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -747,7 +747,6 @@ def media_previous_track(self): if self.device and 'playback' in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) - # pylint: disable=W0613 def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" if not (self.device and diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 15a2b41795e8c7..c3de341d607fd0 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.samsungtv/ """ +import asyncio import logging import socket from datetime import timedelta @@ -15,8 +16,9 @@ from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON) + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_PLAY, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY_MEDIA, + MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_CHANNEL) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT, CONF_MAC) @@ -32,12 +34,13 @@ DEFAULT_NAME = 'Samsung TV Remote' DEFAULT_PORT = 55000 DEFAULT_TIMEOUT = 0 +KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = 'samsungtv_known_devices' SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_PLAY + SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -256,6 +259,23 @@ def media_previous_track(self): """Send the previous track command.""" self.send_key('KEY_REWIND') + async def async_play_media(self, media_type, media_id, **kwargs): + """Support changing a channel.""" + if media_type != MEDIA_TYPE_CHANNEL: + _LOGGER.error('Unsupported media type') + return + + # media_id should only be a channel number + try: + cv.positive_int(media_id) + except vol.Invalid: + _LOGGER.error('Media ID must be positive integer') + return + + for digit in media_id: + await self.hass.async_add_job(self.send_key, 'KEY_' + digit) + await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop) + def turn_on(self): """Turn the media player on.""" if self._mac: diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 03f847ae40c19e..66d12190320f4b 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/media_player.universal/ """ import logging -# pylint: disable=import-error from copy import copy import voluptuous as vol diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index fe46c858b5119f..fc6db96e029b03 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -75,7 +75,6 @@ def setup(hass, config): """Set up Modbus component.""" # Modbus connection type - # pylint: disable=import-error client_type = config[DOMAIN][CONF_TYPE] # Connect to Modbus network diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d5a3b4a2efb7e7..3916714b8d1626 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -21,7 +21,7 @@ SUPPORTED_COMPONENTS = [ 'binary_sensor', 'camera', 'cover', 'fan', - 'light', 'sensor', 'switch', 'lock'] + 'light', 'sensor', 'switch', 'lock', 'climate'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], @@ -32,6 +32,7 @@ 'lock': ['mqtt'], 'sensor': ['mqtt'], 'switch': ['mqtt'], + 'climate': ['mqtt'], } ALREADY_DISCOVERED = 'mqtt_discovered_components' diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py deleted file mode 100644 index 1e7e252bd9db28..00000000000000 --- a/homeassistant/components/mysensors.py +++ /dev/null @@ -1,705 +0,0 @@ -""" -Connect to a MySensors gateway via pymysensors API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mysensors/ -""" -import asyncio -from collections import defaultdict -import logging -import os -import socket -import sys -from timeit import default_timer as timer - -import async_timeout -import voluptuous as vol - -from homeassistant.components.mqtt import ( - valid_publish_topic, valid_subscribe_topic) -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP, - STATE_OFF, STATE_ON) -from homeassistant.core import callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) -from homeassistant.helpers.entity import Entity -from homeassistant.setup import async_setup_component - -REQUIREMENTS = ['pymysensors==0.14.0'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_CHILD_ID = 'child_id' -ATTR_DESCRIPTION = 'description' -ATTR_DEVICE = 'device' -ATTR_DEVICES = 'devices' -ATTR_NODE_ID = 'node_id' - -CONF_BAUD_RATE = 'baud_rate' -CONF_DEBUG = 'debug' -CONF_DEVICE = 'device' -CONF_GATEWAYS = 'gateways' -CONF_PERSISTENCE = 'persistence' -CONF_PERSISTENCE_FILE = 'persistence_file' -CONF_RETAIN = 'retain' -CONF_TCP_PORT = 'tcp_port' -CONF_TOPIC_IN_PREFIX = 'topic_in_prefix' -CONF_TOPIC_OUT_PREFIX = 'topic_out_prefix' -CONF_VERSION = 'version' - -CONF_NODES = 'nodes' -CONF_NODE_NAME = 'name' - -DEFAULT_BAUD_RATE = 115200 -DEFAULT_TCP_PORT = 5003 -DEFAULT_VERSION = '1.4' -DOMAIN = 'mysensors' - -GATEWAY_READY_TIMEOUT = 15.0 -MQTT_COMPONENT = 'mqtt' -MYSENSORS_GATEWAYS = 'mysensors_gateways' -MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' -MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}' -PLATFORM = 'platform' -SCHEMA = 'schema' -SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' -TYPE = 'type' - - -def is_socket_address(value): - """Validate that value is a valid address.""" - try: - socket.getaddrinfo(value, None) - return value - except OSError: - raise vol.Invalid('Device is not a valid domain name or ip address') - - -def has_parent_dir(value): - """Validate that value is in an existing directory which is writeable.""" - parent = os.path.dirname(os.path.realpath(value)) - is_dir_writable = os.path.isdir(parent) and os.access(parent, os.W_OK) - if not is_dir_writable: - raise vol.Invalid( - '{} directory does not exist or is not writeable'.format(parent)) - return value - - -def has_all_unique_files(value): - """Validate that all persistence files are unique and set if any is set.""" - persistence_files = [ - gateway.get(CONF_PERSISTENCE_FILE) for gateway in value] - if None in persistence_files and any( - name is not None for name in persistence_files): - raise vol.Invalid( - 'persistence file name of all devices must be set if any is set') - if not all(name is None for name in persistence_files): - schema = vol.Schema(vol.Unique()) - schema(persistence_files) - return value - - -def is_persistence_file(value): - """Validate that persistence file path ends in either .pickle or .json.""" - if value.endswith(('.json', '.pickle')): - return value - else: - raise vol.Invalid( - '{} does not end in either `.json` or `.pickle`'.format(value)) - - -def is_serial_port(value): - """Validate that value is a windows serial port or a unix device.""" - if sys.platform.startswith('win'): - ports = ('COM{}'.format(idx + 1) for idx in range(256)) - if value in ports: - return value - else: - raise vol.Invalid('{} is not a serial port'.format(value)) - else: - return cv.isdevice(value) - - -def deprecated(key): - """Mark key as deprecated in configuration.""" - def validator(config): - """Check if key is in config, log warning and remove key.""" - if key not in config: - return config - _LOGGER.warning( - '%s option for %s is deprecated. Please remove %s from your ' - 'configuration file', key, DOMAIN, key) - config.pop(key) - return config - return validator - - -NODE_SCHEMA = vol.Schema({ - cv.positive_int: { - vol.Required(CONF_NODE_NAME): cv.string - } -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema(vol.All(deprecated(CONF_DEBUG), { - vol.Required(CONF_GATEWAYS): vol.All( - cv.ensure_list, has_all_unique_files, - [{ - vol.Required(CONF_DEVICE): - vol.Any(MQTT_COMPONENT, is_socket_address, is_serial_port), - vol.Optional(CONF_PERSISTENCE_FILE): - vol.All(cv.string, is_persistence_file, has_parent_dir), - vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): - cv.positive_int, - vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, - vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, - vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, - vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, - }] - ), - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, - vol.Optional(CONF_RETAIN, default=True): cv.boolean, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, - })) -}, extra=vol.ALLOW_EXTRA) - - -# MySensors const schemas -BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} -CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'} -LIGHT_DIMMER_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_DIMMER', - SCHEMA: {'V_DIMMER': cv.string, 'V_LIGHT': cv.string}} -LIGHT_PERCENTAGE_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_PERCENTAGE', - SCHEMA: {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string}} -LIGHT_RGB_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_RGB', SCHEMA: { - 'V_RGB': cv.string, 'V_STATUS': cv.string}} -LIGHT_RGBW_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_RGBW', SCHEMA: { - 'V_RGBW': cv.string, 'V_STATUS': cv.string}} -NOTIFY_SCHEMA = {PLATFORM: 'notify', TYPE: 'V_TEXT'} -DEVICE_TRACKER_SCHEMA = {PLATFORM: 'device_tracker', TYPE: 'V_POSITION'} -DUST_SCHEMA = [ - {PLATFORM: 'sensor', TYPE: 'V_DUST_LEVEL'}, - {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}] -SWITCH_LIGHT_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_LIGHT'} -SWITCH_STATUS_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_STATUS'} -MYSENSORS_CONST_SCHEMA = { - 'S_DOOR': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_MOTION': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SMOKE': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SPRINKLER': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_STATUS'}], - 'S_WATER_LEAK': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SOUND': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_VIBRATION': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_MOISTURE': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_HVAC': [CLIMATE_SCHEMA], - 'S_COVER': [ - {PLATFORM: 'cover', TYPE: 'V_DIMMER'}, - {PLATFORM: 'cover', TYPE: 'V_PERCENTAGE'}, - {PLATFORM: 'cover', TYPE: 'V_LIGHT'}, - {PLATFORM: 'cover', TYPE: 'V_STATUS'}], - 'S_DIMMER': [LIGHT_DIMMER_SCHEMA, LIGHT_PERCENTAGE_SCHEMA], - 'S_RGB_LIGHT': [LIGHT_RGB_SCHEMA], - 'S_RGBW_LIGHT': [LIGHT_RGBW_SCHEMA], - 'S_INFO': [NOTIFY_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_TEXT'}], - 'S_GPS': [ - DEVICE_TRACKER_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_POSITION'}], - 'S_TEMP': [{PLATFORM: 'sensor', TYPE: 'V_TEMP'}], - 'S_HUM': [{PLATFORM: 'sensor', TYPE: 'V_HUM'}], - 'S_BARO': [ - {PLATFORM: 'sensor', TYPE: 'V_PRESSURE'}, - {PLATFORM: 'sensor', TYPE: 'V_FORECAST'}], - 'S_WIND': [ - {PLATFORM: 'sensor', TYPE: 'V_WIND'}, - {PLATFORM: 'sensor', TYPE: 'V_GUST'}, - {PLATFORM: 'sensor', TYPE: 'V_DIRECTION'}], - 'S_RAIN': [ - {PLATFORM: 'sensor', TYPE: 'V_RAIN'}, - {PLATFORM: 'sensor', TYPE: 'V_RAINRATE'}], - 'S_UV': [{PLATFORM: 'sensor', TYPE: 'V_UV'}], - 'S_WEIGHT': [ - {PLATFORM: 'sensor', TYPE: 'V_WEIGHT'}, - {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], - 'S_POWER': [ - {PLATFORM: 'sensor', TYPE: 'V_WATT'}, - {PLATFORM: 'sensor', TYPE: 'V_KWH'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR'}, - {PLATFORM: 'sensor', TYPE: 'V_VA'}, - {PLATFORM: 'sensor', TYPE: 'V_POWER_FACTOR'}], - 'S_DISTANCE': [{PLATFORM: 'sensor', TYPE: 'V_DISTANCE'}], - 'S_LIGHT_LEVEL': [ - {PLATFORM: 'sensor', TYPE: 'V_LIGHT_LEVEL'}, - {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}], - 'S_IR': [ - {PLATFORM: 'sensor', TYPE: 'V_IR_RECEIVE'}, - {PLATFORM: 'switch', TYPE: 'V_IR_SEND', - SCHEMA: {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string}}], - 'S_WATER': [ - {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, - {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], - 'S_CUSTOM': [ - {PLATFORM: 'sensor', TYPE: 'V_VAR1'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR2'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR3'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR4'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR5'}, - {PLATFORM: 'sensor', TYPE: 'V_CUSTOM'}], - 'S_SCENE_CONTROLLER': [ - {PLATFORM: 'sensor', TYPE: 'V_SCENE_ON'}, - {PLATFORM: 'sensor', TYPE: 'V_SCENE_OFF'}], - 'S_COLOR_SENSOR': [{PLATFORM: 'sensor', TYPE: 'V_RGB'}], - 'S_MULTIMETER': [ - {PLATFORM: 'sensor', TYPE: 'V_VOLTAGE'}, - {PLATFORM: 'sensor', TYPE: 'V_CURRENT'}, - {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], - 'S_GAS': [ - {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, - {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], - 'S_WATER_QUALITY': [ - {PLATFORM: 'sensor', TYPE: 'V_TEMP'}, - {PLATFORM: 'sensor', TYPE: 'V_PH'}, - {PLATFORM: 'sensor', TYPE: 'V_ORP'}, - {PLATFORM: 'sensor', TYPE: 'V_EC'}, - {PLATFORM: 'switch', TYPE: 'V_STATUS'}], - 'S_AIR_QUALITY': DUST_SCHEMA, - 'S_DUST': DUST_SCHEMA, - 'S_LIGHT': [SWITCH_LIGHT_SCHEMA], - 'S_BINARY': [SWITCH_STATUS_SCHEMA], - 'S_LOCK': [{PLATFORM: 'switch', TYPE: 'V_LOCK_STATUS'}], -} - - -async def async_setup(hass, config): - """Set up the MySensors component.""" - import mysensors.mysensors as mysensors - - version = config[DOMAIN].get(CONF_VERSION) - persistence = config[DOMAIN].get(CONF_PERSISTENCE) - - async def setup_gateway( - device, persistence_file, baud_rate, tcp_port, in_prefix, - out_prefix): - """Return gateway after setup of the gateway.""" - if device == MQTT_COMPONENT: - if not await async_setup_component(hass, MQTT_COMPONENT, config): - return None - mqtt = hass.components.mqtt - retain = config[DOMAIN].get(CONF_RETAIN) - - def pub_callback(topic, payload, qos, retain): - """Call MQTT publish function.""" - mqtt.async_publish(topic, payload, qos, retain) - - def sub_callback(topic, sub_cb, qos): - """Call MQTT subscribe function.""" - @callback - def internal_callback(*args): - """Call callback.""" - sub_cb(*args) - - hass.async_add_job( - mqtt.async_subscribe(topic, internal_callback, qos)) - - gateway = mysensors.AsyncMQTTGateway( - pub_callback, sub_callback, in_prefix=in_prefix, - out_prefix=out_prefix, retain=retain, loop=hass.loop, - event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - else: - try: - await hass.async_add_job(is_serial_port, device) - gateway = mysensors.AsyncSerialGateway( - device, baud=baud_rate, loop=hass.loop, - event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - except vol.Invalid: - gateway = mysensors.AsyncTCPGateway( - device, port=tcp_port, loop=hass.loop, event_callback=None, - persistence=persistence, persistence_file=persistence_file, - protocol_version=version) - gateway.metric = hass.config.units.is_metric - gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) - gateway.device = device - gateway.event_callback = gw_callback_factory(hass) - if persistence: - await gateway.start_persistence() - - return gateway - - # Setup all devices from config - gateways = {} - conf_gateways = config[DOMAIN][CONF_GATEWAYS] - - for index, gway in enumerate(conf_gateways): - device = gway[CONF_DEVICE] - persistence_file = gway.get( - CONF_PERSISTENCE_FILE, - hass.config.path('mysensors{}.pickle'.format(index + 1))) - baud_rate = gway.get(CONF_BAUD_RATE) - tcp_port = gway.get(CONF_TCP_PORT) - in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') - out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - gateway = await setup_gateway( - device, persistence_file, baud_rate, tcp_port, in_prefix, - out_prefix) - if gateway is not None: - gateway.nodes_config = gway.get(CONF_NODES) - gateways[id(gateway)] = gateway - - if not gateways: - _LOGGER.error( - "No devices could be setup as gateways, check your configuration") - return False - - hass.data[MYSENSORS_GATEWAYS] = gateways - - hass.async_add_job(finish_setup(hass, gateways)) - - return True - - -async def finish_setup(hass, gateways): - """Load any persistent devices and platforms and start gateway.""" - discover_tasks = [] - start_tasks = [] - for gateway in gateways.values(): - discover_tasks.append(discover_persistent_devices(hass, gateway)) - start_tasks.append(gw_start(hass, gateway)) - if discover_tasks: - # Make sure all devices and platforms are loaded before gateway start. - await asyncio.wait(discover_tasks, loop=hass.loop) - if start_tasks: - await asyncio.wait(start_tasks, loop=hass.loop) - - -async def gw_start(hass, gateway): - """Start the gateway.""" - @callback - def gw_stop(event): - """Trigger to stop the gateway.""" - hass.async_add_job(gateway.stop()) - - await gateway.start() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) - if gateway.device == 'mqtt': - # Gatways connected via mqtt doesn't send gateway ready message. - return - gateway_ready = asyncio.Future() - gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway)) - hass.data[gateway_ready_key] = gateway_ready - - try: - with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop): - await gateway_ready - except asyncio.TimeoutError: - _LOGGER.warning( - "Gateway %s not ready after %s secs so continuing with setup", - gateway.device, GATEWAY_READY_TIMEOUT) - finally: - hass.data.pop(gateway_ready_key, None) - - -@callback -def set_gateway_ready(hass, msg): - """Set asyncio future result if gateway is ready.""" - if (msg.type != msg.gateway.const.MessageType.internal or - msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY): - return - gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format( - id(msg.gateway))) - if gateway_ready is None or gateway_ready.cancelled(): - return - gateway_ready.set_result(True) - - -def validate_child(gateway, node_id, child): - """Validate that a child has the correct values according to schema. - - Return a dict of platform with a list of device ids for validated devices. - """ - validated = defaultdict(list) - - if not child.values: - _LOGGER.debug( - "No child values for node %s child %s", node_id, child.id) - return validated - if gateway.sensors[node_id].sketch_name is None: - _LOGGER.debug("Node %s is missing sketch name", node_id) - return validated - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - s_name = next( - (member.name for member in pres if member.value == child.type), None) - if s_name not in MYSENSORS_CONST_SCHEMA: - _LOGGER.warning("Child type %s is not supported", s_name) - return validated - child_schemas = MYSENSORS_CONST_SCHEMA[s_name] - - def msg(name): - """Return a message for an invalid schema.""" - return "{} requires value_type {}".format( - pres(child.type).name, set_req[name].name) - - for schema in child_schemas: - platform = schema[PLATFORM] - v_name = schema[TYPE] - value_type = next( - (member.value for member in set_req if member.name == v_name), - None) - if value_type is None: - continue - _child_schema = child.get_schema(gateway.protocol_version) - vol_schema = _child_schema.extend( - {vol.Required(set_req[key].value, msg=msg(key)): - _child_schema.schema.get(set_req[key].value, val) - for key, val in schema.get(SCHEMA, {v_name: cv.string}).items()}, - extra=vol.ALLOW_EXTRA) - try: - vol_schema(child.values) - except vol.Invalid as exc: - level = (logging.WARNING if value_type in child.values - else logging.DEBUG) - _LOGGER.log( - level, - "Invalid values: %s: %s platform: node %s child %s: %s", - child.values, platform, node_id, child.id, exc) - continue - dev_id = id(gateway), node_id, child.id, value_type - validated[platform].append(dev_id) - return validated - - -@callback -def discover_mysensors_platform(hass, platform, new_devices): - """Discover a MySensors platform.""" - task = hass.async_add_job(discovery.async_load_platform( - hass, platform, DOMAIN, - {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) - return task - - -async def discover_persistent_devices(hass, gateway): - """Discover platforms for devices loaded via persistence file.""" - tasks = [] - new_devices = defaultdict(list) - for node_id in gateway.sensors: - node = gateway.sensors[node_id] - for child in node.children.values(): - validated = validate_child(gateway, node_id, child) - for platform, dev_ids in validated.items(): - new_devices[platform].extend(dev_ids) - for platform, dev_ids in new_devices.items(): - tasks.append(discover_mysensors_platform(hass, platform, dev_ids)) - if tasks: - await asyncio.wait(tasks, loop=hass.loop) - - -def get_mysensors_devices(hass, domain): - """Return MySensors devices for a platform.""" - if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data: - hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} - return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] - - -def gw_callback_factory(hass): - """Return a new callback for the gateway.""" - @callback - def mysensors_callback(msg): - """Handle messages from a MySensors gateway.""" - start = timer() - _LOGGER.debug( - "Node update: node %s child %s", msg.node_id, msg.child_id) - - set_gateway_ready(hass, msg) - - try: - child = msg.gateway.sensors[msg.node_id].children[msg.child_id] - except KeyError: - _LOGGER.debug("Not a child update for node %s", msg.node_id) - return - - signals = [] - - # Update all platforms for the device via dispatcher. - # Add/update entity if schema validates to true. - validated = validate_child(msg.gateway, msg.node_id, child) - for platform, dev_ids in validated.items(): - devices = get_mysensors_devices(hass, platform) - new_dev_ids = [] - for dev_id in dev_ids: - if dev_id in devices: - signals.append(SIGNAL_CALLBACK.format(*dev_id)) - else: - new_dev_ids.append(dev_id) - if new_dev_ids: - discover_mysensors_platform(hass, platform, new_dev_ids) - for signal in set(signals): - # Only one signal per device is needed. - # A device can have multiple platforms, ie multiple schemas. - # FOR LATER: Add timer to not signal if another update comes in. - async_dispatcher_send(hass, signal) - end = timer() - if end - start > 0.1: - _LOGGER.debug( - "Callback for node %s child %s took %.3f seconds", - msg.node_id, msg.child_id, end - start) - return mysensors_callback - - -def get_mysensors_name(gateway, node_id, child_id): - """Return a name for a node child.""" - node_name = '{} {}'.format( - gateway.sensors[node_id].sketch_name, node_id) - node_name = next( - (node[CONF_NODE_NAME] for conf_id, node in gateway.nodes_config.items() - if node.get(CONF_NODE_NAME) is not None and conf_id == node_id), - node_name) - return '{} {}'.format(node_name, child_id) - - -def get_mysensors_gateway(hass, gateway_id): - """Return MySensors gateway.""" - if MYSENSORS_GATEWAYS not in hass.data: - hass.data[MYSENSORS_GATEWAYS] = {} - gateways = hass.data.get(MYSENSORS_GATEWAYS) - return gateways.get(gateway_id) - - -@callback -def setup_mysensors_platform( - hass, domain, discovery_info, device_class, device_args=None, - async_add_devices=None): - """Set up a MySensors platform.""" - # Only act if called via mysensors by discovery event. - # Otherwise gateway is not setup. - if not discovery_info: - return - if device_args is None: - device_args = () - new_devices = [] - new_dev_ids = discovery_info[ATTR_DEVICES] - for dev_id in new_dev_ids: - devices = get_mysensors_devices(hass, domain) - if dev_id in devices: - continue - gateway_id, node_id, child_id, value_type = dev_id - gateway = get_mysensors_gateway(hass, gateway_id) - if not gateway: - continue - device_class_copy = device_class - if isinstance(device_class, dict): - child = gateway.sensors[node_id].children[child_id] - s_type = gateway.const.Presentation(child.type).name - device_class_copy = device_class[s_type] - name = get_mysensors_name(gateway, node_id, child_id) - - args_copy = (*device_args, gateway, node_id, child_id, name, - value_type) - devices[dev_id] = device_class_copy(*args_copy) - new_devices.append(devices[dev_id]) - if new_devices: - _LOGGER.info("Adding new devices: %s", new_devices) - if async_add_devices is not None: - async_add_devices(new_devices, True) - return new_devices - - -class MySensorsDevice(object): - """Representation of a MySensors device.""" - - def __init__(self, gateway, node_id, child_id, name, value_type): - """Set up the MySensors device.""" - self.gateway = gateway - self.node_id = node_id - self.child_id = child_id - self._name = name - self.value_type = value_type - child = gateway.sensors[node_id].children[child_id] - self.child_type = child.type - self._values = {} - - @property - def name(self): - """Return the name of this entity.""" - return self._name - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - attr = { - ATTR_BATTERY_LEVEL: node.battery_level, - ATTR_CHILD_ID: self.child_id, - ATTR_DESCRIPTION: child.description, - ATTR_DEVICE: self.gateway.device, - ATTR_NODE_ID: self.node_id, - } - - set_req = self.gateway.const.SetReq - - for value_type, value in self._values.items(): - attr[set_req(value_type).name] = value - - return attr - - async def async_update(self): - """Update the controller with the latest value from a sensor.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - set_req = self.gateway.const.SetReq - for value_type, value in child.values.items(): - _LOGGER.debug( - "Entity update: %s: value_type %s, value = %s", - self._name, value_type, value) - if value_type in (set_req.V_ARMED, set_req.V_LIGHT, - set_req.V_LOCK_STATUS, set_req.V_TRIPPED): - self._values[value_type] = ( - STATE_ON if int(value) == 1 else STATE_OFF) - elif value_type == set_req.V_DIMMER: - self._values[value_type] = int(value) - else: - self._values[value_type] = value - - -class MySensorsEntity(MySensorsDevice, Entity): - """Representation of a MySensors entity.""" - - @property - def should_poll(self): - """Return the polling state. The gateway pushes its states.""" - return False - - @property - def available(self): - """Return true if entity is available.""" - return self.value_type in self._values - - @callback - def async_update_callback(self): - """Update the entity.""" - self.async_schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Register update callback.""" - dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type - async_dispatcher_connect( - self.hass, SIGNAL_CALLBACK.format(*dev_id), - self.async_update_callback) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py new file mode 100644 index 00000000000000..3aa8e82911eff5 --- /dev/null +++ b/homeassistant/components/mysensors/__init__.py @@ -0,0 +1,167 @@ +""" +Connect to a MySensors gateway via pymysensors API. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mysensors/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.mqtt import ( + valid_publish_topic, valid_subscribe_topic) +from homeassistant.const import CONF_OPTIMISTIC +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from .const import ( + ATTR_DEVICES, CONF_BAUD_RATE, CONF_DEVICE, CONF_GATEWAYS, + CONF_NODES, CONF_PERSISTENCE, CONF_PERSISTENCE_FILE, CONF_RETAIN, + CONF_TCP_PORT, CONF_TOPIC_IN_PREFIX, CONF_TOPIC_OUT_PREFIX, CONF_VERSION, + DOMAIN, MYSENSORS_GATEWAYS) +from .device import get_mysensors_devices +from .gateway import get_mysensors_gateway, setup_gateways, finish_setup + +REQUIREMENTS = ['pymysensors==0.14.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DEBUG = 'debug' +CONF_NODE_NAME = 'name' + +DEFAULT_BAUD_RATE = 115200 +DEFAULT_TCP_PORT = 5003 +DEFAULT_VERSION = '1.4' + + +def has_all_unique_files(value): + """Validate that all persistence files are unique and set if any is set.""" + persistence_files = [ + gateway.get(CONF_PERSISTENCE_FILE) for gateway in value] + if None in persistence_files and any( + name is not None for name in persistence_files): + raise vol.Invalid( + 'persistence file name of all devices must be set if any is set') + if not all(name is None for name in persistence_files): + schema = vol.Schema(vol.Unique()) + schema(persistence_files) + return value + + +def is_persistence_file(value): + """Validate that persistence file path ends in either .pickle or .json.""" + if value.endswith(('.json', '.pickle')): + return value + else: + raise vol.Invalid( + '{} does not end in either `.json` or `.pickle`'.format(value)) + + +def deprecated(key): + """Mark key as deprecated in configuration.""" + def validator(config): + """Check if key is in config, log warning and remove key.""" + if key not in config: + return config + _LOGGER.warning( + '%s option for %s is deprecated. Please remove %s from your ' + 'configuration file', key, DOMAIN, key) + config.pop(key) + return config + return validator + + +NODE_SCHEMA = vol.Schema({ + cv.positive_int: { + vol.Required(CONF_NODE_NAME): cv.string + } +}) + +GATEWAY_SCHEMA = { + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_PERSISTENCE_FILE): + vol.All(cv.string, is_persistence_file), + vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): + cv.positive_int, + vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, + vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, + vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, + vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, +} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema(vol.All(deprecated(CONF_DEBUG), { + vol.Required(CONF_GATEWAYS): vol.All( + cv.ensure_list, has_all_unique_files, [GATEWAY_SCHEMA]), + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, + vol.Optional(CONF_RETAIN, default=True): cv.boolean, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, + })) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the MySensors component.""" + gateways = await setup_gateways(hass, config) + + if not gateways: + _LOGGER.error( + "No devices could be setup as gateways, check your configuration") + return False + + hass.data[MYSENSORS_GATEWAYS] = gateways + + hass.async_add_job(finish_setup(hass, gateways)) + + return True + + +def _get_mysensors_name(gateway, node_id, child_id): + """Return a name for a node child.""" + node_name = '{} {}'.format( + gateway.sensors[node_id].sketch_name, node_id) + node_name = next( + (node[CONF_NODE_NAME] for conf_id, node in gateway.nodes_config.items() + if node.get(CONF_NODE_NAME) is not None and conf_id == node_id), + node_name) + return '{} {}'.format(node_name, child_id) + + +@callback +def setup_mysensors_platform( + hass, domain, discovery_info, device_class, device_args=None, + async_add_devices=None): + """Set up a MySensors platform.""" + # Only act if called via MySensors by discovery event. + # Otherwise gateway is not setup. + if not discovery_info: + return + if device_args is None: + device_args = () + new_devices = [] + new_dev_ids = discovery_info[ATTR_DEVICES] + for dev_id in new_dev_ids: + devices = get_mysensors_devices(hass, domain) + if dev_id in devices: + continue + gateway_id, node_id, child_id, value_type = dev_id + gateway = get_mysensors_gateway(hass, gateway_id) + if not gateway: + continue + device_class_copy = device_class + if isinstance(device_class, dict): + child = gateway.sensors[node_id].children[child_id] + s_type = gateway.const.Presentation(child.type).name + device_class_copy = device_class[s_type] + name = _get_mysensors_name(gateway, node_id, child_id) + + args_copy = (*device_args, gateway, node_id, child_id, name, + value_type) + devices[dev_id] = device_class_copy(*args_copy) + new_devices.append(devices[dev_id]) + if new_devices: + _LOGGER.info("Adding new devices: %s", new_devices) + if async_add_devices is not None: + async_add_devices(new_devices, True) + return new_devices diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py new file mode 100644 index 00000000000000..4f9718a39dbf2d --- /dev/null +++ b/homeassistant/components/mysensors/const.py @@ -0,0 +1,138 @@ +"""MySensors constants.""" +import homeassistant.helpers.config_validation as cv + +ATTR_DEVICES = 'devices' + +CONF_BAUD_RATE = 'baud_rate' +CONF_DEVICE = 'device' +CONF_GATEWAYS = 'gateways' +CONF_NODES = 'nodes' +CONF_PERSISTENCE = 'persistence' +CONF_PERSISTENCE_FILE = 'persistence_file' +CONF_RETAIN = 'retain' +CONF_TCP_PORT = 'tcp_port' +CONF_TOPIC_IN_PREFIX = 'topic_in_prefix' +CONF_TOPIC_OUT_PREFIX = 'topic_out_prefix' +CONF_VERSION = 'version' + +DOMAIN = 'mysensors' +MYSENSORS_GATEWAYS = 'mysensors_gateways' +PLATFORM = 'platform' +SCHEMA = 'schema' +SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' +TYPE = 'type' + +# MySensors const schemas +BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} +CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'} +LIGHT_DIMMER_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_DIMMER', + SCHEMA: {'V_DIMMER': cv.string, 'V_LIGHT': cv.string}} +LIGHT_PERCENTAGE_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_PERCENTAGE', + SCHEMA: {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string}} +LIGHT_RGB_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_RGB', SCHEMA: { + 'V_RGB': cv.string, 'V_STATUS': cv.string}} +LIGHT_RGBW_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_RGBW', SCHEMA: { + 'V_RGBW': cv.string, 'V_STATUS': cv.string}} +NOTIFY_SCHEMA = {PLATFORM: 'notify', TYPE: 'V_TEXT'} +DEVICE_TRACKER_SCHEMA = {PLATFORM: 'device_tracker', TYPE: 'V_POSITION'} +DUST_SCHEMA = [ + {PLATFORM: 'sensor', TYPE: 'V_DUST_LEVEL'}, + {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}] +SWITCH_LIGHT_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_LIGHT'} +SWITCH_STATUS_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_STATUS'} +MYSENSORS_CONST_SCHEMA = { + 'S_DOOR': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_MOTION': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SMOKE': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SPRINKLER': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_STATUS'}], + 'S_WATER_LEAK': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SOUND': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_VIBRATION': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_MOISTURE': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_HVAC': [CLIMATE_SCHEMA], + 'S_COVER': [ + {PLATFORM: 'cover', TYPE: 'V_DIMMER'}, + {PLATFORM: 'cover', TYPE: 'V_PERCENTAGE'}, + {PLATFORM: 'cover', TYPE: 'V_LIGHT'}, + {PLATFORM: 'cover', TYPE: 'V_STATUS'}], + 'S_DIMMER': [LIGHT_DIMMER_SCHEMA, LIGHT_PERCENTAGE_SCHEMA], + 'S_RGB_LIGHT': [LIGHT_RGB_SCHEMA], + 'S_RGBW_LIGHT': [LIGHT_RGBW_SCHEMA], + 'S_INFO': [NOTIFY_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_TEXT'}], + 'S_GPS': [ + DEVICE_TRACKER_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_POSITION'}], + 'S_TEMP': [{PLATFORM: 'sensor', TYPE: 'V_TEMP'}], + 'S_HUM': [{PLATFORM: 'sensor', TYPE: 'V_HUM'}], + 'S_BARO': [ + {PLATFORM: 'sensor', TYPE: 'V_PRESSURE'}, + {PLATFORM: 'sensor', TYPE: 'V_FORECAST'}], + 'S_WIND': [ + {PLATFORM: 'sensor', TYPE: 'V_WIND'}, + {PLATFORM: 'sensor', TYPE: 'V_GUST'}, + {PLATFORM: 'sensor', TYPE: 'V_DIRECTION'}], + 'S_RAIN': [ + {PLATFORM: 'sensor', TYPE: 'V_RAIN'}, + {PLATFORM: 'sensor', TYPE: 'V_RAINRATE'}], + 'S_UV': [{PLATFORM: 'sensor', TYPE: 'V_UV'}], + 'S_WEIGHT': [ + {PLATFORM: 'sensor', TYPE: 'V_WEIGHT'}, + {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], + 'S_POWER': [ + {PLATFORM: 'sensor', TYPE: 'V_WATT'}, + {PLATFORM: 'sensor', TYPE: 'V_KWH'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR'}, + {PLATFORM: 'sensor', TYPE: 'V_VA'}, + {PLATFORM: 'sensor', TYPE: 'V_POWER_FACTOR'}], + 'S_DISTANCE': [{PLATFORM: 'sensor', TYPE: 'V_DISTANCE'}], + 'S_LIGHT_LEVEL': [ + {PLATFORM: 'sensor', TYPE: 'V_LIGHT_LEVEL'}, + {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}], + 'S_IR': [ + {PLATFORM: 'sensor', TYPE: 'V_IR_RECEIVE'}, + {PLATFORM: 'switch', TYPE: 'V_IR_SEND', + SCHEMA: {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string}}], + 'S_WATER': [ + {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, + {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], + 'S_CUSTOM': [ + {PLATFORM: 'sensor', TYPE: 'V_VAR1'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR2'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR3'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR4'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR5'}, + {PLATFORM: 'sensor', TYPE: 'V_CUSTOM'}], + 'S_SCENE_CONTROLLER': [ + {PLATFORM: 'sensor', TYPE: 'V_SCENE_ON'}, + {PLATFORM: 'sensor', TYPE: 'V_SCENE_OFF'}], + 'S_COLOR_SENSOR': [{PLATFORM: 'sensor', TYPE: 'V_RGB'}], + 'S_MULTIMETER': [ + {PLATFORM: 'sensor', TYPE: 'V_VOLTAGE'}, + {PLATFORM: 'sensor', TYPE: 'V_CURRENT'}, + {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], + 'S_GAS': [ + {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, + {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], + 'S_WATER_QUALITY': [ + {PLATFORM: 'sensor', TYPE: 'V_TEMP'}, + {PLATFORM: 'sensor', TYPE: 'V_PH'}, + {PLATFORM: 'sensor', TYPE: 'V_ORP'}, + {PLATFORM: 'sensor', TYPE: 'V_EC'}, + {PLATFORM: 'switch', TYPE: 'V_STATUS'}], + 'S_AIR_QUALITY': DUST_SCHEMA, + 'S_DUST': DUST_SCHEMA, + 'S_LIGHT': [SWITCH_LIGHT_SCHEMA], + 'S_BINARY': [SWITCH_STATUS_SCHEMA], + 'S_LOCK': [{PLATFORM: 'switch', TYPE: 'V_LOCK_STATUS'}], +} diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py new file mode 100644 index 00000000000000..b0770f90c1db76 --- /dev/null +++ b/homeassistant/components/mysensors/device.py @@ -0,0 +1,109 @@ +"""Handle MySensors devices.""" +import logging + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import SIGNAL_CALLBACK + +_LOGGER = logging.getLogger(__name__) + +ATTR_CHILD_ID = 'child_id' +ATTR_DESCRIPTION = 'description' +ATTR_DEVICE = 'device' +ATTR_NODE_ID = 'node_id' +MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' + + +def get_mysensors_devices(hass, domain): + """Return MySensors devices for a platform.""" + if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data: + hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} + return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] + + +class MySensorsDevice(object): + """Representation of a MySensors device.""" + + def __init__(self, gateway, node_id, child_id, name, value_type): + """Set up the MySensors device.""" + self.gateway = gateway + self.node_id = node_id + self.child_id = child_id + self._name = name + self.value_type = value_type + child = gateway.sensors[node_id].children[child_id] + self.child_type = child.type + self._values = {} + + @property + def name(self): + """Return the name of this entity.""" + return self._name + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + attr = { + ATTR_BATTERY_LEVEL: node.battery_level, + ATTR_CHILD_ID: self.child_id, + ATTR_DESCRIPTION: child.description, + ATTR_DEVICE: self.gateway.device, + ATTR_NODE_ID: self.node_id, + } + + set_req = self.gateway.const.SetReq + + for value_type, value in self._values.items(): + attr[set_req(value_type).name] = value + + return attr + + async def async_update(self): + """Update the controller with the latest value from a sensor.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + set_req = self.gateway.const.SetReq + for value_type, value in child.values.items(): + _LOGGER.debug( + "Entity update: %s: value_type %s, value = %s", + self._name, value_type, value) + if value_type in (set_req.V_ARMED, set_req.V_LIGHT, + set_req.V_LOCK_STATUS, set_req.V_TRIPPED): + self._values[value_type] = ( + STATE_ON if int(value) == 1 else STATE_OFF) + elif value_type == set_req.V_DIMMER: + self._values[value_type] = int(value) + else: + self._values[value_type] = value + + +class MySensorsEntity(MySensorsDevice, Entity): + """Representation of a MySensors entity.""" + + @property + def should_poll(self): + """Return the polling state. The gateway pushes its states.""" + return False + + @property + def available(self): + """Return true if entity is available.""" + return self.value_type in self._values + + @callback + def async_update_callback(self): + """Update the entity.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register update callback.""" + dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type + async_dispatcher_connect( + self.hass, SIGNAL_CALLBACK.format(*dev_id), + self.async_update_callback) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py new file mode 100644 index 00000000000000..a7719a80d99536 --- /dev/null +++ b/homeassistant/components/mysensors/gateway.py @@ -0,0 +1,328 @@ +"""Handle MySensors gateways.""" +import asyncio +from collections import defaultdict +import logging +import socket +import sys +from timeit import default_timer as timer + +import async_timeout +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_setup_component + +from .const import ( + ATTR_DEVICES, CONF_BAUD_RATE, CONF_DEVICE, CONF_GATEWAYS, CONF_NODES, + CONF_PERSISTENCE, CONF_PERSISTENCE_FILE, CONF_RETAIN, CONF_TCP_PORT, + CONF_TOPIC_IN_PREFIX, CONF_TOPIC_OUT_PREFIX, CONF_VERSION, DOMAIN, + MYSENSORS_CONST_SCHEMA, MYSENSORS_GATEWAYS, PLATFORM, SCHEMA, + SIGNAL_CALLBACK, TYPE) +from .device import get_mysensors_devices + +_LOGGER = logging.getLogger(__name__) + +GATEWAY_READY_TIMEOUT = 15.0 +MQTT_COMPONENT = 'mqtt' +MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}' + + +def is_serial_port(value): + """Validate that value is a windows serial port or a unix device.""" + if sys.platform.startswith('win'): + ports = ('COM{}'.format(idx + 1) for idx in range(256)) + if value in ports: + return value + else: + raise vol.Invalid('{} is not a serial port'.format(value)) + else: + return cv.isdevice(value) + + +def is_socket_address(value): + """Validate that value is a valid address.""" + try: + socket.getaddrinfo(value, None) + return value + except OSError: + raise vol.Invalid('Device is not a valid domain name or ip address') + + +def get_mysensors_gateway(hass, gateway_id): + """Return MySensors gateway.""" + if MYSENSORS_GATEWAYS not in hass.data: + hass.data[MYSENSORS_GATEWAYS] = {} + gateways = hass.data.get(MYSENSORS_GATEWAYS) + return gateways.get(gateway_id) + + +async def setup_gateways(hass, config): + """Set up all gateways.""" + conf = config[DOMAIN] + gateways = {} + + for index, gateway_conf in enumerate(conf[CONF_GATEWAYS]): + persistence_file = gateway_conf.get( + CONF_PERSISTENCE_FILE, + hass.config.path('mysensors{}.pickle'.format(index + 1))) + ready_gateway = await _get_gateway( + hass, config, gateway_conf, persistence_file) + if ready_gateway is not None: + gateways[id(ready_gateway)] = ready_gateway + + return gateways + + +async def _get_gateway(hass, config, gateway_conf, persistence_file): + """Return gateway after setup of the gateway.""" + import mysensors.mysensors as mysensors + + conf = config[DOMAIN] + persistence = conf[CONF_PERSISTENCE] + version = conf[CONF_VERSION] + device = gateway_conf[CONF_DEVICE] + baud_rate = gateway_conf[CONF_BAUD_RATE] + tcp_port = gateway_conf[CONF_TCP_PORT] + in_prefix = gateway_conf.get(CONF_TOPIC_IN_PREFIX, '') + out_prefix = gateway_conf.get(CONF_TOPIC_OUT_PREFIX, '') + + if device == MQTT_COMPONENT: + if not await async_setup_component(hass, MQTT_COMPONENT, config): + return None + mqtt = hass.components.mqtt + retain = conf[CONF_RETAIN] + + def pub_callback(topic, payload, qos, retain): + """Call MQTT publish function.""" + mqtt.async_publish(topic, payload, qos, retain) + + def sub_callback(topic, sub_cb, qos): + """Call MQTT subscribe function.""" + @callback + def internal_callback(*args): + """Call callback.""" + sub_cb(*args) + + hass.async_add_job( + mqtt.async_subscribe(topic, internal_callback, qos)) + + gateway = mysensors.AsyncMQTTGateway( + pub_callback, sub_callback, in_prefix=in_prefix, + out_prefix=out_prefix, retain=retain, loop=hass.loop, + event_callback=None, persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + else: + try: + await hass.async_add_job(is_serial_port, device) + gateway = mysensors.AsyncSerialGateway( + device, baud=baud_rate, loop=hass.loop, + event_callback=None, persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + except vol.Invalid: + try: + await hass.async_add_job(is_socket_address, device) + # valid ip address + gateway = mysensors.AsyncTCPGateway( + device, port=tcp_port, loop=hass.loop, event_callback=None, + persistence=persistence, persistence_file=persistence_file, + protocol_version=version) + except vol.Invalid: + # invalid ip address + return None + gateway.metric = hass.config.units.is_metric + gateway.optimistic = conf[CONF_OPTIMISTIC] + gateway.device = device + gateway.event_callback = _gw_callback_factory(hass) + gateway.nodes_config = gateway_conf[CONF_NODES] + if persistence: + await gateway.start_persistence() + + return gateway + + +async def finish_setup(hass, gateways): + """Load any persistent devices and platforms and start gateway.""" + discover_tasks = [] + start_tasks = [] + for gateway in gateways.values(): + discover_tasks.append(_discover_persistent_devices(hass, gateway)) + start_tasks.append(_gw_start(hass, gateway)) + if discover_tasks: + # Make sure all devices and platforms are loaded before gateway start. + await asyncio.wait(discover_tasks, loop=hass.loop) + if start_tasks: + await asyncio.wait(start_tasks, loop=hass.loop) + + +async def _discover_persistent_devices(hass, gateway): + """Discover platforms for devices loaded via persistence file.""" + tasks = [] + new_devices = defaultdict(list) + for node_id in gateway.sensors: + node = gateway.sensors[node_id] + for child in node.children.values(): + validated = _validate_child(gateway, node_id, child) + for platform, dev_ids in validated.items(): + new_devices[platform].extend(dev_ids) + for platform, dev_ids in new_devices.items(): + tasks.append(_discover_mysensors_platform(hass, platform, dev_ids)) + if tasks: + await asyncio.wait(tasks, loop=hass.loop) + + +@callback +def _discover_mysensors_platform(hass, platform, new_devices): + """Discover a MySensors platform.""" + task = hass.async_add_job(discovery.async_load_platform( + hass, platform, DOMAIN, + {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) + return task + + +async def _gw_start(hass, gateway): + """Start the gateway.""" + @callback + def gw_stop(event): + """Trigger to stop the gateway.""" + hass.async_add_job(gateway.stop()) + + await gateway.start() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) + if gateway.device == 'mqtt': + # Gatways connected via mqtt doesn't send gateway ready message. + return + gateway_ready = asyncio.Future() + gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway)) + hass.data[gateway_ready_key] = gateway_ready + + try: + with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop): + await gateway_ready + except asyncio.TimeoutError: + _LOGGER.warning( + "Gateway %s not ready after %s secs so continuing with setup", + gateway.device, GATEWAY_READY_TIMEOUT) + finally: + hass.data.pop(gateway_ready_key, None) + + +def _gw_callback_factory(hass): + """Return a new callback for the gateway.""" + @callback + def mysensors_callback(msg): + """Handle messages from a MySensors gateway.""" + start = timer() + _LOGGER.debug( + "Node update: node %s child %s", msg.node_id, msg.child_id) + + _set_gateway_ready(hass, msg) + + try: + child = msg.gateway.sensors[msg.node_id].children[msg.child_id] + except KeyError: + _LOGGER.debug("Not a child update for node %s", msg.node_id) + return + + signals = [] + + # Update all platforms for the device via dispatcher. + # Add/update entity if schema validates to true. + validated = _validate_child(msg.gateway, msg.node_id, child) + for platform, dev_ids in validated.items(): + devices = get_mysensors_devices(hass, platform) + new_dev_ids = [] + for dev_id in dev_ids: + if dev_id in devices: + signals.append(SIGNAL_CALLBACK.format(*dev_id)) + else: + new_dev_ids.append(dev_id) + if new_dev_ids: + _discover_mysensors_platform(hass, platform, new_dev_ids) + for signal in set(signals): + # Only one signal per device is needed. + # A device can have multiple platforms, ie multiple schemas. + # FOR LATER: Add timer to not signal if another update comes in. + async_dispatcher_send(hass, signal) + end = timer() + if end - start > 0.1: + _LOGGER.debug( + "Callback for node %s child %s took %.3f seconds", + msg.node_id, msg.child_id, end - start) + return mysensors_callback + + +@callback +def _set_gateway_ready(hass, msg): + """Set asyncio future result if gateway is ready.""" + if (msg.type != msg.gateway.const.MessageType.internal or + msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY): + return + gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format( + id(msg.gateway))) + if gateway_ready is None or gateway_ready.cancelled(): + return + gateway_ready.set_result(True) + + +def _validate_child(gateway, node_id, child): + """Validate that a child has the correct values according to schema. + + Return a dict of platform with a list of device ids for validated devices. + """ + validated = defaultdict(list) + + if not child.values: + _LOGGER.debug( + "No child values for node %s child %s", node_id, child.id) + return validated + if gateway.sensors[node_id].sketch_name is None: + _LOGGER.debug("Node %s is missing sketch name", node_id) + return validated + pres = gateway.const.Presentation + set_req = gateway.const.SetReq + s_name = next( + (member.name for member in pres if member.value == child.type), None) + if s_name not in MYSENSORS_CONST_SCHEMA: + _LOGGER.warning("Child type %s is not supported", s_name) + return validated + child_schemas = MYSENSORS_CONST_SCHEMA[s_name] + + def msg(name): + """Return a message for an invalid schema.""" + return "{} requires value_type {}".format( + pres(child.type).name, set_req[name].name) + + for schema in child_schemas: + platform = schema[PLATFORM] + v_name = schema[TYPE] + value_type = next( + (member.value for member in set_req if member.name == v_name), + None) + if value_type is None: + continue + _child_schema = child.get_schema(gateway.protocol_version) + vol_schema = _child_schema.extend( + {vol.Required(set_req[key].value, msg=msg(key)): + _child_schema.schema.get(set_req[key].value, val) + for key, val in schema.get(SCHEMA, {v_name: cv.string}).items()}, + extra=vol.ALLOW_EXTRA) + try: + vol_schema(child.values) + except vol.Invalid as exc: + level = (logging.WARNING if value_type in child.values + else logging.DEBUG) + _LOGGER.log( + level, + "Invalid values: %s: %s platform: node %s child %s: %s", + child.values, platform, node_id, child.id, exc) + continue + dev_id = id(gateway), node_id, child.id, value_type + validated[platform].append(dev_id) + return validated diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index c6a3dcf9c9a605..fc407de0a6b79d 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,8 +17,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.6.zip' - '#pybotvac==0.0.6'] +REQUIREMENTS = ['pybotvac==0.0.7'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' @@ -55,7 +54,12 @@ 7: 'Updating...', 8: 'Copying logs...', 9: 'Calculating position...', - 10: 'IEC test' + 10: 'IEC test', + 11: 'Map cleaning', + 12: 'Exploring map (creating a persistent map)', + 13: 'Acquiring Persistent Map IDs', + 14: 'Creating & Uploading Map', + 15: 'Suspended Exploration' } ERRORS = { @@ -71,12 +75,30 @@ 'ui_error_navigation_pathproblems_returninghome': 'Cannot return to base', 'ui_error_navigation_falling': 'Clear my path', 'ui_error_picked_up': 'Picked up', - 'ui_error_stuck': 'Stuck!' + 'ui_error_stuck': 'Stuck!', + 'dustbin_full': 'Dust bin full', + 'dustbin_missing': 'Dust bin missing', + 'maint_brush_stuck': 'Brush stuck', + 'maint_brush_overload': 'Brush overloaded', + 'maint_bumper_stuck': 'Bumper stuck', + 'maint_vacuum_stuck': 'Vacuum is stuck', + 'maint_left_drop_stuck': 'Vacuum is stuck', + 'maint_left_wheel_stuck': 'Vacuum is stuck', + 'maint_right_drop_stuck': 'Vacuum is stuck', + 'maint_right_wheel_stuck': 'Vacuum is stuck', + 'not_on_charge_base': 'Not on the charge base', + 'nav_robot_falling': 'Clear my path', + 'nav_no_path': 'Clear my path', + 'nav_path_problem': 'Clear my path' } ALERTS = { 'ui_alert_dust_bin_full': 'Please empty dust bin', - 'ui_alert_recovering_location': 'Returning to start' + 'ui_alert_recovering_location': 'Returning to start', + 'dustbin_full': 'Please empty dust bin', + 'maint_brush_change': 'Change the brush', + 'maint_filter_change': 'Change the filter', + 'clean_completed_to_start': 'Cleaning completed' } @@ -122,7 +144,7 @@ def login(self): _LOGGER.error("Unable to connect to Neato API") return False - @Throttle(timedelta(seconds=60)) + @Throttle(timedelta(seconds=300)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", diff --git a/homeassistant/components/nest/.translations/cs.json b/homeassistant/components/nest/.translations/cs.json new file mode 100644 index 00000000000000..c884226174b00b --- /dev/null +++ b/homeassistant/components/nest/.translations/cs.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "M\u016f\u017eete nastavit pouze jeden Nest \u00fa\u010det.", + "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00ed URL vypr\u0161el", + "no_flows": "Pot\u0159ebujete nakonfigurovat Nest, abyste se s n\u00edm mohli autentizovat. [P\u0159e\u010dt\u011bte si pros\u00edm pokyny] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Intern\u00ed chyba ov\u011b\u0159en\u00ed k\u00f3du", + "invalid_code": "Neplatn\u00fd k\u00f3d", + "timeout": "\u010casov\u00fd limit ov\u011b\u0159ov\u00e1n\u00ed k\u00f3du vypr\u0161el", + "unknown": "Nezn\u00e1m\u00e1 chyba ov\u011b\u0159en\u00ed k\u00f3du" + }, + "step": { + "init": { + "data": { + "flow_impl": "Poskytovatel" + }, + "description": "Zvolte pomoc\u00ed kter\u00e9ho poskytovatele ov\u011b\u0159en\u00ed chcete ov\u011b\u0159it slu\u017ebu Nest.", + "title": "Poskytovatel ov\u011b\u0159en\u00ed" + }, + "link": { + "data": { + "code": "K\u00f3d PIN" + }, + "description": "Chcete-li propojit \u00fa\u010det Nest, [autorizujte sv\u016fj \u00fa\u010det]({url}). \n\n Po autorizaci zkop\u00edrujte n\u00ed\u017ee uveden\u00fd k\u00f3d PIN.", + "title": "Propojit s Nest \u00fa\u010dtem" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json new file mode 100644 index 00000000000000..721eafa807fb6d --- /dev/null +++ b/homeassistant/components/nest/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "init": { + "data": { + "flow_impl": "Anbieter" + }, + "description": "W\u00e4hlen Sie, \u00fcber welchen Authentifizierungsanbieter Sie sich bei Nest authentifizieren m\u00f6chten.", + "title": "Authentifizierungsanbieter" + }, + "link": { + "data": { + "code": "PIN Code" + }, + "description": "[Autorisieren Sie ihr Konto] ( {url} ), um ihren Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcgen Sie anschlie\u00dfend den erhaltenen PIN Code hier ein.", + "title": "Nest-Konto verkn\u00fcpfen" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json new file mode 100644 index 00000000000000..abf8f79599f505 --- /dev/null +++ b/homeassistant/components/nest/.translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d" + }, + "step": { + "init": { + "data": { + "flow_impl": "Szolg\u00e1ltat\u00f3" + } + }, + "link": { + "data": { + "code": "PIN-k\u00f3d" + } + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/it.json b/homeassistant/components/nest/.translations/it.json new file mode 100644 index 00000000000000..ca34179cf5b191 --- /dev/null +++ b/homeassistant/components/nest/.translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "init": { + "title": "Fornitore di autenticazione" + }, + "link": { + "data": { + "code": "Codice PIN" + }, + "description": "Per collegare l'account Nido, [autorizzare l'account]({url}).\n\nDopo l'autorizzazione, copia-incolla il codice PIN fornito di seguito.", + "title": "Collega un account Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/lb.json b/homeassistant/components/nest/.translations/lb.json new file mode 100644 index 00000000000000..197cc8206d0510 --- /dev/null +++ b/homeassistant/components/nest/.translations/lb.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen\u00a0Nest Kont\u00a0konfigur\u00e9ieren.", + "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung\u00a0beim gener\u00e9ieren\u00a0vun der Autorisatiouns\u00a0URL.", + "no_flows": "Dir musst Nest konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung\u00a0k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Interne Feeler beim valid\u00e9ieren vum Code", + "invalid_code": "Ong\u00ebltege Code", + "timeout": "Z\u00e4it Iwwerschreidung\u00a0beim valid\u00e9ieren vum Code", + "unknown": "Onbekannte Feeler beim valid\u00e9ieren vum Code" + }, + "step": { + "init": { + "data": { + "flow_impl": "Ubidder" + }, + "description": "Wielt den Authentifikatioun Ubidder deen sech mat Nest verbanne soll.", + "title": "Authentifikatioun Ubidder" + }, + "link": { + "data": { + "code": "Pin code" + }, + "description": "Fir den Nest Kont ze verbannen, [autoris\u00e9iert \u00e4ren Kont]({url}).\nKop\u00e9iert no der Autorisatioun den Pin hei \u00ebnnendr\u00ebnner", + "title": "Nest Kont verbannen" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/nl.json b/homeassistant/components/nest/.translations/nl.json new file mode 100644 index 00000000000000..756eb07189a2be --- /dev/null +++ b/homeassistant/components/nest/.translations/nl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Je kunt slechts \u00e9\u00e9n Nest-account configureren.", + "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", + "authorize_url_timeout": "Toestemming voor het genereren van autoriseer-url.", + "no_flows": "U moet Nest configureren voordat u zich ermee kunt authenticeren. [Gelieve de instructies te lezen](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Interne foutvalidatiecode", + "invalid_code": "Ongeldige code", + "timeout": "Time-out validatie van code", + "unknown": "Onbekende foutvalidatiecode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Leverancier" + }, + "description": "Kies met welke authenticatieleverancier u wilt verifi\u00ebren met Nest.", + "title": "Authenticatieleverancier" + }, + "link": { + "data": { + "code": "Pincode" + }, + "description": "Als je je Nest-account wilt koppelen, [autoriseer je account] ( {url} ). \n\nNa autorisatie, kopieer en plak de voorziene pincode hieronder.", + "title": "Koppel Nest-account" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/sl.json b/homeassistant/components/nest/.translations/sl.json new file mode 100644 index 00000000000000..d038ed4157fab0 --- /dev/null +++ b/homeassistant/components/nest/.translations/sl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Nastavite lahko samo en ra\u010dun Nest.", + "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "no_flows": "Preden lahko preverite pristnost, morate konfigurirati Nest. [Preberite navodila](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Notranja napaka pri preverjanju kode", + "invalid_code": "Neveljavna koda", + "timeout": "\u010casovna omejitev je potekla pri preverjanju kode", + "unknown": "Neznana napaka pri preverjanju kode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Ponudnik" + }, + "description": "Izberite prek katerega ponudnika overjanja \u017eelite overiti Nest.", + "title": "Ponudnik za preverjanje pristnosti" + }, + "link": { + "data": { + "code": "PIN koda" + }, + "description": "\u010ce \u017eelite povezati svoj ra\u010dun Nest, [pooblastite svoj ra\u010dun]({url}). \n\n Po odobritvi kopirajte in prilepite podano kodo PIN.", + "title": "Pove\u017eite Nest ra\u010dun" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/zh-Hant.json b/homeassistant/components/nest/.translations/zh-Hant.json new file mode 100644 index 00000000000000..6b9dbdb19b1148 --- /dev/null +++ b/homeassistant/components/nest/.translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Nest \u5e33\u865f\u3002", + "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Nest \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/nest/\uff09\u3002" + }, + "error": { + "internal_error": "\u8a8d\u8b49\u78bc\u5167\u90e8\u932f\u8aa4", + "invalid_code": "\u8a8d\u8b49\u78bc\u7121\u6548", + "timeout": "\u8a8d\u8b49\u78bc\u903e\u6642", + "unknown": "\u8a8d\u8b49\u78bc\u672a\u77e5\u932f\u8aa4" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u8a8d\u8b49\u63d0\u4f9b\u8005" + }, + "description": "\u65bc\u8a8d\u8b49\u63d0\u4f9b\u8005\u4e2d\u6311\u9078\u6240\u8981\u9032\u884c Nest \u8a8d\u8b49\u63d0\u4f9b\u8005\u3002", + "title": "\u8a8d\u8b49\u63d0\u4f9b\u8005" + }, + "link": { + "data": { + "code": "PIN \u78bc" + }, + "description": "\u6b32\u9023\u7d50 Nest \u5e33\u865f\uff0c[\u8a8d\u8b49\u5e33\u865f]({url}).\n\n\u65bc\u8a8d\u8b49\u5f8c\uff0c\u8907\u88fd\u4e26\u8cbc\u4e0a\u4e0b\u65b9\u7684\u8a8d\u8b49\u78bc\u3002", + "title": "\u9023\u7d50 Nest \u5e33\u865f" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index bd74897371ad05..58fa1953ef0d4c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -23,7 +23,7 @@ from .const import DOMAIN from . import local_auth -REQUIREMENTS = ['python-nest==4.0.2'] +REQUIREMENTS = ['python-nest==4.0.3'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -86,6 +86,7 @@ async def async_nest_update_event_broker(hass, nest): _LOGGER.debug("dispatching nest data update") async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) else: + _LOGGER.debug("stop listening nest.update_event") return @@ -122,7 +123,8 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("proceeding with setup") conf = hass.data.get(DATA_NEST_CONFIG, {}) hass.data[DATA_NEST] = NestDevice(hass, conf, nest) - await hass.async_add_job(hass.data[DATA_NEST].initialize) + if not await hass.async_add_job(hass.data[DATA_NEST].initialize): + return False for component in 'climate', 'camera', 'sensor', 'binary_sensor': hass.async_add_job(hass.config_entries.async_forward_entry_setup( @@ -192,63 +194,73 @@ def __init__(self, hass, conf, nest): def initialize(self): """Initialize Nest.""" - if self.local_structure is None: - self.local_structure = [s.name for s in self.nest.structures] + from nest.nest import AuthorizationError, APIError + try: + # Do not optimize next statement, it is here for initialize + # persistence Nest API connection. + structure_names = [s.name for s in self.nest.structures] + if self.local_structure is None: + self.local_structure = structure_names + + except (AuthorizationError, APIError, socket.error) as err: + _LOGGER.error( + "Connection error while access Nest web service: %s", err) + return False + return True def structures(self): """Generate a list of structures.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - yield structure - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + yield structure + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) def thermostats(self): - """Generate a list of thermostats and their location.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.thermostats: - yield (structure, device) - else: - _LOGGER.debug("Ignoring structure %s, not in %s", - structure.name, self.local_structure) - except socket.error: - _LOGGER.error( - "Connection error logging into the nest web service.") + """Generate a list of thermostats.""" + return self._devices('thermostats') def smoke_co_alarms(self): """Generate a list of smoke co alarms.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.smoke_co_alarms: - yield (structure, device) - else: - _LOGGER.debug("Ignoring structure %s, not in %s", - structure.name, self.local_structure) - except socket.error: - _LOGGER.error( - "Connection error logging into the nest web service.") + return self._devices('smoke_co_alarms') def cameras(self): """Generate a list of cameras.""" + return self._devices('cameras') + + def _devices(self, device_type): + """Generate a list of Nest devices.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.cameras: - yield (structure, device) - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + + for device in getattr(structure, device_type, []): + try: + # Do not optimize next statement, + # it is here for verify Nest API permission. + device.name_long + except KeyError: + _LOGGER.warning("Cannot retrieve device name for [%s]" + ", please check your Nest developer " + "account permission settings.", + device.serial) + continue + yield (structure, device) + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) class NestSensorDevice(Entity): diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte.py index 4887ea1aa67596..23a01d37c2b81e 100644 --- a/homeassistant/components/netgear_lte.py +++ b/homeassistant/components/netgear_lte.py @@ -9,12 +9,15 @@ import voluptuous as vol import attr +import aiohttp -from homeassistant.const import CONF_HOST, CONF_PASSWORD -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.util import Throttle -REQUIREMENTS = ['eternalegypt==0.0.1'] +REQUIREMENTS = ['eternalegypt==0.0.2'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -30,33 +33,34 @@ @attr.s -class LTEData: - """Class for LTE state.""" +class ModemData: + """Class for modem state.""" - eternalegypt = attr.ib() + modem = attr.ib() unread_count = attr.ib(init=False) usage = attr.ib(init=False) @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Call the API to update the data.""" - information = await self.eternalegypt.information() + information = await self.modem.information() self.unread_count = sum(1 for x in information.sms if x.unread) self.usage = information.usage @attr.s -class LTEHostData: - """Container for LTE states.""" +class LTEData: + """Shared state.""" - hostdata = attr.ib(init=False, factory=dict) + websession = attr.ib() + modem_data = attr.ib(init=False, factory=dict) - def get(self, config): - """Get the requested or the only hostdata value.""" + def get_modem_data(self, config): + """Get the requested or the only modem_data value.""" if CONF_HOST in config: - return self.hostdata.get(config[CONF_HOST]) - elif len(self.hostdata) == 1: - return next(iter(self.hostdata.values())) + return self.modem_data.get(config[CONF_HOST]) + elif len(self.modem_data) == 1: + return next(iter(self.modem_data.values())) return None @@ -64,7 +68,9 @@ def get(self, config): async def async_setup(hass, config): """Set up Netgear LTE component.""" if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = LTEHostData() + websession = async_create_clientsession( + hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) + hass.data[DATA_KEY] = LTEData(websession) tasks = [_setup_lte(hass, conf) for conf in config.get(DOMAIN, [])] if tasks: @@ -80,7 +86,17 @@ async def _setup_lte(hass, lte_config): host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] - eternalegypt = eternalegypt.LB2120(host, password) - lte_data = LTEData(eternalegypt) - await lte_data.async_update() - hass.data[DATA_KEY].hostdata[host] = lte_data + websession = hass.data[DATA_KEY].websession + + modem = eternalegypt.Modem(hostname=host, websession=websession) + await modem.login(password=password) + + modem_data = ModemData(modem) + await modem_data.async_update() + hass.data[DATA_KEY].modem_data[host] = modem_data + + async def cleanup(event): + """Clean up resources.""" + await modem.logout() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) diff --git a/homeassistant/components/notify/aws_lambda.py b/homeassistant/components/notify/aws_lambda.py index b0cc4a0121d5f9..46ac2f89d33cbf 100644 --- a/homeassistant/components/notify/aws_lambda.py +++ b/homeassistant/components/notify/aws_lambda.py @@ -44,7 +44,6 @@ def get_service(hass, config, discovery_info=None): context_b64 = base64.b64encode(context_str.encode('utf-8')) context = context_b64.decode('utf-8') - # pylint: disable=import-error import boto3 aws_config = config.copy() diff --git a/homeassistant/components/notify/aws_sns.py b/homeassistant/components/notify/aws_sns.py index c94e3abaa96fca..7ecf5a7cc7f88c 100644 --- a/homeassistant/components/notify/aws_sns.py +++ b/homeassistant/components/notify/aws_sns.py @@ -35,7 +35,6 @@ def get_service(hass, config, discovery_info=None): """Get the AWS SNS notification service.""" - # pylint: disable=import-error import boto3 aws_config = config.copy() diff --git a/homeassistant/components/notify/aws_sqs.py b/homeassistant/components/notify/aws_sqs.py index 43c04ed16d055b..30b673846e7f96 100644 --- a/homeassistant/components/notify/aws_sqs.py +++ b/homeassistant/components/notify/aws_sqs.py @@ -34,7 +34,6 @@ def get_service(hass, config, discovery_info=None): """Get the AWS SQS notification service.""" - # pylint: disable=import-error import boto3 aws_config = config.copy() diff --git a/homeassistant/components/notify/ciscospark.py b/homeassistant/components/notify/ciscospark.py index 0bf184023d7af6..e83e0e9024fff8 100644 --- a/homeassistant/components/notify/ciscospark.py +++ b/homeassistant/components/notify/ciscospark.py @@ -25,7 +25,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the CiscoSpark notification service.""" return CiscoSparkNotificationService( diff --git a/homeassistant/components/notify/ecobee.py b/homeassistant/components/notify/ecobee.py index c718149b4b553b..31e4c4751c8000 100644 --- a/homeassistant/components/notify/ecobee.py +++ b/homeassistant/components/notify/ecobee.py @@ -11,7 +11,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components import ecobee from homeassistant.components.notify import ( - BaseNotificationService, PLATFORM_SCHEMA) # NOQA + BaseNotificationService, PLATFORM_SCHEMA) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 7ccf4f8db9066f..7529608387d86b 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -413,7 +413,6 @@ def send_message(self, message="", **kwargs): json.dumps(payload), gcm_key=gcm_key, ttl='86400' ) - # pylint: disable=no-member if response.status_code == 410: _LOGGER.info("Notification channel has expired") reg = self.registrations.pop(target) diff --git a/homeassistant/components/notify/joaoapps_join.py b/homeassistant/components/notify/joaoapps_join.py index e391d6559e5537..a75ff9cd165b7c 100644 --- a/homeassistant/components/notify/joaoapps_join.py +++ b/homeassistant/components/notify/joaoapps_join.py @@ -28,7 +28,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the Join notification service.""" api_key = config.get(CONF_API_KEY) diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py index f6c3e152b0a3fd..0cc3a0213b3f00 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/notify/lametric.py @@ -36,7 +36,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the LaMetric notification service.""" hlmn = hass.data.get(LAMETRIC_DOMAIN) @@ -59,7 +58,6 @@ def __init__(self, hasslametricmanager, icon, lifetime, cycles, priority): self._priority = priority self._devices = [] - # pylint: disable=broad-except def send_message(self, message="", **kwargs): """Send a message to some LaMetric device.""" from lmnotify import SimpleFrame, Sound, Model diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index db568514dea25a..71ce7fb0b74eec 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -18,7 +18,7 @@ async def async_get_service(hass, config, discovery_info=None): return MySensorsNotificationService(hass) -class MySensorsNotificationDevice(mysensors.MySensorsDevice): +class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): """Represent a MySensors Notification device.""" def send_msg(self, msg): diff --git a/homeassistant/components/notify/netgear_lte.py b/homeassistant/components/notify/netgear_lte.py index b4ed53b828d6b3..97dfe504a51308 100644 --- a/homeassistant/components/notify/netgear_lte.py +++ b/homeassistant/components/notify/netgear_lte.py @@ -25,16 +25,16 @@ async def async_get_service(hass, config, discovery_info=None): """Get the notification service.""" - lte_data = hass.data[DATA_KEY].get(config) + modem_data = hass.data[DATA_KEY].get_modem_data(config) phone = config.get(ATTR_TARGET) - return NetgearNotifyService(lte_data, phone) + return NetgearNotifyService(modem_data, phone) @attr.s class NetgearNotifyService(BaseNotificationService): """Implementation of a notification service.""" - lte_data = attr.ib() + modem_data = attr.ib() phone = attr.ib() async def async_send_message(self, message="", **kwargs): @@ -42,4 +42,4 @@ async def async_send_message(self, message="", **kwargs): targets = kwargs.get(ATTR_TARGET, self.phone) if targets and message: for target in targets: - await self.lte_data.eternalegypt.sms(target, message) + await self.modem_data.modem.sms(target, message) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index cd73bbba4bfe8f..3ec0b27e7c4e09 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -26,7 +26,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the Pushover notification service.""" from pushover import InitError diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index b73f3a17ee74d9..92b709af8ad10c 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -14,7 +14,7 @@ CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT, CONTENT_TYPE_TEXT_PLAIN) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sendgrid==5.4.0'] +REQUIREMENTS = ['sendgrid==5.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index b50260e4c613b4..d4c5a196a3fbbe 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -44,7 +44,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the Slack notification service.""" import slacker diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index 5caaa1b372d701..c1059227f7a722 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -144,7 +144,6 @@ def update(self, sensor_type, end_point, group, tool=None): return response -# pylint: disable=unused-variable def get_value_from_json(json_dict, sensor_type, group, tool): """Return the value for sensor_type from the JSON.""" if group not in json_dict: diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 96ed098567d1d0..0a6c959f243d06 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -10,17 +10,17 @@ import voluptuous as vol from aiohttp import web +from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE from homeassistant.components.http import HomeAssistantView -from homeassistant.components import recorder from homeassistant.const import ( - CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT) from homeassistant import core as hacore -from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import entityfilter, state as state_helper from homeassistant.util.temperature import fahrenheit_to_celsius -REQUIREMENTS = ['prometheus_client==0.1.0'] +REQUIREMENTS = ['prometheus_client==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -29,8 +29,14 @@ DOMAIN = 'prometheus' DEPENDENCIES = ['http'] +CONF_FILTER = 'filter' +CONF_PROM_NAMESPACE = 'namespace' + CONFIG_SCHEMA = vol.Schema({ - DOMAIN: recorder.FILTER_SCHEMA, + DOMAIN: vol.All({ + vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, + vol.Optional(CONF_PROM_NAMESPACE): cv.string, + }) }, extra=vol.ALLOW_EXTRA) @@ -40,25 +46,26 @@ def setup(hass, config): hass.http.register_view(PrometheusView(prometheus_client)) - conf = config.get(DOMAIN, {}) - exclude = conf.get(CONF_EXCLUDE, {}) - include = conf.get(CONF_INCLUDE, {}) - metrics = Metrics(prometheus_client, exclude, include) + conf = config[DOMAIN] + entity_filter = conf[CONF_FILTER] + namespace = conf.get(CONF_PROM_NAMESPACE) + metrics = PrometheusMetrics(prometheus_client, entity_filter, namespace) hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event) return True -class Metrics(object): +class PrometheusMetrics(object): """Model all of the metrics which should be exposed to Prometheus.""" - def __init__(self, prometheus_client, exclude, include): + def __init__(self, prometheus_client, entity_filter, namespace): """Initialize Prometheus Metrics.""" self.prometheus_client = prometheus_client - self.exclude = exclude.get(CONF_ENTITIES, []) + \ - exclude.get(CONF_DOMAINS, []) - self.include_domains = include.get(CONF_DOMAINS, []) - self.include_entities = include.get(CONF_ENTITIES, []) + self._filter = entity_filter + if namespace: + self.metrics_prefix = "{}_".format(namespace) + else: + self.metrics_prefix = "" self._metrics = {} def handle_event(self, event): @@ -71,14 +78,7 @@ def handle_event(self, event): _LOGGER.debug("Handling state update for %s", entity_id) domain, _ = hacore.split_entity_id(entity_id) - if entity_id in self.exclude: - return - if domain in self.exclude and entity_id not in self.include_entities: - return - if self.include_domains and domain not in self.include_domains: - return - if not self.exclude and (self.include_entities and - entity_id not in self.include_entities): + if not self._filter(state.entity_id): return handler = '_handle_{}'.format(domain) @@ -100,7 +100,9 @@ def _metric(self, metric, factory, documentation, labels=None): try: return self._metrics[metric] except KeyError: - self._metrics[metric] = factory(metric, documentation, labels) + full_metric_name = "{}{}".format(self.metrics_prefix, metric) + self._metrics[metric] = factory( + full_metric_name, documentation, labels) return self._metrics[metric] @staticmethod @@ -179,6 +181,15 @@ def _handle_climate(self, state): 'Temperature in degrees Celsius') metric.labels(**self._labels(state)).set(temp) + current_temp = state.attributes.get(ATTR_CURRENT_TEMPERATURE) + if current_temp: + if unit == TEMP_FAHRENHEIT: + current_temp = fahrenheit_to_celsius(current_temp) + metric = self._metric( + 'current_temperature_c', self.prometheus_client.Gauge, + 'Current Temperature in degrees Celsius') + metric.labels(**self._labels(state)).set(current_temp) + metric = self._metric( 'climate_state', self.prometheus_client.Gauge, 'State of the thermostat (0/1)') diff --git a/homeassistant/components/rachio.py b/homeassistant/components/rachio.py new file mode 100644 index 00000000000000..b3b2d05e933485 --- /dev/null +++ b/homeassistant/components/rachio.py @@ -0,0 +1,289 @@ +""" +Integration with the Rachio Iro sprinkler system controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rachio/ +""" +import asyncio +import logging + +from aiohttp import web +import voluptuous as vol + +from homeassistant.auth import generate_secret +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send + +REQUIREMENTS = ['rachiopy==0.1.3'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'rachio' + +CONF_CUSTOM_URL = 'hass_url_override' +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_CUSTOM_URL): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +# Keys used in the API JSON +KEY_DEVICE_ID = 'deviceId' +KEY_DEVICES = 'devices' +KEY_ENABLED = 'enabled' +KEY_EXTERNAL_ID = 'externalId' +KEY_ID = 'id' +KEY_NAME = 'name' +KEY_ON = 'on' +KEY_STATUS = 'status' +KEY_SUBTYPE = 'subType' +KEY_SUMMARY = 'summary' +KEY_TYPE = 'type' +KEY_URL = 'url' +KEY_USERNAME = 'username' +KEY_ZONE_ID = 'zoneId' +KEY_ZONE_NUMBER = 'zoneNumber' +KEY_ZONES = 'zones' + +STATUS_ONLINE = 'ONLINE' +STATUS_OFFLINE = 'OFFLINE' + +# Device webhook values +TYPE_CONTROLLER_STATUS = 'DEVICE_STATUS' +SUBTYPE_OFFLINE = 'OFFLINE' +SUBTYPE_ONLINE = 'ONLINE' +SUBTYPE_OFFLINE_NOTIFICATION = 'OFFLINE_NOTIFICATION' +SUBTYPE_COLD_REBOOT = 'COLD_REBOOT' +SUBTYPE_SLEEP_MODE_ON = 'SLEEP_MODE_ON' +SUBTYPE_SLEEP_MODE_OFF = 'SLEEP_MODE_OFF' +SUBTYPE_BROWNOUT_VALVE = 'BROWNOUT_VALVE' +SUBTYPE_RAIN_SENSOR_DETECTION_ON = 'RAIN_SENSOR_DETECTION_ON' +SUBTYPE_RAIN_SENSOR_DETECTION_OFF = 'RAIN_SENSOR_DETECTION_OFF' +SUBTYPE_RAIN_DELAY_ON = 'RAIN_DELAY_ON' +SUBTYPE_RAIN_DELAY_OFF = 'RAIN_DELAY_OFF' + +# Schedule webhook values +TYPE_SCHEDULE_STATUS = 'SCHEDULE_STATUS' +SUBTYPE_SCHEDULE_STARTED = 'SCHEDULE_STARTED' +SUBTYPE_SCHEDULE_STOPPED = 'SCHEDULE_STOPPED' +SUBTYPE_SCHEDULE_COMPLETED = 'SCHEDULE_COMPLETED' +SUBTYPE_WEATHER_NO_SKIP = 'WEATHER_INTELLIGENCE_NO_SKIP' +SUBTYPE_WEATHER_SKIP = 'WEATHER_INTELLIGENCE_SKIP' +SUBTYPE_WEATHER_CLIMATE_SKIP = 'WEATHER_INTELLIGENCE_CLIMATE_SKIP' +SUBTYPE_WEATHER_FREEZE = 'WEATHER_INTELLIGENCE_FREEZE' + +# Zone webhook values +TYPE_ZONE_STATUS = 'ZONE_STATUS' +SUBTYPE_ZONE_STARTED = 'ZONE_STARTED' +SUBTYPE_ZONE_STOPPED = 'ZONE_STOPPED' +SUBTYPE_ZONE_COMPLETED = 'ZONE_COMPLETED' +SUBTYPE_ZONE_CYCLING = 'ZONE_CYCLING' +SUBTYPE_ZONE_CYCLING_COMPLETED = 'ZONE_CYCLING_COMPLETED' + +# Webhook callbacks +LISTEN_EVENT_TYPES = ['DEVICE_STATUS_EVENT', 'ZONE_STATUS_EVENT'] +WEBHOOK_CONST_ID = 'homeassistant.rachio:' +WEBHOOK_PATH = URL_API + DOMAIN +SIGNAL_RACHIO_UPDATE = DOMAIN + '_update' +SIGNAL_RACHIO_CONTROLLER_UPDATE = SIGNAL_RACHIO_UPDATE + '_controller' +SIGNAL_RACHIO_ZONE_UPDATE = SIGNAL_RACHIO_UPDATE + '_zone' +SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + '_schedule' + + +def setup(hass, config) -> bool: + """Set up the Rachio component.""" + from rachiopy import Rachio + + # Listen for incoming webhook connections + hass.http.register_view(RachioWebhookView()) + + # Configure API + api_key = config[DOMAIN].get(CONF_API_KEY) + rachio = Rachio(api_key) + + # Get the URL of this server + custom_url = config[DOMAIN].get(CONF_CUSTOM_URL) + hass_url = hass.config.api.base_url if custom_url is None else custom_url + rachio.webhook_auth = generate_secret() + rachio.webhook_url = hass_url + WEBHOOK_PATH + + # Get the API user + try: + person = RachioPerson(hass, rachio) + except AssertionError as error: + _LOGGER.error("Could not reach the Rachio API: %s", error) + return False + + # Check for Rachio controller devices + if not person.controllers: + _LOGGER.error("No Rachio devices found in account %s", + person.username) + return False + else: + _LOGGER.info("%d Rachio device(s) found", len(person.controllers)) + + # Enable component + hass.data[DOMAIN] = person + return True + + +class RachioPerson(object): + """Represent a Rachio user.""" + + def __init__(self, hass, rachio): + """Create an object from the provided API instance.""" + # Use API token to get user ID + self._hass = hass + self.rachio = rachio + + response = rachio.person.getInfo() + assert int(response[0][KEY_STATUS]) == 200, "API key error" + self._id = response[1][KEY_ID] + + # Use user ID to get user data + data = rachio.person.get(self._id) + assert int(data[0][KEY_STATUS]) == 200, "User ID error" + self.username = data[1][KEY_USERNAME] + self._controllers = [RachioIro(self._hass, self.rachio, controller) + for controller in data[1][KEY_DEVICES]] + _LOGGER.info('Using Rachio API as user "%s"', self.username) + + @property + def user_id(self) -> str: + """Get the user ID as defined by the Rachio API.""" + return self._id + + @property + def controllers(self) -> list: + """Get a list of controllers managed by this account.""" + return self._controllers + + +class RachioIro(object): + """Represent a Rachio Iro.""" + + def __init__(self, hass, rachio, data): + """Initialize a Rachio device.""" + self.hass = hass + self.rachio = rachio + self._id = data[KEY_ID] + self._name = data[KEY_NAME] + self._zones = data[KEY_ZONES] + self._init_data = data + _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) + + # Listen for all updates + self._init_webhooks() + + def _init_webhooks(self) -> None: + """Start getting updates from the Rachio API.""" + current_webhook_id = None + + # First delete any old webhooks that may have stuck around + def _deinit_webhooks(event) -> None: + """Stop getting updates from the Rachio API.""" + webhooks = self.rachio.notification.getDeviceWebhook( + self.controller_id)[1] + for webhook in webhooks: + if webhook[KEY_EXTERNAL_ID].startswith(WEBHOOK_CONST_ID) or\ + webhook[KEY_ID] == current_webhook_id: + self.rachio.notification.deleteWebhook(webhook[KEY_ID]) + _deinit_webhooks(None) + + # Choose which events to listen for and get their IDs + event_types = [] + for event_type in self.rachio.notification.getWebhookEventType()[1]: + if event_type[KEY_NAME] in LISTEN_EVENT_TYPES: + event_types.append({"id": event_type[KEY_ID]}) + + # Register to listen to these events from the device + url = self.rachio.webhook_url + auth = WEBHOOK_CONST_ID + self.rachio.webhook_auth + new_webhook = self.rachio.notification.postWebhook(self.controller_id, + auth, url, + event_types) + # Save ID for deletion at shutdown + current_webhook_id = new_webhook[1][KEY_ID] + self.hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _deinit_webhooks) + + def __str__(self) -> str: + """Display the controller as a string.""" + return 'Rachio controller "{}"'.format(self.name) + + @property + def controller_id(self) -> str: + """Return the Rachio API controller ID.""" + return self._id + + @property + def name(self) -> str: + """Return the user-defined name of the controller.""" + return self._name + + @property + def current_schedule(self) -> str: + """Return the schedule that the device is running right now.""" + return self.rachio.device.getCurrentSchedule(self.controller_id)[1] + + @property + def init_data(self) -> dict: + """Return the information used to set up the controller.""" + return self._init_data + + def list_zones(self, include_disabled=False) -> list: + """Return a list of the zone dicts connected to the device.""" + # All zones + if include_disabled: + return self._zones + + # Only enabled zones + return [z for z in self._zones if z[KEY_ENABLED]] + + def get_zone(self, zone_id) -> dict or None: + """Return the zone with the given ID.""" + for zone in self.list_zones(include_disabled=True): + if zone[KEY_ID] == zone_id: + return zone + + return None + + def stop_watering(self) -> None: + """Stop watering all zones connected to this controller.""" + self.rachio.device.stopWater(self.controller_id) + _LOGGER.info("Stopped watering of all zones on %s", str(self)) + + +class RachioWebhookView(HomeAssistantView): + """Provide a page for the server to call.""" + + SIGNALS = { + TYPE_CONTROLLER_STATUS: SIGNAL_RACHIO_CONTROLLER_UPDATE, + TYPE_SCHEDULE_STATUS: SIGNAL_RACHIO_SCHEDULE_UPDATE, + TYPE_ZONE_STATUS: SIGNAL_RACHIO_ZONE_UPDATE, + } + + requires_auth = False # Handled separately + url = WEBHOOK_PATH + name = url[1:].replace('/', ':') + + # pylint: disable=no-self-use + @asyncio.coroutine + async def post(self, request) -> web.Response: + """Handle webhook calls from the server.""" + hass = request.app['hass'] + data = await request.json() + + try: + auth = data.get(KEY_EXTERNAL_ID, str()).split(':')[1] + assert auth == hass.data[DOMAIN].rachio.webhook_auth + except (AssertionError, IndexError): + return web.Response(status=web.HTTPForbidden.status_code) + + update_type = data[KEY_TYPE] + if update_type in self.SIGNALS: + async_dispatcher_send(hass, self.SIGNALS[update_type], data) + + return web.Response(status=web.HTTPNoContent.status_code) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 38ba593261f3dc..43c2aa5c7b11bb 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.8'] +REQUIREMENTS = ['sqlalchemy==1.2.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 8a3e51b55b32b7..59a2dc861a62ab 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -229,7 +229,6 @@ def device_state_attributes(self): return {'hidden': 'true'} return - # pylint: disable=R0201 @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on.""" diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 2f170a206461fc..afe777ff7ccfdf 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -162,7 +162,6 @@ def get_pt2262_cmd(device_id, data_bits): return hex(data[-1] & mask) -# pylint: disable=unused-variable def get_pt2262_device(device_id): """Look for the device which id matches the given device_id parameter.""" for device in RFX_DEVICES.values(): @@ -176,7 +175,6 @@ def get_pt2262_device(device_id): return None -# pylint: disable=unused-variable def find_possible_pt2262_device(device_id): """Look for the device which id matches the given device_id parameter.""" for dev_id, device in RFX_DEVICES.items(): diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio.py index dfc60b5e45ee7a..5cb7bb337ce9ab 100644 --- a/homeassistant/components/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio.py @@ -17,7 +17,6 @@ DOMAIN = 'rpi_gpio' -# pylint: disable=no-member def setup(hass, config): """Set up the Raspberry PI GPIO component.""" import RPi.GPIO as GPIO diff --git a/homeassistant/components/satel_integra.py b/homeassistant/components/satel_integra.py index 4b61ff15c08579..4247855da39d60 100644 --- a/homeassistant/components/satel_integra.py +++ b/homeassistant/components/satel_integra.py @@ -4,7 +4,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/satel_integra/ """ -# pylint: disable=invalid-name import asyncio import logging diff --git a/homeassistant/components/sensor/arlo.py b/homeassistant/components/sensor/arlo.py index 18029691dc7bae..609887e9690256 100644 --- a/homeassistant/components/sensor/arlo.py +++ b/homeassistant/components/sensor/arlo.py @@ -13,7 +13,10 @@ from homeassistant.components.arlo import ( CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS) +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) + from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -28,7 +31,10 @@ 'total_cameras': ['Arlo Cameras', None, 'video'], 'captured_today': ['Captured Today', None, 'file-video'], 'battery_level': ['Battery Level', '%', 'battery-50'], - 'signal_strength': ['Signal Strength', None, 'signal'] + 'signal_strength': ['Signal Strength', None, 'signal'], + 'temperature': ['Temperature', TEMP_CELSIUS, 'thermometer'], + 'humidity': ['Humidity', '%', 'water-percent'], + 'air_quality': ['Air Quality', 'ppm', 'biohazard'] } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -41,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up an Arlo IP sensor.""" arlo = hass.data.get(DATA_ARLO) if not arlo: - return False + return sensors = [] for sensor_type in config.get(CONF_MONITORED_CONDITIONS): @@ -50,10 +56,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) else: for camera in arlo.cameras: + if sensor_type == 'temperature' or \ + sensor_type == 'humidity' or \ + sensor_type == 'air_quality': + continue + name = '{0} {1}'.format( SENSOR_TYPES[sensor_type][0], camera.name) sensors.append(ArloSensor(name, camera, sensor_type)) + for base_station in arlo.base_stations: + if ((sensor_type == 'temperature' or + sensor_type == 'humidity' or + sensor_type == 'air_quality') and + base_station.model_id == 'ABC1000'): + name = '{0} {1}'.format( + SENSOR_TYPES[sensor_type][0], base_station.name) + sensors.append(ArloSensor(name, base_station, sensor_type)) + add_devices(sensors, True) @@ -62,6 +82,7 @@ class ArloSensor(Entity): def __init__(self, name, device, sensor_type): """Initialize an Arlo sensor.""" + _LOGGER.debug('ArloSensor created for %s', name) self._name = name self._data = device self._sensor_type = sensor_type @@ -101,6 +122,15 @@ def unit_of_measurement(self): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[1] + @property + def device_class(self): + """Return the device class of the sensor.""" + if self._sensor_type == 'temperature': + return DEVICE_CLASS_TEMPERATURE + elif self._sensor_type == 'humidity': + return DEVICE_CLASS_HUMIDITY + return None + def update(self): """Get the latest data and updates the state.""" _LOGGER.debug("Updating Arlo sensor %s", self.name) @@ -133,6 +163,24 @@ def update(self): except TypeError: self._state = None + elif self._sensor_type == 'temperature': + try: + self._state = self._data.ambient_temperature + except TypeError: + self._state = None + + elif self._sensor_type == 'humidity': + try: + self._state = self._data.ambient_humidity + except TypeError: + self._state = None + + elif self._sensor_type == 'air_quality': + try: + self._state = self._data.ambient_air_quality + except TypeError: + self._state = None + @property def device_state_attributes(self): """Return the device state attributes.""" @@ -141,10 +189,7 @@ def device_state_attributes(self): attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION attrs['brand'] = DEFAULT_BRAND - if self._sensor_type == 'last_capture' or \ - self._sensor_type == 'captured_today' or \ - self._sensor_type == 'battery_level' or \ - self._sensor_type == 'signal_strength': + if self._sensor_type != 'total_cameras': attrs['model'] = self._data.model_id return attrs diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 38d2226012c8ae..bd23b9850f773f 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -121,7 +121,6 @@ def update(self): stats = self.data.stats ticker = self.data.ticker - # pylint: disable=no-member if self.type == 'exchangerate': self._state = ticker[self._currency].p15min self._unit_of_measurement = self._currency diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 590d5a8f1ceb46..10a96ded43739e 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -287,7 +287,6 @@ def load_data(self, data): img = condition.get(IMAGE, None) - # pylint: disable=protected-access if new_state != self._state or img != self._entity_picture: self._state = new_state self._entity_picture = img @@ -299,12 +298,10 @@ def load_data(self, data): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) self._timeframe = nested.get(TIMEFRAME) - # pylint: disable=protected-access self._state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) return True # update all other sensors - # pylint: disable=protected-access self._state = data.get(self.type) return True @@ -329,7 +326,7 @@ def state(self): return self._state @property - def should_poll(self): # pylint: disable=no-self-use + def should_poll(self): """No polling needed.""" return False diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py index c39ae43aef0ad5..c6a7106663f699 100644 --- a/homeassistant/components/sensor/cpuspeed.py +++ b/homeassistant/components/sensor/cpuspeed.py @@ -30,7 +30,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the CPU speed sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/cups.py b/homeassistant/components/sensor/cups.py index 7c1d9fc3d49cd8..6d55853d724221 100644 --- a/homeassistant/components/sensor/cups.py +++ b/homeassistant/components/sensor/cups.py @@ -128,7 +128,7 @@ def update(self): self._printer = self.data.printers.get(self._name) -# pylint: disable=import-error, no-name-in-module +# pylint: disable=no-name-in-module class CupsData(object): """Get the latest data from CUPS and update the state.""" diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 0db06622ad8338..7c492fd496d263 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -4,9 +4,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ -from homeassistant.components.deconz import ( - CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, - DATA_DECONZ_UNSUB) +from homeassistant.components.deconz.const import ( + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, + DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback @@ -72,7 +72,8 @@ def async_update_callback(self, reason): """ if reason['state'] or \ 'reachable' in reason['attr'] or \ - 'battery' in reason['attr']: + 'battery' in reason['attr'] or \ + 'on' in reason['attr']: self.async_schedule_update_ha_state() @property @@ -122,8 +123,10 @@ def device_state_attributes(self): attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.on is not None: + attr[ATTR_ON] = self._sensor.on if self._sensor.type in LIGHTLEVEL and self._sensor.dark is not None: - attr['dark'] = self._sensor.dark + attr[ATTR_DARK] = self._sensor.dark if self.unit_of_measurement == 'Watts': attr[ATTR_CURRENT] = self._sensor.current attr[ATTR_VOLTAGE] = self._sensor.voltage diff --git a/homeassistant/components/sensor/duke_energy.py b/homeassistant/components/sensor/duke_energy.py new file mode 100644 index 00000000000000..458a2929d0b6b3 --- /dev/null +++ b/homeassistant/components/sensor/duke_energy.py @@ -0,0 +1,84 @@ +""" +Support for Duke Energy Gas and Electric meters. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.duke_energy/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pydukeenergy==0.0.6'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + +LAST_BILL_USAGE = "last_bills_usage" +LAST_BILL_AVERAGE_USAGE = "last_bills_average_usage" +LAST_BILL_DAYS_BILLED = "last_bills_days_billed" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup all Duke Energy meters.""" + from pydukeenergy.api import DukeEnergy, DukeEnergyException + + try: + duke = DukeEnergy(config[CONF_USERNAME], + config[CONF_PASSWORD], + update_interval=120) + except DukeEnergyException: + _LOGGER.error("Failed to setup Duke Energy") + return + + add_devices([DukeEnergyMeter(meter) for meter in duke.get_meters()]) + + +class DukeEnergyMeter(Entity): + """Representation of a Duke Energy meter.""" + + def __init__(self, meter): + """Initialize the meter.""" + self.duke_meter = meter + + @property + def name(self): + """Return the name.""" + return "duke_energy_{}".format(self.duke_meter.id) + + @property + def unique_id(self): + """Return the unique ID.""" + return self.duke_meter.id + + @property + def state(self): + """Return yesterdays usage.""" + return self.duke_meter.get_usage() + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self.duke_meter.get_unit() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attributes = { + LAST_BILL_USAGE: self.duke_meter.get_total(), + LAST_BILL_AVERAGE_USAGE: self.duke_meter.get_average(), + LAST_BILL_DAYS_BILLED: self.duke_meter.get_days_billed() + } + return attributes + + def update(self): + """Update meter.""" + self.duke_meter.update() diff --git a/homeassistant/components/sensor/dwd_weather_warnings.py b/homeassistant/components/sensor/dwd_weather_warnings.py index 9105e30eb422d1..e023dfcc49ff09 100644 --- a/homeassistant/components/sensor/dwd_weather_warnings.py +++ b/homeassistant/components/sensor/dwd_weather_warnings.py @@ -95,7 +95,6 @@ def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._var_units - # pylint: disable=no-member @property def state(self): """Return the state of the device.""" @@ -104,7 +103,6 @@ def state(self): except TypeError: return self._api.data[self._var_id] - # pylint: disable=no-member @property def device_state_attributes(self): """Return the state attributes of the DWD-Weather-Warnings.""" diff --git a/homeassistant/components/sensor/dweet.py b/homeassistant/components/sensor/dweet.py index 157f366c0c40c1..cca06bd9782d33 100644 --- a/homeassistant/components/sensor/dweet.py +++ b/homeassistant/components/sensor/dweet.py @@ -34,7 +34,6 @@ }) -# pylint: disable=unused-variable, too-many-function-args def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dweet sensor.""" import dweepy diff --git a/homeassistant/components/sensor/envirophat.py b/homeassistant/components/sensor/envirophat.py index b11dae8e1682e7..265350f3e95104 100644 --- a/homeassistant/components/sensor/envirophat.py +++ b/homeassistant/components/sensor/envirophat.py @@ -55,7 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sense HAT sensor platform.""" try: - # pylint: disable=import-error import envirophat except OSError: _LOGGER.error("No Enviro pHAT was found.") @@ -175,7 +174,6 @@ def update(self): self.light_red, self.light_green, self.light_blue = \ self.envirophat.light.rgb() if self.use_leds: - # pylint: disable=no-value-for-parameter self.envirophat.leds.off() # accelerometer readings in G diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 9c05028b3944ff..261f6e2b510a3f 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) +FILTER_NAME_RANGE = 'range' FILTER_NAME_LOWPASS = 'lowpass' FILTER_NAME_OUTLIER = 'outlier' FILTER_NAME_THROTTLE = 'throttle' @@ -40,6 +41,8 @@ CONF_FILTER_PRECISION = 'precision' CONF_FILTER_RADIUS = 'radius' CONF_FILTER_TIME_CONSTANT = 'time_constant' +CONF_FILTER_LOWER_BOUND = 'lower_bound' +CONF_FILTER_UPPER_BOUND = 'upper_bound' CONF_TIME_SMA_TYPE = 'type' TIME_SMA_LAST = 'last' @@ -77,6 +80,12 @@ default=DEFAULT_FILTER_TIME_CONSTANT): vol.Coerce(int), }) +FILTER_RANGE_SCHEMA = FILTER_SCHEMA.extend({ + vol.Required(CONF_FILTER_NAME): FILTER_NAME_RANGE, + vol.Optional(CONF_FILTER_LOWER_BOUND): vol.Coerce(float), + vol.Optional(CONF_FILTER_UPPER_BOUND): vol.Coerce(float), +}) + FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, vol.Optional(CONF_TIME_SMA_TYPE, @@ -100,7 +109,8 @@ [vol.Any(FILTER_OUTLIER_SCHEMA, FILTER_LOWPASS_SCHEMA, FILTER_TIME_SMA_SCHEMA, - FILTER_THROTTLE_SCHEMA)]) + FILTER_THROTTLE_SCHEMA, + FILTER_RANGE_SCHEMA)]) }) @@ -325,6 +335,49 @@ def filter_state(self, new_state): return new_state +@FILTERS.register(FILTER_NAME_RANGE) +class RangeFilter(Filter): + """Range filter. + + Determines if new state is in the range of upper_bound and lower_bound. + If not inside, lower or upper bound is returned instead. + + Args: + upper_bound (float): band upper bound + lower_bound (float): band lower bound + """ + + def __init__(self, entity, + lower_bound, upper_bound): + """Initialize Filter.""" + super().__init__(FILTER_NAME_RANGE, entity=entity) + self._lower_bound = lower_bound + self._upper_bound = upper_bound + self._stats_internal = Counter() + + def _filter_state(self, new_state): + """Implement the range filter.""" + if self._upper_bound and new_state.state > self._upper_bound: + + self._stats_internal['erasures_up'] += 1 + + _LOGGER.debug("Upper outlier nr. %s in %s: %s", + self._stats_internal['erasures_up'], + self._entity, new_state) + new_state.state = self._upper_bound + + elif self._lower_bound and new_state.state < self._lower_bound: + + self._stats_internal['erasures_low'] += 1 + + _LOGGER.debug("Lower outlier nr. %s in %s: %s", + self._stats_internal['erasures_low'], + self._entity, new_state) + new_state.state = self._lower_bound + + return new_state + + @FILTERS.register(FILTER_NAME_OUTLIER) class OutlierFilter(Filter): """BASIC outlier filter. diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index f312d1f22cc1e1..87bd735a03df1d 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -225,7 +225,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config, add_devices, config_path, discovery_info=None) return False else: - config_file = save_json(config_path, DEFAULT_CONFIG) + save_json(config_path, DEFAULT_CONFIG) request_app_setup( hass, config, add_devices, config_path, discovery_info=None) return False diff --git a/homeassistant/components/sensor/fixer.py b/homeassistant/components/sensor/fixer.py index 3e909b7b21de55..438366ae5558e0 100644 --- a/homeassistant/components/sensor/fixer.py +++ b/homeassistant/components/sensor/fixer.py @@ -10,15 +10,14 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_BASE, CONF_NAME +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['fixerio==0.1.1'] +REQUIREMENTS = ['fixerio==1.0.0a0'] _LOGGER = logging.getLogger(__name__) -ATTR_BASE = 'Base currency' ATTR_EXCHANGE_RATE = 'Exchange rate' ATTR_TARGET = 'Target currency' @@ -33,8 +32,8 @@ SCAN_INTERVAL = timedelta(days=1) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_TARGET): cv.string, - vol.Optional(CONF_BASE, default=DEFAULT_BASE): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -43,17 +42,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Fixer.io sensor.""" from fixerio import Fixerio, exceptions + api_key = config.get(CONF_API_KEY) name = config.get(CONF_NAME) - base = config.get(CONF_BASE) target = config.get(CONF_TARGET) try: - Fixerio(base=base, symbols=[target], secure=True).latest() + Fixerio(symbols=[target], access_key=api_key).latest() except exceptions.FixerioException: _LOGGER.error("One of the given currencies is not supported") - return False + return - data = ExchangeData(base, target) + data = ExchangeData(target, api_key) add_devices([ExchangeRateSensor(data, name, target)], True) @@ -87,10 +86,9 @@ def device_state_attributes(self): """Return the state attributes.""" if self.data.rate is not None: return { - ATTR_BASE: self.data.rate['base'], - ATTR_TARGET: self._target, - ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target], ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target], + ATTR_TARGET: self._target, } @property @@ -107,16 +105,15 @@ def update(self): class ExchangeData(object): """Get the latest data and update the states.""" - def __init__(self, base_currency, target_currency): + def __init__(self, target_currency, api_key): """Initialize the data object.""" from fixerio import Fixerio + self.api_key = api_key self.rate = None - self.base_currency = base_currency self.target_currency = target_currency self.exchange = Fixerio( - base=self.base_currency, symbols=[self.target_currency], - secure=True) + symbols=[self.target_currency], access_key=self.api_key) def update(self): """Get the latest data from Fixer.io.""" diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 4fed3793c50c00..bd6e91c7b531b0 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -56,7 +56,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Glances sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/gpsd.py b/homeassistant/components/sensor/gpsd.py index 472dd1d70f6cb8..1d270419933a18 100644 --- a/homeassistant/components/sensor/gpsd.py +++ b/homeassistant/components/sensor/gpsd.py @@ -86,7 +86,6 @@ def name(self): """Return the name.""" return self._name - # pylint: disable=no-member @property def state(self): """Return the state of GPSD.""" diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index ccd1949cc3b2bd..0596bc0b6ccd3e 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -8,8 +8,8 @@ import logging from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) from homeassistant.const import ( TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE) @@ -36,15 +36,17 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the HomematicIP sensors devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP sensors from a config entry.""" from homematicip.device import ( HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, TemperatureHumiditySensorDisplay, MotionDetectorIndoor) - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [HomematicipAccesspointStatus(home)] - for device in home.devices: if isinstance(device, HeatingThermostat): devices.append(HomematicipHeatingThermostat(home, device)) diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index ca8c19bbc7a193..1048c04d43dc13 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -5,7 +5,7 @@ https://home-assistant.io/components/sensor.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.sensor import DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_WEATHER, diff --git a/homeassistant/components/sensor/kira.py b/homeassistant/components/sensor/kira.py index 74a1bd19d34428..19566100f99534 100644 --- a/homeassistant/components/sensor/kira.py +++ b/homeassistant/components/sensor/kira.py @@ -18,7 +18,6 @@ CONF_SENSOR = 'sensor' -# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices, discovery_info=None): """Set up a Kira sensor.""" if discovery_info is not None: diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index ee9ab146c87fcb..6ee3f7d16d0835 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -68,7 +68,6 @@ def state(self): """Return the state of the sensor.""" return self._state - # pylint: disable=no-member def update(self): """Update device state.""" self._cover = self._user.get_image() diff --git a/homeassistant/components/sensor/loopenergy.py b/homeassistant/components/sensor/loopenergy.py index 09ed4ab3d4988b..d888a6c634d655 100644 --- a/homeassistant/components/sensor/loopenergy.py +++ b/homeassistant/components/sensor/loopenergy.py @@ -63,7 +63,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elec_config = config.get(CONF_ELEC) gas_config = config.get(CONF_GAS, {}) - # pylint: disable=too-many-function-args controller = pyloopenergy.LoopEnergy( elec_config.get(CONF_ELEC_SERIAL), elec_config.get(CONF_ELEC_SECRET), diff --git a/homeassistant/components/sensor/mfi.py b/homeassistant/components/sensor/mfi.py index f6bec3284c341f..ab6bd8270ce18c 100644 --- a/homeassistant/components/sensor/mfi.py +++ b/homeassistant/components/sensor/mfi.py @@ -49,7 +49,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up mFi sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 1add4157f0e952..2fbfc0e97a4169 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -42,7 +42,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsSensor(mysensors.MySensorsEntity): +class MySensorsSensor(mysensors.device.MySensorsEntity): """Representation of a MySensors Sensor child node.""" @property diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index bf1b3f65c4a9fc..d2e1501ad7e961 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -24,10 +24,14 @@ # color_status: "gray", "green", "yellow", "red" 'color_status'] -STRUCTURE_SENSOR_TYPES = ['eta', 'security_state'] +STRUCTURE_SENSOR_TYPES = ['eta'] + +# security_state is structure level sensor, but only meaningful when +# Nest Cam exist +STRUCTURE_CAMERA_SENSOR_TYPES = ['security_state'] _VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ - + STRUCTURE_SENSOR_TYPES + + STRUCTURE_SENSOR_TYPES + STRUCTURE_CAMERA_SENSOR_TYPES SENSOR_UNITS = {'humidity': '%'} @@ -105,6 +109,14 @@ def get_sensors(): for variable in conditions if variable in PROTECT_SENSOR_TYPES] + structures_has_camera = {} + for structure, device in nest.cameras(): + structures_has_camera[structure] = True + for structure in structures_has_camera: + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_CAMERA_SENSOR_TYPES] + return all_sensors async_add_devices(await hass.async_add_job(get_sensors), True) @@ -133,7 +145,8 @@ def update(self): elif self.variable in PROTECT_SENSOR_TYPES \ and self.variable != 'color_status': # keep backward compatibility - self._state = getattr(self.device, self.variable).capitalize() + state = getattr(self.device, self.variable) + self._state = state.capitalize() if state is not None else None else: self._state = getattr(self.device, self.variable) diff --git a/homeassistant/components/sensor/netgear_lte.py b/homeassistant/components/sensor/netgear_lte.py index 859435edbc99ea..b4a3e2a1155981 100644 --- a/homeassistant/components/sensor/netgear_lte.py +++ b/homeassistant/components/sensor/netgear_lte.py @@ -29,14 +29,14 @@ async def async_setup_platform( hass, config, async_add_devices, discovery_info): """Set up Netgear LTE sensor devices.""" - lte_data = hass.data[DATA_KEY].get(config) + modem_data = hass.data[DATA_KEY].get_modem_data(config) sensors = [] for sensortype in config[CONF_SENSORS]: if sensortype == SENSOR_SMS: - sensors.append(SMSSensor(lte_data)) + sensors.append(SMSSensor(modem_data)) elif sensortype == SENSOR_USAGE: - sensors.append(UsageSensor(lte_data)) + sensors.append(UsageSensor(modem_data)) async_add_devices(sensors, True) @@ -45,11 +45,11 @@ async def async_setup_platform( class LTESensor(Entity): """Data usage sensor entity.""" - lte_data = attr.ib() + modem_data = attr.ib() async def async_update(self): """Update state.""" - await self.lte_data.async_update() + await self.modem_data.async_update() class SMSSensor(LTESensor): @@ -63,7 +63,7 @@ def name(self): @property def state(self): """Return the state of the sensor.""" - return self.lte_data.unread_count + return self.modem_data.unread_count class UsageSensor(LTESensor): @@ -82,4 +82,4 @@ def name(self): @property def state(self): """Return the state of the sensor.""" - return round(self.lte_data.usage / 1024**2, 1) + return round(self.modem_data.usage / 1024**2, 1) diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py index bf440728a2ee55..7c7ff3480b00e0 100644 --- a/homeassistant/components/sensor/nut.py +++ b/homeassistant/components/sensor/nut.py @@ -107,6 +107,20 @@ ['Voltage Transfer Reason', '', 'mdi:information-outline'], 'input.voltage': ['Input Voltage', 'V', 'mdi:flash'], 'input.voltage.nominal': ['Nominal Input Voltage', 'V', 'mdi:flash'], + 'input.frequency': ['Input Line Frequency', 'hz', 'mdi:flash'], + 'input.frequency.nominal': + ['Nominal Input Line Frequency', 'hz', 'mdi:flash'], + 'input.frequency.status': + ['Input Frequency Status', '', 'mdi:information-outline'], + 'output.current': ['Output Current', 'A', 'mdi:flash'], + 'output.current.nominal': + ['Nominal Output Current', 'A', 'mdi:flash'], + 'output.voltage': ['Output Voltage', 'V', 'mdi:flash'], + 'output.voltage.nominal': + ['Nominal Output Voltage', 'V', 'mdi:flash'], + 'output.frequency': ['Output Frequency', 'hz', 'mdi:flash'], + 'output.frequency.nominal': + ['Nominal Output Frequency', 'hz', 'mdi:flash'], } STATE_TYPES = { diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index 1ef5a27cf3dffc..c11c83ab40e1c8 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -13,17 +13,17 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS -) + ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS) +from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle, slugify +from homeassistant.util import Throttle -REQUIREMENTS = ['pypollencom==1.1.2'] +REQUIREMENTS = ['pypollencom==2.1.0'] _LOGGER = logging.getLogger(__name__) -ATTR_ALLERGEN_GENUS = 'primary_allergen_genus' -ATTR_ALLERGEN_NAME = 'primary_allergen_name' -ATTR_ALLERGEN_TYPE = 'primary_allergen_type' +ATTR_ALLERGEN_GENUS = 'allergen_genus' +ATTR_ALLERGEN_NAME = 'allergen_name' +ATTR_ALLERGEN_TYPE = 'allergen_type' ATTR_CITY = 'city' ATTR_OUTLOOK = 'outlook' ATTR_RATING = 'rating' @@ -34,53 +34,30 @@ CONF_ZIP_CODE = 'zip_code' DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' - -MIN_TIME_UPDATE_AVERAGES = timedelta(hours=12) -MIN_TIME_UPDATE_INDICES = timedelta(minutes=10) - -CONDITIONS = { - 'allergy_average_forecasted': ( - 'Allergy Index: Forecasted Average', - 'AllergyAverageSensor', - 'allergy_average_data', - {'data_attr': 'extended_data'}, - 'mdi:flower' - ), - 'allergy_average_historical': ( - 'Allergy Index: Historical Average', - 'AllergyAverageSensor', - 'allergy_average_data', - {'data_attr': 'historic_data'}, - 'mdi:flower' - ), - 'allergy_index_today': ( - 'Allergy Index: Today', - 'AllergyIndexSensor', - 'allergy_index_data', - {'key': 'Today'}, - 'mdi:flower' - ), - 'allergy_index_tomorrow': ( - 'Allergy Index: Tomorrow', - 'AllergyIndexSensor', - 'allergy_index_data', - {'key': 'Tomorrow'}, - 'mdi:flower' - ), - 'allergy_index_yesterday': ( - 'Allergy Index: Yesterday', - 'AllergyIndexSensor', - 'allergy_index_data', - {'key': 'Yesterday'}, - 'mdi:flower' - ), - 'disease_average_forecasted': ( - 'Cold & Flu: Forecasted Average', - 'AllergyAverageSensor', - 'disease_average_data', - {'data_attr': 'extended_data'}, - 'mdi:snowflake' - ) +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) + +TYPE_ALLERGY_FORECAST = 'allergy_average_forecasted' +TYPE_ALLERGY_HISTORIC = 'allergy_average_historical' +TYPE_ALLERGY_INDEX = 'allergy_index' +TYPE_ALLERGY_OUTLOOK = 'allergy_outlook' +TYPE_ALLERGY_TODAY = 'allergy_index_today' +TYPE_ALLERGY_TOMORROW = 'allergy_index_tomorrow' +TYPE_ALLERGY_YESTERDAY = 'allergy_index_yesterday' +TYPE_DISEASE_FORECAST = 'disease_average_forecasted' + +SENSORS = { + TYPE_ALLERGY_FORECAST: ( + 'Allergy Index: Forecasted Average', None, 'mdi:flower', 'index'), + TYPE_ALLERGY_HISTORIC: ( + 'Allergy Index: Historical Average', None, 'mdi:flower', 'index'), + TYPE_ALLERGY_TODAY: ( + 'Allergy Index: Today', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'), + TYPE_ALLERGY_TOMORROW: ( + 'Allergy Index: Tomorrow', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'), + TYPE_ALLERGY_YESTERDAY: ( + 'Allergy Index: Yesterday', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'), + TYPE_DISEASE_FORECAST: ( + 'Cold & Flu: Forecasted Average', None, 'mdi:snowflake', 'index') } RATING_MAPPING = [{ @@ -105,69 +82,69 @@ 'maximum': 12 }] +TREND_FLAT = 'Flat' +TREND_INCREASING = 'Increasing' +TREND_SUBSIDING = 'Subsiding' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ZIP_CODE): str, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(CONDITIONS)]), + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) }) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Configure the platform and add the sensors.""" from pypollencom import Client - _LOGGER.debug('Configuration data: %s', config) + websession = aiohttp_client.async_get_clientsession(hass) - client = Client(config[CONF_ZIP_CODE]) - datas = { - 'allergy_average_data': AllergyAveragesData(client), - 'allergy_index_data': AllergyIndexData(client), - 'disease_average_data': DiseaseData(client) - } - classes = { - 'AllergyAverageSensor': AllergyAverageSensor, - 'AllergyIndexSensor': AllergyIndexSensor - } + data = PollenComData( + Client(config[CONF_ZIP_CODE], websession), + config[CONF_MONITORED_CONDITIONS]) - for data in datas.values(): - data.update() + await data.async_update() sensors = [] - for condition in config[CONF_MONITORED_CONDITIONS]: - name, sensor_class, data_key, params, icon = CONDITIONS[condition] - sensors.append(classes[sensor_class]( - datas[data_key], - params, - name, - icon, - config[CONF_ZIP_CODE] - )) + for kind in config[CONF_MONITORED_CONDITIONS]: + name, category, icon, unit = SENSORS[kind] + sensors.append( + PollencomSensor( + data, config[CONF_ZIP_CODE], kind, category, name, icon, unit)) - add_devices(sensors, True) + async_add_devices(sensors, True) -def calculate_trend(list_of_nums): - """Calculate the most common rating as a trend.""" +def calculate_average_rating(indices): + """Calculate the human-friendly historical allergy average.""" ratings = list( - r['label'] for n in list_of_nums - for r in RATING_MAPPING + r['label'] for n in indices for r in RATING_MAPPING if r['minimum'] <= n <= r['maximum']) return max(set(ratings), key=ratings.count) -class BaseSensor(Entity): - """Define a base class for all of our sensors.""" +class PollencomSensor(Entity): + """Define a Pollen.com sensor.""" - def __init__(self, data, data_params, name, icon, unique_id): + def __init__(self, pollencom, zip_code, kind, category, name, icon, unit): """Initialize the sensor.""" self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._category = category self._icon = icon self._name = name - self._data_params = data_params self._state = None - self._unit = None - self._unique_id = unique_id - self.data = data + self._type = kind + self._unit = unit + self._zip_code = zip_code + self.pollencom = pollencom + + @property + def available(self): + """Return True if entity is available.""" + return bool( + self.pollencom.data.get(self._type) + or self.pollencom.data.get(self._category)) @property def device_state_attributes(self): @@ -192,187 +169,164 @@ def state(self): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_{1}'.format(self._unique_id, slugify(self._name)) + return '{0}_{1}'.format(self._zip_code, self._type) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit + async def async_update(self): + """Update the sensor.""" + await self.pollencom.async_update() + if not self.pollencom.data: + return -class AllergyAverageSensor(BaseSensor): - """Define a sensor to show allergy average information.""" - - def update(self): - """Update the status of the sensor.""" - self.data.update() + if self._category: + data = self.pollencom.data[self._category].get('Location') + else: + data = self.pollencom.data[self._type].get('Location') - try: - data_attr = getattr(self.data, self._data_params['data_attr']) - indices = [p['Index'] for p in data_attr['Location']['periods']] - self._attrs[ATTR_TREND] = calculate_trend(indices) - except KeyError: - _LOGGER.error("Pollen.com API didn't return any data") + if not data: return - try: - self._attrs[ATTR_CITY] = data_attr['Location']['City'].title() - self._attrs[ATTR_STATE] = data_attr['Location']['State'] - self._attrs[ATTR_ZIP_CODE] = data_attr['Location']['ZIP'] - except KeyError: - _LOGGER.debug('Location data not included in API response') - self._attrs[ATTR_CITY] = None - self._attrs[ATTR_STATE] = None - self._attrs[ATTR_ZIP_CODE] = None - + indices = [p['Index'] for p in data['periods']] average = round(mean(indices), 1) [rating] = [ i['label'] for i in RATING_MAPPING if i['minimum'] <= average <= i['maximum'] ] - self._attrs[ATTR_RATING] = rating - - self._state = average - self._unit = 'index' - - -class AllergyIndexSensor(BaseSensor): - """Define a sensor to show allergy index information.""" - - def update(self): - """Update the status of the sensor.""" - self.data.update() - - try: - location_data = self.data.current_data['Location'] - [period] = [ - p for p in location_data['periods'] - if p['Type'] == self._data_params['key'] - ] + slope = (data['periods'][-1]['Index'] - data['periods'][-2]['Index']) + trend = TREND_FLAT + if slope > 0: + trend = TREND_INCREASING + elif slope < 0: + trend = TREND_SUBSIDING + + if self._type == TYPE_ALLERGY_FORECAST: + outlook = self.pollencom.data[TYPE_ALLERGY_OUTLOOK] + + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_OUTLOOK: outlook['Outlook'], + ATTR_RATING: rating, + ATTR_SEASON: outlook['Season'].title(), + ATTR_STATE: data['State'], + ATTR_TREND: outlook['Trend'].title(), + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = average + elif self._type == TYPE_ALLERGY_HISTORIC: + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: calculate_average_rating(indices), + ATTR_STATE: data['State'], + ATTR_TREND: trend, + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = average + elif self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY): + key = self._type.split('_')[-1].title() + [period] = [p for p in data['periods'] if p['Type'] == key] [rating] = [ i['label'] for i in RATING_MAPPING if i['minimum'] <= period['Index'] <= i['maximum'] ] - 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: - _LOGGER.error("Pollen.com API didn't return any data") - return - - try: - self._attrs[ATTR_CITY] = location_data['City'].title() - self._attrs[ATTR_STATE] = location_data['State'] - self._attrs[ATTR_ZIP_CODE] = location_data['ZIP'] - except KeyError: - _LOGGER.debug('Location data not included in API response') - self._attrs[ATTR_CITY] = None - self._attrs[ATTR_STATE] = None - self._attrs[ATTR_ZIP_CODE] = None - - try: - self._attrs[ATTR_OUTLOOK] = self.data.outlook_data['Outlook'] - except KeyError: - _LOGGER.debug('Outlook data not included in API response') - self._attrs[ATTR_OUTLOOK] = None - - try: - self._attrs[ATTR_SEASON] = self.data.outlook_data['Season'] - except KeyError: - _LOGGER.debug('Season data not included in API response') - self._attrs[ATTR_SEASON] = None - - try: - self._attrs[ATTR_TREND] = self.data.outlook_data['Trend'].title() - except KeyError: - _LOGGER.debug('Trend data not included in API response') - self._attrs[ATTR_TREND] = None - - self._state = period['Index'] - self._unit = 'index' - - -class DataBase(object): - """Define a generic data object.""" - - def __init__(self, client): + for idx, attrs in enumerate(period['Triggers']): + index = idx + 1 + self._attrs.update({ + '{0}_{1}'.format(ATTR_ALLERGEN_GENUS, index): + attrs['Genus'], + '{0}_{1}'.format(ATTR_ALLERGEN_NAME, index): + attrs['Name'], + '{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index): + attrs['PlantType'], + }) + + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: rating, + ATTR_STATE: data['State'], + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = period['Index'] + elif self._type == TYPE_DISEASE_FORECAST: + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: rating, + ATTR_STATE: data['State'], + ATTR_TREND: trend, + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = average + + +class PollenComData(object): + """Define a data object to retrieve info from Pollen.com.""" + + def __init__(self, client, sensor_types): """Initialize.""" self._client = client + self._sensor_types = sensor_types + self.data = {} - def _get_client_data(self, module, operation): - """Get data from a particular point in the API.""" - from pypollencom.exceptions import HTTPError - - data = {} - try: - data = getattr(getattr(self._client, module), operation)() - _LOGGER.debug('Received "%s_%s" data: %s', module, operation, data) - except HTTPError as exc: - _LOGGER.error('An error occurred while retrieving data') - _LOGGER.debug(exc) - - return data - - -class AllergyAveragesData(DataBase): - """Define an object to averages on future and historical allergy data.""" - - def __init__(self, client): - """Initialize.""" - super().__init__(client) - self.extended_data = None - self.historic_data = None - - @Throttle(MIN_TIME_UPDATE_AVERAGES) - def update(self): - """Update with new data.""" - self.extended_data = self._get_client_data('allergens', 'extended') - self.historic_data = self._get_client_data('allergens', 'historic') - - -class AllergyIndexData(DataBase): - """Define an object to retrieve current allergy index info.""" + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update Pollen.com data.""" + from pypollencom.errors import InvalidZipError, PollenComError - def __init__(self, client): - """Initialize.""" - super().__init__(client) - self.current_data = None - self.outlook_data = None - - @Throttle(MIN_TIME_UPDATE_INDICES) - def update(self): - """Update with new index data.""" - self.current_data = self._get_client_data('allergens', 'current') - self.outlook_data = self._get_client_data('allergens', 'outlook') + # Pollen.com requires a bit more complicated error handling, given that + # it sometimes has parts (but not the whole thing) go down: + # + # 1. If `InvalidZipError` is thrown, quit everything immediately. + # 2. If an individual request throws any other error, try the others. + try: + if TYPE_ALLERGY_FORECAST in self._sensor_types: + try: + data = await self._client.allergens.extended() + self.data[TYPE_ALLERGY_FORECAST] = data + except PollenComError as err: + _LOGGER.error('Unable to get allergy forecast: %s', err) + self.data[TYPE_ALLERGY_FORECAST] = {} -class DiseaseData(DataBase): - """Define an object to retrieve current disease index info.""" + try: + data = await self._client.allergens.outlook() + self.data[TYPE_ALLERGY_OUTLOOK] = data + except PollenComError as err: + _LOGGER.error('Unable to get allergy outlook: %s', err) + self.data[TYPE_ALLERGY_OUTLOOK] = {} - def __init__(self, client): - """Initialize.""" - super().__init__(client) - self.extended_data = None + if TYPE_ALLERGY_HISTORIC in self._sensor_types: + try: + data = await self._client.allergens.historic() + self.data[TYPE_ALLERGY_HISTORIC] = data + except PollenComError as err: + _LOGGER.error('Unable to get allergy history: %s', err) + self.data[TYPE_ALLERGY_HISTORIC] = {} + + if all(s in self._sensor_types + for s in [TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY]): + try: + data = await self._client.allergens.current() + self.data[TYPE_ALLERGY_INDEX] = data + except PollenComError as err: + _LOGGER.error('Unable to get current allergies: %s', err) + self.data[TYPE_ALLERGY_TODAY] = {} - @Throttle(MIN_TIME_UPDATE_INDICES) - def update(self): - """Update with new cold/flu data.""" - self.extended_data = self._get_client_data('disease', 'extended') + if TYPE_DISEASE_FORECAST in self._sensor_types: + try: + data = await self._client.disease.extended() + self.data[TYPE_DISEASE_FORECAST] = data + except PollenComError as err: + _LOGGER.error('Unable to get disease forecast: %s', err) + self.data[TYPE_DISEASE_FORECAST] = {} + + _LOGGER.debug('New data retrieved: %s', self.data) + except InvalidZipError: + _LOGGER.error( + 'Cannot retrieve data for ZIP code: %s', self._client.zip_code) + self.data = {} diff --git a/homeassistant/components/sensor/pvoutput.py b/homeassistant/components/sensor/pvoutput.py index 26c3e27bba51ba..d4307d50228691 100644 --- a/homeassistant/components/sensor/pvoutput.py +++ b/homeassistant/components/sensor/pvoutput.py @@ -64,7 +64,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([PvoutputSensor(rest, name)], True) -# pylint: disable=no-member class PvoutputSensor(Entity): """Representation of a PVOutput sensor.""" diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index 53cbaab19a58eb..2731587ed710e3 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Skybeacon sensor.""" - # pylint: disable=unreachable name = config.get(CONF_NAME) mac = config.get(CONF_MAC) _LOGGER.debug("Setting up...") @@ -139,7 +138,7 @@ def __init__(self, hass, mac, name): def run(self): """Thread that keeps connection alive.""" - # pylint: disable=import-error, no-name-in-module, no-member + # pylint: disable=import-error import pygatt from pygatt.backends import Characteristic from pygatt.exceptions import ( diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py index 3451789424b08c..2be46da0bdb01c 100644 --- a/homeassistant/components/sensor/sma.py +++ b/homeassistant/components/sensor/sma.py @@ -198,5 +198,4 @@ def async_update_values(self, key_values): update = True self._state = new_state - return self.async_update_ha_state() if update else None \ - # pylint: disable=protected-access + return self.async_update_ha_state() if update else None diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 7fefb0f450b54f..8574a7231da5b3 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.8'] +REQUIREMENTS = ['sqlalchemy==1.2.9'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/homeassistant/components/sensor/steam_online.py b/homeassistant/components/sensor/steam_online.py index e22e1594b55479..7521b74cd28e43 100644 --- a/homeassistant/components/sensor/steam_online.py +++ b/homeassistant/components/sensor/steam_online.py @@ -76,7 +76,6 @@ def state(self): """Return the state of the sensor.""" return self._state - # pylint: disable=no-member def update(self): """Update device state.""" try: diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py index a0198169b6d3d4..e3c3a0cf5caaed 100644 --- a/homeassistant/components/sensor/synologydsm.py +++ b/homeassistant/components/sensor/synologydsm.py @@ -12,18 +12,20 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, TEMP_CELSIUS, - CONF_MONITORED_CONDITIONS, EVENT_HOMEASSISTANT_START, CONF_DISKS) + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + ATTR_ATTRIBUTION, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, + EVENT_HOMEASSISTANT_START, CONF_DISKS) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['python-synology==0.1.0'] +REQUIREMENTS = ['python-synology==0.2.0'] _LOGGER = logging.getLogger(__name__) +CONF_ATTRIBUTION = 'Data provided by Synology' CONF_VOLUMES = 'volumes' DEFAULT_NAME = 'Synology DSM' -DEFAULT_PORT = 5000 +DEFAULT_PORT = 5001 MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) @@ -74,6 +76,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=True): cv.boolean, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS): @@ -95,10 +98,11 @@ def run_setup(event): port = config.get(CONF_PORT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) + use_ssl = config.get(CONF_SSL) unit = hass.config.units.temperature_unit monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) - api = SynoApi(host, port, username, password, unit) + api = SynoApi(host, port, username, password, unit, use_ssl) sensors = [SynoNasUtilSensor( api, variable, _UTILISATION_MON_COND[variable]) @@ -128,13 +132,14 @@ def run_setup(event): class SynoApi(object): """Class to interface with Synology DSM API.""" - def __init__(self, host, port, username, password, temp_unit): + def __init__(self, host, port, username, password, temp_unit, use_ssl): """Initialize the API wrapper class.""" from SynologyDSM import SynologyDSM self.temp_unit = temp_unit try: - self._api = SynologyDSM(host, port, username, password) + self._api = SynologyDSM(host, port, username, password, + use_https=use_ssl) except: # noqa: E722 # pylint: disable=bare-except _LOGGER.error("Error setting up Synology DSM") @@ -185,6 +190,13 @@ def update(self): if self._api is not None: self._api.update() + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + class SynoNasUtilSensor(SynoNasSensor): """Representation a Synology Utilisation Sensor.""" diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index ff8ad7fe8496f5..737b3d08368922 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -147,7 +147,6 @@ def update(self): unit = TEMP_CELSIUS - # pylint: disable=R0912 if self.zone_variable == 'temperature': if 'sensorDataPoints' in data: sensor_data = data['sensorDataPoints'] diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py index 55d520cf6ca67f..c2ef1d4c6b90b3 100644 --- a/homeassistant/components/sensor/ted5000.py +++ b/homeassistant/components/sensor/ted5000.py @@ -32,7 +32,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Ted5000 sensor.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 42568a6b9ada4b..c75c40dd929ca3 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -123,7 +123,7 @@ def unique_id(self): async def _fetch_data(self): try: await self._tibber_home.update_info() - await self._tibber_home.update_price_info() + await self._tibber_home.update_price_info() except (asyncio.TimeoutError, aiohttp.ClientError): return data = self._tibber_home.info['viewer']['home'] diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index fc40d17d0afd29..0b059379c11805 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['WazeRouteCalculator==0.5'] +REQUIREMENTS = ['WazeRouteCalculator==0.6'] _LOGGER = logging.getLogger(__name__) @@ -40,6 +40,8 @@ SCAN_INTERVAL = timedelta(minutes=5) +TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ORIGIN): cv.string, vol.Required(CONF_DESTINATION): cv.string, @@ -49,8 +51,6 @@ vol.Optional(CONF_EXCL_FILTER): cv.string, }) -TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Waze travel time sensor platform.""" @@ -72,10 +72,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def _get_location_from_attributes(state): """Get the lat/long string from an states attributes.""" attr = state.attributes - return '{},{}'.format( - attr.get(ATTR_LATITUDE), - attr.get(ATTR_LONGITUDE) - ) + return '{},{}'.format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) class WazeTravelTime(Entity): @@ -186,13 +183,11 @@ def update(self): if self._origin_entity_id is not None: self._origin = self._get_location_from_entity( - self._origin_entity_id - ) + self._origin_entity_id) if self._destination_entity_id is not None: self._destination = self._get_location_from_entity( - self._destination_entity_id - ) + self._destination_entity_id) self._destination = self._resolve_zone(self._destination) self._origin = self._resolve_zone(self._origin) @@ -217,7 +212,8 @@ def update(self): self._state = { 'duration': duration, 'distance': distance, - 'route': route} + 'route': route, + } except WazeRouteCalculator.WRCError as exp: _LOGGER.error("Error on retrieving data: %s", exp) return diff --git a/homeassistant/components/sensor/wirelesstag.py b/homeassistant/components/sensor/wirelesstag.py index c93da3c791f107..ad2115e9bd30c5 100755 --- a/homeassistant/components/sensor/wirelesstag.py +++ b/homeassistant/components/sensor/wirelesstag.py @@ -168,7 +168,7 @@ def _update_tag_info_callback(self, event): new_value = event.data.get('cap') elif self._sensor_type == SENSOR_LIGHT: new_value = event.data.get('lux') - except Exception as error: # pylint: disable=W0703 + except Exception as error: # pylint: disable=broad-except _LOGGER.info("Unable to update value of entity: \ %s error: %s event: %s", self, error, event) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index a70d701fac639b..63d93d31cf37a4 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -30,7 +30,11 @@ ATTR_POWER = 'power' ATTR_CHARGING = 'charging' ATTR_BATTERY_LEVEL = 'battery_level' -ATTR_TIME_STATE = 'time_state' +ATTR_DISPLAY_CLOCK = 'display_clock' +ATTR_NIGHT_MODE = 'night_mode' +ATTR_NIGHT_TIME_BEGIN = 'night_time_begin' +ATTR_NIGHT_TIME_END = 'night_time_end' +ATTR_SENSOR_STATE = 'sensor_state' ATTR_MODEL = 'model' SUCCESS = ['ok'] @@ -85,7 +89,11 @@ def __init__(self, name, device, model, unique_id): ATTR_POWER: None, ATTR_BATTERY_LEVEL: None, ATTR_CHARGING: None, - ATTR_TIME_STATE: None, + ATTR_DISPLAY_CLOCK: None, + ATTR_NIGHT_MODE: None, + ATTR_NIGHT_TIME_BEGIN: None, + ATTR_NIGHT_TIME_END: None, + ATTR_SENSOR_STATE: None, ATTR_MODEL: self._model, } @@ -143,7 +151,11 @@ async def async_update(self): ATTR_POWER: state.power, ATTR_CHARGING: state.usb_power, ATTR_BATTERY_LEVEL: state.battery, - ATTR_TIME_STATE: state.time_state, + ATTR_DISPLAY_CLOCK: state.display_clock, + ATTR_NIGHT_MODE: state.night_mode, + ATTR_NIGHT_TIME_BEGIN: state.night_time_begin, + ATTR_NIGHT_TIME_END: state.night_time_end, + ATTR_SENSOR_STATE: state.sensor_state, }) except DeviceException as ex: diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 88c23771bd4c39..c7ff967723b68f 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -117,7 +117,7 @@ def state(self): return self._state @property - def should_poll(self): # pylint: disable=no-self-use + def should_poll(self): """No polling needed.""" return False diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index fe295d84d4991b..b2a913c2af8b0c 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -5,8 +5,6 @@ at https://home-assistant.io/components/sensor.zwave/ """ import logging -# Because we do not compile openzwave on CI -# pylint: disable=import-error from homeassistant.components.sensor import DOMAIN from homeassistant.components import zwave from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT diff --git a/homeassistant/components/sonos/.translations/cs.json b/homeassistant/components/sonos/.translations/cs.json new file mode 100644 index 00000000000000..c0b26284cdff39 --- /dev/null +++ b/homeassistant/components/sonos/.translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyly nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed Sonos.", + "single_instance_allowed": "Je t\u0159eba jen jedna konfigurace Sonos." + }, + "step": { + "confirm": { + "description": "Chcete nastavit Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/de.json b/homeassistant/components/sonos/.translations/de.json new file mode 100644 index 00000000000000..f1b76b0d155787 --- /dev/null +++ b/homeassistant/components/sonos/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie Sonos konfigurieren?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/hu.json b/homeassistant/components/sonos/.translations/hu.json new file mode 100644 index 00000000000000..4726d57ad249a7 --- /dev/null +++ b/homeassistant/components/sonos/.translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3k Sonos eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Sonos-t?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/it.json b/homeassistant/components/sonos/.translations/it.json new file mode 100644 index 00000000000000..e32557f1d95566 --- /dev/null +++ b/homeassistant/components/sonos/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Non sono presenti dispositivi Sonos in rete.", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Sonos." + }, + "step": { + "confirm": { + "description": "Vuoi installare Sonos", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/lb.json b/homeassistant/components/sonos/.translations/lb.json new file mode 100644 index 00000000000000..26eaec4584d4dc --- /dev/null +++ b/homeassistant/components/sonos/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Sonos Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Sonos ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll Sonos konfigur\u00e9iert ginn?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/nl.json b/homeassistant/components/sonos/.translations/nl.json new file mode 100644 index 00000000000000..de84482cc63c45 --- /dev/null +++ b/homeassistant/components/sonos/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen Sonos-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Sonos nodig." + }, + "step": { + "confirm": { + "description": "Wilt u Sonos instellen?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/sl.json b/homeassistant/components/sonos/.translations/sl.json new file mode 100644 index 00000000000000..6773465bbbfd22 --- /dev/null +++ b/homeassistant/components/sonos/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav Sonos.", + "single_instance_allowed": "Potrebna je samo ena konfiguracija Sonosa." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/zh-Hant.json b/homeassistant/components/sonos/.translations/zh-Hant.json new file mode 100644 index 00000000000000..c6fb13c3605d30 --- /dev/null +++ b/homeassistant/components/sonos/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Sonos\uff1f", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 9a35198628a649..b9ee8126ed3c29 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -95,7 +95,7 @@ def toggle(hass, entity_id=None): async def async_setup(hass, config): """Track states and offer events for switches.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES) await component.async_setup(config) @@ -132,10 +132,19 @@ async def async_handle_switch_service(service): 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 SwitchDevice(ToggleEntity): """Representation of a switch.""" - # pylint: disable=no-self-use @property def current_power_w(self): """Return the current power usage in W.""" diff --git a/homeassistant/components/switch/anel_pwrctrl.py b/homeassistant/components/switch/anel_pwrctrl.py index 30739676f17950..4e62b711979558 100644 --- a/homeassistant/components/switch/anel_pwrctrl.py +++ b/homeassistant/components/switch/anel_pwrctrl.py @@ -33,7 +33,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up PwrCtrl devices/switches.""" host = config.get(CONF_HOST, None) diff --git a/homeassistant/components/switch/digital_ocean.py b/homeassistant/components/switch/digital_ocean.py index 081eea80e2dcbd..12a6aabb170079 100644 --- a/homeassistant/components/switch/digital_ocean.py +++ b/homeassistant/components/switch/digital_ocean.py @@ -13,7 +13,8 @@ from homeassistant.components.digital_ocean import ( CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) +from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -69,6 +70,7 @@ def is_on(self): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index f57843cdaa0f7f..7df8f0e1aa620c 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -218,7 +218,6 @@ def flux_update(self, now=None): else: sunset_time = sunset - # pylint: disable=no-member night_length = int(stop_time.timestamp() - sunset_time.timestamp()) seconds_from_sunset = int(now.timestamp() - diff --git a/homeassistant/components/switch/fritzdect.py b/homeassistant/components/switch/fritzdect.py index 58ad745a2d2dd8..9968f631260d97 100644 --- a/homeassistant/components/switch/fritzdect.py +++ b/homeassistant/components/switch/fritzdect.py @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): fritz = FritzBox(host, username, password) try: fritz.login() - except Exception: # pylint: disable=W0703 + except Exception: # pylint: disable=broad-except _LOGGER.error("Login to Fritz!Box failed") return diff --git a/homeassistant/components/switch/gc100.py b/homeassistant/components/switch/gc100.py index 54c3b5e942aeae..34a29483d3cceb 100644 --- a/homeassistant/components/switch/gc100.py +++ b/homeassistant/components/switch/gc100.py @@ -39,7 +39,6 @@ class GC100Switch(ToggleEntity): def __init__(self, name, port_addr, gc100): """Initialize the GC100 switch.""" - # pylint: disable=no-member self._name = name or DEVICE_DEFAULT_NAME self._port_addr = port_addr self._gc100 = gc100 diff --git a/homeassistant/components/switch/homekit_controller.py b/homeassistant/components/switch/homekit_controller.py index 6b97200ba499a3..3293c8fe1953bb 100644 --- a/homeassistant/components/switch/homekit_controller.py +++ b/homeassistant/components/switch/homekit_controller.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.homekit_controller/ """ -import json import logging from homeassistant.components.homekit_controller import (HomeKitEntity, @@ -56,13 +55,11 @@ def turn_on(self, **kwargs): characteristics = [{'aid': self._aid, 'iid': self._chars['on'], 'value': True}] - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) def turn_off(self, **kwargs): """Turn the specified switch off.""" characteristics = [{'aid': self._aid, 'iid': self._chars['on'], 'value': False}] - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) diff --git a/homeassistant/components/switch/homematicip_cloud.py b/homeassistant/components/switch/homematicip_cloud.py index 9123d46c87b2ef..68884aaaa02963 100644 --- a/homeassistant/components/switch/homematicip_cloud.py +++ b/homeassistant/components/switch/homematicip_cloud.py @@ -9,8 +9,8 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) DEPENDENCIES = ['homematicip_cloud'] @@ -24,13 +24,16 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the HomematicIP switch devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP switch from a config entry.""" from homematicip.device import ( PlugableSwitch, PlugableSwitchMeasuring, BrandSwitchMeasuring) - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: if isinstance(device, BrandSwitchMeasuring): diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 3d29c53bd7cb08..2a7dee87747db7 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -5,12 +5,12 @@ https://home-assistant.io/components/switch.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.switch import SwitchDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, ISYDevice) -from homeassistant.helpers.typing import ConfigType # noqa +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/mfi.py b/homeassistant/components/switch/mfi.py index c0dc72440d3308..2c547fa210f1b2 100644 --- a/homeassistant/components/switch/mfi.py +++ b/homeassistant/components/switch/mfi.py @@ -39,7 +39,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up mFi sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index a91ca6d11e74f9..340eed83b567e8 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -65,7 +65,7 @@ async def async_send_ir_code_service(service): schema=SEND_IR_CODE_SERVICE_SCHEMA) -class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): +class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): """Representation of the value of a MySensors Switch child node.""" @property diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index 1d149383f6fad6..34dad9bb5818ad 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -10,12 +10,13 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] +SCAN_INTERVAL = timedelta(minutes=10) + SWITCH_TYPE_SCHEDULE = 'schedule' SWITCH_TYPES = { @@ -52,7 +53,6 @@ def __init__(self, hass, robot, switch_type): self._schedule_state = None self._clean_state = None - @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato switches.""" _LOGGER.debug("Running switch update") @@ -67,7 +67,7 @@ def update(self): _LOGGER.debug('self._state=%s', self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) - if self.robot.schedule_enabled: + if self._state['details']['isScheduleEnabled']: self._schedule_state = STATE_ON else: self._schedule_state = STATE_OFF diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index dc661c3e5bfb10..5f0ca995c90f20 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -4,227 +4,239 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.rachio/ """ +from abc import abstractmethod from datetime import timedelta import logging - import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, + KEY_DEVICE_ID, + KEY_ENABLED, + KEY_ID, + KEY_NAME, + KEY_ON, + KEY_SUBTYPE, + KEY_SUMMARY, + KEY_ZONE_ID, + KEY_ZONE_NUMBER, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + SIGNAL_RACHIO_ZONE_UPDATE, + SUBTYPE_ZONE_STARTED, + SUBTYPE_ZONE_STOPPED, + SUBTYPE_ZONE_COMPLETED, + SUBTYPE_SLEEP_MODE_ON, + SUBTYPE_SLEEP_MODE_OFF) import homeassistant.helpers.config_validation as cv -import homeassistant.util as util +from homeassistant.helpers.dispatcher import dispatcher_connect -REQUIREMENTS = ['rachiopy==0.1.2'] +DEPENDENCIES = ['rachio'] _LOGGER = logging.getLogger(__name__) +# Manual run length CONF_MANUAL_RUN_MINS = 'manual_run_mins' - -DATA_RACHIO = 'rachio' - DEFAULT_MANUAL_RUN_MINS = 10 -MIN_UPDATE_INTERVAL = timedelta(seconds=30) -MIN_FORCED_UPDATE_INTERVAL = timedelta(seconds=1) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Optional(CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS): cv.positive_int, }) +ATTR_ZONE_SUMMARY = 'Summary' +ATTR_ZONE_NUMBER = 'Zone number' + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Rachio switches.""" - from rachiopy import Rachio + manual_run_time = timedelta(minutes=config.get(CONF_MANUAL_RUN_MINS)) + _LOGGER.info("Rachio run time is %s", str(manual_run_time)) - # Get options - manual_run_mins = config.get(CONF_MANUAL_RUN_MINS) - _LOGGER.debug("Rachio run time is %d min", manual_run_mins) + # Add all zones from all controllers as switches + devices = [] + for controller in hass.data[DOMAIN_RACHIO].controllers: + devices.append(RachioStandbySwitch(hass, controller)) - access_token = config.get(CONF_ACCESS_TOKEN) + for zone in controller.list_zones(): + devices.append(RachioZone(hass, controller, zone, manual_run_time)) - # Configure API - _LOGGER.debug("Configuring Rachio API") - rachio = Rachio(access_token) + add_devices(devices) + _LOGGER.info("%d Rachio switch(es) added", len(devices)) - person = None - try: - person = _get_person(rachio) - except KeyError: - _LOGGER.error( - "Could not reach the Rachio API. Is your access token valid?") - return - # Get and persist devices - devices = _list_devices(rachio, manual_run_mins) - if not devices: - _LOGGER.error( - "No Rachio devices found in account %s", person['username']) - return +class RachioSwitch(SwitchDevice): + """Represent a Rachio state that can be toggled.""" - hass.data[DATA_RACHIO] = devices[0] + def __init__(self, controller, poll=True): + """Initialize a new Rachio switch.""" + self._controller = controller - if len(devices) > 1: - _LOGGER.warning("Multiple Rachio devices found in account, " - "using %s", hass.data[DATA_RACHIO].device_id) - else: - _LOGGER.debug("Found Rachio device") + if poll: + self._state = self._poll_update() + else: + self._state = None - hass.data[DATA_RACHIO].update() - add_devices(hass.data[DATA_RACHIO].list_zones()) + @property + def should_poll(self) -> bool: + """Declare that this entity pushes its state to HA.""" + return False + @property + def name(self) -> str: + """Get a name for this switch.""" + return "Switch on {}".format(self._controller.name) -def _get_person(rachio): - """Pull the account info of the person whose access token was provided.""" - person_id = rachio.person.getInfo()[1]['id'] - return rachio.person.get(person_id)[1] + @property + def is_on(self) -> bool: + """Return whether the switch is currently on.""" + return self._state + @abstractmethod + def _poll_update(self, data=None) -> bool: + """Poll the API.""" + pass -def _list_devices(rachio, manual_run_mins): - """Pull a list of devices on the account.""" - return [RachioIro(rachio, d['id'], manual_run_mins) - for d in _get_person(rachio)['devices']] + def _handle_any_update(self, *args, **kwargs) -> None: + """Determine whether an update event applies to this device.""" + if args[0][KEY_DEVICE_ID] != self._controller.controller_id: + # For another device + return + # For this device + self._handle_update(args, kwargs) -class RachioIro(object): - """Representation of a Rachio Iro.""" + @abstractmethod + def _handle_update(self, *args, **kwargs) -> None: + """Handle incoming webhook data.""" + pass - def __init__(self, rachio, device_id, manual_run_mins): - """Initialize a Rachio device.""" - self.rachio = rachio - self._device_id = device_id - self.manual_run_mins = manual_run_mins - self._device = None - self._running = None - self._zones = None - def __str__(self): - """Display the device as a string.""" - return "Rachio Iro {}".format(self.serial_number) +class RachioStandbySwitch(RachioSwitch): + """Representation of a standby status/button.""" - @property - def device_id(self): - """Return the Rachio API device ID.""" - return self._device['id'] + def __init__(self, hass, controller): + """Instantiate a new Rachio standby mode switch.""" + dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, + self._handle_any_update) + super().__init__(controller, poll=False) + self._poll_update(controller.init_data) @property - def status(self): - """Return the current status of the device.""" - return self._device['status'] + def name(self) -> str: + """Return the name of the standby switch.""" + return "{} in standby mode".format(self._controller.name) @property - def serial_number(self): - """Return the serial number of the device.""" - return self._device['serialNumber'] + def icon(self) -> str: + """Return an icon for the standby switch.""" + return "mdi:power" - @property - def is_paused(self): - """Return whether the device is temporarily disabled.""" - return self._device['paused'] + def _poll_update(self, data=None) -> bool: + """Request the state from the API.""" + if data is None: + data = self._controller.rachio.device.get( + self._controller.controller_id)[1] - @property - def is_on(self): - """Return whether the device is powered on and connected.""" - return self._device['on'] + return not data[KEY_ON] - @property - def current_schedule(self): - """Return the schedule that the device is running right now.""" - return self._running + def _handle_update(self, *args, **kwargs) -> None: + """Update the state using webhook data.""" + if args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: + self._state = True + elif args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: + self._state = False - def list_zones(self, include_disabled=False): - """Return a list of the zones connected to the device, incl. data.""" - if not self._zones: - self._zones = [RachioZone(self.rachio, self, zone['id'], - self.manual_run_mins) - for zone in self._device['zones']] + self.schedule_update_ha_state() - if include_disabled: - return self._zones + def turn_on(self, **kwargs) -> None: + """Put the controller in standby mode.""" + self._controller.rachio.device.off(self._controller.controller_id) - self.update(no_throttle=True) - return [z for z in self._zones if z.is_enabled] + def turn_off(self, **kwargs) -> None: + """Resume controller functionality.""" + self._controller.rachio.device.on(self._controller.controller_id) - @util.Throttle(MIN_UPDATE_INTERVAL, MIN_FORCED_UPDATE_INTERVAL) - def update(self, **kwargs): - """Pull updated device info from the Rachio API.""" - self._device = self.rachio.device.get(self._device_id)[1] - self._running = self.rachio.device\ - .getCurrentSchedule(self._device_id)[1] - # Possibly update all zones - for zone in self.list_zones(include_disabled=True): - zone.update() - - _LOGGER.debug("Updated %s", str(self)) - - -class RachioZone(SwitchDevice): +class RachioZone(RachioSwitch): """Representation of one zone of sprinklers connected to the Rachio Iro.""" - def __init__(self, rachio, device, zone_id, manual_run_mins): + def __init__(self, hass, controller, data, manual_run_time): """Initialize a new Rachio Zone.""" - self.rachio = rachio - self._device = device - self._zone_id = zone_id - self._zone = None - self._manual_run_secs = manual_run_mins * 60 + self._id = data[KEY_ID] + self._zone_name = data[KEY_NAME] + self._zone_number = data[KEY_ZONE_NUMBER] + self._zone_enabled = data[KEY_ENABLED] + self._manual_run_time = manual_run_time + self._summary = str() + super().__init__(controller) + + # Listen for all zone updates + dispatcher_connect(hass, SIGNAL_RACHIO_ZONE_UPDATE, + self._handle_update) def __str__(self): """Display the zone as a string.""" - return "Rachio Zone {}".format(self.name) + return 'Rachio Zone "{}" on {}'.format(self.name, + str(self._controller)) @property - def zone_id(self): + def zone_id(self) -> str: """How the Rachio API refers to the zone.""" - return self._zone['id'] + return self._id @property - def unique_id(self): - """Return the unique string ID for the zone.""" - return '{iro}-{zone}'.format( - iro=self._device.device_id, zone=self.zone_id) - - @property - def number(self): - """Return the physical connection of the zone pump.""" - return self._zone['zoneNumber'] + def name(self) -> str: + """Return the friendly name of the zone.""" + return self._zone_name @property - def name(self): - """Return the friendly name of the zone.""" - return self._zone['name'] + def icon(self) -> str: + """Return the icon to display.""" + return "mdi:water" @property - def is_enabled(self): + def zone_is_enabled(self) -> bool: """Return whether the zone is allowed to run.""" - return self._zone['enabled'] + return self._zone_enabled @property - def is_on(self): - """Return whether the zone is currently running.""" - schedule = self._device.current_schedule - return self.zone_id == schedule.get('zoneId') + def state_attributes(self) -> dict: + """Return the optional state attributes.""" + return { + ATTR_ZONE_NUMBER: self._zone_number, + ATTR_ZONE_SUMMARY: self._summary, + } + + def turn_on(self, **kwargs) -> None: + """Start watering this zone.""" + # Stop other zones first + self.turn_off() - def update(self): - """Pull updated zone info from the Rachio API.""" - self._zone = self.rachio.zone.get(self._zone_id)[1] + # Start this zone + self._controller.rachio.zone.start(self.zone_id, + self._manual_run_time.seconds) + _LOGGER.debug("Watering %s on %s", self.name, self._controller.name) - # Possibly update device - self._device.update() + def turn_off(self, **kwargs) -> None: + """Stop watering all zones.""" + self._controller.stop_watering() - _LOGGER.debug("Updated %s", str(self)) + def _poll_update(self, data=None) -> bool: + """Poll the API to check whether the zone is running.""" + schedule = self._controller.current_schedule + return self.zone_id == schedule.get(KEY_ZONE_ID) - def turn_on(self, **kwargs): - """Start the zone.""" - # Stop other zones first - self.turn_off() + def _handle_update(self, *args, **kwargs) -> None: + """Handle incoming webhook zone data.""" + if args[0][KEY_ZONE_ID] != self.zone_id: + return + + self._summary = kwargs.get(KEY_SUMMARY, str()) - _LOGGER.info("Watering %s for %d s", self.name, self._manual_run_secs) - self.rachio.zone.start(self.zone_id, self._manual_run_secs) + if args[0][KEY_SUBTYPE] == SUBTYPE_ZONE_STARTED: + self._state = True + elif args[0][KEY_SUBTYPE] in [SUBTYPE_ZONE_STOPPED, + SUBTYPE_ZONE_COMPLETED]: + self._state = False - def turn_off(self, **kwargs): - """Stop all zones.""" - _LOGGER.info("Stopping watering of all zones") - self.rachio.device.stopWater(self._device.device_id) + self.schedule_update_ha_state() diff --git a/homeassistant/components/switch/rpi_rf.py b/homeassistant/components/switch/rpi_rf.py index 62c92ad2d968c6..03f11de21f708c 100644 --- a/homeassistant/components/switch/rpi_rf.py +++ b/homeassistant/components/switch/rpi_rf.py @@ -44,7 +44,7 @@ }) -# pylint: disable=import-error, no-member +# pylint: disable=no-member def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return switches controlled by a generic RF device via GPIO.""" import rpi_rf diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 569566bcbfb6f4..c18ad492d40ae3 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -33,7 +33,6 @@ WEMO_STANDBY = 8 -# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up discovered WeMo switches.""" import pywemo.discovery as discovery diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/switch/zwave.py index 3b82d87d7e75a3..8a0a1683aa41aa 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/switch/zwave.py @@ -6,8 +6,6 @@ """ import logging import time -# Because we do not compile openzwave on CI -# pylint: disable=import-error from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.components import zwave from homeassistant.components.zwave import workaround, async_setup_platform # noqa # pylint: disable=unused-import diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 84edd9afd40990..ba91dd7c1fc27d 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -40,6 +40,8 @@ 'rts:CurtainRTSComponent': 'cover', 'rts:BlindRTSComponent': 'cover', 'rts:VenetianBlindRTSComponent': 'cover', + 'rts:DualCurtainRTSComponent': 'cover', + 'rts:ExteriorVenetianBlindRTSComponent': 'cover', 'io:ExteriorVenetianBlindIOComponent': 'cover', 'io:RollerShutterUnoIOComponent': 'cover', 'io:RollerShutterWithLowSpeedManagementIOComponent': 'cover', diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py index bd501167ffa8d7..45fd8de269612e 100644 --- a/homeassistant/components/vacuum/demo.py +++ b/homeassistant/components/vacuum/demo.py @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoVacuum(VacuumDevice): """Representation of a demo vacuum.""" - # pylint: disable=no-self-use def __init__(self, name, supported_features): """Initialize the vacuum.""" self._name = name diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index ef3bb0f636b7ef..8c2f110257f7b0 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -210,7 +210,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttVacuum(MqttAvailability, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" - # pylint: disable=no-self-use def __init__( self, name, supported_features, qos, retain, command_topic, payload_turn_on, payload_turn_off, payload_return_to_base, diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 128bece8494274..6289fed265d065 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -15,12 +15,13 @@ SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON) from homeassistant.components.neato import ( NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] +SCAN_INTERVAL = timedelta(minutes=5) + SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ SUPPORT_STATUS | SUPPORT_MAP @@ -63,7 +64,6 @@ def __init__(self, hass, robot): self.clean_suspension_charge_count = None self.clean_suspension_time = None - @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update") @@ -96,10 +96,14 @@ def update(self): elif self._state['state'] == 4: self._status_state = ERRORS.get(self._state['error']) - if (self.robot.state['action'] == 1 or - self.robot.state['action'] == 2 or - self.robot.state['action'] == 3 and - self.robot.state['state'] == 2): + if (self._state['action'] == 1 or + self._state['action'] == 2 or + self._state['action'] == 3 and + self._state['state'] == 2): + self._clean_state = STATE_ON + elif (self._state['action'] == 11 or + self._state['action'] == 12 and + self._state['state'] == 2): self._clean_state = STATE_ON else: self._clean_state = STATE_OFF diff --git a/homeassistant/components/vacuum/roomba.py b/homeassistant/components/vacuum/roomba.py index 44d22e03f41624..750c2c0ae0abad 100644 --- a/homeassistant/components/vacuum/roomba.py +++ b/homeassistant/components/vacuum/roomba.py @@ -284,7 +284,9 @@ def async_update(self): software_version = state.get('softwareVer') # Error message in plain english - error_msg = self.vacuum.error_message + error_msg = 'None' + if hasattr(self.vacuum, 'error_message'): + error_msg = self.vacuum.error_message self._battery_level = state.get('batPct') self._status = self.vacuum.current_state diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index cbbf279bb8c8d8..0ab5e7ce39aafe 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -53,7 +53,6 @@ ] -# pylint: disable=too-many-function-args def setup(hass, base_config): """Set up for Vera devices.""" import pyvera as veraApi diff --git a/homeassistant/components/watson_iot.py b/homeassistant/components/watson_iot.py index 246cf3a96c28ea..889984eb223bf8 100644 --- a/homeassistant/components/watson_iot.py +++ b/homeassistant/components/watson_iot.py @@ -4,7 +4,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/watson_iot/ """ - import logging import queue import threading @@ -13,8 +12,8 @@ import voluptuous as vol from homeassistant.const import ( - CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, - CONF_TOKEN, CONF_TYPE, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, + CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_ID, CONF_INCLUDE, + CONF_TOKEN, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN) from homeassistant.helpers import state as state_helper import homeassistant.helpers.config_validation as cv @@ -24,13 +23,13 @@ _LOGGER = logging.getLogger(__name__) CONF_ORG = 'organization' -CONF_ID = 'id' DOMAIN = 'watson_iot' -RETRY_DELAY = 20 MAX_TRIES = 3 +RETRY_DELAY = 20 + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(vol.Schema({ vol.Required(CONF_ORG): cv.string, @@ -103,7 +102,7 @@ def event_to_json(event): }, 'time': event.time_fired.isoformat(), 'fields': { - 'state': state.state + 'state': state.state, } } if _state_as_value is not None: @@ -113,7 +112,7 @@ def event_to_json(event): if key != 'unit_of_measurement': # If the key is already in fields if key in out_event['fields']: - key = key + "_" + key = '{}_'.format(key) # For each value we try to cast it as float # But if we can not do it we store the value # as string @@ -153,7 +152,7 @@ def __init__(self, hass, gateway, event_to_json): hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) def _event_listener(self, event): - """Listen for new messages on the bus and queue them for Watson IOT.""" + """Listen for new messages on the bus and queue them for Watson IoT.""" item = (time.monotonic(), event) self.queue.put(item) @@ -191,7 +190,7 @@ def write_to_watson(self, events): field, 'json', value) if not device_success: _LOGGER.error( - "Failed to publish message to watson iot") + "Failed to publish message to Watson IoT") continue break except (ibmiotf.MissingMessageEncoderException, IOError): @@ -199,7 +198,7 @@ def write_to_watson(self, events): time.sleep(RETRY_DELAY) else: _LOGGER.exception( - "Failed to publish message to watson iot") + "Failed to publish message to Watson IoT") def run(self): """Process incoming events.""" diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index c36c960c4fcf4f..a43999f2276255 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -46,7 +46,6 @@ def async_setup(hass, config): return True -# pylint: disable=no-member, no-self-use class WeatherEntity(Entity): """ABC for weather data.""" diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index f0712542ea544b..7afa97fd4f61db 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -12,7 +12,8 @@ import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_CONDITION, + PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -25,6 +26,22 @@ ATTRIBUTION = "Powered by Dark Sky" +MAP_CONDITION = { + 'clear-day': 'sunny', + 'clear-night': 'clear-night', + 'rain': 'rainy', + 'snow': 'snowy', + 'sleet': 'snowy-rainy', + 'wind': 'windy', + 'fog': 'fog', + 'cloudy': 'cloudy', + 'partly-cloudy-day': 'partlycloudy', + 'partly-cloudy-night': 'partlycloudy', + 'hail': 'hail', + 'thunderstorm': 'lightning', + 'tornado': None, +} + CONF_UNITS = 'units' DEFAULT_NAME = 'Dark Sky' @@ -108,7 +125,7 @@ def pressure(self): @property def condition(self): """Return the weather condition.""" - return self._ds_currently.get('summary') + return MAP_CONDITION.get(self._ds_currently.get('icon')) @property def forecast(self): @@ -116,8 +133,11 @@ def forecast(self): return [{ ATTR_FORECAST_TIME: datetime.fromtimestamp(entry.d.get('time')).isoformat(), - ATTR_FORECAST_TEMP: entry.d.get('temperature')} - for entry in self._ds_hourly.data] + ATTR_FORECAST_TEMP: + entry.d.get('temperature'), + ATTR_FORECAST_CONDITION: + MAP_CONDITION.get(entry.d.get('icon')) + } for entry in self._ds_hourly.data] def update(self): """Get the latest data from Dark Sky.""" diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 8354757ff33eed..65fa7c8cb0f9e8 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -156,6 +156,8 @@ def forecast(self): entry.get_temperature('celsius').get('day'), ATTR_FORECAST_TEMP_LOW: entry.get_temperature('celsius').get('night'), + ATTR_FORECAST_PRECIPITATION: + entry.get_rain().get('all'), ATTR_FORECAST_WIND_SPEED: entry.get_wind().get('speed'), ATTR_FORECAST_WIND_BEARING: @@ -223,12 +225,10 @@ def update_forecast(self): try: if self._mode == 'daily': fcd = self.owm.daily_forecast_at_coords( - self.latitude, self.longitude, 15 - ) + self.latitude, self.longitude, 15) else: fcd = self.owm.three_hours_forecast_at_coords( - self.latitude, self.longitude - ) + self.latitude, self.longitude) except APICallError: _LOGGER.error("Exception when calling OWM web API " "to update forecast") diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index e16e5524f95456..c26f68a2c29f0f 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -228,7 +228,6 @@ class WebsocketAPIView(HomeAssistantView): async def get(self, request): """Handle an incoming websocket connection.""" - # pylint: disable=no-self-use return await ActiveConnection(request.app['hass'], request).handle() @@ -316,25 +315,32 @@ def handle_hass_stop(event): authenticated = True else: + self.debug("Request auth") await self.wsock.send_json(auth_required_message()) msg = await wsock.receive_json() msg = AUTH_MESSAGE_SCHEMA(msg) - if 'api_password' in msg: + if self.hass.auth.active and 'access_token' in msg: + self.debug("Received access_token") + token = self.hass.auth.async_get_access_token( + msg['access_token']) + authenticated = token is not None + + elif ((not self.hass.auth.active or + self.hass.auth.support_legacy) and + 'api_password' in msg): + self.debug("Received api_password") authenticated = validate_password( request, msg['api_password']) - elif 'access_token' in msg: - authenticated = \ - msg['access_token'] in self.hass.auth.access_tokens - if not authenticated: - self.debug("Invalid password") + self.debug("Authorization failed") await self.wsock.send_json( - auth_invalid_message('Invalid password')) + auth_invalid_message('Invalid access token or password')) await process_wrong_login(request) return wsock + self.debug("Auth OK") await self.wsock.send_json(auth_ok_message()) # ---------- AUTH PHASE OVER ---------- @@ -392,7 +398,7 @@ def handle_hass_stop(event): if wsock.closed: self.debug("Connection closed by client") else: - _LOGGER.exception("Unexpected TypeError: %s", msg) + _LOGGER.exception("Unexpected TypeError: %s", err) except ValueError as err: msg = "Received invalid JSON" @@ -403,7 +409,7 @@ def handle_hass_stop(event): self._writer_task.cancel() except CANCELLATION_ERRORS: - self.debug("Connection cancelled by server") + self.debug("Connection cancelled") except asyncio.QueueFull: self.log_error("Client exceeded max pending messages [1]:", diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index d38a42e2cbf891..e8c7db5efe106d 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -44,7 +44,6 @@ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-function-args def setup(hass, config): """Set up for WeMo devices.""" import pywemo diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 7016250c6b184d..7c171d74967234 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['python-wink==1.9.0', 'pubnubsub-handler==1.0.2'] +REQUIREMENTS = ['python-wink==1.9.1', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wirelesstag.py b/homeassistant/components/wirelesstag.py index 9fabcb1cd5aefb..0f8f47f5100c6d 100644 --- a/homeassistant/components/wirelesstag.py +++ b/homeassistant/components/wirelesstag.py @@ -146,7 +146,7 @@ def handle_binary_event(self, event): self.hass, SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type), event) - except Exception as ex: # pylint: disable=W0703 + except Exception as ex: # pylint: disable=broad-except _LOGGER.error("Unable to handle binary event:\ %s error: %s", str(event), str(ex)) diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index 86531401774198..471c1c6e82cad4 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -67,7 +67,6 @@ def setup(hass, config): return login() -# pylint: disable=no-member def login(): """Login to the ZoneMinder API.""" _LOGGER.debug("Attempting to login to ZoneMinder") @@ -118,13 +117,11 @@ def _zm_request(method, api_url, data=None): 'decode "%s"', req.text) -# pylint: disable=no-member def get_state(api_url): """Get a state from the ZoneMinder API service.""" return _zm_request('get', api_url) -# pylint: disable=no-member def change_state(api_url, post_data): """Update a state using the Zoneminder API.""" return _zm_request('post', api_url, data=post_data) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index a8ba5e4a6d3242..e540259edd5544 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -218,7 +218,6 @@ async def async_setup_platform(hass, config, async_add_devices, return True -# pylint: disable=R0914 async def async_setup(hass, config): """Set up Z-Wave. diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 3e503e4d9a4d7e..0228e64cf6ef41 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -345,7 +345,6 @@ DISC_TYPE = "type" DISC_VALUES = "values" -# noqa # https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L49 # See also: # https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L275 diff --git a/homeassistant/config.py b/homeassistant/config.py index 2906f07a307c0f..52ff0e19c598b2 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,7 +7,7 @@ import re import shutil # pylint: disable=unused-import -from typing import Any, List, Tuple, Optional # NOQA +from typing import Any, Tuple, Optional # noqa: F401 import voluptuous as vol from voluptuous.humanize import humanize_error diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index db2912d7b42297..2e5613057f148f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -112,15 +112,13 @@ async def async_step_discovery(info): """ import logging -import os import uuid -from . import data_entry_flow -from .core import callback -from .exceptions import HomeAssistantError -from .setup import async_setup_component, async_process_deps_reqs -from .util.json import load_json, save_json -from .util.decorator import Registry +from homeassistant import data_entry_flow +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component, async_process_deps_reqs +from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) @@ -129,6 +127,7 @@ async def async_step_discovery(info): FLOWS = [ 'cast', 'deconz', + 'homematicip_cloud', 'hue', 'nest', 'sonos', @@ -136,6 +135,10 @@ async def async_step_discovery(info): ] +STORAGE_KEY = 'core.config_entries' +STORAGE_VERSION = 1 + +# Deprecated since 0.73 PATH_CONFIG = '.config_entries.json' SAVE_DELAY = 1 @@ -271,7 +274,7 @@ def __init__(self, hass, hass_config): hass, self._async_create_flow, self._async_finish_flow) self._hass_config = hass_config self._entries = None - self._sched_save = None + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback def async_domains(self): @@ -305,7 +308,7 @@ async def async_remove(self, entry_id): raise UnknownEntry entry = self._entries.pop(found) - self._async_schedule_save() + await self._async_schedule_save() unloaded = await entry.async_unload(self.hass) @@ -314,14 +317,18 @@ async def async_remove(self, entry_id): } async def async_load(self): - """Load the config.""" - path = self.hass.config.path(PATH_CONFIG) - if not os.path.isfile(path): + """Handle loading the config.""" + # Migrating for config entries stored before 0.73 + config = await self.hass.helpers.storage.async_migrator( + self.hass.config.path(PATH_CONFIG), self._store, + old_conf_migrate_func=_old_conf_migrator + ) + + if config is None: self._entries = [] return - entries = await self.hass.async_add_job(load_json, path) - self._entries = [ConfigEntry(**entry) for entry in entries] + self._entries = [ConfigEntry(**entry) for entry in config['entries']] async def async_forward_entry_setup(self, entry, component): """Forward the setup of an entry to a different component. @@ -372,7 +379,7 @@ async def _async_finish_flow(self, result): source=result['source'], ) self._entries.append(entry) - self._async_schedule_save() + await self._async_schedule_save() # Setup entry if entry.domain in self.hass.config.components: @@ -416,20 +423,14 @@ async def _async_create_flow(self, handler, *, source, data): return handler() - @callback - def _async_schedule_save(self): - """Schedule saving the entity registry.""" - if self._sched_save is not None: - self._sched_save.cancel() - - self._sched_save = self.hass.loop.call_later( - SAVE_DELAY, self.hass.async_add_job, self._async_save - ) - - async def _async_save(self): + async def _async_schedule_save(self): """Save the entity registry to a file.""" - self._sched_save = None - data = [entry.as_dict() for entry in self._entries] + data = { + 'entries': [entry.as_dict() for entry in self._entries] + } + await self._store.async_save(data, delay=SAVE_DELAY) + - await self.hass.async_add_job( - save_json, self.hass.config.path(PATH_CONFIG), data) +async def _old_conf_migrator(old_config): + """Migrate the pre-0.73 config format to the latest version.""" + return {'entries': old_config} diff --git a/homeassistant/core.py b/homeassistant/core.py index 5e6dcd81310b0d..e09501729139bb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -230,6 +230,20 @@ def async_add_job( return task + @callback + def async_add_executor_job( + self, + target: Callable[..., Any], + *args: Any) -> asyncio.tasks.Task: + """Add an executor job from within the event loop.""" + task = self.loop.run_in_executor(None, target, *args) + + # If a task is scheduled + if self._track_task: + self._pending_tasks.append(task) + + return task + @callback def async_track_tasks(self): """Track tasks so you can wait for all tasks to be done.""" diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 91ec50515524bf..54cd569acebc53 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -6,7 +6,7 @@ from homeassistant.const import CONF_PLATFORM # Typing Imports and TypeAlias -# pylint: disable=using-constant-test,unused-import,wrong-import-order +# pylint: disable=using-constant-test,unused-import if False: from logging import Logger # NOQA @@ -14,7 +14,6 @@ ConfigType = Dict[str, Any] -# pylint: disable=invalid-sequence-index def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, Any]]: """Break a component config into different platforms. diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index bb34942ad795dd..5ee2cd560819d8 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -128,7 +128,6 @@ async def async_aiohttp_proxy_stream(hass, request, stream, content_type, @callback -# pylint: disable=invalid-name def _async_register_clientsession_shutdown(hass, clientsession): """Register ClientSession close on Home Assistant shutdown. diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 85050b5736f4ec..7dc5d2524eccbc 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -59,7 +59,6 @@ def async_generate_entity_id(entity_id_format: str, name: Optional[str], class Entity(object): """An abstract class for Home Assistant entities.""" - # pylint: disable=no-self-use # SAFE TO OVERWRITE # The properties and methods here are safe to overwrite when inheriting # this class. These may be used to customize the behavior of the entity. @@ -365,7 +364,6 @@ def __repr__(self): class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" - # pylint: disable=no-self-use @property def state(self) -> str: """Return the state.""" diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4a2cd5fa50c31d..04d9cc450ba0da 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -37,8 +37,6 @@ class RegistryEntry: """Entity Registry Entry.""" - # pylint: disable=no-member - entity_id = attr.ib(type=str) unique_id = attr.ib(type=str) platform = attr.ib(type=str) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d69a556b0cc37c..712b48da0d76df 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -133,7 +133,6 @@ def clear_listener(): """Clear all unsub listener.""" nonlocal async_remove_state_for_cancel, async_remove_state_for_listener - # pylint: disable=not-callable if async_remove_state_for_listener is not None: async_remove_state_for_listener() async_remove_state_for_listener = None diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 5aa53f17e7bba0..4357c4109ebbf1 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -137,7 +137,8 @@ def async_validate_slots(self, slots): if self._slot_schema is None: self._slot_schema = vol.Schema({ key: SLOT_SCHEMA.extend({'value': validator}) - for key, validator in self.slot_schema.items()}) + for key, validator in self.slot_schema.items()}, + extra=vol.ALLOW_EXTRA) return self._slot_schema(slots) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 9114a4db941cfc..7ab90b7a048910 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1,7 +1,5 @@ """Service calling related helpers.""" import logging -# pylint: disable=unused-import -from typing import Optional # NOQA from os import path import voluptuous as vol diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index f97d70514591c7..72deabaae2844d 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -27,14 +27,15 @@ ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME, ATTR_RESUME_ALL, SERVICE_RESUME_PROGRAM) from homeassistant.components.cover import ( - ATTR_POSITION) + ATTR_POSITION, ATTR_TILT_POSITION) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_OPTION, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, - SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, STATE_ALARM_ARMED_AWAY, + SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_HOME, STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, @@ -68,7 +69,8 @@ SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], SERVICE_SEND_IR_CODE: [ATTR_IR_CODE], SERVICE_SELECT_OPTION: [ATTR_OPTION], - SERVICE_SET_COVER_POSITION: [ATTR_POSITION] + SERVICE_SET_COVER_POSITION: [ATTR_POSITION], + SERVICE_SET_COVER_TILT_POSITION: [ATTR_TILT_POSITION] } # Update this dict when new services are added to HA. diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py new file mode 100644 index 00000000000000..962074ec3affd0 --- /dev/null +++ b/homeassistant/helpers/storage.py @@ -0,0 +1,174 @@ +"""Helper to help store data.""" +import asyncio +import logging +import os +from typing import Dict, Optional + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.loader import bind_hass +from homeassistant.util import json +from homeassistant.helpers.event import async_call_later + +STORAGE_DIR = '.storage' +_LOGGER = logging.getLogger(__name__) + + +@bind_hass +async def async_migrator(hass, old_path, store, *, old_conf_migrate_func=None): + """Helper function to migrate old data to a store and then load data. + + async def old_conf_migrate_func(old_data) + """ + def load_old_config(): + """Helper to load old config.""" + if not os.path.isfile(old_path): + return None + + return json.load_json(old_path) + + config = await hass.async_add_executor_job(load_old_config) + + if config is None: + return await store.async_load() + + if old_conf_migrate_func is not None: + config = await old_conf_migrate_func(config) + + await store.async_save(config) + await hass.async_add_executor_job(os.remove, old_path) + return config + + +@bind_hass +class Store: + """Class to help storing data.""" + + def __init__(self, hass, version: int, key: str): + """Initialize storage class.""" + self.version = version + self.key = key + self.hass = hass + self._data = None + self._unsub_delay_listener = None + self._unsub_stop_listener = None + self._write_lock = asyncio.Lock() + self._load_task = None + + @property + def path(self): + """Return the config path.""" + return self.hass.config.path(STORAGE_DIR, self.key) + + async def async_load(self): + """Load data. + + If the expected version does not match the given version, the migrate + function will be invoked with await migrate_func(version, config). + + Will ensure that when a call comes in while another one is in progress, + the second call will wait and return the result of the first call. + """ + if self._load_task is None: + self._load_task = self.hass.async_add_job(self._async_load()) + + return await self._load_task + + async def _async_load(self): + """Helper to load the data.""" + if self._data is not None: + data = self._data + else: + data = await self.hass.async_add_executor_job( + json.load_json, self.path, None) + + if data is None: + return None + + if data['version'] == self.version: + stored = data['data'] + else: + _LOGGER.info('Migrating %s storage from %s to %s', + self.key, data['version'], self.version) + stored = await self._async_migrate_func( + data['version'], data['data']) + + self._load_task = None + return stored + + async def async_save(self, data: Dict, *, delay: Optional[int] = None): + """Save data with an optional delay.""" + self._data = { + 'version': self.version, + 'key': self.key, + 'data': data, + } + + self._async_cleanup_delay_listener() + + if delay is None: + self._async_cleanup_stop_listener() + await self._async_handle_write_data() + return + + self._unsub_delay_listener = async_call_later( + self.hass, delay, self._async_callback_delayed_write) + + self._async_ensure_stop_listener() + + @callback + def _async_ensure_stop_listener(self): + """Ensure that we write if we quit before delay has passed.""" + if self._unsub_stop_listener is None: + self._unsub_stop_listener = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_callback_stop_write) + + @callback + def _async_cleanup_stop_listener(self): + """Clean up a stop listener.""" + if self._unsub_stop_listener is not None: + self._unsub_stop_listener() + self._unsub_stop_listener = None + + @callback + def _async_cleanup_delay_listener(self): + """Clean up a delay listener.""" + if self._unsub_delay_listener is not None: + self._unsub_delay_listener() + self._unsub_delay_listener = None + + async def _async_callback_delayed_write(self, _now): + """Handle a delayed write callback.""" + self._unsub_delay_listener = None + self._async_cleanup_stop_listener() + await self._async_handle_write_data() + + async def _async_callback_stop_write(self, _event): + """Handle a write because Home Assistant is stopping.""" + self._unsub_stop_listener = None + self._async_cleanup_delay_listener() + await self._async_handle_write_data() + + async def _async_handle_write_data(self, *_args): + """Handler to handle writing the config.""" + data = self._data + self._data = None + + async with self._write_lock: + try: + await self.hass.async_add_executor_job( + self._write_data, self.path, data) + except (json.SerializationError, json.WriteError) as err: + _LOGGER.error('Error writing config for %s: %s', self.key, err) + + def _write_data(self, path: str, data: Dict): + """Write the data.""" + if not os.path.isdir(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + + _LOGGER.debug('Writing data for %s', self.key) + json.save_json(path, data) + + async def _async_migrate_func(self, old_version, old_data): + """Migrate to the new version.""" + raise NotImplementedError diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index f1335f733466e5..81ec046f2e9966 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -1,7 +1,5 @@ """Translation string lookup helpers.""" import logging -# pylint: disable=unused-import -from typing import Optional # NOQA from os import path from homeassistant import config_entries diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ce93c8705b5984..9e5efffdccbab1 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -16,17 +16,11 @@ import sys from types import ModuleType -# pylint: disable=unused-import -from typing import Dict, List, Optional, Sequence, Set # NOQA +from typing import Optional, Set from homeassistant.const import PLATFORM_FORMAT from homeassistant.util import OrderedSet -# Typing imports -# pylint: disable=using-constant-test,unused-import -if False: - from homeassistant.core import HomeAssistant # NOQA - PREPARED = False DEPENDENCY_BLACKLIST = set(('config',)) @@ -81,7 +75,7 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: potential_paths = ['custom_components.{}'.format(comp_or_platform), 'homeassistant.components.{}'.format(comp_or_platform)] - for path in potential_paths: + for index, path in enumerate(potential_paths): try: module = importlib.import_module(path) @@ -100,6 +94,14 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: cache[comp_or_platform] = module + if index == 0: + _LOGGER.warning( + 'You are using a custom component for %s which has not ' + 'been tested by Home Assistant. This component might ' + 'cause stability problems, be sure to disable it if you ' + 'do experience issues with Home Assistant.', + comp_or_platform) + return module except ImportError as err: diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 5a33bd58641a8b..b3e5f417618497 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -31,7 +31,6 @@ class APIStatus(enum.Enum): """Representation of an API status.""" - # pylint: disable=no-init, invalid-name OK = "ok" INVALID_PASSWORD = "invalid_password" CANNOT_CONNECT = "cannot_connect" diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py index b4f1ddd2f11be7..dacdc7b18e24ed 100644 --- a/homeassistant/scripts/auth.py +++ b/homeassistant/scripts/auth.py @@ -1,7 +1,9 @@ """Script to manage users for the Home Assistant auth provider.""" import argparse +import asyncio import os +from homeassistant.core import HomeAssistant from homeassistant.config import get_default_config_dir from homeassistant.auth_providers import homeassistant as hass_auth @@ -17,7 +19,8 @@ def run(args): default=get_default_config_dir(), help="Directory that contains the Home Assistant configuration") - subparsers = parser.add_subparsers() + subparsers = parser.add_subparsers(dest='func') + subparsers.required = True parser_list = subparsers.add_parser('list') parser_list.set_defaults(func=list_users) @@ -37,11 +40,15 @@ def run(args): parser_change_pw.set_defaults(func=change_password) args = parser.parse_args(args) - path = os.path.join(os.getcwd(), args.config, hass_auth.PATH_DATA) - args.func(hass_auth.load_data(path), args) + loop = asyncio.get_event_loop() + hass = HomeAssistant(loop=loop) + hass.config.config_dir = os.path.join(os.getcwd(), args.config) + data = hass_auth.Data(hass) + loop.run_until_complete(data.async_load()) + loop.run_until_complete(args.func(data, args)) -def list_users(data, args): +async def list_users(data, args): """List the users.""" count = 0 for user in data.users: @@ -52,14 +59,14 @@ def list_users(data, args): print("Total users:", count) -def add_user(data, args): +async def add_user(data, args): """Create a user.""" data.add_user(args.username, args.password) - data.save() + await data.async_save() print("User created") -def validate_login(data, args): +async def validate_login(data, args): """Validate a login.""" try: data.validate_login(args.username, args.password) @@ -68,11 +75,11 @@ def validate_login(data, args): print("Auth invalid") -def change_password(data, args): +async def change_password(data, args): """Change password.""" try: data.change_password(args.username, args.new_password) - data.save() + await data.async_save() print("Password changed") except hass_auth.InvalidUser: print("User not found") diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 3a1ffa82d47e88..69b1bf21c088fc 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -267,7 +267,7 @@ def sort_dict_key(val): print(' ', indent_str, i) -CheckConfigError = namedtuple( # pylint: disable=invalid-name +CheckConfigError = namedtuple( 'CheckConfigError', "message domain config") @@ -378,7 +378,6 @@ def _comp_error(ex, domain, config): # Validate platform specific schema if hasattr(platform, 'PLATFORM_SCHEMA'): - # pylint: disable=no-member try: p_validated = platform.PLATFORM_SCHEMA(p_validated) except vol.Invalid as ex: diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index e02305b5fbbec5..0ca60894f9b983 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==12.2.1', 'keyrings.alt==3.1'] +REQUIREMENTS = ['keyring==13.1.0', 'keyrings.alt==3.1'] def run(args): diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index a8a84c6c880730..bbf0f7e11e23d8 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -120,7 +120,6 @@ def get_random_string(length=10): class OrderedEnum(enum.Enum): """Taken from Python 3.4.0 docs.""" - # pylint: disable=no-init def __ge__(self, other): """Return the greater than element.""" if self.__class__ is other.__class__: diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 5676a1d08440a9..b3aa370da2ede8 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -107,7 +107,6 @@ def run_coroutine_threadsafe(coro, loop): def callback(): """Handle the call to the coroutine.""" try: - # pylint: disable=deprecated-method _chain_future(ensure_future(coro, loop=loop), future) # pylint: disable=broad-except except Exception as exc: @@ -136,7 +135,6 @@ def fire_coroutine_threadsafe(coro, loop): def callback(): """Handle the firing of a coroutine.""" - # pylint: disable=deprecated-method ensure_future(coro, loop=loop) loop.call_soon_threadsafe(callback) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 32e9df70a03e1c..d2138f4293c59c 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -173,7 +173,7 @@ def color_name_to_rgb(color_name): return hex_value -# pylint: disable=invalid-name, invalid-sequence-index +# pylint: disable=invalid-name def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: """Convert from RGB color to XY color.""" return color_RGB_to_xy_brightness(iR, iG, iB)[:2] @@ -182,7 +182,7 @@ def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: # Taken from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # License: Code is given as is. Use at your own risk and discretion. -# pylint: disable=invalid-name, invalid-sequence-index +# pylint: disable=invalid-name def color_RGB_to_xy_brightness( iR: int, iG: int, iB: int) -> Tuple[float, float, int]: """Convert from RGB color to XY color.""" @@ -224,7 +224,6 @@ def color_xy_to_RGB(vX: float, vY: float) -> Tuple[int, int, int]: # Converted to Python from Obj-C, original source from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy -# pylint: disable=invalid-sequence-index def color_xy_brightness_to_RGB(vX: float, vY: float, ibrightness: int) -> Tuple[int, int, int]: """Convert from XYZ to RGB.""" @@ -265,7 +264,6 @@ def color_xy_brightness_to_RGB(vX: float, vY: float, return (ir, ig, ib) -# pylint: disable=invalid-sequence-index def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]: """Convert a hsb into its rgb representation.""" if fS == 0: @@ -307,7 +305,6 @@ def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]: return (r, g, b) -# pylint: disable=invalid-sequence-index def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: """Convert an rgb color to its hsv representation. @@ -319,13 +316,11 @@ def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: return round(fHSV[0]*360, 3), round(fHSV[1]*100, 3), round(fHSV[2]*100, 3) -# pylint: disable=invalid-sequence-index def color_RGB_to_hs(iR: int, iG: int, iB: int) -> Tuple[float, float]: """Convert an rgb color to its hs representation.""" return color_RGB_to_hsv(iR, iG, iB)[:2] -# pylint: disable=invalid-sequence-index def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: """Convert an hsv color into its rgb representation. @@ -337,26 +332,22 @@ def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: return (int(fRGB[0]*255), int(fRGB[1]*255), int(fRGB[2]*255)) -# pylint: disable=invalid-sequence-index def color_hs_to_RGB(iH: float, iS: float) -> Tuple[int, int, int]: """Convert an hsv color into its rgb representation.""" return color_hsv_to_RGB(iH, iS, 100) -# pylint: disable=invalid-sequence-index def color_xy_to_hs(vX: float, vY: float) -> Tuple[float, float]: """Convert an xy color to its hs representation.""" h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY)) return (h, s) -# pylint: disable=invalid-sequence-index def color_hs_to_xy(iH: float, iS: float) -> Tuple[float, float]: """Convert an hs color to its xy representation.""" return color_RGB_to_xy(*color_hs_to_RGB(iH, iS)) -# pylint: disable=invalid-sequence-index def _match_max_scale(input_colors: Tuple[int, ...], output_colors: Tuple[int, ...]) -> Tuple[int, ...]: """Match the maximum value of the output to the input.""" diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index cd440783cc3e01..37b917baa2efa7 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -184,7 +184,6 @@ def formatn(number: int, unit: str) -> str: elif number > 1: return "%d %ss" % (number, unit) - # pylint: disable=invalid-sequence-index def q_n_r(first: int, second: int) -> Tuple[int, int]: """Return quotient and remaining.""" return first // second, first % second diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index b2577ff6be6da6..0e53342b0cafb8 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -11,6 +11,14 @@ _UNDEFINED = object() +class SerializationError(HomeAssistantError): + """Error serializing the data to JSON.""" + + +class WriteError(HomeAssistantError): + """Error writing the data.""" + + def load_json(filename: str, default: Union[List, Dict] = _UNDEFINED) \ -> Union[List, Dict]: """Load JSON data from a file and return as dict or list. @@ -41,13 +49,11 @@ def save_json(filename: str, data: Union[List, Dict]): data = json.dumps(data, sort_keys=True, indent=4) with open(filename, 'w', encoding='utf-8') as fdesc: fdesc.write(data) - return True except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', filename) - raise HomeAssistantError(error) + raise SerializationError(error) except OSError as error: _LOGGER.exception('Saving JSON file failed: %s', filename) - raise HomeAssistantError(error) - return False + raise WriteError(error) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index dae8ed17dc95ad..e390b537d34784 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -82,7 +82,7 @@ def elevation(latitude, longitude): # Author: https://github.com/maurycyp # Source: https://github.com/maurycyp/vincenty # License: https://github.com/maurycyp/vincenty/blob/master/LICENSE -# pylint: disable=invalid-name, unused-variable, invalid-sequence-index +# pylint: disable=invalid-name def vincenty(point1: Tuple[float, float], point2: Tuple[float, float], miles: bool = False) -> Optional[float]: """ diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 66d673987a3789..0e7befd5e9ebb9 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -13,7 +13,7 @@ keyring = None try: - import credstash # pylint: disable=import-error, no-member + import credstash except ImportError: credstash = None @@ -246,7 +246,6 @@ def _load_secret_yaml(secret_path: str) -> Dict: return secrets -# pylint: disable=protected-access def _secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node): """Load secrets and embed it into the configuration YAML.""" diff --git a/pylintrc b/pylintrc index df839b379b549b..d47437cb121823 100644 --- a/pylintrc +++ b/pylintrc @@ -1,6 +1,4 @@ -[MASTER] -reports=no - +[MESSAGES CONTROL] # Reasons disabled: # locally-disabled - it spams too much # duplicate-code - unavoidable @@ -14,9 +12,6 @@ reports=no # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise - -generated-members=botocore.errorfactory - disable= abstract-class-little-used, abstract-class-not-used, @@ -39,9 +34,13 @@ disable= too-many-statements, unused-argument -[EXCEPTIONS] -overgeneral-exceptions=Exception,HomeAssistantError +[REPORTS] +reports=no +[TYPECHECK] # For attrs -[typecheck] ignored-classes=_CountingAttr +generated-members=botocore.errorfactory + +[EXCEPTIONS] +overgeneral-exceptions=Exception,HomeAssistantError diff --git a/requirements_all.txt b/requirements_all.txt index d47496fea595c8..c72e56821d6f11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -66,7 +66,7 @@ TravisPy==0.3.5 TwitterAPI==2.5.4 # homeassistant.components.sensor.waze_travel_time -WazeRouteCalculator==0.5 +WazeRouteCalculator==0.6 # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 @@ -84,7 +84,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.device_tracker.freebox -aiofreepybox==0.0.3 +aiofreepybox==0.0.4 # homeassistant.components.camera.yi aioftp==0.10.1 @@ -309,7 +309,7 @@ ephem==3.7.6.0 epson-projector==0.1.3 # homeassistant.components.netgear_lte -eternalegypt==0.0.1 +eternalegypt==0.0.2 # homeassistant.components.keyboard_remote # evdev==0.6.1 @@ -338,7 +338,7 @@ fints==0.2.1 fitbit==0.3.0 # homeassistant.components.sensor.fixer -fixerio==0.1.1 +fixerio==1.0.0a0 # homeassistant.components.light.flux_led flux_led==0.21 @@ -415,29 +415,20 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180620.0 +home-assistant-frontend==20180704.0 # homeassistant.components.homekit_controller # homekit==0.6 # homeassistant.components.homematicip_cloud -homematicip==0.9.4 - -# homeassistant.components.camera.onvif -http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a +homematicip==0.9.6 # homeassistant.components.remember_the_milk httplib2==0.10.3 -# homeassistant.components.neato -https://github.com/jabesq/pybotvac/archive/v0.0.6.zip#pybotvac==0.0.6 - # homeassistant.components.sensor.gtfs https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3 -# homeassistant.components.media_player.lg_netcast -https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 - # homeassistant.components.hydrawise hydrawiser==0.1.1 @@ -463,7 +454,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.10.0 +insteonplm==0.11.3 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 @@ -479,7 +470,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==12.2.1 +keyring==13.1.0 # homeassistant.scripts.keyring keyrings.alt==3.1 @@ -677,7 +668,7 @@ postnl_api==1.0.2 proliphix==0.4.1 # homeassistant.components.prometheus -prometheus_client==0.1.0 +prometheus_client==0.2.0 # homeassistant.components.sensor.systemmonitor psutil==5.4.6 @@ -739,7 +730,7 @@ pyairvisual==2.0.1 pyalarmdotcom==0.3.2 # homeassistant.components.arlo -pyarlo==0.1.8 +pyarlo==0.1.9 # homeassistant.components.notify.xmpp pyasn1-modules==0.1.5 @@ -763,6 +754,9 @@ pyblackbird==0.5 # homeassistant.components.device_tracker.bluetooth_tracker # pybluez==0.22 +# homeassistant.components.neato +pybotvac==0.0.7 + # homeassistant.components.media_player.channels pychannels==1.0.0 @@ -786,7 +780,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==38 +pydeconz==42 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -794,6 +788,9 @@ pydispatcher==2.0.5 # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 +# homeassistant.components.sensor.duke_energy +pydukeenergy==0.0.6 + # homeassistant.components.sensor.ebox pyebox==1.1.4 @@ -881,6 +878,9 @@ pylacrosse==0.3.1 # homeassistant.components.sensor.lastfm pylast==2.3.0 +# homeassistant.components.media_player.lg_netcast +pylgnetcast-homeassistant==0.2.0.dev0 + # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv pylgtv==0.1.7 @@ -954,7 +954,7 @@ pyotp==2.2.6 pyowm==2.8.0 # homeassistant.components.sensor.pollen -pypollencom==1.1.2 +pypollencom==2.1.0 # homeassistant.components.qwikswitch pyqwikswitch==0.8 @@ -1059,7 +1059,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.4 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 @@ -1080,7 +1080,7 @@ python-sochain-api==0.0.2 python-songpal==0.0.7 # homeassistant.components.sensor.synologydsm -python-synology==0.1.0 +python-synology==0.2.0 # homeassistant.components.tado python-tado==0.2.3 @@ -1098,7 +1098,7 @@ python-velbus==2.0.11 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.9.0 +python-wink==1.9.1 # homeassistant.components.sensor.swiss_public_transport python_opendata_transport==0.1.3 @@ -1113,7 +1113,7 @@ pythonegardia==1.0.39 pythonwhois==2.4.3 # homeassistant.components.device_tracker.tile -pytile==1.1.0 +pytile==2.0.2 # homeassistant.components.climate.touchline pytouchline==0.7 @@ -1163,8 +1163,8 @@ pyzabbix==0.7.4 # homeassistant.components.sensor.qnap qnapstats==0.2.6 -# homeassistant.components.switch.rachio -rachiopy==0.1.2 +# homeassistant.components.rachio +rachiopy==0.1.3 # homeassistant.components.climate.radiotherm radiotherm==1.3 @@ -1218,7 +1218,7 @@ schiene==0.22 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==5.4.0 +sendgrid==5.4.1 # homeassistant.components.light.sensehat # homeassistant.components.sensor.sensehat @@ -1283,7 +1283,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.8 +sqlalchemy==1.2.9 # homeassistant.components.statsd statsd==3.2.1 @@ -1291,6 +1291,9 @@ statsd==3.2.1 # homeassistant.components.sensor.steam_online steamodd==4.21 +# homeassistant.components.camera.onvif +suds-passworddigest-homeassistant==0.1.2a0.dev0 + # homeassistant.components.camera.onvif suds-py3==1.3.3.0 @@ -1432,7 +1435,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.06.14 +youtube_dl==2018.06.25 # homeassistant.components.light.zengge zengge==0.2 diff --git a/requirements_test.txt b/requirements_test.txt index 7ee0e166cf2839..d6e92d5b8ffe30 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.0 -pytest==3.6.1 +pytest==3.6.2 requests_mock==1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e12ef3910a543..aabbdc44bea154 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.0 -pytest==3.6.1 +pytest==3.6.2 requests_mock==1.5 @@ -81,7 +81,10 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180620.0 +home-assistant-frontend==20180704.0 + +# homeassistant.components.homematicip_cloud +homematicip==0.9.6 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb @@ -120,7 +123,7 @@ pilight==0.1.1 pmsensor==0.4 # homeassistant.components.prometheus -prometheus_client==0.1.0 +prometheus_client==0.2.0 # homeassistant.components.notify.pushbullet # homeassistant.components.sensor.pushbullet @@ -133,7 +136,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==38 +pydeconz==42 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -156,7 +159,7 @@ pyqwikswitch==0.8 python-forecastio==1.4.0 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.sensor.whois pythonwhois==2.4.3 @@ -194,7 +197,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.8 +sqlalchemy==1.2.9 # homeassistant.components.statsd statsd==3.2.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7bf87c74de7266..9a5b4dd1a43e15 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -56,6 +56,7 @@ 'hbmqtt', 'holidays', 'home-assistant-frontend', + 'homematicip', 'influxdb', 'libpurecoollink', 'libsoundtouch', diff --git a/script/lazytox.py b/script/lazytox.py index 19af5560dfb132..f0388a0fdcbb47 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -39,7 +39,6 @@ def printc(the_color, *args): def validate_requirements_ok(): """Validate requirements, returns True of ok.""" - # pylint: disable=E0402 from gen_requirements_all import main as req_main return req_main(True) == 0 @@ -70,7 +69,6 @@ async def async_exec(*args, display=False): 'stderr': asyncio.subprocess.STDOUT} if display: kwargs['stderr'] = asyncio.subprocess.PIPE - # pylint: disable=E1120 proc = await asyncio.create_subprocess_exec(*args, **kwargs) except FileNotFoundError as err: printc(FAIL, "Could not execute {}. Did you install test requirements?" diff --git a/script/monkeytype b/script/monkeytype new file mode 100755 index 00000000000000..dc1894c91edea2 --- /dev/null +++ b/script/monkeytype @@ -0,0 +1,25 @@ +#!/bin/sh +# Run monkeytype on test suite or optionally on a test module or directory. + +# Stop on errors +set -e + +cd "$(dirname "$0")/.." + +command -v pytest >/dev/null 2>&1 || { + echo >&2 "This script requires pytest but it's not installed." \ + "Aborting. Try: pip install pytest"; exit 1; } + +command -v monkeytype >/dev/null 2>&1 || { + echo >&2 "This script requires monkeytype but it's not installed." \ + "Aborting. Try: pip install monkeytype"; exit 1; } + +if [ $# -eq 0 ] + then + echo "Run monkeytype on test suite" + monkeytype run "`command -v pytest`" + exit +fi + +echo "Run monkeytype on tests in $1" +monkeytype run "`command -v pytest`" "$1" diff --git a/script/translations_download b/script/translations_download index 099e32c9d1b164..15b6a6810563d9 100755 --- a/script/translations_download +++ b/script/translations_download @@ -28,7 +28,7 @@ mkdir -p ${LOCAL_DIR} docker run \ -v ${LOCAL_DIR}:/opt/dest/locale \ - lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7 lokalise \ + lokalise/lokalise-cli@sha256:ddf5677f58551261008342df5849731c88bcdc152ab645b133b21819aede8218 lokalise \ --token ${LOKALISE_TOKEN} \ export ${PROJECT_ID} \ --export_empty skip \ diff --git a/script/version_bump.py b/script/version_bump.py index 59060a7075b024..e324b231d0667d 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -2,6 +2,7 @@ """Helper script to bump the current version.""" import argparse import re +import subprocess from packaging.version import Version @@ -117,12 +118,21 @@ def main(): help="The type of the bump the version to.", choices=['beta', 'dev', 'patch', 'minor'], ) + parser.add_argument( + '--commit', action='store_true', + help='Create a version bump commit.') arguments = parser.parse_args() current = Version(const.__version__) bumped = bump_version(current, arguments.type) assert bumped > current, 'BUG! New version is not newer than old version' write_version(bumped) + if not arguments.commit: + return + + subprocess.run([ + 'git', 'commit', '-am', 'Bumped version to {}'.format(bumped)]) + def test_bump_version(): """Make sure it all works.""" diff --git a/setup.cfg b/setup.cfg index 8b17da455dc949..7813cc5c0472ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,20 @@ +[metadata] +license = Apache License 2.0 +license_file = LICENSE.md +platforms = any +description = Open-source home automation platform running on Python 3. +long_description = file: README.rst +keywords = home, automation +classifier = + Development Status :: 4 - Beta + Intended Audience :: End Users/Desktop + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Topic :: Home Automation + [tool:pytest] testpaths = tests norecursedirs = .git testing_config diff --git a/setup.py b/setup.py index 69929285f78dff..928d894c9d1a9f 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Home Assistant setup script.""" +from datetime import datetime as dt from setuptools import setup, find_packages import homeassistant.const as hass_const @@ -8,26 +9,9 @@ PROJECT_PACKAGE_NAME = 'homeassistant' PROJECT_LICENSE = 'Apache License 2.0' PROJECT_AUTHOR = 'The Home Assistant Authors' -PROJECT_COPYRIGHT = ' 2013-2018, {}'.format(PROJECT_AUTHOR) +PROJECT_COPYRIGHT = ' 2013-{}, {}'.format(dt.now().year, PROJECT_AUTHOR) PROJECT_URL = 'https://home-assistant.io/' PROJECT_EMAIL = 'hello@home-assistant.io' -PROJECT_DESCRIPTION = ('Open-source home automation platform ' - 'running on Python 3.') -PROJECT_LONG_DESCRIPTION = ('Home Assistant is an open-source ' - 'home automation platform running on Python 3. ' - 'Track and control all devices at home and ' - 'automate control. ' - 'Installation in less than a minute.') -PROJECT_CLASSIFIERS = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Home Automation' -] PROJECT_GITHUB_USERNAME = 'home-assistant' PROJECT_GITHUB_REPOSITORY = 'home-assistant' @@ -38,6 +22,12 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) +PROJECT_URLS = { + 'Bug Reports': '{}/issues'.format(GITHUB_URL), + 'Dev Docs': 'https://developers.home-assistant.io/', + 'Discord': 'https://discordapp.com/invite/c5DvZ4e', + 'Forum': 'https://community.home-assistant.io/', +} PACKAGES = find_packages(exclude=['tests', 'tests.*']) @@ -60,24 +50,20 @@ setup( name=PROJECT_PACKAGE_NAME, version=hass_const.__version__, - license=PROJECT_LICENSE, url=PROJECT_URL, download_url=DOWNLOAD_URL, + project_urls=PROJECT_URLS, author=PROJECT_AUTHOR, author_email=PROJECT_EMAIL, - description=PROJECT_DESCRIPTION, packages=PACKAGES, include_package_data=True, zip_safe=False, - platforms='any', install_requires=REQUIRES, python_requires='>={}'.format(MIN_PY_VERSION), test_suite='tests', - keywords=['home', 'automation'], entry_points={ 'console_scripts': [ 'hass = homeassistant.__main__:main' ] }, - classifiers=PROJECT_CLASSIFIERS, ) diff --git a/tests/auth_providers/test_homeassistant.py b/tests/auth_providers/test_homeassistant.py index 8b12e682865746..1d9a29bf48b7be 100644 --- a/tests/auth_providers/test_homeassistant.py +++ b/tests/auth_providers/test_homeassistant.py @@ -1,60 +1,48 @@ """Test the Home Assistant local auth provider.""" -from unittest.mock import patch, mock_open - import pytest from homeassistant import data_entry_flow from homeassistant.auth_providers import homeassistant as hass_auth -MOCK_PATH = '/bla/users.json' -JSON__OPEN_PATH = 'homeassistant.util.json.open' - - -def test_initialize_empty_config_file_not_found(): - """Test that we initialize an empty config.""" - with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): - data = hass_auth.load_data(MOCK_PATH) - - assert data is not None +@pytest.fixture +def data(hass): + """Create a loaded data class.""" + data = hass_auth.Data(hass) + hass.loop.run_until_complete(data.async_load()) + return data -def test_adding_user(): +async def test_adding_user(data, hass): """Test adding a user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') data.validate_login('test-user', 'test-pass') -def test_adding_user_duplicate_username(): +async def test_adding_user_duplicate_username(data, hass): """Test adding a user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidUser): data.add_user('test-user', 'other-pass') -def test_validating_password_invalid_user(): +async def test_validating_password_invalid_user(data, hass): """Test validating an invalid user.""" - data = hass_auth.Data(MOCK_PATH, None) - with pytest.raises(hass_auth.InvalidAuth): data.validate_login('non-existing', 'pw') -def test_validating_password_invalid_password(): +async def test_validating_password_invalid_password(data, hass): """Test validating an invalid user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidAuth): data.validate_login('test-user', 'invalid-pass') -def test_changing_password(): +async def test_changing_password(data, hass): """Test adding a user.""" user = 'test-user' - data = hass_auth.Data(MOCK_PATH, None) data.add_user(user, 'test-pass') data.change_password(user, 'new-pass') @@ -64,61 +52,50 @@ def test_changing_password(): data.validate_login(user, 'new-pass') -def test_changing_password_raises_invalid_user(): +async def test_changing_password_raises_invalid_user(data, hass): """Test that we initialize an empty config.""" - data = hass_auth.Data(MOCK_PATH, None) - with pytest.raises(hass_auth.InvalidUser): data.change_password('non-existing', 'pw') -async def test_login_flow_validates(hass): +async def test_login_flow_validates(data, hass): """Test login flow.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') + await data.async_save() provider = hass_auth.HassAuthProvider(hass, None, {}) flow = hass_auth.LoginFlow(provider) result = await flow.async_step_init() assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - with patch.object(provider, '_auth_data', return_value=data): - result = await flow.async_step_init({ - 'username': 'incorrect-user', - 'password': 'test-pass', - }) - assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - assert result['errors']['base'] == 'invalid_auth' - - result = await flow.async_step_init({ - 'username': 'test-user', - 'password': 'incorrect-pass', - }) - assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - assert result['errors']['base'] == 'invalid_auth' - - result = await flow.async_step_init({ - 'username': 'test-user', - 'password': 'test-pass', - }) - assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - -async def test_saving_loading(hass): - """Test saving and loading JSON.""" - data = hass_auth.Data(MOCK_PATH, None) - data.add_user('test-user', 'test-pass') - data.add_user('second-user', 'second-pass') + result = await flow.async_step_init({ + 'username': 'incorrect-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' + + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'incorrect-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' - with patch(JSON__OPEN_PATH, mock_open(), create=True) as mock_write: - await hass.async_add_job(data.save) + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - # Mock open calls are: open file, context enter, write, context leave - written = mock_write.mock_calls[2][1][0] - with patch('os.path.isfile', return_value=True), \ - patch(JSON__OPEN_PATH, mock_open(read_data=written), create=True): - await hass.async_add_job(hass_auth.load_data, MOCK_PATH) +async def test_saving_loading(data, hass): + """Test saving and loading JSON.""" + data.add_user('test-user', 'test-pass') + data.add_user('second-user', 'second-pass') + await data.async_save() + data = hass_auth.Data(hass) + await data.async_load() data.validate_login('test-user', 'test-pass') data.validate_login('second-user', 'second-pass') diff --git a/tests/auth_providers/test_insecure_example.py b/tests/auth_providers/test_insecure_example.py index 0b481f93099bcb..cb0bab4afed04f 100644 --- a/tests/auth_providers/test_insecure_example.py +++ b/tests/auth_providers/test_insecure_example.py @@ -11,15 +11,15 @@ @pytest.fixture -def store(): +def store(hass): """Mock store.""" - return auth.AuthStore(Mock()) + return auth.AuthStore(hass) @pytest.fixture -def provider(store): +def provider(hass, store): """Mock provider.""" - return insecure_example.ExampleAuthProvider(None, store, { + return insecure_example.ExampleAuthProvider(hass, store, { 'type': 'insecure_example', 'users': [ { @@ -54,7 +54,7 @@ async def test_match_existing_credentials(store, provider): }, is_new=False, ) - store.credentials_for_provider = Mock(return_value=mock_coro([existing])) + provider.async_credentials = Mock(return_value=mock_coro([existing])) credentials = await provider.async_get_or_create_credentials({ 'username': 'user-test', 'password': 'password-test', diff --git a/tests/auth_providers/test_legacy_api_password.py b/tests/auth_providers/test_legacy_api_password.py new file mode 100644 index 00000000000000..3a186a0454c6db --- /dev/null +++ b/tests/auth_providers/test_legacy_api_password.py @@ -0,0 +1,75 @@ +"""Tests for the legacy_api_password auth provider.""" +from unittest.mock import Mock + +import pytest + +from homeassistant import auth +from homeassistant.auth_providers import legacy_api_password + + +@pytest.fixture +def store(hass): + """Mock store.""" + return auth.AuthStore(hass) + + +@pytest.fixture +def provider(hass, store): + """Mock provider.""" + return legacy_api_password.LegacyApiPasswordAuthProvider(hass, store, { + 'type': 'legacy_api_password', + }) + + +@pytest.fixture +def manager(hass, store, provider): + """Mock manager.""" + return auth.AuthManager(hass, store, { + (provider.type, provider.id): provider + }) + + +async def test_create_new_credential(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({}) + assert credentials.data["username"] is legacy_api_password.LEGACY_USER + assert credentials.is_new is True + + +async def test_only_one_credentials(manager, provider): + """Call create twice will return same credential.""" + credentials = await provider.async_get_or_create_credentials({}) + await manager.async_get_or_create_user(credentials) + credentials2 = await provider.async_get_or_create_credentials({}) + assert credentials2.data["username"] == legacy_api_password.LEGACY_USER + assert credentials2.id == credentials.id + assert credentials2.is_new is False + + +async def test_verify_not_load(hass, provider): + """Test we raise if http module not load.""" + with pytest.raises(ValueError): + provider.async_validate_login('test-password') + hass.http = Mock(api_password=None) + with pytest.raises(ValueError): + provider.async_validate_login('test-password') + hass.http = Mock(api_password='test-password') + provider.async_validate_login('test-password') + + +async def test_verify_login(hass, provider): + """Test we raise if http module not load.""" + hass.http = Mock(api_password='test-password') + provider.async_validate_login('test-password') + hass.http = Mock(api_password='test-password') + with pytest.raises(legacy_api_password.InvalidAuthError): + provider.async_validate_login('invalid-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 556935a6ac173a..ccb8f49ea97923 100644 --- a/tests/common.py +++ b/tests/common.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import functools as ft +import json import os import sys from unittest.mock import patch, MagicMock, Mock @@ -14,8 +15,8 @@ from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( - intent, entity, restore_state, entity_registry, - entity_platform) + intent, entity, restore_state, entity_registry, + entity_platform, storage) from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util import homeassistant.util.yaml as yaml @@ -110,8 +111,6 @@ def stop_hass(): def async_test_home_assistant(loop): """Return a Home Assistant object pointing at test config dir.""" hass = ha.HomeAssistant(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, {}) @@ -137,6 +136,10 @@ def async_add_job(target, *args): hass.config.units = METRIC_SYSTEM hass.config.skip_pip = True + hass.config_entries = config_entries.ConfigEntries(hass, {}) + hass.config_entries._entries = [] + hass.config_entries._store._async_ensure_stop_listener = lambda: None + hass.state = ha.CoreState.running # Mock async_start @@ -309,7 +312,8 @@ class MockUser(auth.User): 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) + super().__init__( + id=id, is_owner=is_owner, is_active=is_active, name=name) def add_to_hass(self, hass): """Test helper to add entry to hass.""" @@ -317,7 +321,8 @@ def add_to_hass(self, hass): def add_to_auth_manager(self, auth_mgr): """Test helper to add entry to hass.""" - auth_mgr._store.users[self.id] = self + ensure_auth_manager_loaded(auth_mgr) + auth_mgr._store._users[self.id] = self return self @@ -325,10 +330,10 @@ def add_to_auth_manager(self, auth_mgr): 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 = {} + if store._clients is None: + store._clients = {} + if store._users is None: + store._users = {} class MockModule(object): @@ -703,3 +708,51 @@ def _handle(self, attr): if attr in self._values: return self._values[attr] return getattr(super(), attr) + + +@contextmanager +def mock_storage(data=None): + """Mock storage. + + Data is a dict {'key': {'version': version, 'data': data}} + + Written data will be converted to JSON to ensure JSON parsing works. + """ + if data is None: + data = {} + + orig_load = storage.Store._async_load + + async def mock_async_load(store): + """Mock version of load.""" + if store._data is None: + # No data to load + if store.key not in data: + return None + + store._data = data.get(store.key) + + # Route through original load so that we trigger migration + loaded = await orig_load(store) + _LOGGER.info('Loading data for %s: %s', store.key, loaded) + return loaded + + def mock_write_data(store, path, data_to_write): + """Mock version of write data.""" + # To ensure that the data can be serialized + _LOGGER.info('Writing data to %s: %s', store.key, data_to_write) + data[store.key] = json.loads(json.dumps(data_to_write)) + + with patch('homeassistant.helpers.storage.Store._async_load', + side_effect=mock_async_load, autospec=True), \ + patch('homeassistant.helpers.storage.Store._write_data', + side_effect=mock_write_data, autospec=True): + yield data + + +async def flush_store(store): + """Make sure all delayed writes of a store are written.""" + if store._data is None: + return + + await store._async_handle_write_data() diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index f0b205ff5ce490..21719c12569b3b 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -34,7 +34,7 @@ async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, }) client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET, redirect_uris=[CLIENT_REDIRECT_URI]) - hass.auth._store.clients[client.id] = client + 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/camera/test_push.py b/tests/components/camera/test_push.py new file mode 100644 index 00000000000000..78053e540f5cd8 --- /dev/null +++ b/tests/components/camera/test_push.py @@ -0,0 +1,63 @@ +"""The tests for generic camera component.""" +import io + +from datetime import timedelta + +from homeassistant import core as ha +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util +from tests.components.auth import async_setup_auth + + +async def test_bad_posting(aioclient_mock, hass, aiohttp_client): + """Test that posting to wrong api endpoint fails.""" + await async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'push', + 'name': 'config_test', + }}) + + client = await async_setup_auth(hass, aiohttp_client) + + # missing file + resp = await client.post('/api/camera_push/camera.config_test') + assert resp.status == 400 + + files = {'image': io.BytesIO(b'fake')} + + # wrong entity + resp = await client.post('/api/camera_push/camera.wrong', data=files) + assert resp.status == 400 + + +async def test_posting_url(aioclient_mock, hass, aiohttp_client): + """Test that posting to api endpoint works.""" + await async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'push', + 'name': 'config_test', + }}) + + client = await async_setup_auth(hass, aiohttp_client) + files = {'image': io.BytesIO(b'fake')} + + # initial state + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'idle' + + # post image + resp = await client.post('/api/camera_push/camera.config_test', data=files) + assert resp.status == 200 + + # state recording + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'recording' + + # await timeout + shifted_time = dt_util.utcnow() + timedelta(seconds=15) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: shifted_time}) + await hass.async_block_till_done() + + # back to initial state + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'idle' diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 255d482d584394..5db77331cd4038 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -137,6 +137,37 @@ def test_set_operation_pessimistic(self): self.assertEqual("cool", state.attributes.get('operation_mode')) self.assertEqual("cool", state.state) + def test_set_operation_with_power_command(self): + """Test setting of new operation mode with power command enabled.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['power_command_topic'] = 'power-command' + assert setup_component(self.hass, climate.DOMAIN, config) + + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("off", state.attributes.get('operation_mode')) + self.assertEqual("off", state.state) + climate.set_operation_mode(self.hass, "on", ENTITY_CLIMATE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("on", state.attributes.get('operation_mode')) + self.assertEqual("on", state.state) + self.mock_publish.async_publish.assert_has_calls([ + unittest.mock.call('power-command', 'ON', 0, False), + unittest.mock.call('mode-topic', 'on', 0, False) + ]) + self.mock_publish.async_publish.reset_mock() + + climate.set_operation_mode(self.hass, "off", ENTITY_CLIMATE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("off", state.attributes.get('operation_mode')) + self.assertEqual("off", state.state) + self.mock_publish.async_publish.assert_has_calls([ + unittest.mock.call('power-command', 'OFF', 0, False), + unittest.mock.call('mode-topic', 'off', 0, False) + ]) + self.mock_publish.async_publish.reset_mock() + def test_set_fan_mode_bad_attr(self): """Test setting fan mode without required attribute.""" assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) @@ -241,6 +272,8 @@ def test_set_target_temperature(self): self.assertEqual(21, state.attributes.get('temperature')) climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('heat', state.attributes.get('operation_mode')) self.mock_publish.async_publish.assert_called_once_with( 'mode-topic', 'heat', 0, False) self.mock_publish.async_publish.reset_mock() @@ -252,6 +285,21 @@ def test_set_target_temperature(self): self.mock_publish.async_publish.assert_called_once_with( 'temperature-topic', 47, 0, False) + # also test directly supplying the operation mode to set_temperature + self.mock_publish.async_publish.reset_mock() + climate.set_temperature(self.hass, temperature=21, + operation_mode="cool", + entity_id=ENTITY_CLIMATE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('cool', state.attributes.get('operation_mode')) + self.assertEqual(21, state.attributes.get('temperature')) + self.mock_publish.async_publish.assert_has_calls([ + unittest.mock.call('mode-topic', 'cool', 0, False), + unittest.mock.call('temperature-topic', 21, 0, False) + ]) + self.mock_publish.async_publish.reset_mock() + def test_set_target_temperature_pessimistic(self): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -508,13 +556,28 @@ def test_set_with_templates(self): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("on", state.attributes.get('swing_mode')) - # Temperature + # Temperature - with valid value self.assertEqual(21, state.attributes.get('temperature')) fire_mqtt_message(self.hass, 'temperature-state', '"1031"') self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(1031, state.attributes.get('temperature')) + # Temperature - with invalid value + with self.assertLogs(level='ERROR') as log: + fire_mqtt_message(self.hass, 'temperature-state', '"-INVALID-"') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + # make sure, the invalid value gets logged... + self.assertEqual(len(log.output), 1) + self.assertEqual(len(log.records), 1) + self.assertIn( + "Could not parse temperature from -INVALID-", + log.output[0] + ) + # ... but the actual value stays unchanged. + self.assertEqual(1031, state.attributes.get('temperature')) + # Away Mode self.assertEqual('off', state.attributes.get('away_mode')) fire_mqtt_message(self.hass, 'away-state', '"ON"') @@ -522,6 +585,17 @@ def test_set_with_templates(self): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('away_mode')) + # Away Mode with JSON values + fire_mqtt_message(self.hass, 'away-state', 'false') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('off', state.attributes.get('away_mode')) + + fire_mqtt_message(self.hass, 'away-state', 'true') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('on', state.attributes.get('away_mode')) + # Hold Mode self.assertEqual(None, state.attributes.get('hold_mode')) fire_mqtt_message(self.hass, 'hold-state', """ @@ -538,6 +612,12 @@ def test_set_with_templates(self): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('aux_heat')) + # anything other than 'switchmeon' should turn Aux mode off + fire_mqtt_message(self.hass, 'aux-state', 'somerandomstring') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('off', state.attributes.get('aux_heat')) + # Current temperature fire_mqtt_message(self.hass, 'current-temperature', '"74656"') self.hass.block_till_done() diff --git a/tests/components/climate/test_zwave.py b/tests/components/climate/test_zwave.py index fbd6ea7f798078..39a85ab493f545 100644 --- a/tests/components/climate/test_zwave.py +++ b/tests/components/climate/test_zwave.py @@ -1,9 +1,9 @@ """Test Z-Wave climate devices.""" import pytest -from homeassistant.components.climate import zwave +from homeassistant.components.climate import zwave, STATE_COOL, STATE_HEAT from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -46,6 +46,24 @@ def device_zxt_120(hass, mock_openzwave): yield device +@pytest.fixture +def device_mapping(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Test state mapping.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + mode=MockValue(data='Off', data_items=['Off', 'Cool', 'Heat'], + node=node), + fan_mode=MockValue(data='test2', data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=6, node=node), + fan_state=MockValue(data=7, node=node), + ) + device = zwave.get_device(hass, node=node, values=values, node_config={}) + + yield device + + def test_zxt_120_swing_mode(device_zxt_120): """Test operation of the zxt 120 swing mode.""" device = device_zxt_120 @@ -109,6 +127,18 @@ def test_operation_value_set(device): assert device.values.mode.data == 'test_set' +def test_operation_value_set_mapping(device_mapping): + """Test values changed for climate device. Mapping.""" + device = device_mapping + assert device.values.mode.data == 'Off' + device.set_operation_mode(STATE_HEAT) + assert device.values.mode.data == 'Heat' + device.set_operation_mode(STATE_COOL) + assert device.values.mode.data == 'Cool' + device.set_operation_mode(STATE_OFF) + assert device.values.mode.data == 'Off' + + def test_fan_mode_value_set(device): """Test values changed for climate device.""" assert device.values.fan_mode.data == 'test2' @@ -140,6 +170,21 @@ def test_operation_value_changed(device): assert device.current_operation == 'test_updated' +def test_operation_value_changed_mapping(device_mapping): + """Test values changed for climate device. Mapping.""" + device = device_mapping + assert device.current_operation == 'off' + device.values.mode.data = 'Heat' + value_changed(device.values.mode) + assert device.current_operation == STATE_HEAT + device.values.mode.data = 'Cool' + value_changed(device.values.mode) + assert device.current_operation == STATE_COOL + device.values.mode.data = 'Off' + value_changed(device.values.mode) + assert device.current_operation == STATE_OFF + + def test_fan_mode_value_changed(device): """Test values changed for climate device.""" assert device.current_fan_mode == 'test2' diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 8a1b934ab76617..00e3ee88d1660b 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -34,5 +34,5 @@ def hass_access_token(hass): no_secret=True, )) refresh_token = hass.loop.run_until_complete( - hass.auth.async_create_refresh_token(user, client.id)) + hass.auth.async_create_refresh_token(user, client)) yield hass.auth.async_create_access_token(refresh_token) diff --git a/tests/components/homematicip_cloud/__init__.py b/tests/components/homematicip_cloud/__init__.py new file mode 100644 index 00000000000000..1d89bd73183c91 --- /dev/null +++ b/tests/components/homematicip_cloud/__init__.py @@ -0,0 +1 @@ +"""Tests for the HomematicIP Cloud component.""" diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py new file mode 100644 index 00000000000000..1c2e54a1a5dfb8 --- /dev/null +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -0,0 +1,150 @@ +"""Tests for HomematicIP Cloud config flow.""" +from unittest.mock import patch + +from homeassistant.components.homematicip_cloud import hap as hmipc +from homeassistant.components.homematicip_cloud import config_flow, const + +from tests.common import MockConfigEntry, mock_coro + + +async def test_flow_works(hass): + """Test config flow works.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + hap = hmipc.HomematicipAuth(hass, config) + with patch.object(hap, 'get_auth', return_value=mock_coro()), \ + patch.object(hmipc.HomematicipAuth, 'async_checkbutton', + return_value=mock_coro(True)), \ + patch.object(hmipc.HomematicipAuth, 'async_register', + return_value=mock_coro(True)): + hap.authtoken = 'ABC' + result = await flow.async_step_init(user_input=config) + + assert hap.authtoken == 'ABC' + assert result['type'] == 'create_entry' + + +async def test_flow_init_connection_error(hass): + """Test config flow with accesspoint connection error.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + with patch.object(hmipc.HomematicipAuth, 'async_setup', + return_value=mock_coro(False)): + result = await flow.async_step_init(user_input=config) + assert result['type'] == 'form' + + +async def test_flow_link_connection_error(hass): + """Test config flow client registration connection error.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + with patch.object(hmipc.HomematicipAuth, 'async_setup', + return_value=mock_coro(True)), \ + patch.object(hmipc.HomematicipAuth, 'async_checkbutton', + return_value=mock_coro(True)), \ + patch.object(hmipc.HomematicipAuth, 'async_register', + return_value=mock_coro(False)): + result = await flow.async_step_init(user_input=config) + assert result['type'] == 'abort' + + +async def test_flow_link_press_button(hass): + """Test config flow ask for pressing the blue button.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + with patch.object(hmipc.HomematicipAuth, 'async_setup', + return_value=mock_coro(True)), \ + patch.object(hmipc.HomematicipAuth, 'async_checkbutton', + return_value=mock_coro(False)): + result = await flow.async_step_init(user_input=config) + assert result['type'] == 'form' + assert result['errors'] == {'base': 'press_the_button'} + + +async def test_init_flow_show_form(hass): + """Test config flow shows up with a form.""" + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input=None) + assert result['type'] == 'form' + + +async def test_init_already_configured(hass): + """Test accesspoint is already configured.""" + MockConfigEntry(domain=const.DOMAIN, data={ + const.HMIPC_HAPID: 'ABC123', + }).add_to_hass(hass) + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input=config) + assert result['type'] == 'abort' + + +async def test_import_config(hass): + """Test importing a host with an existing config file.""" + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + result = await flow.async_step_import({ + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip' + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'ABC123' + assert result['data'] == { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip' + } + + +async def test_import_existing_config(hass): + """Test abort of an existing accesspoint from config.""" + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + MockConfigEntry(domain=const.DOMAIN, data={ + hmipc.HMIPC_HAPID: 'ABC123', + }).add_to_hass(hass) + + result = await flow.async_step_import({ + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip' + }) + + assert result['type'] == 'abort' diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py new file mode 100644 index 00000000000000..5344773fde6595 --- /dev/null +++ b/tests/components/homematicip_cloud/test_hap.py @@ -0,0 +1,113 @@ +"""Test HomematicIP Cloud accesspoint.""" +from unittest.mock import Mock, patch + +from homeassistant.components.homematicip_cloud import hap as hmipc +from homeassistant.components.homematicip_cloud import const, errors +from tests.common import mock_coro + + +async def test_auth_setup(hass): + """Test auth setup for client registration.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipAuth(hass, config) + with patch.object(hap, 'get_auth', return_value=mock_coro()): + assert await hap.async_setup() is True + + +async def test_auth_setup_connection_error(hass): + """Test auth setup connection error behaviour.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipAuth(hass, config) + with patch.object(hap, 'get_auth', + side_effect=errors.HmipcConnectionError): + assert await hap.async_setup() is False + + +async def test_auth_auth_check_and_register(hass): + """Test auth client registration.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipAuth(hass, config) + hap.auth = Mock() + with patch.object(hap.auth, 'isRequestAcknowledged', + return_value=mock_coro()), \ + patch.object(hap.auth, 'requestAuthToken', + return_value=mock_coro('ABC')), \ + patch.object(hap.auth, 'confirmAuthToken', + return_value=mock_coro()): + assert await hap.async_checkbutton() is True + assert await hap.async_register() == 'ABC' + + +async def test_hap_setup_works(aioclient_mock): + """Test a successful setup of a accesspoint.""" + hass = Mock() + entry = Mock() + home = Mock() + entry.data = { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipHAP(hass, entry) + with patch.object(hap, 'get_hap', return_value=mock_coro(home)): + assert await hap.async_setup() is True + + assert hap.home is home + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 5 + assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ + (entry, 'binary_sensor') + + +async def test_hap_setup_connection_error(): + """Test a failed accesspoint setup.""" + hass = Mock() + entry = Mock() + entry.data = { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipHAP(hass, entry) + with patch.object(hap, 'get_hap', + side_effect=errors.HmipcConnectionError): + assert await hap.async_setup() is False + + assert len(hass.async_add_job.mock_calls) == 0 + assert len(hass.config_entries.flow.async_init.mock_calls) == 0 + + +async def test_hap_reset_unloads_entry_if_setup(): + """Test calling reset while the entry has been setup.""" + hass = Mock() + entry = Mock() + home = Mock() + entry.data = { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipHAP(hass, entry) + with patch.object(hap, 'get_hap', return_value=mock_coro(home)): + assert await hap.async_setup() is True + + assert hap.home is home + assert len(hass.services.async_register.mock_calls) == 0 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 5 + + hass.config_entries.async_forward_entry_unload.return_value = \ + mock_coro(True) + await hap.async_reset() + + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 5 diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py new file mode 100644 index 00000000000000..185372272471bd --- /dev/null +++ b/tests/components/homematicip_cloud/test_init.py @@ -0,0 +1,103 @@ +"""Test HomematicIP Cloud setup process.""" + +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +from homeassistant.components import homematicip_cloud as hmipc + +from tests.common import mock_coro, MockConfigEntry + + +async def test_config_with_accesspoint_passed_to_config_entry(hass): + """Test that config for a accesspoint are loaded via config entry.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hmipc, 'configured_haps', return_value=[]): + assert await async_setup_component(hass, hmipc.DOMAIN, { + hmipc.DOMAIN: { + hmipc.CONF_ACCESSPOINT: 'ABC123', + hmipc.CONF_AUTHTOKEN: '123', + hmipc.CONF_NAME: 'name', + } + }) is True + + # Flow started for the access point + assert len(mock_config_entries.flow.mock_calls) == 2 + + +async def test_config_already_registered_not_passed_to_config_entry(hass): + """Test that an already registered accesspoint does not get imported.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hmipc, 'configured_haps', return_value=['ABC123']): + assert await async_setup_component(hass, hmipc.DOMAIN, { + hmipc.DOMAIN: { + hmipc.CONF_ACCESSPOINT: 'ABC123', + hmipc.CONF_AUTHTOKEN: '123', + hmipc.CONF_NAME: 'name', + } + }) is True + + # No flow started + assert len(mock_config_entries.flow.mock_calls) == 0 + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = MockConfigEntry(domain=hmipc.DOMAIN, data={ + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + }) + entry.add_to_hass(hass) + with patch.object(hmipc, 'HomematicipHAP') as mock_hap: + mock_hap.return_value.async_setup.return_value = mock_coro(True) + assert await async_setup_component(hass, hmipc.DOMAIN, { + hmipc.DOMAIN: { + hmipc.CONF_ACCESSPOINT: 'ABC123', + hmipc.CONF_AUTHTOKEN: '123', + hmipc.CONF_NAME: 'hmip', + } + }) is True + + assert len(mock_hap.mock_calls) == 2 + + +async def test_setup_defined_accesspoint(hass): + """Test we initiate config entry for the accesspoint.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hmipc, 'configured_haps', return_value=[]): + mock_config_entries.flow.async_init.return_value = mock_coro() + assert await async_setup_component(hass, hmipc.DOMAIN, { + hmipc.DOMAIN: { + hmipc.CONF_ACCESSPOINT: 'ABC123', + hmipc.CONF_AUTHTOKEN: '123', + hmipc.CONF_NAME: 'hmip', + } + }) is True + + assert len(mock_config_entries.flow.mock_calls) == 1 + assert mock_config_entries.flow.mock_calls[0][2]['data'] == { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + } + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = MockConfigEntry(domain=hmipc.DOMAIN, data={ + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + }) + entry.add_to_hass(hass) + + with patch.object(hmipc, 'HomematicipHAP') as mock_hap: + mock_hap.return_value.async_setup.return_value = mock_coro(True) + assert await async_setup_component(hass, hmipc.DOMAIN, {}) is True + + assert len(mock_hap.return_value.mock_calls) == 1 + + mock_hap.return_value.async_reset.return_value = mock_coro(True) + assert await hmipc.async_unload_entry(hass, entry) + assert len(mock_hap.return_value.async_reset.mock_calls) == 1 + assert hass.data[hmipc.DOMAIN] == {} diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index a44d17d513db98..3e5eed4c924b55 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,20 +1,23 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access from ipaddress import ip_network -from unittest.mock import patch +from unittest.mock import patch, Mock +import pytest from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized -import pytest -from homeassistant.const import HTTP_HEADER_HA_AUTH -from homeassistant.setup import async_setup_component +from homeassistant.auth import AccessToken, RefreshToken from homeassistant.components.http.auth import setup_auth -from homeassistant.components.http.real_ip import setup_real_ip from homeassistant.components.http.const import KEY_AUTHENTICATED - +from homeassistant.components.http.real_ip import setup_real_ip +from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.setup import async_setup_component from . import mock_real_ip + +ACCESS_TOKEN = 'tk.1234' + API_PASSWORD = 'test1234' # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases @@ -36,12 +39,34 @@ async def mock_handler(request): return web.Response(status=200) +def mock_async_get_access_token(token): + """Return if token is valid.""" + if token == ACCESS_TOKEN: + return Mock(spec=AccessToken, + token=ACCESS_TOKEN, + refresh_token=Mock(spec=RefreshToken)) + else: + return None + + @pytest.fixture def app(): """Fixture to setup a web.Application.""" app = web.Application() + mock_auth = Mock(async_get_access_token=mock_async_get_access_token) + app['hass'] = Mock(auth=mock_auth) + app.router.add_get('/', mock_handler) + setup_real_ip(app, False, []) + return app + + +@pytest.fixture +def app2(): + """Fixture to setup a web.Application without real_ip middleware.""" + app = web.Application() + mock_auth = Mock(async_get_access_token=mock_async_get_access_token) + app['hass'] = Mock(auth=mock_auth) app.router.add_get('/', mock_handler) - setup_real_ip(app, False) return app @@ -57,7 +82,7 @@ async def test_auth_middleware_loaded_by_default(hass): async def test_access_without_password(app, aiohttp_client): """Test access without password.""" - setup_auth(app, [], None) + setup_auth(app, [], False, api_password=None) client = await aiohttp_client(app) resp = await client.get('/') @@ -65,8 +90,8 @@ async def test_access_without_password(app, aiohttp_client): async def test_access_with_password_in_header(app, aiohttp_client): - """Test access with password in URL.""" - setup_auth(app, [], API_PASSWORD) + """Test access with password in header.""" + setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) req = await client.get( @@ -79,8 +104,8 @@ async def test_access_with_password_in_header(app, aiohttp_client): async def test_access_with_password_in_query(app, aiohttp_client): - """Test access without password.""" - setup_auth(app, [], API_PASSWORD) + """Test access with password in URL.""" + setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) resp = await client.get('/', params={ @@ -99,7 +124,7 @@ async def test_access_with_password_in_query(app, aiohttp_client): async def test_basic_auth_works(app, aiohttp_client): """Test access with basic authentication.""" - setup_auth(app, [], API_PASSWORD) + setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) req = await client.get( @@ -125,16 +150,64 @@ async def test_basic_auth_works(app, aiohttp_client): assert req.status == 401 -async def test_access_with_trusted_ip(aiohttp_client): +async def test_access_with_trusted_ip(app2, aiohttp_client): """Test access with an untrusted ip address.""" - app = web.Application() - app.router.add_get('/', mock_handler) + setup_auth(app2, TRUSTED_NETWORKS, False, api_password='some-pass') - setup_auth(app, TRUSTED_NETWORKS, 'some-pass') + set_mock_ip = mock_real_ip(app2) + client = await aiohttp_client(app2) - set_mock_ip = mock_real_ip(app) + for remote_addr in UNTRUSTED_ADDRESSES: + set_mock_ip(remote_addr) + resp = await client.get('/') + assert resp.status == 401, \ + "{} shouldn't be trusted".format(remote_addr) + + for remote_addr in TRUSTED_ADDRESSES: + set_mock_ip(remote_addr) + resp = await client.get('/') + assert resp.status == 200, \ + "{} should be trusted".format(remote_addr) + + +async def test_auth_active_access_with_access_token_in_header( + app, aiohttp_client): + """Test access with access token in header.""" + setup_auth(app, [], True, api_password=None) client = await aiohttp_client(app) + req = await client.get( + '/', headers={'Authorization': 'Bearer {}'.format(ACCESS_TOKEN)}) + assert req.status == 200 + + req = await client.get( + '/', headers={'AUTHORIZATION': 'Bearer {}'.format(ACCESS_TOKEN)}) + assert req.status == 200 + + req = await client.get( + '/', headers={'authorization': 'Bearer {}'.format(ACCESS_TOKEN)}) + assert req.status == 200 + + req = await client.get( + '/', headers={'Authorization': ACCESS_TOKEN}) + assert req.status == 401 + + req = await client.get( + '/', headers={'Authorization': 'BEARER {}'.format(ACCESS_TOKEN)}) + assert req.status == 401 + + req = await client.get( + '/', headers={'Authorization': 'Bearer wrong-pass'}) + assert req.status == 401 + + +async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client): + """Test access with an untrusted ip address.""" + setup_auth(app2, TRUSTED_NETWORKS, True, api_password=None) + + set_mock_ip = mock_real_ip(app2) + client = await aiohttp_client(app2) + for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get('/') @@ -146,3 +219,43 @@ async def test_access_with_trusted_ip(aiohttp_client): resp = await client.get('/') assert resp.status == 200, \ "{} should be trusted".format(remote_addr) + + +async def test_auth_active_blocked_api_password_access(app, aiohttp_client): + """Test access using api_password should be blocked when auth.active.""" + setup_auth(app, [], True, api_password=API_PASSWORD) + client = await aiohttp_client(app) + + req = await client.get( + '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) + assert req.status == 401 + + resp = await client.get('/', params={ + 'api_password': API_PASSWORD + }) + assert resp.status == 401 + + req = await client.get( + '/', + auth=BasicAuth('homeassistant', API_PASSWORD)) + assert req.status == 401 + + +async def test_auth_legacy_support_api_password_access(app, aiohttp_client): + """Test access using api_password if auth.support_legacy.""" + setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD) + client = await aiohttp_client(app) + + req = await client.get( + '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) + assert req.status == 200 + + resp = await client.get('/', params={ + 'api_password': API_PASSWORD + }) + assert resp.status == 200 + + req = await client.get( + '/', + auth=BasicAuth('homeassistant', API_PASSWORD)) + assert req.status == 200 diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index 61846eb94c242f..6cf6fec6bce994 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -1,6 +1,7 @@ """Test real IP middleware.""" from aiohttp import web from aiohttp.hdrs import X_FORWARDED_FOR +from ipaddress import ip_network from homeassistant.components.http.real_ip import setup_real_ip from homeassistant.components.http.const import KEY_REAL_IP @@ -15,7 +16,7 @@ async def test_ignore_x_forwarded_for(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) - setup_real_ip(app, False) + setup_real_ip(app, False, []) mock_api_client = await aiohttp_client(app) @@ -27,11 +28,27 @@ async def test_ignore_x_forwarded_for(aiohttp_client): assert text != '255.255.255.255' -async def test_use_x_forwarded_for(aiohttp_client): +async def test_use_x_forwarded_for_without_trusted_proxy(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) - setup_real_ip(app, True) + setup_real_ip(app, True, []) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: '255.255.255.255' + }) + assert resp.status == 200 + text = await resp.text() + assert text != '255.255.255.255' + + +async def test_use_x_forwarded_for_with_trusted_proxy(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('127.0.0.1')]) mock_api_client = await aiohttp_client(app) @@ -41,3 +58,51 @@ async def test_use_x_forwarded_for(aiohttp_client): assert resp.status == 200 text = await resp.text() assert text == '255.255.255.255' + + +async def test_use_x_forwarded_for_with_untrusted_proxy(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('1.1.1.1')]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: '255.255.255.255' + }) + assert resp.status == 200 + text = await resp.text() + assert text != '255.255.255.255' + + +async def test_use_x_forwarded_for_with_spoofed_header(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('127.0.0.1')]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: '222.222.222.222, 255.255.255.255' + }) + assert resp.status == 200 + text = await resp.text() + assert text == '255.255.255.255' + + +async def test_use_x_forwarded_for_with_nonsense_header(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('127.0.0.1')]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: 'This value is invalid' + }) + assert resp.status == 200 + text = await resp.text() + assert text == '127.0.0.1' diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 8b51adb2187399..49bcd8a73ecc0c 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -523,24 +523,24 @@ def test_sending_mqtt_commands_and_optimistic(self): \ self.mock_publish.reset_mock() light.turn_on(self.hass, 'light.test', brightness=50, xy_color=[0.123, 0.123]) - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75], + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0], white_value=80) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 2, False), - mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), + mock.call('test_light_rgb/rgb/set', '255,128,0', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False), + mock.call('test_light_rgb/xy/set', '0.14,0.131', 2, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((255, 255, 255), state.attributes['rgb_color']) + self.assertEqual((255, 128, 0), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.323, 0.329), state.attributes['xy_color']) + self.assertEqual((0.611, 0.375), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -808,11 +808,11 @@ def test_on_command_brightness(self): # Turn on w/ just a color to insure brightness gets # added and sent. - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75]) + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0]) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ - mock.call('test_light/rgb', '50,50,50', 0, False), + mock.call('test_light/rgb', '255,128,0', 0, False), mock.call('test_light/bright', 50, 0, False) ], any_order=True) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 275fb42ede917c..af560bff9c3224 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -381,8 +381,8 @@ def test_sending_mqtt_commands_and_optimistic(self): \ self.assertEqual(50, message_json["brightness"]) self.assertEqual({ 'r': 0, - 'g': 50, - 'b': 4, + 'g': 255, + 'b': 21, }, message_json["color"]) self.assertEqual("ON", message_json["state"]) diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 41cf6749b7158b..47be39c68e5836 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -17,6 +17,8 @@ from homeassistant.components.media_player import cast from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry, mock_coro + @pytest.fixture(autouse=True) def cast_mock(): @@ -359,3 +361,56 @@ async def test_disconnect_on_stop(hass: HomeAssistantType): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert chromecast.disconnect.call_count == 1 + + +async def test_entry_setup_no_config(hass: HomeAssistantType): + """Test setting up entry with no config..""" + await async_setup_component(hass, 'cast', {}) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {} + + +async def test_entry_setup_single_config(hass: HomeAssistantType): + """Test setting up entry and having a single config option.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': { + 'host': 'bla' + } + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + + +async def test_entry_setup_list_config(hass: HomeAssistantType): + """Test setting up entry and having multiple config options.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': [ + {'host': 'bla'}, + {'host': 'blu'}, + ] + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 2 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + assert mock_setup.mock_calls[1][1][1] == {'host': 'blu'} diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py index b5baf8b078b6ff..349067f7cd30c8 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -1,11 +1,16 @@ """Tests for samsungtv Components.""" +import asyncio import unittest +from unittest.mock import call, patch, MagicMock from subprocess import CalledProcessError from asynctest import mock +import pytest + import tests.common -from homeassistant.components.media_player import SUPPORT_TURN_ON +from homeassistant.components.media_player import SUPPORT_TURN_ON, \ + MEDIA_TYPE_CHANNEL, MEDIA_TYPE_URL from homeassistant.components.media_player.samsungtv import setup_platform, \ CONF_TIMEOUT, SamsungTVDevice, SUPPORT_SAMSUNGTV from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_ON, \ @@ -301,3 +306,59 @@ def test_turn_on(self): self.device._mac = "fake" self.device.turn_on() self.device._wol.send_magic_packet.assert_called_once_with("fake") + + +@pytest.fixture +def samsung_mock(): + """Mock samsungctl.""" + with patch.dict('sys.modules', { + 'samsungctl': MagicMock(), + }): + yield + + +async def test_play_media(hass, samsung_mock): + """Test for play_media.""" + asyncio_sleep = asyncio.sleep + sleeps = [] + + async def sleep(duration, loop): + sleeps.append(duration) + await asyncio_sleep(0, loop=loop) + + with patch('asyncio.sleep', new=sleep): + device = SamsungTVDevice(**WORKING_CONFIG) + device.hass = hass + + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_CHANNEL, "576") + + exp = [call("KEY_5"), call("KEY_7"), call("KEY_6")] + assert device.send_key.call_args_list == exp + assert len(sleeps) == 3 + + +async def test_play_media_invalid_type(hass, samsung_mock): + """Test for play_media with invalid media type.""" + url = "https://example.com" + device = SamsungTVDevice(**WORKING_CONFIG) + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_URL, url) + assert device.send_key.call_count == 0 + + +async def test_play_media_channel_as_string(hass, samsung_mock): + """Test for play_media with invalid channel as string.""" + url = "https://example.com" + device = SamsungTVDevice(**WORKING_CONFIG) + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_CHANNEL, url) + assert device.send_key.call_count == 0 + + +async def test_play_media_channel_as_non_positive(hass, samsung_mock): + """Test for play_media with invalid channel as non positive integer.""" + device = SamsungTVDevice(**WORKING_CONFIG) + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_CHANNEL, "-4") + assert device.send_key.call_count == 0 diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 1dd29909ffdfb4..ed6c77f676ce16 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -52,12 +52,21 @@ def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog): @asyncio.coroutine def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog): """Test for a valid component.""" + invalid_component = "timer" + mock_load_platform.return_value = mock_coro() yield from async_start(hass, 'homeassistant', {}) - async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', '{}') + async_fire_mqtt_message(hass, 'homeassistant/{}/bla/config'.format( + invalid_component + ), '{}') + yield from hass.async_block_till_done() - assert 'Component climate is not supported' in caplog.text + + assert 'Component {} is not supported'.format( + invalid_component + ) in caplog.text + assert not mock_load_platform.called @@ -94,6 +103,27 @@ def test_discover_fan(hass, mqtt_mock, caplog): assert ('fan', 'bla') in hass.data[ALREADY_DISCOVERED] +@asyncio.coroutine +def test_discover_climate(hass, mqtt_mock, caplog): + """Test discovering an MQTT climate component.""" + yield from async_start(hass, 'homeassistant', {}) + + data = ( + '{ "name": "ClimateTest",' + ' "current_temperature_topic": "climate/bla/current_temp",' + ' "temperature_command_topic": "climate/bla/target_temp" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data) + yield from hass.async_block_till_done() + + state = hass.states.get('climate.ClimateTest') + + assert state is not None + assert state.name == 'ClimateTest' + assert ('climate', 'bla') in hass.data[ALREADY_DISCOVERED] + + @asyncio.coroutine def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): """Test sending in correct JSON with optional node_id included.""" diff --git a/tests/components/sensor/test_arlo.py b/tests/components/sensor/test_arlo.py new file mode 100644 index 00000000000000..d31490ab2afa10 --- /dev/null +++ b/tests/components/sensor/test_arlo.py @@ -0,0 +1,240 @@ +"""The tests for the Netgear Arlo sensors.""" +from collections import namedtuple +from unittest.mock import patch, MagicMock +import pytest +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, ATTR_ATTRIBUTION) +from homeassistant.components.sensor import arlo +from homeassistant.components.arlo import DATA_ARLO + + +def _get_named_tuple(input_dict): + return namedtuple('Struct', input_dict.keys())(*input_dict.values()) + + +def _get_sensor(name='Last', sensor_type='last_capture', data=None): + if data is None: + data = {} + return arlo.ArloSensor(name, data, sensor_type) + + +@pytest.fixture() +def default_sensor(): + """Create an ArloSensor with default values.""" + return _get_sensor() + + +@pytest.fixture() +def battery_sensor(): + """Create an ArloSensor with battery data.""" + data = _get_named_tuple({ + 'battery_level': 50 + }) + return _get_sensor('Battery Level', 'battery_level', data) + + +@pytest.fixture() +def temperature_sensor(): + """Create a temperature ArloSensor.""" + return _get_sensor('Temperature', 'temperature') + + +@pytest.fixture() +def humidity_sensor(): + """Create a humidity ArloSensor.""" + return _get_sensor('Humidity', 'humidity') + + +@pytest.fixture() +def cameras_sensor(): + """Create a total cameras ArloSensor.""" + data = _get_named_tuple({ + 'cameras': [0, 0] + }) + return _get_sensor('Arlo Cameras', 'total_cameras', data) + + +@pytest.fixture() +def captured_sensor(): + """Create a captured today ArloSensor.""" + data = _get_named_tuple({ + 'captured_today': [0, 0, 0, 0, 0] + }) + return _get_sensor('Captured Today', 'captured_today', data) + + +class PlatformSetupFixture(): + """Fixture for testing platform setup call to add_devices().""" + + def __init__(self): + """Instantiate the platform setup fixture.""" + self.sensors = None + self.update = False + + def add_devices(self, sensors, update): + """Mock method for adding devices.""" + self.sensors = sensors + self.update = update + + +@pytest.fixture() +def platform_setup(): + """Create an instance of the PlatformSetupFixture class.""" + return PlatformSetupFixture() + + +@pytest.fixture() +def sensor_with_hass_data(default_sensor, hass): + """Create a sensor with async_dispatcher_connected mocked.""" + hass.data = {} + default_sensor.hass = hass + return default_sensor + + +@pytest.fixture() +def mock_dispatch(): + """Mock the dispatcher connect method.""" + target = 'homeassistant.components.sensor.arlo.async_dispatcher_connect' + with patch(target, MagicMock()) as _mock: + yield _mock + + +def test_setup_with_no_data(platform_setup, hass): + """Test setup_platform with no data.""" + arlo.setup_platform(hass, None, platform_setup.add_devices) + assert platform_setup.sensors is None + assert not platform_setup.update + + +def test_setup_with_valid_data(platform_setup, hass): + """Test setup_platform with valid data.""" + config = { + 'monitored_conditions': [ + 'last_capture', + 'total_cameras', + 'captured_today', + 'battery_level', + 'signal_strength', + 'temperature', + 'humidity', + 'air_quality' + ] + } + + hass.data[DATA_ARLO] = _get_named_tuple({ + 'cameras': [_get_named_tuple({ + 'name': 'Camera', + 'model_id': 'ABC1000' + })], + 'base_stations': [_get_named_tuple({ + 'name': 'Base Station', + 'model_id': 'ABC1000' + })] + }) + + arlo.setup_platform(hass, config, platform_setup.add_devices) + assert len(platform_setup.sensors) == 8 + assert platform_setup.update + + +def test_sensor_name(default_sensor): + """Test the name property.""" + assert default_sensor.name == 'Last' + + +async def test_async_added_to_hass(sensor_with_hass_data, mock_dispatch): + """Test dispatcher called when added.""" + await sensor_with_hass_data.async_added_to_hass() + assert len(mock_dispatch.mock_calls) == 1 + kall = mock_dispatch.call_args + args, kwargs = kall + assert len(args) == 3 + assert args[0] == sensor_with_hass_data.hass + assert args[1] == 'arlo_update' + assert not kwargs + + +def test_sensor_state_default(default_sensor): + """Test the state property.""" + assert default_sensor.state is None + + +def test_sensor_icon_battery(battery_sensor): + """Test the battery icon.""" + assert battery_sensor.icon == 'mdi:battery-50' + + +def test_sensor_icon(temperature_sensor): + """Test the icon property.""" + assert temperature_sensor.icon == 'mdi:thermometer' + + +def test_unit_of_measure(default_sensor, battery_sensor): + """Test the unit_of_measurement property.""" + assert default_sensor.unit_of_measurement is None + assert battery_sensor.unit_of_measurement == '%' + + +def test_device_class(default_sensor, temperature_sensor, humidity_sensor): + """Test the device_class property.""" + assert default_sensor.device_class is None + assert temperature_sensor.device_class == DEVICE_CLASS_TEMPERATURE + assert humidity_sensor.device_class == DEVICE_CLASS_HUMIDITY + + +def test_update_total_cameras(cameras_sensor): + """Test update method for total_cameras sensor type.""" + cameras_sensor.update() + assert cameras_sensor.state == 2 + + +def test_update_captured_today(captured_sensor): + """Test update method for captured_today sensor type.""" + captured_sensor.update() + assert captured_sensor.state == 5 + + +def _test_attributes(sensor_type): + data = _get_named_tuple({ + 'model_id': 'TEST123' + }) + sensor = _get_sensor('test', sensor_type, data) + attrs = sensor.device_state_attributes + assert attrs.get(ATTR_ATTRIBUTION) == 'Data provided by arlo.netgear.com' + assert attrs.get('brand') == 'Netgear Arlo' + assert attrs.get('model') == 'TEST123' + + +def test_state_attributes(): + """Test attributes for camera sensor types.""" + _test_attributes('battery_level') + _test_attributes('signal_strength') + _test_attributes('temperature') + _test_attributes('humidity') + _test_attributes('air_quality') + + +def test_attributes_total_cameras(cameras_sensor): + """Test attributes for total cameras sensor type.""" + attrs = cameras_sensor.device_state_attributes + assert attrs.get(ATTR_ATTRIBUTION) == 'Data provided by arlo.netgear.com' + assert attrs.get('brand') == 'Netgear Arlo' + assert attrs.get('model') is None + + +def _test_update(sensor_type, key, value): + data = _get_named_tuple({ + key: value + }) + sensor = _get_sensor('test', sensor_type, data) + sensor.update() + assert sensor.state == value + + +def test_update(): + """Test update method for direct transcription sensor types.""" + _test_update('battery_level', 'battery_level', 100) + _test_update('signal_strength', 'signal_strength', 100) + _test_update('temperature', 'ambient_temperature', 21.4) + _test_update('humidity', 'ambient_humidity', 45.1) + _test_update('air_quality', 'ambient_air_quality', 14.2) diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 8e79306fe136a2..cf2cc9c42054db 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -4,7 +4,8 @@ from unittest.mock import patch from homeassistant.components.sensor.filter import ( - LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter) + LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter, + RangeFilter) import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component import homeassistant.core as ha @@ -131,6 +132,23 @@ def test_lowpass(self): filtered = filt.filter_state(state) self.assertEqual(18.05, filtered.state) + def test_range(self): + """Test if range filter works.""" + lower = 10 + upper = 20 + filt = RangeFilter(entity=None, + lower_bound=lower, + upper_bound=upper) + for unf_state in self.values: + unf = float(unf_state.state) + filtered = filt.filter_state(unf_state) + if unf < lower: + self.assertEqual(lower, filtered.state) + elif unf > upper: + self.assertEqual(upper, filtered.state) + else: + self.assertEqual(unf, filtered.state) + def test_throttle(self): """Test if lowpass filter works.""" filt = ThrottleFilter(window_size=3, diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index e336a28eb0399e..49744421c726ec 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -12,7 +12,7 @@ def prometheus_client(loop, hass, aiohttp_client): assert loop.run_until_complete(async_setup_component( hass, prometheus.DOMAIN, - {}, + {prometheus.DOMAIN: {}}, )) return loop.run_until_complete(aiohttp_client(hass.http.app)) diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index d9238336768f9d..baeda2c49a839b 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -5,6 +5,7 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA import homeassistant.components.snips as snips +from homeassistant.helpers.intent import (ServiceIntentHandler, async_register) from tests.common import (async_fire_mqtt_message, async_mock_intent, async_mock_service) @@ -124,6 +125,49 @@ async def test_snips_intent(hass, mqtt_mock): assert intent.text_input == 'turn the lights green' +async def test_snips_service_intent(hass, mqtt_mock): + """Test ServiceIntentHandler via Snips.""" + hass.states.async_set('light.kitchen', 'off') + calls = async_mock_service(hass, 'light', 'turn_on') + result = await async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + payload = """ + { + "input": "turn the light on", + "intent": { + "intentName": "Lights", + "probability": 0.85 + }, + "siteId": "default", + "slots": [ + { + "slotName": "name", + "value": { + "kind": "Custom", + "value": "kitchen" + } + } + ] + } + """ + + async_register(hass, ServiceIntentHandler( + "Lights", "light", 'turn_on', "Turned {} on")) + + async_fire_mqtt_message(hass, 'hermes/intent/Lights', + payload) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'light' + assert calls[0].service == 'turn_on' + assert calls[0].data['entity_id'] == 'light.kitchen' + assert 'probability' not in calls[0].data + assert 'site_id' not in calls[0].data + + async def test_snips_intent_with_duration(hass, mqtt_mock): """Test intent with Snips duration.""" result = await async_setup_component(hass, "snips", { diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index fbd8584a7d14ae..6ea90bcdb88f24 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -77,7 +77,7 @@ def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): assert mock_process_wrong_login.called assert msg['type'] == wapi.TYPE_AUTH_INVALID - assert msg['message'] == 'Invalid password' + assert msg['message'] == 'Invalid access token or password' @asyncio.coroutine @@ -316,47 +316,103 @@ def test_unknown_command(websocket_client): assert msg['error']['code'] == wapi.ERR_UNKNOWN_COMMAND -async def test_auth_with_token(hass, aiohttp_client, hass_access_token): +async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): """Test authenticating with a token.""" assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) + 'http': { + 'api_password': API_PASSWORD + } + }) client = await aiohttp_client(hass.http.app) async with client.ws_connect(wapi.URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token.token - }) + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token.token + }) - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK -async def test_auth_with_invalid_token(hass, aiohttp_client): +async def test_auth_active_with_password_not_allow(hass, aiohttp_client): """Test authenticating with a token.""" assert await async_setup_component(hass, 'websocket_api', { - 'http': { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, 'api_password': API_PASSWORD - } - }) + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + + +async def test_auth_legacy_support_with_password(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) client = await aiohttp_client(hass.http.app) async with client.ws_connect(wapi.URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + with patch('homeassistant.auth.AuthManager.active', + return_value=True),\ + patch('homeassistant.auth.AuthManager.support_legacy', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': 'incorrect' - }) - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID +async def test_auth_with_invalid_token(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': 'incorrect' + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID diff --git a/tests/components/weather/test_darksky.py b/tests/components/weather/test_darksky.py index 7faa033e0a86c4..41687451cd6493 100644 --- a/tests/components/weather/test_darksky.py +++ b/tests/components/weather/test_darksky.py @@ -48,4 +48,4 @@ def test_setup(self, mock_req, mock_get_forecast): self.assertEqual(mock_get_forecast.call_count, 1) state = self.hass.states.get('weather.test') - self.assertEqual(state.state, 'Clear') + self.assertEqual(state.state, 'sunny') diff --git a/tests/conftest.py b/tests/conftest.py index 4d619c5ef61d02..0a350b62fc1cb9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,8 @@ from homeassistant.util import location from tests.common import ( - async_test_home_assistant, INSTANCES, async_mock_mqtt_component, mock_coro) + async_test_home_assistant, INSTANCES, async_mock_mqtt_component, mock_coro, + mock_storage as mock_storage) from tests.test_util.aiohttp import mock_aiohttp_client from tests.mock.zwave import MockNetwork, MockOption @@ -59,7 +60,14 @@ def verify_cleanup(): @pytest.fixture -def hass(loop): +def hass_storage(): + """Fixture to mock storage.""" + with mock_storage() as stored_data: + yield stored_data + + +@pytest.fixture +def hass(loop, hass_storage): """Fixture to provide a test instance of HASS.""" hass = loop.run_until_complete(async_test_home_assistant(loop)) diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index a8d37a249bc92c..707129ae531743 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -1,6 +1,18 @@ """Tests for the intent helpers.""" + +import unittest +import voluptuous as vol + from homeassistant.core import State -from homeassistant.helpers import intent +from homeassistant.helpers import (intent, config_validation as cv) + + +class MockIntentHandler(intent.IntentHandler): + """Provide a mock intent handler.""" + + def __init__(self, slot_schema): + """Initialize the mock handler.""" + self.slot_schema = slot_schema def test_async_match_state(): @@ -10,3 +22,25 @@ def test_async_match_state(): state = intent.async_match_state(None, 'kitch', [state1, state2]) assert state is state1 + + +class TestIntentHandler(unittest.TestCase): + """Test the Home Assistant event helpers.""" + + def test_async_validate_slots(self): + """Test async_validate_slots of IntentHandler.""" + handler1 = MockIntentHandler({ + vol.Required('name'): cv.string, + }) + + self.assertRaises(vol.error.MultipleInvalid, + handler1.async_validate_slots, {}) + self.assertRaises(vol.error.MultipleInvalid, + handler1.async_validate_slots, {'name': 1}) + self.assertRaises(vol.error.MultipleInvalid, + handler1.async_validate_slots, {'name': 'kitchen'}) + handler1.async_validate_slots({'name': {'value': 'kitchen'}}) + handler1.async_validate_slots({ + 'name': {'value': 'kitchen'}, + 'probability': {'value': '0.5'} + }) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py new file mode 100644 index 00000000000000..f414eaec97c844 --- /dev/null +++ b/tests/helpers/test_storage.py @@ -0,0 +1,179 @@ +"""Tests for the storage helper.""" +import asyncio +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import storage +from homeassistant.util import dt + +from tests.common import async_fire_time_changed, mock_coro + + +MOCK_VERSION = 1 +MOCK_KEY = 'storage-test' +MOCK_DATA = {'hello': 'world'} + + +@pytest.fixture +def store(hass): + """Fixture of a store that prevents writing on HASS stop.""" + yield storage.Store(hass, MOCK_VERSION, MOCK_KEY) + + +async def test_loading(hass, store): + """Test we can save and load data.""" + await store.async_save(MOCK_DATA) + data = await store.async_load() + assert data == MOCK_DATA + + +async def test_loading_non_existing(hass, store): + """Test we can save and load data.""" + with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): + data = await store.async_load() + assert data is None + + +async def test_loading_parallel(hass, store, hass_storage, caplog): + """Test we can save and load data.""" + hass_storage[store.key] = { + 'version': MOCK_VERSION, + 'data': MOCK_DATA, + } + + results = await asyncio.gather( + store.async_load(), + store.async_load() + ) + + assert results[0] is MOCK_DATA + assert results[1] is MOCK_DATA + assert caplog.text.count('Loading data for {}'.format(store.key)) + + +async def test_saving_with_delay(hass, store, hass_storage): + """Test saving data after a delay.""" + await store.async_save(MOCK_DATA, delay=1) + assert store.key not in hass_storage + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': MOCK_DATA, + } + + +async def test_saving_on_stop(hass, hass_storage): + """Test delayed saves trigger when we quit Home Assistant.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + await store.async_save(MOCK_DATA, delay=1) + assert store.key not in hass_storage + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': MOCK_DATA, + } + + +async def test_loading_while_delay(hass, store, hass_storage): + """Test we load new data even if not written yet.""" + await store.async_save({'delay': 'no'}) + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } + + await store.async_save({'delay': 'yes'}, delay=1) + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } + + data = await store.async_load() + assert data == {'delay': 'yes'} + + +async def test_writing_while_writing_delay(hass, store, hass_storage): + """Test a write while a write with delay is active.""" + await store.async_save({'delay': 'yes'}, delay=1) + assert store.key not in hass_storage + await store.async_save({'delay': 'no'}) + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } + + data = await store.async_load() + assert data == {'delay': 'no'} + + +async def test_migrator_no_existing_config(hass, store, hass_storage): + """Test migrator with no existing config.""" + with patch('os.path.isfile', return_value=False), \ + patch.object(store, 'async_load', + return_value=mock_coro({'cur': 'config'})): + data = await storage.async_migrator( + hass, 'old-path', store) + + assert data == {'cur': 'config'} + assert store.key not in hass_storage + + +async def test_migrator_existing_config(hass, store, hass_storage): + """Test migrating existing config.""" + with patch('os.path.isfile', return_value=True), \ + patch('os.remove') as mock_remove, \ + patch('homeassistant.util.json.load_json', + return_value={'old': 'config'}): + data = await storage.async_migrator( + hass, 'old-path', store) + + assert len(mock_remove.mock_calls) == 1 + assert data == {'old': 'config'} + assert hass_storage[store.key] == { + 'key': MOCK_KEY, + 'version': MOCK_VERSION, + 'data': data, + } + + +async def test_migrator_transforming_config(hass, store, hass_storage): + """Test migrating config to new format.""" + async def old_conf_migrate_func(old_config): + """Migrate old config to new format.""" + return {'new': old_config['old']} + + with patch('os.path.isfile', return_value=True), \ + patch('os.remove') as mock_remove, \ + patch('homeassistant.util.json.load_json', + return_value={'old': 'config'}): + data = await storage.async_migrator( + hass, 'old-path', store, + old_conf_migrate_func=old_conf_migrate_func) + + assert len(mock_remove.mock_calls) == 1 + assert data == {'new': 'config'} + assert hass_storage[store.key] == { + 'key': MOCK_KEY, + 'version': MOCK_VERSION, + 'data': data, + } diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 2e837b06b58b81..e6aa7893f33b0a 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -6,16 +6,21 @@ from homeassistant.scripts import auth as script_auth from homeassistant.auth_providers import homeassistant as hass_auth -MOCK_PATH = '/bla/users.json' +@pytest.fixture +def data(hass): + """Create a loaded data class.""" + data = hass_auth.Data(hass) + hass.loop.run_until_complete(data.async_load()) + return data -def test_list_user(capsys): + +async def test_list_user(data, capsys): """Test we can list users.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') data.add_user('second-user', 'second-pass') - script_auth.list_users(data, None) + await script_auth.list_users(data, None) captured = capsys.readouterr() @@ -28,15 +33,12 @@ def test_list_user(capsys): ]) -def test_add_user(capsys): +async def test_add_user(data, capsys, hass_storage): """Test we can add a user.""" - data = hass_auth.Data(MOCK_PATH, None) - - with patch.object(data, 'save') as mock_save: - script_auth.add_user( - data, Mock(username='paulus', password='test-pass')) + await script_auth.add_user( + data, Mock(username='paulus', password='test-pass')) - assert len(mock_save.mock_calls) == 1 + assert len(hass_storage[hass_auth.STORAGE_KEY]['data']['users']) == 1 captured = capsys.readouterr() assert captured.out == 'User created\n' @@ -45,37 +47,34 @@ def test_add_user(capsys): data.validate_login('paulus', 'test-pass') -def test_validate_login(capsys): +async def test_validate_login(data, capsys): """Test we can validate a user login.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') - script_auth.validate_login( + await script_auth.validate_login( data, Mock(username='test-user', password='test-pass')) captured = capsys.readouterr() assert captured.out == 'Auth valid\n' - script_auth.validate_login( + await script_auth.validate_login( data, Mock(username='test-user', password='invalid-pass')) captured = capsys.readouterr() assert captured.out == 'Auth invalid\n' - script_auth.validate_login( + await script_auth.validate_login( data, Mock(username='invalid-user', password='test-pass')) captured = capsys.readouterr() assert captured.out == 'Auth invalid\n' -def test_change_password(capsys): +async def test_change_password(data, capsys, hass_storage): """Test we can change a password.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') - with patch.object(data, 'save') as mock_save: - script_auth.change_password( - data, Mock(username='test-user', new_password='new-pass')) + await script_auth.change_password( + data, Mock(username='test-user', new_password='new-pass')) - assert len(mock_save.mock_calls) == 1 + assert len(hass_storage[hass_auth.STORAGE_KEY]['data']['users']) == 1 captured = capsys.readouterr() assert captured.out == 'Password changed\n' data.validate_login('test-user', 'new-pass') @@ -83,18 +82,35 @@ def test_change_password(capsys): data.validate_login('test-user', 'test-pass') -def test_change_password_invalid_user(capsys): +async def test_change_password_invalid_user(data, capsys, hass_storage): """Test changing password of non-existing user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') - with patch.object(data, 'save') as mock_save: - script_auth.change_password( - data, Mock(username='invalid-user', new_password='new-pass')) + await script_auth.change_password( + data, Mock(username='invalid-user', new_password='new-pass')) - assert len(mock_save.mock_calls) == 0 + assert hass_auth.STORAGE_KEY not in hass_storage captured = capsys.readouterr() assert captured.out == 'User not found\n' data.validate_login('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidAuth): data.validate_login('invalid-user', 'new-pass') + + +def test_parsing_args(loop): + """Test we parse args correctly.""" + called = False + + async def mock_func(data, args2): + """Mock function to be called.""" + nonlocal called + called = True + assert data.hass.config.config_dir == '/somewhere/config' + assert args2 is args + + args = Mock(config='/somewhere/config', func=mock_func) + + with patch('argparse.ArgumentParser.parse_args', return_value=args): + script_auth.run(None) + + assert called, 'Mock function did not get called' diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 8dfc5db90e0bd4..33154090286d76 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -160,6 +160,7 @@ def test_secrets(self, isfile_patch): 'server_host': '0.0.0.0', 'server_port': 8123, 'trusted_networks': [], + 'trusted_proxies': [], 'use_x_forwarded_for': False} assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} assert res['secrets'] == {'http_pw': 'abc123'} diff --git a/tests/test_auth.py b/tests/test_auth.py index 4bbf218fd23ca7..8096a081679232 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,14 +1,16 @@ """Tests for the Home Assistant auth module.""" -from unittest.mock import Mock +from datetime import timedelta +from unittest.mock import Mock, patch import pytest from homeassistant import auth, data_entry_flow -from tests.common import MockUser, ensure_auth_manager_loaded +from homeassistant.util import dt as dt_util +from tests.common import MockUser, ensure_auth_manager_loaded, flush_store @pytest.fixture -def mock_hass(): +def mock_hass(loop): """Hass mock with minimum amount of data set to make it work with auth.""" hass = Mock() hass.config.skip_pip = True @@ -53,9 +55,9 @@ async def test_auth_manager_from_config_validates_config_and_id(mock_hass): }] -async def test_create_new_user(mock_hass): +async def test_create_new_user(hass, hass_storage): """Test creating new user.""" - manager = await auth.auth_manager_from_config(mock_hass, [{ + manager = await auth.auth_manager_from_config(hass, [{ 'type': 'insecure_example', 'users': [{ 'username': 'test-user', @@ -124,9 +126,9 @@ async def test_login_as_existing_user(mock_hass): assert user.name == 'Paulus' -async def test_linking_user_to_two_auth_providers(mock_hass): +async def test_linking_user_to_two_auth_providers(hass, hass_storage): """Test linking user to two auth providers.""" - manager = await auth.auth_manager_from_config(mock_hass, [{ + manager = await auth.auth_manager_from_config(hass, [{ 'type': 'insecure_example', 'users': [{ 'username': 'test-user', @@ -157,3 +159,137 @@ async def test_linking_user_to_two_auth_providers(mock_hass): }) await manager.async_link_user(user, step['result']) assert len(user.credentials) == 2 + + +async def test_saving_loading(hass, hass_storage): + """Test storing and saving data. + + Creates one of each type that we store to test we restore correctly. + """ + manager = await auth.auth_manager_from_config(hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + }] + }]) + + 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']) + + client = await manager.async_create_client( + 'test', redirect_uris=['https://example.com']) + + refresh_token = await manager.async_create_refresh_token(user, client) + + manager.async_create_access_token(refresh_token) + + await flush_store(manager._store._store) + + store2 = auth.AuthStore(hass) + users = await store2.async_get_users() + assert len(users) == 1 + assert users[0] == user + + clients = await store2.async_get_clients() + assert len(clients) == 1 + assert clients[0] == client + + +def test_access_token_expired(): + """Test that the expired property on access tokens work.""" + refresh_token = auth.RefreshToken( + user=None, + client_id='bla' + ) + + access_token = auth.AccessToken( + refresh_token=refresh_token + ) + + assert access_token.expired is False + + with patch('homeassistant.auth.dt_util.utcnow', + return_value=dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION): + assert access_token.expired is True + + almost_exp = dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION - timedelta(1) + with patch('homeassistant.auth.dt_util.utcnow', return_value=almost_exp): + assert access_token.expired is False + + +async def test_cannot_retrieve_expired_access_token(hass): + """Test that we cannot retrieve expired access tokens.""" + manager = await auth.auth_manager_from_config(hass, []) + client = await manager.async_create_client('test') + user = MockUser().add_to_auth_manager(manager) + refresh_token = await manager.async_create_refresh_token(user, client) + assert refresh_token.user.id is user.id + assert refresh_token.client_id is client.id + + access_token = manager.async_create_access_token(refresh_token) + assert manager.async_get_access_token(access_token.token) is access_token + + with patch('homeassistant.auth.dt_util.utcnow', + return_value=dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION): + assert manager.async_get_access_token(access_token.token) is None + + # Even with unpatched time, it should have been removed from manager + assert manager.async_get_access_token(access_token.token) is None + + +async def test_get_or_create_client(hass): + """Test that get_or_create_client works.""" + manager = await auth.auth_manager_from_config(hass, []) + + client1 = await manager.async_get_or_create_client( + 'Test Client', redirect_uris=['https://test.com/1']) + assert client1.name is 'Test Client' + + client2 = await manager.async_get_or_create_client( + 'Test Client', redirect_uris=['https://test.com/1']) + assert client2.id is client1.id + + +async def test_generating_system_user(hass): + """Test that we can add a system user.""" + manager = await auth.auth_manager_from_config(hass, []) + user = await manager.async_create_system_user('Hass.io') + token = await manager.async_create_refresh_token(user) + assert user.system_generated + assert token is not None + assert token.client_id is None + + +async def test_refresh_token_requires_client_for_user(hass): + """Test that we can add a system user.""" + manager = await auth.auth_manager_from_config(hass, []) + user = MockUser().add_to_auth_manager(manager) + assert user.system_generated is False + + with pytest.raises(ValueError): + await manager.async_create_refresh_token(user) + + client = await manager.async_get_or_create_client('Test client') + token = await manager.async_create_refresh_token(user, client) + assert token is not None + assert token.client_id == client.id + + +async def test_refresh_token_not_requires_client_for_system_user(hass): + """Test that we can add a system user.""" + manager = await auth.auth_manager_from_config(hass, []) + user = await manager.async_create_system_user('Hass.io') + assert user.system_generated is True + client = await manager.async_get_or_create_client('Test client') + + with pytest.raises(ValueError): + await manager.async_create_refresh_token(user, client) + + token = await manager.async_create_refresh_token(user) + assert token is not None + assert token.client_id is None diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 84bd077154253d..d7a7ec4b82bf90 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,13 +1,16 @@ """Test the config manager.""" import asyncio -from unittest.mock import MagicMock, patch, mock_open +from datetime import timedelta +from unittest.mock import MagicMock, patch import pytest from homeassistant import config_entries, loader, data_entry_flow from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from tests.common import MockModule, mock_coro, MockConfigEntry +from tests.common import ( + MockModule, mock_coro, MockConfigEntry, async_fire_time_changed) @pytest.fixture @@ -15,6 +18,7 @@ def manager(hass): """Fixture of a loaded config manager.""" manager = config_entries.ConfigEntries(hass, {}) manager._entries = [] + manager._store._async_ensure_stop_listener = lambda: None hass.config_entries = manager return manager @@ -148,10 +152,11 @@ def test_domains_gets_uniques(manager): assert manager.async_domains() == ['test', 'test2', 'test3'] -@asyncio.coroutine -def test_saving_and_loading(hass): +async def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" - loader.set_component(hass, 'test', MockModule('test')) + loader.set_component( + hass, 'test', + MockModule('test', async_setup_entry=lambda *args: mock_coro(True))) class TestFlow(data_entry_flow.FlowHandler): VERSION = 5 @@ -166,7 +171,7 @@ def async_step_init(self, user_input=None): ) with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - yield from hass.config_entries.flow.async_init('test') + await hass.config_entries.flow.async_init('test') class Test2Flow(data_entry_flow.FlowHandler): VERSION = 3 @@ -180,28 +185,18 @@ def async_step_init(self, user_input=None): } ) - json_path = 'homeassistant.util.json.open' - with patch('homeassistant.config_entries.HANDLERS.get', - return_value=Test2Flow), \ - patch.object(config_entries, 'SAVE_DELAY', 0): - yield from hass.config_entries.flow.async_init('test') - - with patch(json_path, mock_open(), create=True) as mock_write: - # To trigger the call_later - yield from asyncio.sleep(0, loop=hass.loop) - # To execute the save - yield from hass.async_block_till_done() + return_value=Test2Flow): + await hass.config_entries.flow.async_init('test') - # Mock open calls are: open file, context enter, write, context leave - written = mock_write.mock_calls[2][1][0] + # To trigger the call_later + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + # To execute the save + await hass.async_block_till_done() # Now load written data in new config manager manager = config_entries.ConfigEntries(hass, {}) - - with patch('os.path.isfile', return_value=True), \ - patch(json_path, mock_open(read_data=written), create=True): - yield from manager.async_load() + await manager.async_load() # Ensure same order for orig, loaded in zip(hass.config_entries.async_entries(), @@ -304,3 +299,13 @@ async def async_step_discovery(self, user_input=None): await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') assert state is None + + +async def test_loading_default_config(hass): + """Test loading the default config.""" + manager = config_entries.ConfigEntries(hass, {}) + + with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): + await manager.async_load() + + assert len(manager.async_entries()) == 0 diff --git a/tests/test_loader.py b/tests/test_loader.py index c97e94a7ce10f1..d87201fb61bdce 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -124,3 +124,13 @@ async def test_custom_component_name(hass): # Test custom components is mounted from custom_components.test_package import TEST assert TEST == 5 + + +async def test_log_warning_custom_component(hass, caplog): + """Test that we log a warning when loading a custom component.""" + loader.get_component(hass, 'test_standalone') + assert \ + 'You are using a custom component for test_standalone' in caplog.text + + loader.get_component(hass, 'light.test') + assert 'You are using a custom component for light.test' in caplog.text diff --git a/tox.ini b/tox.ini index 8b034346475f54..ca82c83d0fcb8e 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pylint homeassistant + pylint {posargs} homeassistant [testenv:lint] basepython = {env:PYTHON3_PATH:python3} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 0cb49fde54eb90..15504ea57aff2f 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -46,7 +46,7 @@ apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} # This is a list of scripts that install additional dependencies. If you only # need to install a package from the official debian repository, just add it # to the list above. Only create a script if you need compiling, manually -# downloading or a 3th party repository. +# downloading or a 3rd party repository. if [ "$INSTALL_TELLSTICK" == "yes" ]; then virtualization/Docker/scripts/tellstick fi