diff --git a/.coveragerc b/.coveragerc index d2192ca2e46..d361cf2ddad 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,8 @@ source = homeassistant omit = homeassistant/__main__.py homeassistant/scripts/*.py + homeassistant/util/async.py + homeassistant/monkey_patch.py homeassistant/helpers/typing.py homeassistant/helpers/signal.py @@ -127,7 +129,7 @@ omit = homeassistant/components/insteon_local.py homeassistant/components/*/insteon_local.py - homeassistant/components/insteon_plm.py + homeassistant/components/insteon_plm/* homeassistant/components/*/insteon_plm.py homeassistant/components/ios.py @@ -151,6 +153,9 @@ omit = homeassistant/components/knx.py homeassistant/components/*/knx.py + homeassistant/components/konnected.py + homeassistant/components/*/konnected.py + homeassistant/components/lametric.py homeassistant/components/*/lametric.py @@ -226,6 +231,9 @@ omit = homeassistant/components/rpi_pfio.py homeassistant/components/*/rpi_pfio.py + homeassistant/components/sabnzbd.py + homeassistant/components/*/sabnzbd.py + homeassistant/components/satel_integra.py homeassistant/components/*/satel_integra.py @@ -342,6 +350,7 @@ omit = homeassistant/components/calendar/todoist.py homeassistant/components/camera/bloomsky.py homeassistant/components/camera/canary.py + homeassistant/components/camera/familyhub.py homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py @@ -412,7 +421,6 @@ omit = homeassistant/components/emoncms_history.py homeassistant/components/emulated_hue/upnp.py homeassistant/components/fan/mqtt.py - homeassistant/components/feedreader.py homeassistant/components/folder_watcher.py homeassistant/components/foursquare.py homeassistant/components/goalfeed.py @@ -534,7 +542,6 @@ omit = homeassistant/components/notify/rest.py homeassistant/components/notify/rocketchat.py homeassistant/components/notify/sendgrid.py - homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/stride.py @@ -592,6 +599,7 @@ omit = homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fedex.py homeassistant/components/sensor/filesize.py + homeassistant/components/sensor/fints.py homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fixer.py homeassistant/components/sensor/folder.py @@ -650,7 +658,6 @@ omit = homeassistant/components/sensor/radarr.py homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/ripple.py - homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sense.py homeassistant/components/sensor/sensehat.py diff --git a/.travis.yml b/.travis.yml index bf2d05bb185..b089d3f89be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ matrix: env: TOXENV=lint - python: "3.5.3" env: TOXENV=pylint - # - python: "3.5" - # env: TOXENV=typing + - python: "3.5.3" + env: TOXENV=typing - python: "3.5.3" env: TOXENV=py35 - python: "3.6" diff --git a/CODEOWNERS b/CODEOWNERS index 33966d1badb..32639fed43c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 +homeassistant/components/konnected.py @heythisisnate +homeassistant/components/*/konnected.py @heythisisnate homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index deb1746c167..7d3d2d2af88 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -8,7 +8,8 @@ import subprocess import sys import threading -from typing import Optional, List +from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import + from homeassistant import monkey_patch from homeassistant.const import ( @@ -259,7 +260,7 @@ def setup_and_run_hass(config_dir: str, config = { 'frontend': {}, 'demo': {} - } + } # type: Dict[str, Any] hass = bootstrap.from_config_dict( config, config_dir=config_dir, verbose=args.verbose, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 55de9309954..5e434b74ca8 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -15,7 +15,6 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.core import callback from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID -from homeassistant.exceptions import HomeAssistantError from homeassistant.util.decorator import Registry from homeassistant.util import dt as dt_util @@ -36,23 +35,7 @@ ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) DATA_REQS = 'auth_reqs_processed' -class AuthError(HomeAssistantError): - """Generic authentication error.""" - - -class InvalidUser(AuthError): - """Raised when an invalid user has been specified.""" - - -class InvalidPassword(AuthError): - """Raised when an invalid password has been supplied.""" - - -class UnknownError(AuthError): - """When an unknown error occurs.""" - - -def generate_secret(entropy=32): +def generate_secret(entropy: int = 32) -> str: """Generate a secret. Backport of secrets.token_hex from Python 3.6 @@ -69,8 +52,9 @@ class AuthProvider: initialized = False - def __init__(self, store, config): + def __init__(self, hass, store, config): """Initialize an auth provider.""" + self.hass = hass self.store = store self.config = config @@ -210,6 +194,7 @@ class Client: name = attr.ib(type=str) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) secret = attr.ib(type=str, default=attr.Factory(generate_secret)) + redirect_uris = attr.ib(type=list, default=attr.Factory(list)) async def load_auth_provider_module(hass, provider): @@ -283,7 +268,7 @@ async def _auth_provider_from_config(hass, store, config): provider_name, humanize_error(config, err)) return None - return AUTH_PROVIDERS[provider_name](store, config) + return AUTH_PROVIDERS[provider_name](hass, store, config) class AuthManager: @@ -340,9 +325,11 @@ class AuthManager: """Get an access token.""" return self.access_tokens.get(token) - async def async_create_client(self, name): + async def async_create_client(self, name, *, redirect_uris=None, + no_secret=False): """Create a new client.""" - return await self._store.async_create_client(name) + return await self._store.async_create_client( + name, redirect_uris, no_secret) async def async_get_client(self, client_id): """Get a client.""" @@ -360,6 +347,9 @@ class AuthManager: async def _async_finish_login_flow(self, result): """Result of a credential login flow.""" + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + auth_provider = self._providers[result['handler']] return await auth_provider.async_get_or_create_credentials( result['data']) @@ -477,12 +467,20 @@ class AuthStore: return None - async def async_create_client(self, name): + async def async_create_client(self, name, redirect_uris, no_secret): """Create a new client.""" if self.clients is None: await self.async_load() - client = Client(name) + kwargs = { + 'name': name, + 'redirect_uris': redirect_uris + } + + if no_secret: + kwargs['secret'] = None + + client = Client(**kwargs) self.clients[client.id] = client await self.async_save() return client diff --git a/homeassistant/auth_providers/homeassistant.py b/homeassistant/auth_providers/homeassistant.py new file mode 100644 index 00000000000..c2db193ce1a --- /dev/null +++ b/homeassistant/auth_providers/homeassistant.py @@ -0,0 +1,181 @@ +"""Home Assistant auth provider.""" +import base64 +from collections import OrderedDict +import hashlib +import hmac + +import voluptuous as vol + +from homeassistant import auth, data_entry_flow +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import json + + +PATH_DATA = '.users.json' + +CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ +}, extra=vol.PREVENT_EXTRA) + + +class InvalidAuth(HomeAssistantError): + """Raised when we encounter invalid authentication.""" + + +class InvalidUser(HomeAssistantError): + """Raised when invalid user is specified. + + Will not be raised when validating authentication. + """ + + +class Data: + """Hold the user data.""" + + def __init__(self, path, data): + """Initialize the user data store.""" + self.path = path + if data is None: + data = { + 'salt': auth.generate_secret(), + 'users': [] + } + self._data = data + + @property + def users(self): + """Return users.""" + return self._data['users'] + + def validate_login(self, username, password): + """Validate a username and password. + + Raises InvalidAuth if auth invalid. + """ + password = self.hash_password(password) + + found = None + + # Compare all users to avoid timing attacks. + for user in self._data['users']: + if username == user['username']: + found = user + + if found is None: + # Do one more compare to make timing the same as if user was found. + hmac.compare_digest(password, password) + raise InvalidAuth + + if not hmac.compare_digest(password, + base64.b64decode(found['password'])): + raise InvalidAuth + + def hash_password(self, password, for_storage=False): + """Encode a password.""" + hashed = hashlib.pbkdf2_hmac( + 'sha512', password.encode(), self._data['salt'].encode(), 100000) + if for_storage: + hashed = base64.b64encode(hashed).decode() + return hashed + + def add_user(self, username, password): + """Add a user.""" + if any(user['username'] == username for user in self.users): + raise InvalidUser + + self.users.append({ + 'username': username, + 'password': self.hash_password(password, True), + }) + + def change_password(self, username, new_password): + """Update the password of a user. + + Raises InvalidUser if user cannot be found. + """ + for user in self.users: + if user['username'] == username: + user['password'] = self.hash_password(new_password, True) + break + else: + raise InvalidUser + + def 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)) + + +@auth.AUTH_PROVIDERS.register('homeassistant') +class HassAuthProvider(auth.AuthProvider): + """Auth provider based on a local storage of users in HASS config dir.""" + + DEFAULT_TITLE = 'Home Assistant Local' + + async def async_credential_flow(self): + """Return a flow to login.""" + return LoginFlow(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) + + async def async_get_or_create_credentials(self, flow_result): + """Get credentials based on the flow result.""" + username = flow_result['username'] + + for credential in await self.async_credentials(): + if credential.data['username'] == username: + return credential + + # Create new credentials. + return self.async_create_credentials({ + '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.""" + + 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: + await self._auth_provider.async_validate_login( + user_input['username'], user_input['password']) + except InvalidAuth: + errors['base'] = 'invalid_auth' + + if not errors: + return self.async_create_entry( + title=self._auth_provider.name, + data=user_input + ) + + schema = OrderedDict() + schema['username'] = str + schema['password'] = str + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/homeassistant/auth_providers/insecure_example.py b/homeassistant/auth_providers/insecure_example.py index 8538e8c2f3e..a8e8cd0cb0e 100644 --- a/homeassistant/auth_providers/insecure_example.py +++ b/homeassistant/auth_providers/insecure_example.py @@ -4,6 +4,7 @@ import hmac import voluptuous as vol +from homeassistant.exceptions import HomeAssistantError from homeassistant import auth, data_entry_flow from homeassistant.core import callback @@ -20,6 +21,10 @@ CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA) +class InvalidAuthError(HomeAssistantError): + """Raised when submitting invalid authentication.""" + + @auth.AUTH_PROVIDERS.register('insecure_example') class ExampleAuthProvider(auth.AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" @@ -43,18 +48,15 @@ class ExampleAuthProvider(auth.AuthProvider): # Do one more compare to make timing the same as if user was found. hmac.compare_digest(password.encode('utf-8'), password.encode('utf-8')) - raise auth.InvalidUser + raise InvalidAuthError if not hmac.compare_digest(user['password'].encode('utf-8'), password.encode('utf-8')): - raise auth.InvalidPassword + raise InvalidAuthError async def async_get_or_create_credentials(self, flow_result): """Get credentials based on the flow result.""" username = flow_result['username'] - password = flow_result['password'] - - self.async_validate_login(username, password) for credential in await self.async_credentials(): if credential.data['username'] == username: @@ -96,7 +98,7 @@ class LoginFlow(data_entry_flow.FlowHandler): try: self._auth_provider.async_validate_login( user_input['username'], user_input['password']) - except (auth.InvalidUser, auth.InvalidPassword): + except InvalidAuthError: errors['base'] = 'invalid_auth' if not errors: diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 826cc563e82..a405362d368 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -278,7 +278,8 @@ def async_enable_logging(hass: core.HomeAssistant, if log_rotate_days: err_handler = logging.handlers.TimedRotatingFileHandler( - err_log_path, when='midnight', backupCount=log_rotate_days) + err_log_path, when='midnight', + backupCount=log_rotate_days) # type: logging.FileHandler else: err_handler = logging.FileHandler( err_log_path, mode='w', delay=True) @@ -297,7 +298,7 @@ def async_enable_logging(hass: core.HomeAssistant, EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) logger = logging.getLogger('') - logger.addHandler(async_handler) + logger.addHandler(async_handler) # type: ignore logger.setLevel(logging.INFO) # Save the log file location for access by other components. diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 83e05dae641..dc34006ad03 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -356,7 +356,8 @@ class APIErrorLog(HomeAssistantView): async def get(self, request): """Retrieve API error log.""" - return await self.file(request, request.app['hass'].data[DATA_LOGGING]) + return web.FileResponse( + request.app['hass'].data[DATA_LOGGING]) async def async_services_json(hass): diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index d4b4b0f4591..0f7295a41e0 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -144,7 +144,7 @@ class AuthProvidersView(HomeAssistantView): requires_auth = False @verify_client - async def get(self, request, client_id): + async def get(self, request, client): """Get available auth providers.""" return self.json([{ 'name': provider.name, @@ -166,8 +166,15 @@ class LoginFlowIndexView(FlowManagerIndexView): # pylint: disable=arguments-differ @verify_client - async def post(self, request, client_id): + @RequestDataValidator(vol.Schema({ + vol.Required('handler'): vol.Any(str, list), + vol.Required('redirect_uri'): str, + })) + async def post(self, request, client, data): """Create a new login flow.""" + if data['redirect_uri'] not in client.redirect_uris: + return self.json_message('invalid redirect uri', ) + # pylint: disable=no-value-for-parameter return await super().post(request) @@ -192,7 +199,7 @@ class LoginFlowResourceView(FlowManagerResourceView): # pylint: disable=arguments-differ @verify_client @RequestDataValidator(vol.Schema(dict), allow_empty=True) - async def post(self, request, client_id, flow_id, data): + async def post(self, request, client, flow_id, data): """Handle progressing a login flow request.""" try: result = await self._flow_mgr.async_configure(flow_id, data) @@ -205,7 +212,7 @@ class LoginFlowResourceView(FlowManagerResourceView): return self.json(self._prepare_result_json(result)) result.pop('data') - result['result'] = self._store_credentials(client_id, result['result']) + result['result'] = self._store_credentials(client.id, result['result']) return self.json(result) @@ -222,7 +229,7 @@ class GrantTokenView(HomeAssistantView): self._retrieve_credentials = retrieve_credentials @verify_client - async def post(self, request, client_id): + async def post(self, request, client): """Grant a token.""" hass = request.app['hass'] data = await request.post() @@ -230,11 +237,11 @@ class GrantTokenView(HomeAssistantView): if grant_type == 'authorization_code': return await self._async_handle_auth_code( - hass, client_id, data) + hass, client.id, data) elif grant_type == 'refresh_token': return await self._async_handle_refresh_token( - hass, client_id, data) + hass, client.id, data) return self.json({ 'error': 'unsupported_grant_type', diff --git a/homeassistant/components/auth/client.py b/homeassistant/components/auth/client.py index 28d72aefe0f..122c3032188 100644 --- a/homeassistant/components/auth/client.py +++ b/homeassistant/components/auth/client.py @@ -11,15 +11,15 @@ def verify_client(method): @wraps(method) async def wrapper(view, request, *args, **kwargs): """Verify client id/secret before doing request.""" - client_id = await _verify_client(request) + client = await _verify_client(request) - if client_id is None: + if client is None: return view.json({ 'error': 'invalid_client', }, status_code=401) return await method( - view, request, *args, client_id=client_id, **kwargs) + view, request, *args, **kwargs, client=client) return wrapper @@ -46,18 +46,34 @@ async def _verify_client(request): client_id, client_secret = decoded.split(':', 1) except ValueError: # If no ':' in decoded - return None + client_id, client_secret = decoded, None - client = await request.app['hass'].auth.async_get_client(client_id) + return await async_secure_get_client( + request.app['hass'], client_id, client_secret) + + +async def async_secure_get_client(hass, client_id, client_secret): + """Get a client id/secret in consistent time.""" + client = await hass.auth.async_get_client(client_id) if client is None: - # Still do a compare so we run same time as if a client was found. - hmac.compare_digest(client_secret.encode('utf-8'), - client_secret.encode('utf-8')) + if client_secret is not None: + # Still do a compare so we run same time as if a client was found. + hmac.compare_digest(client_secret.encode('utf-8'), + client_secret.encode('utf-8')) return None - if hmac.compare_digest(client_secret.encode('utf-8'), - client.secret.encode('utf-8')): - return client_id + if client.secret is None: + return client + + elif client_secret is None: + # Still do a compare so we run same time as if a secret was passed. + hmac.compare_digest(client.secret.encode('utf-8'), + client.secret.encode('utf-8')) + return None + + elif hmac.compare_digest(client_secret.encode('utf-8'), + client.secret.encode('utf-8')): + return client return None diff --git a/homeassistant/components/binary_sensor/bayesian.py b/homeassistant/components/binary_sensor/bayesian.py index f3dbc912ade..72110eb50c9 100644 --- a/homeassistant/components/binary_sensor/bayesian.py +++ b/homeassistant/components/binary_sensor/bayesian.py @@ -217,4 +217,4 @@ class BayesianBinarySensor(BinarySensorDevice): @asyncio.coroutine def async_update(self): """Get the latest data and update the states.""" - self._deviation = bool(self.probability > self._probability_threshold) + self._deviation = bool(self.probability >= self._probability_threshold) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 0abf6eb1064..e214610f46d 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -17,9 +17,19 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'lids': ['Doors', 'opening'], 'windows': ['Windows', 'opening'], - 'door_lock_state': ['Door lock state', 'safety'] + 'door_lock_state': ['Door lock state', 'safety'], + 'lights_parking': ['Parking lights', 'light'], + 'condition_based_services': ['Condition based services', 'problem'], + 'check_control_messages': ['Control messages', 'problem'] } +SENSOR_TYPES_ELEC = { + 'charging_status': ['Charging status', 'power'], + 'connection_status': ['Connection status', 'plug'] +} + +SENSOR_TYPES_ELEC.update(SENSOR_TYPES) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BMW sensors.""" @@ -29,10 +39,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for account in accounts: for vehicle in account.account.vehicles: - for key, value in sorted(SENSOR_TYPES.items()): - device = BMWConnectedDriveSensor(account, vehicle, key, - value[0], value[1]) - devices.append(device) + if vehicle.has_hv_battery: + _LOGGER.debug('BMW with a high voltage battery') + for key, value in sorted(SENSOR_TYPES_ELEC.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) + devices.append(device) + elif vehicle.has_internal_combustion_engine: + _LOGGER.debug('BMW with an internal combustion engine') + for key, value in sorted(SENSOR_TYPES.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) + devices.append(device) add_devices(devices, True) @@ -92,12 +110,34 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result[window.name] = window.state.value elif self._attribute == 'door_lock_state': result['door_lock_state'] = vehicle_state.door_lock_state.value + result['last_update_reason'] = vehicle_state.last_update_reason + elif self._attribute == 'lights_parking': + result['lights_parking'] = vehicle_state.parking_lights.value + elif self._attribute == 'condition_based_services': + for report in vehicle_state.condition_based_services: + result.update(self._format_cbs_report(report)) + elif self._attribute == 'check_control_messages': + check_control_messages = vehicle_state.check_control_messages + if not check_control_messages: + result['check_control_messages'] = 'OK' + else: + result['check_control_messages'] = check_control_messages + elif self._attribute == 'charging_status': + result['charging_status'] = vehicle_state.charging_status.value + # pylint: disable=W0212 + result['last_charging_end_result'] = \ + vehicle_state._attributes['lastChargingEndResult'] + if self._attribute == 'connection_status': + # pylint: disable=W0212 + result['connection_status'] = \ + vehicle_state._attributes['connectionStatus'] - return result + return sorted(result.items()) def update(self): """Read new state data from the library.""" from bimmer_connected.state import LockState + from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state # device class opening: On means open, Off means closed @@ -111,6 +151,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice): # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED self._state = vehicle_state.door_lock_state not in \ [LockState.LOCKED, LockState.SECURED] + # device class light: On means light detected, Off means no light + if self._attribute == 'lights_parking': + self._state = vehicle_state.are_parking_lights_on + # device class problem: On means problem detected, Off means no problem + if self._attribute == 'condition_based_services': + self._state = not vehicle_state.are_all_cbs_ok + if self._attribute == 'check_control_messages': + self._state = vehicle_state.has_check_control_messages + # device class power: On means power detected, Off means no power + if self._attribute == 'charging_status': + self._state = vehicle_state.charging_status in \ + [ChargingState.CHARGING] + # device class plug: On means device is plugged in, + # Off means device is unplugged + if self._attribute == 'connection_status': + # pylint: disable=W0212 + self._state = (vehicle_state._attributes['connectionStatus'] == + 'CONNECTED') + + @staticmethod + def _format_cbs_report(report): + result = {} + service_type = report.service_type.lower().replace('_', ' ') + result['{} status'.format(service_type)] = report.state.value + if report.due_date is not None: + result['{} date'.format(service_type)] = \ + report.due_date.strftime('%Y-%m-%d') + if report.due_distance is not None: + result['{} distance'.format(service_type)] = \ + '{} km'.format(report.due_distance) + return result def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py new file mode 100644 index 00000000000..40ffe498402 --- /dev/null +++ b/homeassistant/components/binary_sensor/homematicip_cloud.py @@ -0,0 +1,85 @@ +""" +Support for HomematicIP binary sensor. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_WINDOW_STATE = 'window_state' +ATTR_EVENT_DELAY = 'event_delay' +ATTR_MOTION_DETECTED = 'motion_detected' +ATTR_ILLUMINATION = 'illumination' + +HMIP_OPEN = 'open' + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP binary sensor devices.""" + from homematicip.device import (ShutterContact, MotionDetectorIndoor) + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [] + for device in home.devices: + if isinstance(device, ShutterContact): + devices.append(HomematicipShutterContact(home, device)) + elif isinstance(device, MotionDetectorIndoor): + devices.append(HomematicipMotionDetector(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): + """HomematicIP shutter contact.""" + + def __init__(self, home, device): + """Initialize the shutter contact.""" + super().__init__(home, device) + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'door' + + @property + def is_on(self): + """Return true if the shutter contact is on/open.""" + if self._device.sabotage: + return True + if self._device.windowState is None: + return None + return self._device.windowState.lower() == HMIP_OPEN + + +class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): + """MomematicIP motion detector.""" + + def __init__(self, home, device): + """Initialize the shutter contact.""" + super().__init__(home, device) + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'motion' + + @property + def is_on(self): + """Return true if motion is detected.""" + if self._device.sabotage: + return True + return self._device.motionDetected diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index fb86244acf3..09f1739cba7 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if _is_val_unknown(self._node.status._val): self._computed_state = None + self._status_was_unknown = True else: self._computed_state = bool(self._node.status._val) + self._status_was_unknown = False @asyncio.coroutine def async_added_to_hass(self) -> None: @@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if not _is_val_unknown(self._negative_node.status._val): # If the negative node has a value, it means the negative node is - # in use for this device. Therefore, we cannot determine the state - # of the sensor until we receive our first ON event. - self._computed_state = None + # in use for this device. Next we need to check to see if the + # negative and positive nodes disagree on the state (both ON or + # both OFF). + if self._negative_node.status._val == self._node.status._val: + # The states disagree, therefore we cannot determine the state + # of the sensor until we receive our first ON event. + self._computed_state = None def _negative_node_control_handler(self, event: object) -> None: """Handle an "On" control event from the "negative" node.""" @@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self.schedule_update_ha_state() self._heartbeat() - # pylint: disable=unused-argument def on_update(self, event: object) -> None: - """Ignore primary node status updates. + """Primary node status updates. - We listen directly to the Control events on all nodes for this - device. + We MOSTLY ignore these updates, as we listen directly to the Control + events on all nodes for this device. However, there is one edge case: + If a leak sensor is unknown, due to a recent reboot of the ISY, the + status will get updated to dry upon the first heartbeat. This status + update is the only way that a leak sensor's status changes without + an accompanying Control event, so we need to watch for it. """ - pass + if self._status_was_unknown and self._computed_state is None: + self._computed_state = bool(int(self._node.status)) + self._status_was_unknown = False + self.schedule_update_ha_state() + self._heartbeat() @property def value(self) -> object: diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py new file mode 100644 index 00000000000..9a16ca5e1ab --- /dev/null +++ b/homeassistant/components/binary_sensor/konnected.py @@ -0,0 +1,82 @@ +""" +Support for wired binary sensors attached to a Konnected device. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.konnected/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.konnected import ( + DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE) +from homeassistant.const import ( + CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_ENTITY_ID, + ATTR_STATE) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up binary sensors attached to a Konnected device.""" + if discovery_info is None: + return + + data = hass.data[KONNECTED_DOMAIN] + device_id = discovery_info['device_id'] + sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) + for pin_num, pin_data in + data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()] + async_add_devices(sensors) + + +class KonnectedBinarySensor(BinarySensorDevice): + """Representation of a Konnected binary sensor.""" + + def __init__(self, device_id, pin_num, data): + """Initialize the binary sensor.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._state = self._data.get(ATTR_STATE) + self._device_class = self._data.get(CONF_TYPE) + self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format( + device_id, PIN_TO_ZONE[pin_num])) + _LOGGER.debug('Created new Konnected sensor: %s', self._name) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._state + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + async def async_added_to_hass(self): + """Store entity_id and register state change callback.""" + self._data[ATTR_ENTITY_ID] = self.entity_id + async_dispatcher_connect( + self.hass, SIGNAL_SENSOR_UPDATE.format(self.entity_id), + self.async_set_state) + + @callback + def async_set_state(self, state): + """Update the sensor's state.""" + self._state = state + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/mychevy.py b/homeassistant/components/binary_sensor/mychevy.py index a89395ed86f..905e60c34d9 100644 --- a/homeassistant/components/binary_sensor/mychevy.py +++ b/homeassistant/components/binary_sensor/mychevy.py @@ -31,7 +31,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): sensors = [] hub = hass.data[MYCHEVY_DOMAIN] for sconfig in SENSORS: - sensors.append(EVBinarySensor(hub, sconfig)) + for car in hub.cars: + sensors.append(EVBinarySensor(hub, sconfig, car.vid)) async_add_devices(sensors) @@ -45,16 +46,18 @@ class EVBinarySensor(BinarySensorDevice): """ - def __init__(self, connection, config): + def __init__(self, connection, config, car_vid): """Initialize sensor with car connection.""" self._conn = connection self._name = config.name self._attr = config.attr self._type = config.device_class self._is_on = None - + self._car_vid = car_vid self.entity_id = ENTITY_ID_FORMAT.format( - '{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name))) + '{}_{}_{}'.format(MYCHEVY_DOMAIN, + slugify(self._car.name), + slugify(self._name))) @property def name(self): @@ -66,6 +69,11 @@ class EVBinarySensor(BinarySensorDevice): """Return if on.""" return self._is_on + @property + def _car(self): + """Return the car.""" + return self._conn.get_car(self._car_vid) + @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" @@ -75,8 +83,8 @@ class EVBinarySensor(BinarySensorDevice): @callback def async_update_callback(self): """Update state.""" - if self._conn.car is not None: - self._is_on = getattr(self._conn.car, self._attr, None) + if self._car is not None: + self._is_on = getattr(self._car, self._attr, None) self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py index 8c026131fd3..6ac604a4f1e 100644 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ b/homeassistant/components/binary_sensor/rfxtrx.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import rfxtrx from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice) + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.components.rfxtrx import ( ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_OFF_DELAY) @@ -29,8 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema({ vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): - DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_OFF_DELAY): vol.Any(cv.time_period, cv.positive_timedelta), diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 49f716b9eb7..1c0b903d868 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'channel_1', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Both)', 'dual_channel', hass, gateway)) - elif model in ['cube', 'sensor_cube']: + elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']: devices.append(XiaomiCube(device, hass, gateway)) add_devices(devices) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 756323f41d9..d3b31188760 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -108,7 +108,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice): @property def is_on(self) -> bool: """Return True if entity is on.""" - if self._state == 'unknown': + if self._state is None: return False return bool(self._state) @@ -133,7 +133,8 @@ class BinarySensor(zha.Entity, BinarySensorDevice): from bellows.types.basic import uint16_t result = await zha.safe_read(self._endpoint.ias_zone, - ['zone_status']) + ['zone_status'], + allow_cache=False) state = result.get('zone_status', self._state) if isinstance(state, (int, uint16_t)): self._state = result.get('zone_status', self._state) & 3 @@ -218,7 +219,10 @@ class Switch(zha.Entity, BinarySensorDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - return {'level': self._state and self._level or 0} + self._device_state_attributes.update({ + 'level': self._state and self._level or 0 + }) + return self._device_state_attributes def move_level(self, change): """Increment the level, setting state if appropriate.""" diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 347bab6f529..a7ed262ac2c 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['bimmer_connected==0.5.0'] +REQUIREMENTS = ['bimmer_connected==0.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml index 3c180271919..b9605429a8e 100644 --- a/homeassistant/components/bmw_connected_drive/services.yaml +++ b/homeassistant/components/bmw_connected_drive/services.yaml @@ -27,7 +27,7 @@ activate_air_conditioning: description: > Start the air conditioning of the vehicle. What exactly is started here depends on the type of vehicle. It might range from just ventilation over - auxilary heating to real air conditioning. The vehicle is identified via + auxiliary heating to real air conditioning. The vehicle is identified via the vin (see below). fields: vin: @@ -39,4 +39,4 @@ update_state: description: > Fetch the last state of the vehicles of all your accounts from the BMW server. This does *not* trigger an update from the vehicle, it just gets - the data from the BMW servers. This service does not require any attributes. \ No newline at end of file + the data from the BMW servers. This service does not require any attributes. diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index c1f92965198..60f8979bb16 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -256,6 +256,11 @@ class Camera(Entity): """Return the camera model.""" return None + @property + def frame_interval(self): + """Return the interval between frames of the mjpeg stream.""" + return 0.5 + def camera_image(self): """Return bytes of camera image.""" raise NotImplementedError() @@ -272,10 +277,6 @@ class Camera(Entity): This method must be run in the event loop. """ - if interval < MIN_STREAM_INTERVAL: - raise ValueError("Stream interval must be be > {}" - .format(MIN_STREAM_INTERVAL)) - response = web.StreamResponse() response.content_type = ('multipart/x-mixed-replace; ' 'boundary=--frameboundary') @@ -325,8 +326,7 @@ class Camera(Entity): a direct stream from the camera. This method must be run in the event loop. """ - await self.handle_async_still_stream(request, - FALLBACK_STREAM_INTERVAL) + await self.handle_async_still_stream(request, self.frame_interval) @property def state(self): @@ -448,6 +448,9 @@ class CameraMjpegStream(CameraView): try: # Compose camera stream from stills interval = float(request.query.get('interval')) + if interval < MIN_STREAM_INTERVAL: + raise ValueError("Stream interval must be be > {}" + .format(MIN_STREAM_INTERVAL)) await camera.handle_async_still_stream(request, interval) return except ValueError: diff --git a/homeassistant/components/camera/familyhub.py b/homeassistant/components/camera/familyhub.py new file mode 100644 index 00000000000..e78d341713b --- /dev/null +++ b/homeassistant/components/camera/familyhub.py @@ -0,0 +1,58 @@ +""" +Family Hub camera for Samsung Refrigerators. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/camera.familyhub/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.camera import Camera +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['python-family-hub-local==0.0.2'] + +DEFAULT_NAME = 'FamilyHub Camera' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the Family Hub Camera.""" + from pyfamilyhublocal import FamilyHubCam + address = config.get(CONF_IP_ADDRESS) + name = config.get(CONF_NAME) + + session = async_get_clientsession(hass) + family_hub_cam = FamilyHubCam(address, hass.loop, session) + + async_add_devices([FamilyHubCamera(name, family_hub_cam)], True) + + +class FamilyHubCamera(Camera): + """The representation of a Family Hub camera.""" + + def __init__(self, name, family_hub_cam): + """Initialize camera component.""" + super().__init__() + self._name = name + self.family_hub_cam = family_hub_cam + + async def async_camera_image(self): + """Return a still image response.""" + return await self.family_hub_cam.async_get_cam_image() + + @property + def name(self): + """Return the name of this camera.""" + return self._name diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 2f5d8d28979..e11bd599e45 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) CONF_CONTENT_TYPE = 'content_type' CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change' CONF_STILL_IMAGE_URL = 'still_image_url' +CONF_FRAMERATE = 'framerate' DEFAULT_NAME = 'Generic Camera' @@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string, + vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, }) @@ -62,6 +64,7 @@ class GenericCamera(Camera): self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] + self._frame_interval = 1 / device_info[CONF_FRAMERATE] self.content_type = device_info[CONF_CONTENT_TYPE] username = device_info.get(CONF_USERNAME) @@ -78,6 +81,11 @@ class GenericCamera(Camera): self._last_url = None self._last_image = None + @property + def frame_interval(self): + """Return the interval between frames of the mjpeg stream.""" + return self._frame_interval + def camera_image(self): """Return bytes of camera image.""" return run_coroutine_threadsafe( diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 2545094ceec..9fab56c61ac 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -115,7 +115,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): """List of available fan modes.""" return ['Auto', 'Min', 'Normal', 'Max'] - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" set_req = self.gateway.const.SetReq temp = kwargs.get(ATTR_TEMPERATURE) @@ -143,9 +143,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[value_type] = value - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -153,9 +153,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[set_req.V_HVAC_SPEED] = fan_mode - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode): """Set new target temperature.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, @@ -163,7 +163,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[self.value_type] = operation_mode - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index e2a455aefc7..2b92d050d3b 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -25,7 +25,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.temperature import convert as convert_temperature -REQUIREMENTS = ['pysensibo==1.0.2'] +REQUIREMENTS = ['pysensibo==1.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py index 6e63cc4092b..c2b82e1cc84 100644 --- a/homeassistant/components/climate/venstar.py +++ b/homeassistant/components/climate/venstar.py @@ -11,9 +11,11 @@ import voluptuous as vol from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE, + SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW, + SUPPORT_HOLD_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, + ClimateDevice) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS, @@ -27,14 +29,20 @@ _LOGGER = logging.getLogger(__name__) ATTR_FAN_STATE = 'fan_state' ATTR_HVAC_STATE = 'hvac_state' +CONF_HUMIDIFIER = 'humidifier' + DEFAULT_SSL = False VALID_FAN_STATES = [STATE_ON, STATE_AUTO] VALID_THERMOSTAT_MODES = [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_AUTO] +HOLD_MODE_OFF = 'off' +HOLD_MODE_TEMPERATURE = 'temperature' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HUMIDIFIER, default=True): cv.boolean, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_TIMEOUT, default=5): vol.All(vol.Coerce(int), vol.Range(min=1)), @@ -50,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) timeout = config.get(CONF_TIMEOUT) + humidifier = config.get(CONF_HUMIDIFIER) if config.get(CONF_SSL): proto = 'https' @@ -60,15 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): addr=host, timeout=timeout, user=username, password=password, proto=proto) - add_devices([VenstarThermostat(client)], True) + add_devices([VenstarThermostat(client, humidifier)], True) class VenstarThermostat(ClimateDevice): """Representation of a Venstar thermostat.""" - def __init__(self, client): + def __init__(self, client, humidifier): """Initialize the thermostat.""" self._client = client + self._humidifier = humidifier def update(self): """Update the data from the thermostat.""" @@ -81,14 +91,18 @@ class VenstarThermostat(ClimateDevice): def supported_features(self): """Return the list of supported features.""" features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE) + SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE | + SUPPORT_HOLD_MODE) if self._client.mode == self._client.MODE_AUTO: features |= (SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW) - if self._client.hum_active == 1: - features |= SUPPORT_TARGET_HUMIDITY + if (self._humidifier and + hasattr(self._client, 'hum_active')): + features |= (SUPPORT_TARGET_HUMIDITY | + SUPPORT_TARGET_HUMIDITY_HIGH | + SUPPORT_TARGET_HUMIDITY_LOW) return features @@ -197,6 +211,18 @@ class VenstarThermostat(ClimateDevice): """Return the maximum humidity. Hardcoded to 60 in API.""" return 60 + @property + def is_away_mode_on(self): + """Return the status of away mode.""" + return self._client.away == self._client.AWAY_AWAY + + @property + def current_hold_mode(self): + """Return the status of hold mode.""" + if self._client.schedule == 0: + return HOLD_MODE_TEMPERATURE + return HOLD_MODE_OFF + def _set_operation_mode(self, operation_mode): """Change the operation mode (internal).""" if operation_mode == STATE_HEAT: @@ -259,3 +285,30 @@ class VenstarThermostat(ClimateDevice): if not success: _LOGGER.error("Failed to change the target humidity level") + + def set_hold_mode(self, hold_mode): + """Set the hold mode.""" + if hold_mode == HOLD_MODE_TEMPERATURE: + success = self._client.set_schedule(0) + elif hold_mode == HOLD_MODE_OFF: + success = self._client.set_schedule(1) + else: + _LOGGER.error("Unknown hold mode: %s", hold_mode) + success = False + + if not success: + _LOGGER.error("Failed to change the schedule/hold state") + + def turn_away_mode_on(self): + """Activate away mode.""" + success = self._client.set_away(self._client.AWAY_AWAY) + + if not success: + _LOGGER.error("Failed to activate away mode") + + def turn_away_mode_off(self): + """Deactivate away mode.""" + success = self._client.set_away(self._client.AWAY_HOME) + + if not success: + _LOGGER.error("Failed to deactivate away mode") diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 8c66567a4aa..c67e032c149 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -190,7 +190,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): @property def cool_on(self): """Return whether or not the heat is actually heating.""" - return self.wink.heat_on() + return self.wink.cool_on() @property def current_operation(self): diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation/__init__.py similarity index 99% rename from homeassistant/components/conversation.py rename to homeassistant/components/conversation/__init__.py index ddd96c99177..9cb00a84583 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation/__init__.py @@ -96,6 +96,7 @@ async def async_setup(hass, config): async def process(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] + _LOGGER.debug('Processing: <%s>', text) try: await _process(hass, text) except intent.IntentHandleError as err: diff --git a/homeassistant/components/conversation/services.yaml b/homeassistant/components/conversation/services.yaml new file mode 100644 index 00000000000..a1b980d8e05 --- /dev/null +++ b/homeassistant/components/conversation/services.yaml @@ -0,0 +1,10 @@ +# Describes the format for available component services + +process: + description: Launch a conversation from a transcribed text. + fields: + text: + description: Transcribed text + example: Turn all lights on + + diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py index 688df62ca6a..2b91591e71b 100644 --- a/homeassistant/components/cover/gogogate2.py +++ b/homeassistant/components/cover/gogogate2.py @@ -1,5 +1,5 @@ """ -Support for Gogogate2 Garage Doors. +Support for Gogogate2 garage Doors. For more details about this platform, please refer to the documentation https://home-assistant.io/components/cover.gogogate2/ @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pygogogate2==0.0.7'] +REQUIREMENTS = ['pygogogate2==0.1.1'] _LOGGER = logging.getLogger(__name__) @@ -25,9 +25,9 @@ NOTIFICATION_ID = 'gogogate2_notification' NOTIFICATION_TITLE = 'Gogogate2 Cover Setup' COVER_SCHEMA = vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -36,10 +36,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Gogogate2 component.""" from pygogogate2 import Gogogate2API as pygogogate2 - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) ip_address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) + password = config.get(CONF_PASSWORD) + username = config.get(CONF_USERNAME) + mygogogate2 = pygogogate2(username, password, ip_address) try: diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 669a7ce6723..3f8eb054710 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -42,7 +42,7 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): set_req = self.gateway.const.SetReq return self._values.get(set_req.V_DIMMER) - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Move the cover up.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -53,9 +53,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): self._values[set_req.V_DIMMER] = 100 else: self._values[set_req.V_LIGHT] = STATE_ON - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Move the cover down.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -66,9 +66,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): self._values[set_req.V_DIMMER] = 0 else: self._values[set_req.V_LIGHT] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs.get(ATTR_POSITION) set_req = self.gateway.const.SetReq @@ -77,9 +77,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): if self.gateway.optimistic: # Optimistically assume that cover has changed state. self._values[set_req.V_DIMMER] = position - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the device.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index 20625143daf..cf8b7dfad48 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -79,7 +79,9 @@ class TahomaCover(TahomaDevice, CoverDevice): if self.tahoma_device.type == \ 'io:RollerShutterWithLowSpeedManagementIOComponent': self.apply_action('setPosition', 'secured') - elif self.tahoma_device.type == 'rts:BlindRTSComponent': + elif self.tahoma_device.type in \ + ('rts:BlindRTSComponent', + 'io:ExteriorVenetianBlindIOComponent'): self.apply_action('my') else: self.apply_action('stopIdentify') diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 47573be6add..bbab4029d7e 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ from .const import ( CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==37'] +REQUIREMENTS = ['pydeconz==38'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index e1dd52a28ea..580c0272e46 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID, - CONF_ICON, ATTR_ICON) + CONF_ICON, ATTR_ICON, ATTR_NAME) _LOGGER = logging.getLogger(__name__) @@ -71,7 +71,6 @@ ATTR_GPS = 'gps' ATTR_HOST_NAME = 'host_name' ATTR_LOCATION_NAME = 'location_name' ATTR_MAC = 'mac' -ATTR_NAME = 'name' ATTR_SOURCE_TYPE = 'source_type' ATTR_CONSIDER_HOME = 'consider_home' diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 1d0058ed229..3bf0cb0e126 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -12,14 +12,18 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, SOURCE_TYPE_GPS) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, ATTR_ID from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify + +REQUIREMENTS = ['locationsharinglib==2.0.2'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['locationsharinglib==1.2.2'] +ATTR_ADDRESS = 'address' +ATTR_FULL_NAME = 'full_name' +ATTR_LAST_SEEN = 'last_seen' +ATTR_NICKNAME = 'nickname' CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' @@ -60,19 +64,23 @@ class GoogleMapsScanner(object): self.success_init = True except InvalidUser: - _LOGGER.error('You have specified invalid login credentials') + _LOGGER.error("You have specified invalid login credentials") self.success_init = False def _update_info(self, now=None): for person in self.service.get_all_people(): - dev_id = 'google_maps_{0}'.format(slugify(person.id)) + try: + dev_id = 'google_maps_{0}'.format(person.id) + except TypeError: + _LOGGER.warning("No location(s) shared with this account") + return attrs = { - 'id': person.id, - 'nickname': person.nickname, - 'full_name': person.full_name, - 'last_seen': person.datetime, - 'address': person.address + ATTR_ADDRESS: person.address, + ATTR_FULL_NAME: person.full_name, + ATTR_ID: person.id, + ATTR_LAST_SEEN: person.datetime, + ATTR_NICKNAME: person.nickname, } self.see( dev_id=dev_id, @@ -80,5 +88,5 @@ class GoogleMapsScanner(object): picture=person.picture_url, source_type=SOURCE_TYPE_GPS, gps_accuracy=person.accuracy, - attributes=attrs + attributes=attrs, ) diff --git a/homeassistant/components/device_tracker/hitron_coda.py b/homeassistant/components/device_tracker/hitron_coda.py index aa437eeef86..72817ca695c 100644 --- a/homeassistant/components/device_tracker/hitron_coda.py +++ b/homeassistant/components/device_tracker/hitron_coda.py @@ -14,15 +14,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE ) _LOGGER = logging.getLogger(__name__) +DEFAULT_TYPE = "rogers" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): cv.string, }) @@ -49,6 +52,11 @@ class HitronCODADeviceScanner(DeviceScanner): self._username = config.get(CONF_USERNAME) self._password = config.get(CONF_PASSWORD) + if config.get(CONF_TYPE) == "shaw": + self._type = 'pwd' + else: + self._type = 'pws' + self._userid = None self.success_init = self._update_info() @@ -74,7 +82,7 @@ class HitronCODADeviceScanner(DeviceScanner): try: data = [ ('user', self._username), - ('pws', self._password), + (self._type, self._password), ] res = requests.post(self._loginurl, data=data, timeout=10) except requests.exceptions.Timeout: diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 5d40f5d533a..8ea81e88440 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -24,8 +24,9 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pyicloud==0.9.1'] -CONF_IGNORED_DEVICES = 'ignored_devices' CONF_ACCOUNTNAME = 'account_name' +CONF_MAX_INTERVAL = 'max_interval' +CONF_GPS_ACCURACY_THRESHOLD = 'gps_accuracy_threshold' # entity attributes ATTR_ACCOUNTNAME = 'account_name' @@ -64,13 +65,15 @@ DEVICESTATUSCODES = { SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]), vol.Optional(ATTR_DEVICENAME): cv.slugify, - vol.Optional(ATTR_INTERVAL): cv.positive_int, + vol.Optional(ATTR_INTERVAL): cv.positive_int }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(ATTR_ACCOUNTNAME): cv.slugify, + vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int, + vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int }) @@ -79,8 +82,11 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0])) + max_interval = config.get(CONF_MAX_INTERVAL) + gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD) - icloudaccount = Icloud(hass, username, password, account, see) + icloudaccount = Icloud(hass, username, password, account, max_interval, + gps_accuracy_threshold, see) if icloudaccount.api is not None: ICLOUDTRACKERS[account] = icloudaccount @@ -96,6 +102,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): for account in accounts: if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].lost_iphone(devicename) + hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone, schema=SERVICE_SCHEMA) @@ -106,6 +113,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): for account in accounts: if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].update_icloud(devicename) + hass.services.register(DOMAIN, 'icloud_update', update_icloud, schema=SERVICE_SCHEMA) @@ -115,6 +123,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): for account in accounts: if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].reset_account_icloud() + hass.services.register(DOMAIN, 'icloud_reset_account', reset_account_icloud, schema=SERVICE_SCHEMA) @@ -137,7 +146,8 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): class Icloud(DeviceScanner): """Representation of an iCloud account.""" - def __init__(self, hass, username, password, name, see): + def __init__(self, hass, username, password, name, max_interval, + gps_accuracy_threshold, see): """Initialize an iCloud account.""" self.hass = hass self.username = username @@ -148,6 +158,8 @@ class Icloud(DeviceScanner): self.seen_devices = {} self._overridestates = {} self._intervals = {} + self._max_interval = max_interval + self._gps_accuracy_threshold = gps_accuracy_threshold self.see = see self._trusted_device = None @@ -348,7 +360,7 @@ class Icloud(DeviceScanner): self._overridestates[devicename] = None if currentzone is not None: - self._intervals[devicename] = 30 + self._intervals[devicename] = self._max_interval return if mindistance is None: @@ -363,7 +375,6 @@ class Icloud(DeviceScanner): if interval > 180: # Three hour drive? This is far enough that they might be flying - # home - check every half hour interval = 30 if battery is not None and battery <= 33 and mindistance > 3: @@ -403,22 +414,24 @@ class Icloud(DeviceScanner): status = device.status(DEVICESTATUSSET) battery = status.get('batteryLevel', 0) * 100 location = status['location'] - if location: - self.determine_interval( - devicename, location['latitude'], - location['longitude'], battery) - interval = self._intervals.get(devicename, 1) - attrs[ATTR_INTERVAL] = interval - accuracy = location['horizontalAccuracy'] - kwargs['dev_id'] = dev_id - kwargs['host_name'] = status['name'] - kwargs['gps'] = (location['latitude'], - location['longitude']) - kwargs['battery'] = battery - kwargs['gps_accuracy'] = accuracy - kwargs[ATTR_ATTRIBUTES] = attrs - self.see(**kwargs) - self.seen_devices[devicename] = True + if location and location['horizontalAccuracy']: + horizontal_accuracy = int(location['horizontalAccuracy']) + if horizontal_accuracy < self._gps_accuracy_threshold: + self.determine_interval( + devicename, location['latitude'], + location['longitude'], battery) + interval = self._intervals.get(devicename, 1) + attrs[ATTR_INTERVAL] = interval + accuracy = location['horizontalAccuracy'] + kwargs['dev_id'] = dev_id + kwargs['host_name'] = status['name'] + kwargs['gps'] = (location['latitude'], + location['longitude']) + kwargs['battery'] = battery + kwargs['gps_accuracy'] = accuracy + kwargs[ATTR_ATTRIBUTES] = attrs + self.see(**kwargs) + self.seen_devices[devicename] = True except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") @@ -434,7 +447,7 @@ class Icloud(DeviceScanner): device.play_sound() def update_icloud(self, devicename=None): - """Authenticate against iCloud and scan for devices.""" + """Request device information from iCloud and update device_tracker.""" from pyicloud.exceptions import PyiCloudNoDevicesException if self.api is None: @@ -443,13 +456,13 @@ class Icloud(DeviceScanner): try: if devicename is not None: if devicename in self.devices: - self.devices[devicename].location() + self.update_device(devicename) else: _LOGGER.error("devicename %s unknown for account %s", devicename, self._attrs[ATTR_ACCOUNTNAME]) else: for device in self.devices: - self.devices[device].location() + self.update_device(device) except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 65d0a1c76f3..a24e82da106 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -37,8 +37,10 @@ SERVICE_WINK = 'wink' SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' +SERVICE_KONNECTED = 'konnected' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' +SERVICE_SABNZBD = 'sabnzbd' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HOMEKIT = 'homekit' @@ -59,7 +61,9 @@ SERVICE_HANDLERS = { SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_DAIKIN: ('daikin', None), + SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), + SERVICE_KONNECTED: ('konnected', None), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), @@ -74,7 +78,6 @@ SERVICE_HANDLERS = { 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), 'harmony': ('remote', 'harmony'), - 'sabnzbd': ('sensor', 'sabnzbd'), 'bose_soundtouch': ('media_player', 'soundtouch'), 'bluesound': ('media_player', 'bluesound'), 'songpal': ('media_player', 'songpal'), @@ -190,6 +193,7 @@ def _discover(netdisco): for disc in netdisco.discover(): for service in netdisco.get_info(disc): results.append((disc, service)) + finally: netdisco.stop() diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py index 733aa0adbfe..892c0b9972a 100644 --- a/homeassistant/components/eufy.py +++ b/homeassistant/components/eufy.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.5'] +REQUIREMENTS = ['lakeside==0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index a74f67b83fb..039cc33f748 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -51,8 +51,8 @@ set_direction: description: Name(s) of the entities to toggle example: 'fan.living_room' direction: - description: The direction to rotate - example: 'left' + description: The direction to rotate. Either 'forward' or 'reverse' + example: 'forward' dyson_set_night_mode: description: Set the fan in night mode. diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py index 31b335eb2bc..a40437e719b 100644 --- a/homeassistant/components/fan/template.py +++ b/homeassistant/components/fan/template.py @@ -18,11 +18,10 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import ToggleEntity -from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, SUPPORT_SET_SPEED, - SUPPORT_OSCILLATE, FanEntity, - ATTR_SPEED, ATTR_OSCILLATING, - ENTITY_ID_FORMAT) +from homeassistant.components.fan import ( + SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, + FanEntity, ATTR_SPEED, ATTR_OSCILLATING, ENTITY_ID_FORMAT, + SUPPORT_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE, ATTR_DIRECTION) from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script @@ -33,25 +32,30 @@ CONF_FANS = 'fans' CONF_SPEED_LIST = 'speeds' CONF_SPEED_TEMPLATE = 'speed_template' CONF_OSCILLATING_TEMPLATE = 'oscillating_template' +CONF_DIRECTION_TEMPLATE = 'direction_template' CONF_ON_ACTION = 'turn_on' CONF_OFF_ACTION = 'turn_off' CONF_SET_SPEED_ACTION = 'set_speed' CONF_SET_OSCILLATING_ACTION = 'set_oscillating' +CONF_SET_DIRECTION_ACTION = 'set_direction' _VALID_STATES = [STATE_ON, STATE_OFF] _VALID_OSC = [True, False] +_VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] FAN_SCHEMA = vol.Schema({ vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, + vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional( CONF_SPEED_LIST, @@ -80,18 +84,21 @@ async def async_setup_platform( oscillating_template = device_config.get( CONF_OSCILLATING_TEMPLATE ) + direction_template = device_config.get(CONF_DIRECTION_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) + set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION) speed_list = device_config[CONF_SPEED_LIST] entity_ids = set() manual_entity_ids = device_config.get(CONF_ENTITY_ID) - for template in (state_template, speed_template, oscillating_template): + for template in (state_template, speed_template, oscillating_template, + direction_template): if template is None: continue template.hass = hass @@ -114,8 +121,9 @@ async def async_setup_platform( TemplateFan( hass, device, friendly_name, state_template, speed_template, oscillating_template, - on_action, off_action, set_speed_action, - set_oscillating_action, speed_list, entity_ids + direction_template, on_action, off_action, set_speed_action, + set_oscillating_action, set_direction_action, speed_list, + entity_ids ) ) @@ -127,8 +135,9 @@ class TemplateFan(FanEntity): def __init__(self, hass, device_id, friendly_name, state_template, speed_template, oscillating_template, - on_action, off_action, set_speed_action, - set_oscillating_action, speed_list, entity_ids): + direction_template, on_action, off_action, set_speed_action, + set_oscillating_action, set_direction_action, speed_list, + entity_ids): """Initialize the fan.""" self.hass = hass self.entity_id = async_generate_entity_id( @@ -138,6 +147,7 @@ class TemplateFan(FanEntity): self._template = state_template self._speed_template = speed_template self._oscillating_template = oscillating_template + self._direction_template = direction_template self._supported_features = 0 self._on_script = Script(hass, on_action) @@ -151,9 +161,14 @@ class TemplateFan(FanEntity): if set_oscillating_action: self._set_oscillating_script = Script(hass, set_oscillating_action) + self._set_direction_script = None + if set_direction_action: + self._set_direction_script = Script(hass, set_direction_action) + self._state = STATE_OFF self._speed = None self._oscillating = None + self._direction = None self._template.hass = self.hass if self._speed_template: @@ -162,6 +177,9 @@ class TemplateFan(FanEntity): if self._oscillating_template: self._oscillating_template.hass = self.hass self._supported_features |= SUPPORT_OSCILLATE + if self._direction_template: + self._direction_template.hass = self.hass + self._supported_features |= SUPPORT_DIRECTION self._entities = entity_ids # List of valid speeds @@ -197,6 +215,11 @@ class TemplateFan(FanEntity): """Return the oscillation state.""" return self._oscillating + @property + def direction(self): + """Return the oscillation state.""" + return self._direction + @property def should_poll(self): """Return the polling state.""" @@ -236,10 +259,30 @@ class TemplateFan(FanEntity): if self._set_oscillating_script is None: return - await self._set_oscillating_script.async_run( - {ATTR_OSCILLATING: oscillating} - ) - self._oscillating = oscillating + if oscillating in _VALID_OSC: + self._oscillating = oscillating + await self._set_oscillating_script.async_run( + {ATTR_OSCILLATING: oscillating}) + else: + _LOGGER.error( + 'Received invalid oscillating value: %s. ' + + 'Expected: %s.', + oscillating, ', '.join(_VALID_OSC)) + + async def async_set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + if self._set_direction_script is None: + return + + if direction in _VALID_DIRECTIONS: + self._direction = direction + await self._set_direction_script.async_run( + {ATTR_DIRECTION: direction}) + else: + _LOGGER.error( + 'Received invalid direction: %s. ' + + 'Expected: %s.', + direction, ', '.join(_VALID_DIRECTIONS)) async def async_added_to_hass(self): """Register callbacks.""" @@ -308,6 +351,7 @@ class TemplateFan(FanEntity): oscillating = self._oscillating_template.async_render() except TemplateError as ex: _LOGGER.error(ex) + oscillating = None self._state = None # Validate osc @@ -322,3 +366,24 @@ class TemplateFan(FanEntity): 'Received invalid oscillating: %s. ' + 'Expected: True/False.', oscillating) self._oscillating = None + + # Update direction if 'direction_template' is configured + if self._direction_template is not None: + try: + direction = self._direction_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + direction = None + self._state = None + + # Validate speed + if direction in _VALID_DIRECTIONS: + self._direction = direction + elif direction == STATE_UNKNOWN: + self._direction = None + else: + _LOGGER.error( + 'Received invalid direction: %s. ' + + 'Expected: %s.', + direction, ', '.join(_VALID_DIRECTIONS)) + self._direction = None diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/fan/zha.py index 3288a788e1f..01b1d0a92cf 100644 --- a/homeassistant/components/fan/zha.py +++ b/homeassistant/components/fan/zha.py @@ -10,7 +10,6 @@ from homeassistant.components import zha from homeassistant.components.fan import ( DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED) -from homeassistant.const import STATE_UNKNOWN DEPENDENCIES = ['zha'] @@ -72,7 +71,7 @@ class ZhaFan(zha.Entity, FanEntity): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state == STATE_UNKNOWN: + if self._state is None: return False return self._state != SPEED_OFF @@ -103,7 +102,7 @@ class ZhaFan(zha.Entity, FanEntity): """Retrieve latest state.""" result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode']) new_value = result.get('fan_mode', None) - self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN) + self._state = VALUE_TO_SPEED.get(new_value, None) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 2c0e146491a..73ab9e8123c 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -4,7 +4,7 @@ Support for RSS/Atom feeds. For more details about this component, please refer to the documentation at https://home-assistant.io/components/feedreader/ """ -from datetime import datetime +from datetime import datetime, timedelta from logging import getLogger from os.path import exists from threading import Lock @@ -12,8 +12,8 @@ import pickle import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL +from homeassistant.helpers.event import track_time_interval import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['feedparser==5.2.1'] @@ -21,16 +21,22 @@ REQUIREMENTS = ['feedparser==5.2.1'] _LOGGER = getLogger(__name__) CONF_URLS = 'urls' +CONF_MAX_ENTRIES = 'max_entries' + +DEFAULT_MAX_ENTRIES = 20 +DEFAULT_SCAN_INTERVAL = timedelta(hours=1) DOMAIN = 'feedreader' EVENT_FEEDREADER = 'feedreader' -MAX_ENTRIES = 20 - CONFIG_SCHEMA = vol.Schema({ DOMAIN: { vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): + cv.positive_int } }, extra=vol.ALLOW_EXTRA) @@ -38,33 +44,50 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Feedreader component.""" urls = config.get(DOMAIN)[CONF_URLS] + scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL) + max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES) data_file = hass.config.path("{}.pickle".format(DOMAIN)) storage = StoredData(data_file) - feeds = [FeedManager(url, hass, storage) for url in urls] + feeds = [FeedManager(url, scan_interval, max_entries, hass, storage) for + url in urls] return len(feeds) > 0 class FeedManager(object): """Abstraction over Feedparser module.""" - def __init__(self, url, hass, storage): - """Initialize the FeedManager object, poll every hour.""" + def __init__(self, url, scan_interval, max_entries, hass, storage): + """Initialize the FeedManager object, poll as per scan interval.""" self._url = url + self._scan_interval = scan_interval + self._max_entries = max_entries self._feed = None self._hass = hass self._firstrun = True self._storage = storage self._last_entry_timestamp = None + self._last_update_successful = False self._has_published_parsed = False + self._event_type = EVENT_FEEDREADER + self._feed_id = url hass.bus.listen_once( EVENT_HOMEASSISTANT_START, lambda _: self._update()) - track_utc_time_change( - hass, lambda now: self._update(), minute=0, second=0) + self._init_regular_updates(hass) def _log_no_entries(self): """Send no entries log at debug level.""" _LOGGER.debug("No new entries to be published in feed %s", self._url) + def _init_regular_updates(self, hass): + """Schedule regular updates at the top of the clock.""" + track_time_interval(hass, lambda now: self._update(), + self._scan_interval) + + @property + def last_update_successful(self): + """Return True if the last feed update was successful.""" + return self._last_update_successful + def _update(self): """Update the feed and publish new entries to the event bus.""" import feedparser @@ -76,26 +99,39 @@ class FeedManager(object): else self._feed.get('modified')) if not self._feed: _LOGGER.error("Error fetching feed data from %s", self._url) + self._last_update_successful = False else: + # The 'bozo' flag really only indicates that there was an issue + # during the initial parsing of the XML, but it doesn't indicate + # whether this is an unrecoverable error. In this case the + # feedparser lib is trying a less strict parsing approach. + # If an error is detected here, log error message but continue + # processing the feed entries if present. if self._feed.bozo != 0: - _LOGGER.error("Error parsing feed %s", self._url) + _LOGGER.error("Error parsing feed %s: %s", self._url, + self._feed.bozo_exception) # Using etag and modified, if there's no new data available, # the entries list will be empty - elif self._feed.entries: + if self._feed.entries: _LOGGER.debug("%s entri(es) available in feed %s", len(self._feed.entries), self._url) - if len(self._feed.entries) > MAX_ENTRIES: - _LOGGER.debug("Processing only the first %s entries " - "in feed %s", MAX_ENTRIES, self._url) - self._feed.entries = self._feed.entries[0:MAX_ENTRIES] + self._filter_entries() self._publish_new_entries() if self._has_published_parsed: self._storage.put_timestamp( - self._url, self._last_entry_timestamp) + self._feed_id, self._last_entry_timestamp) else: self._log_no_entries() + self._last_update_successful = True _LOGGER.info("Fetch from feed %s completed", self._url) + def _filter_entries(self): + """Filter the entries provided and return the ones to keep.""" + if len(self._feed.entries) > self._max_entries: + _LOGGER.debug("Processing only the first %s entries " + "in feed %s", self._max_entries, self._url) + self._feed.entries = self._feed.entries[0:self._max_entries] + def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" # We are lucky, `published_parsed` data available, let's make use of @@ -109,12 +145,12 @@ class FeedManager(object): _LOGGER.debug("No published_parsed info available for entry %s", entry.title) entry.update({'feed_url': self._url}) - self._hass.bus.fire(EVENT_FEEDREADER, entry) + self._hass.bus.fire(self._event_type, entry) def _publish_new_entries(self): """Publish new entries to the event bus.""" new_entries = False - self._last_entry_timestamp = self._storage.get_timestamp(self._url) + self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id) if self._last_entry_timestamp: self._firstrun = False else: @@ -157,18 +193,18 @@ class StoredData(object): _LOGGER.error("Error loading data from pickled file %s", self._data_file) - def get_timestamp(self, url): - """Return stored timestamp for given url.""" + def get_timestamp(self, feed_id): + """Return stored timestamp for given feed id (usually the url).""" self._fetch_data() - return self._data.get(url) + return self._data.get(feed_id) - def put_timestamp(self, url, timestamp): - """Update timestamp for given URL.""" + def put_timestamp(self, feed_id, timestamp): + """Update timestamp for given feed id (usually the url).""" self._fetch_data() with self._lock, open(self._data_file, 'wb') as myfile: - self._data.update({url: timestamp}) + self._data.update({feed_id: timestamp}) _LOGGER.debug("Overwriting feed %s timestamp in storage file %s", - url, self._data_file) + feed_id, self._data_file) try: pickle.dump(self._data, myfile) except: # noqa: E722 # pylint: disable=bare-except diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py index 44110647632..098b34ac948 100644 --- a/homeassistant/components/folder_watcher.py +++ b/homeassistant/components/folder_watcher.py @@ -43,7 +43,7 @@ def setup(hass, config): def create_event_handler(patterns, hass): - """"Return the Watchdog EventHandler object.""" + """Return the Watchdog EventHandler object.""" from watchdog.events import PatternMatchingEventHandler class EventHandler(PatternMatchingEventHandler): diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0d267077991..2bd7283e38e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180509.0'] +REQUIREMENTS = ['home-assistant-frontend==20180526.4'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -147,21 +147,6 @@ class AbstractPanel: 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path), index_view.get) - def to_response(self, hass, request): - """Panel as dictionary.""" - result = { - 'component_name': self.component_name, - 'icon': self.sidebar_icon, - 'title': self.sidebar_title, - 'url_path': self.frontend_url_path, - 'config': self.config, - } - if _is_latest(hass.data[DATA_JS_VERSION], request): - result['url'] = self.webcomponent_url_latest - else: - result['url'] = self.webcomponent_url_es5 - return result - class BuiltInPanel(AbstractPanel): """Panel that is part of hass_frontend.""" @@ -175,30 +160,15 @@ class BuiltInPanel(AbstractPanel): self.frontend_url_path = frontend_url_path or component_name self.config = config - @asyncio.coroutine - def async_finalize(self, hass, frontend_repository_path): - """Finalize this panel for usage. - - If frontend_repository_path is set, will be prepended to path of - built-in components. - """ - if frontend_repository_path is None: - import hass_frontend - import hass_frontend_es5 - - self.webcomponent_url_latest = \ - '/frontend_latest/panels/ha-panel-{}-{}.html'.format( - self.component_name, - hass_frontend.FINGERPRINTS[self.component_name]) - self.webcomponent_url_es5 = \ - '/frontend_es5/panels/ha-panel-{}-{}.html'.format( - self.component_name, - hass_frontend_es5.FINGERPRINTS[self.component_name]) - else: - # Dev mode - self.webcomponent_url_es5 = self.webcomponent_url_latest = \ - '/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format( - self.component_name, self.component_name) + def to_response(self, hass, request): + """Panel as dictionary.""" + return { + 'component_name': self.component_name, + 'icon': self.sidebar_icon, + 'title': self.sidebar_title, + 'config': self.config, + 'url_path': self.frontend_url_path, + } class ExternalPanel(AbstractPanel): @@ -244,6 +214,21 @@ class ExternalPanel(AbstractPanel): frontend_repository_path is None) self.REGISTERED_COMPONENTS.add(self.component_name) + def to_response(self, hass, request): + """Panel as dictionary.""" + result = { + 'component_name': self.component_name, + 'icon': self.sidebar_icon, + 'title': self.sidebar_title, + 'url_path': self.frontend_url_path, + 'config': self.config, + } + if _is_latest(hass.data[DATA_JS_VERSION], request): + result['url'] = self.webcomponent_url_latest + else: + result['url'] = self.webcomponent_url_es5 + return result + @bind_hass @asyncio.coroutine @@ -296,6 +281,15 @@ def add_manifest_json_key(key, val): @asyncio.coroutine def async_setup(hass, config): """Set up the serving of the frontend.""" + if list(hass.auth.async_auth_providers): + client = yield from hass.auth.async_create_client( + 'Home Assistant Frontend', + redirect_uris=['/'], + no_secret=True, + ) + else: + client = None + hass.components.websocket_api.async_register_command( WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS) hass.http.register_view(ManifestJSONView) @@ -307,59 +301,40 @@ def async_setup(hass, config): hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION) if is_dev: - for subpath in ["src", "build-translations", "build-temp", "build", - "hass_frontend", "bower_components", "panels", - "hassio"]: - hass.http.register_static_path( - "/home-assistant-polymer/{}".format(subpath), - os.path.join(repo_path, subpath), - False) - - hass.http.register_static_path( - "/static/translations", - os.path.join(repo_path, "build-translations/output"), False) - sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js") - sw_path_latest = os.path.join(repo_path, "build/service_worker.js") - static_path = os.path.join(repo_path, 'hass_frontend') - frontend_es5_path = os.path.join(repo_path, 'build-es5') - frontend_latest_path = os.path.join(repo_path, 'build') + hass_frontend_path = os.path.join(repo_path, 'hass_frontend') + hass_frontend_es5_path = os.path.join(repo_path, 'hass_frontend_es5') else: import hass_frontend import hass_frontend_es5 - sw_path_es5 = os.path.join(hass_frontend_es5.where(), - "service_worker.js") - sw_path_latest = os.path.join(hass_frontend.where(), - "service_worker.js") - # /static points to dir with files that are JS-type agnostic. - # ES5 files are served from /frontend_es5. - # ES6 files are served from /frontend_latest. - static_path = hass_frontend.where() - frontend_es5_path = hass_frontend_es5.where() - frontend_latest_path = static_path + hass_frontend_path = hass_frontend.where() + hass_frontend_es5_path = hass_frontend_es5.where() hass.http.register_static_path( - "/service_worker_es5.js", sw_path_es5, False) + "/service_worker_es5.js", + os.path.join(hass_frontend_es5_path, "service_worker.js"), False) hass.http.register_static_path( - "/service_worker.js", sw_path_latest, False) + "/service_worker.js", + os.path.join(hass_frontend_path, "service_worker.js"), False) hass.http.register_static_path( - "/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev) - hass.http.register_static_path("/static", static_path, not is_dev) + "/robots.txt", + os.path.join(hass_frontend_path, "robots.txt"), False) + hass.http.register_static_path("/static", hass_frontend_path, not is_dev) hass.http.register_static_path( - "/frontend_latest", frontend_latest_path, not is_dev) + "/frontend_latest", hass_frontend_path, not is_dev) hass.http.register_static_path( - "/frontend_es5", frontend_es5_path, not is_dev) + "/frontend_es5", hass_frontend_es5_path, not is_dev) local = hass.config.path('www') if os.path.isdir(local): hass.http.register_static_path("/local", local, not is_dev) - index_view = IndexView(repo_path, js_version) + index_view = IndexView(repo_path, js_version, client) hass.http.register_view(index_view) - @asyncio.coroutine - def finalize_panel(panel): + async def finalize_panel(panel): """Finalize setup of a panel.""" - yield from panel.async_finalize(hass, repo_path) + if hasattr(panel, 'async_finalize'): + await panel.async_finalize(hass, repo_path) panel.async_register_index_routes(hass.http.app.router, index_view) yield from asyncio.wait([ @@ -451,10 +426,11 @@ class IndexView(HomeAssistantView): requires_auth = False extra_urls = ['/states', '/states/{extra}'] - def __init__(self, repo_path, js_option): + def __init__(self, repo_path, js_option, client): """Initialize the frontend view.""" self.repo_path = repo_path self.js_option = js_option + self.client = client self._template_cache = {} def get_template(self, latest): @@ -508,7 +484,7 @@ class IndexView(HomeAssistantView): extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5 - resp = template.render( + template_params = dict( no_auth=no_auth, panel_url=panel_url, panels=hass.data[DATA_PANELS], @@ -516,7 +492,11 @@ class IndexView(HomeAssistantView): extra_urls=hass.data[extra_key], ) - return web.Response(text=resp, content_type='text/html') + if self.client is not None: + template_params['client_id'] = self.client.id + + return web.Response(text=template.render(**template_params), + content_type='text/html') class ManifestJSONView(HomeAssistantView): diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index f70a2d29351..a33e91f3aa9 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, - ATTR_ASSUMED_STATE, SERVICE_RELOAD) + ATTR_ASSUMED_STATE, SERVICE_RELOAD, ATTR_NAME, ATTR_ICON) from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -35,8 +35,6 @@ ATTR_ADD_ENTITIES = 'add_entities' ATTR_AUTO = 'auto' ATTR_CONTROL = 'control' ATTR_ENTITIES = 'entities' -ATTR_ICON = 'icon' -ATTR_NAME = 'name' ATTR_OBJECT_ID = 'object_id' ATTR_ORDER = 'order' ATTR_VIEW = 'view' diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 87251a2745c..aa24cc61af3 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -13,12 +13,13 @@ import voluptuous as vol from homeassistant.components import SERVICE_CHECK_CONFIG from homeassistant.const import ( - SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) + ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) from homeassistant.core import DOMAIN as HASS_DOMAIN from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow + from .handler import HassIO from .http import HassIOView @@ -47,7 +48,6 @@ ATTR_SNAPSHOT = 'snapshot' ATTR_ADDONS = 'addons' ATTR_FOLDERS = 'folders' ATTR_HOMEASSISTANT = 'homeassistant' -ATTR_NAME = 'name' ATTR_PASSWORD = 'password' SCHEMA_NO_DATA = vol.Schema({}) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index a954aaccbd4..c3caf40ba62 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -33,7 +33,7 @@ def _api_bool(funct): def _api_data(funct): - """Return a api data.""" + """Return data of an api.""" @asyncio.coroutine def _wrapper(*argv, **kwargs): """Wrap function.""" diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 9dd6427ec38..bb4f8219a33 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -36,7 +36,7 @@ NO_TIMEOUT = { } NO_AUTH = { - re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'), + re.compile(r'^app-(es5|latest)/.+$'), re.compile(r'^addons/[^/]*/logo$') } diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c31093a5eb8..202f9694689 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -12,25 +12,25 @@ import voluptuous as vol from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, - TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) + ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, - DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START, - DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) -from .util import ( - validate_entity_config, show_setup_message) + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, + DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, + SERVICE_HOMEKIT_START) +from .util import show_setup_message, validate_entity_config TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==2.0.0'] +REQUIREMENTS = ['HAP-python==2.1.0'] # #### Driver Status #### STATUS_READY = 0 @@ -93,7 +93,7 @@ def get_accessory(hass, state, aid, config): return None a_type = None - config = config or {} + name = config.get(CONF_NAME, state.name) if state.domain == 'alarm_control_panel': a_type = 'SecuritySystem' @@ -116,6 +116,9 @@ def get_accessory(hass, state, aid, config): elif features & (SUPPORT_OPEN | SUPPORT_CLOSE): a_type = 'WindowCoveringBasic' + elif state.domain == 'fan': + a_type = 'Fan' + elif state.domain == 'light': a_type = 'Light' @@ -147,7 +150,7 @@ def get_accessory(hass, state, aid, config): return None _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) - return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config) + return TYPES[a_type](hass, name, state.entity_id, aid, config) def generate_aid(entity_id): @@ -183,7 +186,8 @@ class HomeKit(): ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.bridge, self._port, ip_addr, path) + self.driver = HomeDriver(self.hass, self.bridge, port=self._port, + address=ip_addr, persist_file=path) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" @@ -203,15 +207,16 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 - type_covers, type_lights, type_locks, type_security_systems, - type_sensors, type_switches, type_thermostats) + type_covers, type_fans, type_lights, type_locks, + type_security_systems, type_sensors, type_switches, + type_thermostats) for state in self.hass.states.all(): self.add_bridge_accessory(state) self.bridge.set_driver(self.driver) - if not self.bridge.paired: - show_setup_message(self.hass, self.bridge) + if not self.driver.state.paired: + show_setup_message(self.hass, self.driver.state.pincode) _LOGGER.debug('Driver start') self.hass.add_job(self.driver.start) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index c47c3f8fbe7..ded4526b008 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -16,8 +16,8 @@ from homeassistant.helpers.event import ( from homeassistant.util import dt as dt_util from .const import ( - DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, - BRIDGE_SERIAL_NUMBER, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, + DEBOUNCE_TIMEOUT, MANUFACTURER) from .util import ( show_setup_message, dismiss_setup_message) @@ -64,14 +64,16 @@ def debounce(func): class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER): + def __init__(self, hass, name, entity_id, aid, config, + category=CATEGORY_OTHER): """Initialize a Accessory object.""" super().__init__(name, aid=aid) - domain = split_entity_id(entity_id)[0].replace("_", " ").title() + model = split_entity_id(entity_id)[0].replace("_", " ").title() self.set_info_service( firmware_revision=__version__, manufacturer=MANUFACTURER, - model=domain, serial_number=entity_id) + model=model, serial_number=entity_id) self.category = category + self.config = config self.entity_id = entity_id self.hass = hass @@ -82,20 +84,21 @@ class HomeAccessory(Accessory): async_track_state_change( self.hass, self.entity_id, self.update_state_callback) + @ha_callback def update_state_callback(self, entity_id=None, old_state=None, new_state=None): """Callback from state change listener.""" _LOGGER.debug('New_state: %s', new_state) if new_state is None: return - self.update_state(new_state) + self.hass.async_add_job(self.update_state, new_state) def update_state(self, new_state): """Method called on state change to update HomeKit value. Overridden by accessory types. """ - pass + raise NotImplementedError() class HomeBridge(Bridge): @@ -113,20 +116,23 @@ class HomeBridge(Bridge): """Prevent print of pyhap setup message to terminal.""" pass - def add_paired_client(self, client_uuid, client_public): - """Override super function to dismiss setup message if paired.""" - super().add_paired_client(client_uuid, client_public) - dismiss_setup_message(self.hass) - - def remove_paired_client(self, client_uuid): - """Override super function to show setup message if unpaired.""" - super().remove_paired_client(client_uuid) - show_setup_message(self.hass, self) - class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" - def __init__(self, *args, **kwargs): + def __init__(self, hass, *args, **kwargs): """Initialize a AccessoryDriver object.""" super().__init__(*args, **kwargs) + self.hass = hass + + def pair(self, client_uuid, client_public): + """Override super function to dismiss setup message if paired.""" + value = super().pair(client_uuid, client_public) + if value: + dismiss_setup_message(self.hass) + return value + + def unpair(self, client_uuid): + """Override super function to show setup message if unpaired.""" + super().unpair(client_uuid) + show_setup_message(self.hass, self.state.pincode) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ce46e84a2ef..21cad2d9cf7 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,23 +1,23 @@ """Constants used be the HomeKit component.""" -# #### MISC #### +# #### Misc #### DEBOUNCE_TIMEOUT = 0.5 DOMAIN = 'homekit' HOMEKIT_FILE = '.homekit.state' HOMEKIT_NOTIFY_ID = 4663548 -# #### CONFIG #### +# #### Config #### CONF_AUTO_START = 'auto_start' CONF_ENTITY_CONFIG = 'entity_config' CONF_FILTER = 'filter' -# #### CONFIG DEFAULTS #### +# #### Config Defaults #### DEFAULT_AUTO_START = True DEFAULT_PORT = 51827 -# #### HOMEKIT COMPONENT SERVICES #### +# #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' -# #### STRING CONSTANTS #### +# #### String Constants #### BRIDGE_MODEL = 'Bridge' BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' @@ -29,11 +29,12 @@ SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' +SERV_FANV2 = 'Fanv2' SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' -SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity +SERV_HUMIDITY_SENSOR = 'HumiditySensor' SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHT_SENSOR = 'LightSensor' -SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_LIGHTBULB = 'Lightbulb' SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' @@ -43,12 +44,12 @@ SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' -# CurrentPosition, TargetPosition, PositionState # #### Characteristics #### +CHAR_ACTIVE = 'Active' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' -CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_BRIGHTNESS = 'Brightness' CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' @@ -59,13 +60,13 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' -CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] -CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent +CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' -CHAR_HUE = 'Hue' # arcdegress | [0, 360] +CHAR_HUE = 'Hue' CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_TARGET_STATE = 'LockTargetState' @@ -75,33 +76,34 @@ CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' -CHAR_ON = 'On' # boolean +CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' -CHAR_SATURATION = 'Saturation' # percent +CHAR_ROTATION_DIRECTION = 'RotationDirection' +CHAR_SATURATION = 'Saturation' CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' +CHAR_SWING_MODE = 'SwingMode' CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' -CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] +CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' # #### Properties #### +PROP_MAX_VALUE = 'maxValue' +PROP_MIN_VALUE = 'minValue' PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} -# #### Device Class #### +# #### Device Classes #### DEVICE_CLASS_CO2 = 'co2' DEVICE_CLASS_DOOR = 'door' DEVICE_CLASS_GARAGE_DOOR = 'garage_door' DEVICE_CLASS_GAS = 'gas' -DEVICE_CLASS_HUMIDITY = 'humidity' -DEVICE_CLASS_LIGHT = 'light' DEVICE_CLASS_MOISTURE = 'moisture' DEVICE_CLASS_MOTION = 'motion' DEVICE_CLASS_OCCUPANCY = 'occupancy' DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' -DEVICE_CLASS_TEMPERATURE = 'temperature' DEVICE_CLASS_WINDOW = 'window' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 3de87cf63e8..cf0620a4e30 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,21 +1,21 @@ """Class to hold all cover accessories.""" import logging -from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER +from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED, - SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER, - ATTR_SUPPORTED_FEATURES) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, + STATE_CLOSED, STATE_OPEN) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, - CHAR_TARGET_POSITION, CHAR_POSITION_STATE, - SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) + CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE, + CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, + SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING) _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,7 @@ class GarageDoorOpener(HomeAccessory): and support no more than open, close, and stop. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a GarageDoorOpener accessory object.""" super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER) self.flag_target_state = False @@ -44,12 +44,13 @@ class GarageDoorOpener(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} if value == 0: self.char_current_state.set_value(3) - self.hass.components.cover.open_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_OPEN_COVER, params) elif value == 1: self.char_current_state.set_value(2) - self.hass.components.cover.close_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, params) def update_state(self, new_state): """Update cover state after state changed.""" @@ -69,7 +70,7 @@ class WindowCovering(HomeAccessory): The cover entity must support: set_cover_position. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) self.homekit_target = None @@ -108,7 +109,7 @@ class WindowCoveringBasic(HomeAccessory): stop_cover (optional). """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) features = self.hass.states.get(self.entity_id) \ @@ -141,8 +142,8 @@ class WindowCoveringBasic(HomeAccessory): else: service, position = (SERVICE_CLOSE_COVER, 0) - self.hass.services.call(DOMAIN, service, - {ATTR_ENTITY_ID: self.entity_id}) + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) # Snap the current/target position to the expected final position. self.char_current_position.set_value(position) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py new file mode 100644 index 00000000000..bf0d4da6a59 --- /dev/null +++ b/homeassistant/components/homekit/type_fans.py @@ -0,0 +1,115 @@ +"""Class to hold all light accessories.""" +import logging + +from pyhap.const import CATEGORY_FAN + +from homeassistant.components.fan import ( + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, + SUPPORT_OSCILLATE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_OFF, STATE_ON) + +from . import TYPES +from .accessories import HomeAccessory +from .const import ( + CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_SWING_MODE, SERV_FANV2) + +_LOGGER = logging.getLogger(__name__) + + +@TYPES.register('Fan') +class Fan(HomeAccessory): + """Generate a Fan accessory for a fan entity. + + Currently supports: state, speed, oscillate, direction. + """ + + def __init__(self, *args): + """Initialize a new Light accessory object.""" + super().__init__(*args, category=CATEGORY_FAN) + self._flag = {CHAR_ACTIVE: False, + CHAR_ROTATION_DIRECTION: False, + CHAR_SWING_MODE: False} + self._state = 0 + + self.chars = [] + features = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_SUPPORTED_FEATURES) + if features & SUPPORT_DIRECTION: + self.chars.append(CHAR_ROTATION_DIRECTION) + if features & SUPPORT_OSCILLATE: + self.chars.append(CHAR_SWING_MODE) + + serv_fan = self.add_preload_service(SERV_FANV2, self.chars) + self.char_active = serv_fan.configure_char( + CHAR_ACTIVE, value=0, setter_callback=self.set_state) + + if CHAR_ROTATION_DIRECTION in self.chars: + self.char_direction = serv_fan.configure_char( + CHAR_ROTATION_DIRECTION, value=0, + setter_callback=self.set_direction) + + if CHAR_SWING_MODE in self.chars: + self.char_swing = serv_fan.configure_char( + CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating) + + def set_state(self, value): + """Set state if call came from HomeKit.""" + if self._state == value: + return + + _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + self._flag[CHAR_ACTIVE] = True + service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_direction(self, value): + """Set state if call came from HomeKit.""" + _LOGGER.debug('%s: Set direction to %d', self.entity_id, value) + self._flag[CHAR_ROTATION_DIRECTION] = True + direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction} + self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params) + + def set_oscillating(self, value): + """Set state if call came from HomeKit.""" + _LOGGER.debug('%s: Set oscillating to %d', self.entity_id, value) + self._flag[CHAR_SWING_MODE] = True + oscillating = True if value == 1 else False + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_OSCILLATING: oscillating} + self.hass.services.call(DOMAIN, SERVICE_OSCILLATE, params) + + def update_state(self, new_state): + """Update fan after state change.""" + # Handle State + state = new_state.state + if state in (STATE_ON, STATE_OFF): + self._state = 1 if state == STATE_ON else 0 + if not self._flag[CHAR_ACTIVE] and \ + self.char_active.value != self._state: + self.char_active.set_value(self._state) + self._flag[CHAR_ACTIVE] = False + + # Handle Direction + if CHAR_ROTATION_DIRECTION in self.chars: + direction = new_state.attributes.get(ATTR_DIRECTION) + if not self._flag[CHAR_ROTATION_DIRECTION] and \ + direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): + hk_direction = 1 if direction == DIRECTION_REVERSE else 0 + if self.char_direction.value != hk_direction: + self.char_direction.set_value(hk_direction) + self._flag[CHAR_ROTATION_DIRECTION] = False + + # Handle Oscillating + if CHAR_SWING_MODE in self.chars: + oscillating = new_state.attributes.get(ATTR_OSCILLATING) + if not self._flag[CHAR_SWING_MODE] and \ + oscillating in (True, False): + hk_oscillating = 1 if oscillating else 0 + if self.char_swing.value != hk_oscillating: + self.char_swing.set_value(hk_oscillating) + self._flag[CHAR_SWING_MODE] = False diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 3efb0e99df6..da012799602 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -4,15 +4,18 @@ import logging from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( - ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, - ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, + SERVICE_TURN_OFF, STATE_OFF, STATE_ON) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, - CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) + CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON, + CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE) _LOGGER = logging.getLogger(__name__) @@ -26,7 +29,7 @@ class Light(HomeAccessory): Currently supports: state, brightness, color temperature, rgb_color. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, @@ -61,7 +64,8 @@ class Light(HomeAccessory): .attributes.get(ATTR_MAX_MIREDS, 500) self.char_color_temperature = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, - properties={'minValue': min_mireds, 'maxValue': max_mireds}, + properties={PROP_MIN_VALUE: min_mireds, + PROP_MAX_VALUE: max_mireds}, setter_callback=self.set_color_temperature) if CHAR_HUE in self.chars: self.char_hue = serv_light.configure_char( @@ -77,28 +81,27 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ON] = True - - if value == 1: - self.hass.components.light.turn_on(self.entity_id) - elif value == 0: - self.hass.components.light.turn_off(self.entity_id) + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF + self.hass.services.call(DOMAIN, service, params) @debounce def set_brightness(self, value): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True - if value != 0: - self.hass.components.light.turn_on( - self.entity_id, brightness_pct=value) - else: - self.hass.components.light.turn_off(self.entity_id) + if value == 0: + self.set_state(0) # Turn off light + return + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) self._flag[CHAR_COLOR_TEMPERATURE] = True - self.hass.components.light.turn_on(self.entity_id, color_temp=value) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -116,15 +119,14 @@ class Light(HomeAccessory): def set_color(self): """Set color if call came from HomeKit.""" - # Handle Color if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: color = (self._hue, self._saturation) _LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) - self.hass.components.light.turn_on( - self.entity_id, hs_color=color) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def update_state(self, new_state): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index e7f18d44805..05ab6c6f822 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -4,12 +4,12 @@ import logging from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import ( - ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) + ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) +from homeassistant.const import ATTR_CODE from . import TYPES from .accessories import HomeAccessory -from .const import ( - SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) +from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _LOGGER = logging.getLogger(__name__) @@ -29,9 +29,10 @@ class Lock(HomeAccessory): The lock entity must support: unlock and lock. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a Lock accessory object.""" super().__init__(*args, category=CATEGORY_DOOR_LOCK) + self._code = self.config.get(ATTR_CODE) self.flag_target_state = False serv_lock_mechanism = self.add_preload_service(SERV_LOCK) @@ -51,7 +52,9 @@ class Lock(HomeAccessory): service = STATE_TO_SERVICE[hass_value] params = {ATTR_ENTITY_ID: self.entity_id} - self.hass.services.call('lock', service, params) + if self._code: + params[ATTR_CODE] = self._code + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update lock after state changed.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index ab16f921e99..bbf8b3f17cb 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -3,16 +3,16 @@ import logging from pyhap.const import CATEGORY_ALARM_SYSTEM +from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE) + ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, - CHAR_TARGET_SECURITY_STATE) + CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE, + SERV_SECURITY_SYSTEM) _LOGGER = logging.getLogger(__name__) @@ -32,10 +32,10 @@ STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home', class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a SecuritySystem accessory object.""" super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) - self._alarm_code = config.get(ATTR_CODE) + self._alarm_code = self.config.get(ATTR_CODE) self.flag_target_state = False serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) @@ -56,7 +56,7 @@ class SecuritySystem(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code - self.hass.services.call('alarm_control_panel', service, params) + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 393b6beffd6..373c1188f2d 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -4,26 +4,26 @@ import logging from pyhap.const import CATEGORY_SENSOR from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, - ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_HOME, + TEMP_CELSIUS) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, - CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, - SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, - CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, - SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, - DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, - DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, - CHAR_CARBON_MONOXIDE_DETECTED, - DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, - DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, - DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, - DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, - DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW, - DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) + CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY, + CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL, + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED, + CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED, + CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED, + DEVICE_CLASS_CO2, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_WINDOW, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, + SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR, + SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR, + SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR, + SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR) from .util import ( convert_to_float, temperature_to_homekit, density_to_air_quality) @@ -51,7 +51,7 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a TemperatureSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) @@ -74,7 +74,7 @@ class TemperatureSensor(HomeAccessory): class HumiditySensor(HomeAccessory): """Generate a HumiditySensor accessory as humidity sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a HumiditySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR) @@ -94,7 +94,7 @@ class HumiditySensor(HomeAccessory): class AirQualitySensor(HomeAccessory): """Generate a AirQualitySensor accessory as air quality sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a AirQualitySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) @@ -108,7 +108,7 @@ class AirQualitySensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) - if density is not None: + if density: self.char_density.set_value(density) self.char_quality.set_value(density_to_air_quality(density)) _LOGGER.debug('%s: Set to %d', self.entity_id, density) @@ -118,7 +118,7 @@ class AirQualitySensor(HomeAccessory): class CarbonDioxideSensor(HomeAccessory): """Generate a CarbonDioxideSensor accessory as CO2 sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a CarbonDioxideSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) @@ -134,7 +134,7 @@ class CarbonDioxideSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" co2 = convert_to_float(new_state.state) - if co2 is not None: + if co2: self.char_co2.set_value(co2) if co2 > self.char_peak.value: self.char_peak.set_value(co2) @@ -146,7 +146,7 @@ class CarbonDioxideSensor(HomeAccessory): class LightSensor(HomeAccessory): """Generate a LightSensor accessory as light sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a LightSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) @@ -157,7 +157,7 @@ class LightSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) - if luminance is not None: + if luminance: self.char_light.set_value(luminance) _LOGGER.debug('%s: Set to %d', self.entity_id, luminance) @@ -166,7 +166,7 @@ class LightSensor(HomeAccessory): class BinarySensor(HomeAccessory): """Generate a BinarySensor accessory as binary sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a BinarySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) device_class = self.hass.states.get(self.entity_id).attributes \ diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 68a4fcdab0a..5754266587c 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) class Switch(HomeAccessory): """Generate a Switch accessory.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a Switch accessory object to represent a remote.""" super().__init__(*args, category=CATEGORY_SWITCH) self._domain = split_entity_id(self.entity_id)[0] @@ -33,9 +33,9 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.hass.services.call(self._domain, service, - {ATTR_ENTITY_ID: self.entity_id}) + self.hass.services.call(self._domain, service, params) def update_state(self, new_state): """Update switch state after state changed.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 15fd8160a7e..d6555d5056d 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,22 +4,23 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, + STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, + TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, - CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, - CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, - CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) + CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, + CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, + CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, + CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -38,7 +39,7 @@ SUPPORT_TEMP_RANGE = SUPPORT_TARGET_TEMPERATURE_LOW | \ class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = TEMP_CELSIUS @@ -99,12 +100,13 @@ class Thermostat(HomeAccessory): if self.support_power_state is True: params = {ATTR_ENTITY_ID: self.entity_id} if hass_value == STATE_OFF: - self.hass.services.call('climate', 'turn_off', params) + self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params) return else: - self.hass.services.call('climate', 'turn_on', params) - self.hass.components.climate.set_operation_mode( - operation_mode=hass_value, entity_id=self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_OPERATION_MODE: hass_value} + self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params) @debounce def set_cooling_threshold(self, value): @@ -113,11 +115,11 @@ class Thermostat(HomeAccessory): self.entity_id, value) self.coolingthresh_flag_target_state = True low = self.char_heating_thresh_temp.value - low = temperature_to_states(low, self._unit) - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - entity_id=self.entity_id, target_temp_high=value, - target_temp_low=low) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(value, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_heating_threshold(self, value): @@ -125,13 +127,12 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self.entity_id, value) self.heatingthresh_flag_target_state = True - # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value - high = temperature_to_states(high, self._unit) - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - entity_id=self.entity_id, target_temp_high=high, - target_temp_low=value) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_target_temperature(self, value): @@ -139,9 +140,10 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) self.temperature_flag_target_state = True - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - temperature=value, entity_id=self.entity_id) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TEMPERATURE: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 29fe3c8f265..447257f9e8f 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, TEMP_CELSIUS) + ATTR_CODE, CONF_NAME, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import HOMEKIT_NOTIFY_ID @@ -16,16 +16,21 @@ _LOGGER = logging.getLogger(__name__) def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" entities = {} - for key, config in values.items(): - entity = cv.entity_id(key) + for entity_id, config in values.items(): + entity = cv.entity_id(entity_id) params = {} if not isinstance(config, dict): raise vol.Invalid('The configuration for "{}" must be ' - ' an dictionary.'.format(entity)) + ' a dictionary.'.format(entity)) + + for key in (CONF_NAME, ): + value = config.get(key, -1) + if value != -1: + params[key] = cv.string(value) domain, _ = split_entity_id(entity) - if domain == 'alarm_control_panel': + if domain in ('alarm_control_panel', 'lock'): code = config.get(ATTR_CODE) params[ATTR_CODE] = cv.string(code) if code else None @@ -33,9 +38,9 @@ def validate_entity_config(values): return entities -def show_setup_message(hass, bridge): +def show_setup_message(hass, pincode): """Display persistent notification with setup information.""" - pin = bridge.pincode.decode() + pin = pincode.decode() _LOGGER.info('Pincode: %s', pin) message = 'To setup Home Assistant in the Home App, enter the ' \ 'following code:\n### {}'.format(pin) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 0291cc28fed..e0f0fafe5b5 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -13,17 +13,19 @@ import socket import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, - CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD, + CONF_PLATFORM, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) from homeassistant.helpers import discovery -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass REQUIREMENTS = ['pyhomematic==0.1.42'] -DOMAIN = 'homematic' + _LOGGER = logging.getLogger(__name__) +DOMAIN = 'homematic' + SCAN_INTERVAL_HUB = timedelta(seconds=300) SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) @@ -38,7 +40,6 @@ DISCOVER_LOCKS = 'homematic.locks' ATTR_DISCOVER_DEVICES = 'devices' ATTR_PARAM = 'param' ATTR_CHANNEL = 'channel' -ATTR_NAME = 'name' ATTR_ADDRESS = 'address' ATTR_VALUE = 'value' ATTR_INTERFACE = 'interface' @@ -70,7 +71,7 @@ HM_DEVICE_TYPES = { 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', - 'IPWeatherSensor'], + 'IPWeatherSensor', 'RotaryHandleSensorIP'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', @@ -97,6 +98,7 @@ HM_ATTRIBUTE_SUPPORT = { 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], + 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'RSSI_DEVICE': ['rssi', {}], 'VALVE_STATE': ['valve', {}], 'BATTERY_STATE': ['battery', {}], diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index 0b15d7a3dfe..d85d867d8f8 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -24,7 +24,10 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'homematicip_cloud' COMPONENTS = [ - 'sensor' + 'sensor', + 'binary_sensor', + 'switch', + 'light' ] CONF_NAME = 'name' diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 5558063c5c4..c4723abccee 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -81,7 +81,12 @@ async def async_validate_auth_header(api_password, request): if hdrs.AUTHORIZATION not in request.headers: return False - auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + try: + auth_type, auth_val = \ + request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + except ValueError: + # If no space in authorization header + return False if auth_type == 'Basic': decoded = base64.b64decode(auth_val).decode('utf-8') diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 81c6ea4bcfb..3de276564eb 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -51,12 +51,6 @@ class HomeAssistantView(object): data['code'] = message_code return self.json(data, status_code, headers=headers) - # pylint: disable=no-self-use - async def file(self, request, fil): - """Return a file.""" - assert isinstance(fil, str), 'only string paths allowed' - return web.FileResponse(fil) - def register(self, router): """Register the view with a router.""" assert self.url is not None, 'No url set for view' diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index c6100ff701d..29f26cc84e6 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -10,14 +10,14 @@ import logging import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID) + ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME) +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,6 @@ ATTR_CONFIDENCE = 'confidence' ATTR_FACES = 'faces' ATTR_GENDER = 'gender' ATTR_GLASSES = 'glasses' -ATTR_NAME = 'name' ATTR_MOTION = 'motion' ATTR_TOTAL_FACES = 'total_faces' @@ -60,7 +59,7 @@ SOURCE_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SOURCE): vol.All(cv.ensure_list, [SOURCE_SCHEMA]), vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE): - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), }) SERVICE_SCAN_SCHEMA = vol.Schema({ @@ -77,7 +76,7 @@ def scan(hass, entity_id=None): @asyncio.coroutine def async_setup(hass, config): - """Set up image processing.""" + """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) yield from component.async_setup(config) diff --git a/homeassistant/components/image_processing/facebox.py b/homeassistant/components/image_processing/facebox.py new file mode 100644 index 00000000000..81b43c1f8e0 --- /dev/null +++ b/homeassistant/components/image_processing/facebox.py @@ -0,0 +1,110 @@ +""" +Component that will perform facial detection and identification via facebox. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/image_processing.facebox +""" +import base64 +import logging + +import requests +import voluptuous as vol + +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv +from homeassistant.components.image_processing import ( + PLATFORM_SCHEMA, ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID, + CONF_NAME) +from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT) + +_LOGGER = logging.getLogger(__name__) + +CLASSIFIER = 'facebox' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PORT): cv.port, +}) + + +def encode_image(image): + """base64 encode an image stream.""" + base64_img = base64.b64encode(image).decode('ascii') + return {"base64": base64_img} + + +def get_matched_faces(faces): + """Return the name and rounded confidence of matched faces.""" + return {face['name']: round(face['confidence'], 2) + for face in faces if face['matched']} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the classifier.""" + entities = [] + for camera in config[CONF_SOURCE]: + entities.append(FaceClassifyEntity( + config[CONF_IP_ADDRESS], + config[CONF_PORT], + camera[CONF_ENTITY_ID], + camera.get(CONF_NAME) + )) + add_devices(entities) + + +class FaceClassifyEntity(ImageProcessingFaceEntity): + """Perform a face classification.""" + + def __init__(self, ip, port, camera_entity, name=None): + """Init with the API key and model id.""" + super().__init__() + self._url = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER) + self._camera = camera_entity + if name: + self._name = name + else: + camera_name = split_entity_id(camera_entity)[1] + self._name = "{} {}".format( + CLASSIFIER, camera_name) + self._matched = {} + + def process_image(self, image): + """Process an image.""" + response = {} + try: + response = requests.post( + self._url, + json=encode_image(image), + timeout=9 + ).json() + except requests.exceptions.ConnectionError: + _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) + response['success'] = False + + if response['success']: + faces = response['faces'] + total = response['facesCount'] + self.process_faces(faces, total) + self._matched = get_matched_faces(faces) + + else: + self.total_faces = None + self.faces = [] + self._matched = {} + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the classifier attributes.""" + return { + 'matched_faces': self._matched, + } diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index cd1e341a218..bda0e1bc550 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -9,12 +9,12 @@ import logging import voluptuous as vol +from homeassistant.components.image_processing import ( + ATTR_AGE, ATTR_GENDER, ATTR_GLASSES, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingFaceEntity) +from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE -from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_AGE, ATTR_GENDER, - ATTR_GLASSES, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 32f02e1820e..8984f25cdf2 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -9,12 +9,13 @@ import logging import voluptuous as vol +from homeassistant.components.image_processing import ( + ATTR_CONFIDENCE, CONF_CONFIDENCE, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE, + PLATFORM_SCHEMA, ImageProcessingFaceEntity) +from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE +from homeassistant.const import ATTR_NAME from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE -from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_NAME, - CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_CONFIDENCE) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index 246e84ec71f..b86f80cbee7 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.9.1'] +REQUIREMENTS = ['insteonplm==0.9.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml index a0e250fef1f..9ea53c10fbf 100644 --- a/homeassistant/components/insteon_plm/services.yaml +++ b/homeassistant/components/insteon_plm/services.yaml @@ -14,7 +14,7 @@ delete_all_link: description: All-Link group number. example: 1 load_all_link_database: - description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistant. This may take a LONG time and may need to be repeated to obtain all records. + description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records. fields: entity_id: description: Name of the device to print diff --git a/homeassistant/components/iota.py b/homeassistant/components/iota.py index 442be6e22e7..ada70f8a9eb 100644 --- a/homeassistant/components/iota.py +++ b/homeassistant/components/iota.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyota==2.0.4'] +REQUIREMENTS = ['pyota==2.0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 48a9499d1a9..ecabcd36a85 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -202,7 +202,7 @@ def _check_for_uom_id(hass: HomeAssistant, node, node_uom = set(map(str.lower, node.uom)) if uom_list: - if node_uom.intersection(NODE_FILTERS[single_domain]['uom']): + if node_uom.intersection(uom_list): hass.data[ISY994_NODES][single_domain].append(node) return True else: diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py new file mode 100644 index 00000000000..70b66f84ae9 --- /dev/null +++ b/homeassistant/components/konnected.py @@ -0,0 +1,319 @@ +""" +Support for Konnected devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/konnected/ +""" +import logging +import hmac +import json +import voluptuous as vol + +from aiohttp.hdrs import AUTHORIZATION +from aiohttp.web import Request, Response # NOQA + +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.components.discovery import SERVICE_KONNECTED +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, + CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, + CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN, + ATTR_ENTITY_ID, ATTR_STATE) +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['konnected==0.1.2'] + +DOMAIN = 'konnected' + +CONF_ACTIVATION = 'activation' +STATE_LOW = 'low' +STATE_HIGH = 'high' + +PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: 'out', 9: 6} +ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} + +_BINARY_SENSOR_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), + vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + +_SWITCH_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): + vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)) + }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_DEVICES): [{ + vol.Required(CONF_ID): cv.string, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [_BINARY_SENSOR_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [_SWITCH_SCHEMA]), + }], + }), + }, + extra=vol.ALLOW_EXTRA, +) + +DEPENDENCIES = ['http', 'discovery'] + +ENDPOINT_ROOT = '/api/konnected' +UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}') +SIGNAL_SENSOR_UPDATE = 'konnected.{}.update' + + +async def async_setup(hass, config): + """Set up the Konnected platform.""" + cfg = config.get(DOMAIN) + if cfg is None: + cfg = {} + + access_token = cfg.get(CONF_ACCESS_TOKEN) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {CONF_ACCESS_TOKEN: access_token} + + def device_discovered(service, info): + """Call when a Konnected device has been discovered.""" + _LOGGER.debug("Discovered a new Konnected device: %s", info) + host = info.get(CONF_HOST) + port = info.get(CONF_PORT) + + device = KonnectedDevice(hass, host, port, cfg) + device.setup() + + discovery.async_listen( + hass, + SERVICE_KONNECTED, + device_discovered) + + hass.http.register_view(KonnectedView(access_token)) + + return True + + +class KonnectedDevice(object): + """A representation of a single Konnected device.""" + + def __init__(self, hass, host, port, config): + """Initialize the Konnected device.""" + self.hass = hass + self.host = host + self.port = port + self.user_config = config + + import konnected + self.client = konnected.Client(host, str(port)) + self.status = self.client.get_status() + _LOGGER.info('Initialized Konnected device %s', self.device_id) + + def setup(self): + """Set up a newly discovered Konnected device.""" + user_config = self.config() + if user_config: + _LOGGER.debug('Configuring Konnected device %s', self.device_id) + self.save_data() + self.sync_device_config() + discovery.load_platform( + self.hass, 'binary_sensor', + DOMAIN, {'device_id': self.device_id}) + discovery.load_platform( + self.hass, 'switch', DOMAIN, + {'device_id': self.device_id}) + + @property + def device_id(self): + """Device id is the MAC address as string with punctuation removed.""" + return self.status['mac'].replace(':', '') + + def config(self): + """Return an object representing the user defined configuration.""" + device_id = self.device_id + valid_keys = [device_id, device_id.upper(), + device_id[6:], device_id.upper()[6:]] + configured_devices = self.user_config[CONF_DEVICES] + return next((device for device in + configured_devices if device[CONF_ID] in valid_keys), + None) + + def save_data(self): + """Save the device configuration to `hass.data`.""" + sensors = {} + for entity in self.config().get(CONF_BINARY_SENSORS) or []: + if CONF_ZONE in entity: + pin = ZONE_TO_PIN[entity[CONF_ZONE]] + else: + pin = entity[CONF_PIN] + + sensor_status = next((sensor for sensor in + self.status.get('sensors') if + sensor.get(CONF_PIN) == pin), {}) + if sensor_status.get(ATTR_STATE): + initial_state = bool(int(sensor_status.get(ATTR_STATE))) + else: + initial_state = None + + sensors[pin] = { + CONF_TYPE: entity[CONF_TYPE], + CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + ATTR_STATE: initial_state + } + _LOGGER.debug('Set up sensor %s (initial state: %s)', + sensors[pin].get('name'), + sensors[pin].get(ATTR_STATE)) + + actuators = {} + for entity in self.config().get(CONF_SWITCHES) or []: + if 'zone' in entity: + pin = ZONE_TO_PIN[entity['zone']] + else: + pin = entity['pin'] + + actuator_status = next((actuator for actuator in + self.status.get('actuators') if + actuator.get('pin') == pin), {}) + if actuator_status.get(ATTR_STATE): + initial_state = bool(int(actuator_status.get(ATTR_STATE))) + else: + initial_state = None + + actuators[pin] = { + CONF_NAME: entity.get( + CONF_NAME, 'Konnected {} Actuator {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + ATTR_STATE: initial_state, + CONF_ACTIVATION: entity[CONF_ACTIVATION], + } + _LOGGER.debug('Set up actuator %s (initial state: %s)', + actuators[pin].get(CONF_NAME), + actuators[pin].get(ATTR_STATE)) + + device_data = { + 'client': self.client, + CONF_BINARY_SENSORS: sensors, + CONF_SWITCHES: actuators, + CONF_HOST: self.host, + CONF_PORT: self.port, + } + + if CONF_DEVICES not in self.hass.data[DOMAIN]: + self.hass.data[DOMAIN][CONF_DEVICES] = {} + + _LOGGER.debug('Storing data in hass.data[konnected]: %s', device_data) + self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data + + @property + def stored_configuration(self): + """Return the configuration stored in `hass.data` for this device.""" + return self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] + + def sensor_configuration(self): + """Return the configuration map for syncing sensors.""" + return [{'pin': p} for p in + self.stored_configuration[CONF_BINARY_SENSORS]] + + def actuator_configuration(self): + """Return the configuration map for syncing actuators.""" + return [{'pin': p, + 'trigger': (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] + else 1)} + for p, data in + self.stored_configuration[CONF_SWITCHES].items()] + + def sync_device_config(self): + """Sync the new pin configuration to the Konnected device.""" + desired_sensor_configuration = self.sensor_configuration() + current_sensor_configuration = [ + {'pin': s[CONF_PIN]} for s in self.status.get('sensors')] + _LOGGER.debug('%s: desired sensor config: %s', self.device_id, + desired_sensor_configuration) + _LOGGER.debug('%s: current sensor config: %s', self.device_id, + current_sensor_configuration) + + desired_actuator_config = self.actuator_configuration() + current_actuator_config = self.status.get('actuators') + _LOGGER.debug('%s: desired actuator config: %s', self.device_id, + desired_actuator_config) + _LOGGER.debug('%s: current actuator config: %s', self.device_id, + current_actuator_config) + + if (desired_sensor_configuration != current_sensor_configuration) or \ + (current_actuator_config != desired_actuator_config): + _LOGGER.debug('pushing settings to device %s', self.device_id) + self.client.put_settings( + desired_sensor_configuration, + desired_actuator_config, + self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN), + self.hass.config.api.base_url + ENDPOINT_ROOT + ) + + +class KonnectedView(HomeAssistantView): + """View creates an endpoint to receive push updates from the device.""" + + url = UPDATE_ENDPOINT + extra_urls = [UPDATE_ENDPOINT + '/{pin_num}/{state}'] + name = 'api:konnected' + requires_auth = False # Uses access token from configuration + + def __init__(self, auth_token): + """Initialize the view.""" + self.auth_token = auth_token + + async def put(self, request: Request, device_id, + pin_num=None, state=None) -> Response: + """Receive a sensor update via PUT request and async set state.""" + hass = request.app['hass'] + data = hass.data[DOMAIN] + + try: # Konnected 2.2.0 and above supports JSON payloads + payload = await request.json() + pin_num = payload['pin'] + state = payload['state'] + except json.decoder.JSONDecodeError: + _LOGGER.warning(("Your Konnected device software may be out of " + "date. Visit https://help.konnected.io for " + "updating instructions.")) + + auth = request.headers.get(AUTHORIZATION, None) + if not hmac.compare_digest('Bearer {}'.format(self.auth_token), auth): + return self.json_message( + "unauthorized", status_code=HTTP_UNAUTHORIZED) + pin_num = int(pin_num) + state = bool(int(state)) + device = data[CONF_DEVICES].get(device_id) + if device is None: + return self.json_message('unregistered device', + status_code=HTTP_BAD_REQUEST) + pin_data = device[CONF_BINARY_SENSORS].get(pin_num) or \ + device[CONF_SWITCHES].get(pin_num) + + if pin_data is None: + return self.json_message('unregistered sensor/actuator', + status_code=HTTP_BAD_REQUEST) + + entity_id = pin_data.get(ATTR_ENTITY_ID) + if entity_id is None: + return self.json_message('uninitialized sensor/actuator', + status_code=HTTP_INTERNAL_SERVER_ERROR) + + async_dispatcher_send( + hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) + return self.json_message('ok') diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 6c7f2e98e37..fc85e05238f 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -222,27 +222,34 @@ class FluxLight(Light): effect = kwargs.get(ATTR_EFFECT) white = kwargs.get(ATTR_WHITE_VALUE) - # color change only - if rgb is not None: - self._bulb.setRgb(*tuple(rgb), brightness=self.brightness) + # Show warning if effect set with rgb, brightness, or white level + if effect and (brightness or white or rgb): + _LOGGER.warning("RGB, brightness and white level are ignored when" + " an effect is specified for a flux bulb") - # brightness change only - elif brightness is not None: - (red, green, blue) = self._bulb.getRgb() - self._bulb.setRgb(red, green, blue, brightness=brightness) - - # random color effect - elif effect == EFFECT_RANDOM: + # Random color effect + if effect == EFFECT_RANDOM: self._bulb.setRgb(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + return - # effect selection + # Effect selection elif effect in EFFECT_MAP: self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) + return - # white change only - elif white is not None: + # Preserve current brightness on color/white level change + if brightness is None: + brightness = self.brightness + + # Preserve color on brightness/white level change + if rgb is None: + rgb = self._bulb.getRgb() + + self._bulb.setRgb(*tuple(rgb), brightness=brightness) + + if white is not None: self._bulb.setWarmWhite255(white) def turn_off(self, **kwargs): diff --git a/homeassistant/components/light/homematicip_cloud.py b/homeassistant/components/light/homematicip_cloud.py new file mode 100644 index 00000000000..e433da44ae7 --- /dev/null +++ b/homeassistant/components/light/homematicip_cloud.py @@ -0,0 +1,76 @@ +""" +Support for HomematicIP light. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/light.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.light import Light +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_POWER_CONSUMPTION = 'power_consumption' +ATTR_ENERGIE_COUNTER = 'energie_counter' +ATTR_PROFILE_MODE = 'profile_mode' + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP light devices.""" + from homematicip.device import ( + BrandSwitchMeasuring) + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [] + for device in home.devices: + if isinstance(device, BrandSwitchMeasuring): + devices.append(HomematicipLightMeasuring(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipLight(HomematicipGenericDevice, Light): + """MomematicIP light device.""" + + def __init__(self, home, device): + """Initialize the light device.""" + super().__init__(home, device) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._device.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._device.turn_off() + + +class HomematicipLightMeasuring(HomematicipLight): + """MomematicIP measuring light device.""" + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self._device.currentPowerConsumption + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + if self._device.energyCounter is None: + return 0 + return round(self._device.energyCounter) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index bb84b3a6fed..bd4fece89e3 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -142,10 +142,9 @@ def state(new_state): from limitlessled.pipeline import Pipeline pipeline = Pipeline() transition_time = DEFAULT_TRANSITION - # Stop any repeating pipeline. - if self.repeating: - self.repeating = False + if self._effect == EFFECT_COLORLOOP: self.group.stop() + self._effect = None # Set transition time. if ATTR_TRANSITION in kwargs: transition_time = int(kwargs[ATTR_TRANSITION]) @@ -183,11 +182,11 @@ class LimitlessLEDGroup(Light): self.group = group self.config = config - self.repeating = False self._is_on = False self._brightness = None self._temperature = None self._color = None + self._effect = None @asyncio.coroutine def async_added_to_hass(self): @@ -222,6 +221,9 @@ class LimitlessLEDGroup(Light): @property def brightness(self): """Return the brightness property.""" + if self._effect == EFFECT_NIGHT: + return 1 + return self._brightness @property @@ -242,6 +244,9 @@ class LimitlessLEDGroup(Light): @property def hs_color(self): """Return the color property.""" + if self._effect == EFFECT_NIGHT: + return None + return self._color @property @@ -249,6 +254,11 @@ class LimitlessLEDGroup(Light): """Flag supported features.""" return self._supported + @property + def effect(self): + """Return the current effect for this light.""" + return self._effect + @property def effect_list(self): """Return the list of supported effects for this light.""" @@ -270,6 +280,7 @@ class LimitlessLEDGroup(Light): if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT: if EFFECT_NIGHT in self._effect_list: pipeline.night_light() + self._effect = EFFECT_NIGHT return pipeline.on() @@ -314,7 +325,7 @@ class LimitlessLEDGroup(Light): if ATTR_EFFECT in kwargs and self._effect_list: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: from limitlessled.presets import COLORLOOP - self.repeating = True + self._effect = EFFECT_COLORLOOP pipeline.append(COLORLOOP) if kwargs[ATTR_EFFECT] == EFFECT_WHITE: pipeline.white() diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index a0534ba4e95..97a4cc8c137 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -17,12 +16,13 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, - CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability) +from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -100,8 +100,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up a MQTT Light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -213,10 +213,9 @@ class MqttLight(MqttAvailability, Light): self._supported_features |= ( topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() templates = {} for key, tpl in list(self._templates.items()): @@ -226,6 +225,8 @@ class MqttLight(MqttAvailability, Light): tpl.hass = self.hass templates[key] = tpl.async_render_with_possible_json_value + last_state = await async_get_last_state(self.hass, self.entity_id) + @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" @@ -237,9 +238,11 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) + elif self._optimistic and last_state: + self._state = last_state.state == STATE_ON @callback def brightness_received(topic, payload, qos): @@ -250,10 +253,13 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_BRIGHTNESS_STATE_TOPIC], brightness_received, self._qos) self._brightness = 255 + elif self._optimistic_brightness and last_state\ + and last_state.attributes.get(ATTR_BRIGHTNESS): + self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) elif self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: self._brightness = 255 else: @@ -268,11 +274,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_RGB_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_RGB_STATE_TOPIC], rgb_received, self._qos) self._hs = (0, 0) - if self._topic[CONF_RGB_COMMAND_TOPIC] is not None: + if self._optimistic_rgb and last_state\ + and last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + elif self._topic[CONF_RGB_COMMAND_TOPIC] is not None: self._hs = (0, 0) @callback @@ -282,11 +291,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_COLOR_TEMP_STATE_TOPIC], color_temp_received, self._qos) self._color_temp = 150 - if self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: + if self._optimistic_color_temp and last_state\ + and last_state.attributes.get(ATTR_COLOR_TEMP): + self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + elif self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: self._color_temp = 150 else: self._color_temp = None @@ -298,11 +310,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_EFFECT_STATE_TOPIC], effect_received, self._qos) self._effect = 'none' - if self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: + if self._optimistic_effect and last_state\ + and last_state.attributes.get(ATTR_EFFECT): + self._effect = last_state.attributes.get(ATTR_EFFECT) + elif self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: self._effect = 'none' else: self._effect = None @@ -316,10 +331,13 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_WHITE_VALUE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_WHITE_VALUE_STATE_TOPIC], white_value_received, self._qos) self._white_value = 255 + elif self._optimistic_white_value and last_state\ + and last_state.attributes.get(ATTR_WHITE_VALUE): + self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) elif self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: self._white_value = 255 else: @@ -334,11 +352,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_XY_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_XY_STATE_TOPIC], xy_received, self._qos) self._hs = (0, 0) - if self._topic[CONF_XY_COMMAND_TOPIC] is not None: + if self._optimistic_xy and last_state\ + and last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + elif self._topic[CONF_XY_COMMAND_TOPIC] is not None: self._hs = (0, 0) @property @@ -396,8 +417,7 @@ class MqttLight(MqttAvailability, Light): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -517,8 +537,7 @@ class MqttLight(MqttAvailability, Light): if should_update: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index ca5c76e905f..14f5ee7a9b9 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE from homeassistant.const import ( - CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, STATE_ON, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, @@ -26,6 +26,7 @@ from homeassistant.components.mqtt import ( MqttAvailability) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -177,6 +178,8 @@ class MqttJson(MqttAvailability, Light): """Subscribe to MQTT events.""" await super().async_added_to_hass() + last_state = await async_get_last_state(self.hass, self.entity_id) + @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" @@ -260,6 +263,19 @@ class MqttJson(MqttAvailability, Light): self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) + if self._optimistic and last_state: + self._state = last_state.state == STATE_ON + if last_state.attributes.get(ATTR_BRIGHTNESS): + self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + if last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + if last_state.attributes.get(ATTR_COLOR_TEMP): + self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + if last_state.attributes.get(ATTR_EFFECT): + self._effect = last_state.attributes.get(ATTR_EFFECT) + if last_state.attributes.get(ATTR_WHITE_VALUE): + self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) + @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index 06a94cd23b4..e32c13fc5b6 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -4,7 +4,6 @@ Support for MQTT Template lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_template/ """ -import asyncio import logging import voluptuous as vol @@ -22,6 +21,7 @@ from homeassistant.components.mqtt import ( MqttAvailability) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util +from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -66,8 +66,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up a MQTT Template light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -152,10 +152,11 @@ class MqttTemplate(MqttAvailability, Light): if tpl is not None: tpl.hass = hass - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() + + last_state = await async_get_last_state(self.hass, self.entity_id) @callback def state_received(topic, payload, qos): @@ -223,10 +224,23 @@ class MqttTemplate(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topics[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topics[CONF_STATE_TOPIC], state_received, self._qos) + if self._optimistic and last_state: + self._state = last_state.state == STATE_ON + if last_state.attributes.get(ATTR_BRIGHTNESS): + self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + if last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + if last_state.attributes.get(ATTR_COLOR_TEMP): + self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + if last_state.attributes.get(ATTR_EFFECT): + self._effect = last_state.attributes.get(ATTR_EFFECT) + if last_state.attributes.get(ATTR_WHITE_VALUE): + self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -280,8 +294,7 @@ class MqttTemplate(MqttAvailability, Light): """Return the current effect.""" return self._effect - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the entity on. This method is a coroutine. @@ -339,8 +352,7 @@ class MqttTemplate(MqttAvailability, Light): if self._optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the entity off. This method is a coroutine. diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 6e41e0f5693..55387288d7f 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -130,7 +130,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): self._white = white self._values[self.value_type] = hex_color - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" value_type = self.gateway.const.SetReq.V_LIGHT self.gateway.set_child_value( @@ -139,7 +139,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): # optimistically assume that light has changed state self._state = False self._values[value_type] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() def _async_update_light(self): """Update the controller with values from light child.""" @@ -171,12 +171,12 @@ class MySensorsLightDimmer(MySensorsLight): """Flag supported features.""" return SUPPORT_BRIGHTNESS - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) if self.gateway.optimistic: - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" @@ -196,13 +196,13 @@ class MySensorsLightRGB(MySensorsLight): return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_COLOR - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs) if self.gateway.optimistic: - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" @@ -225,10 +225,10 @@ class MySensorsLightRGBW(MySensorsLightRGB): return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW return SUPPORT_MYSENSORS_RGBW - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs) if self.gateway.optimistic: - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py index 99c07166037..c26766d8deb 100644 --- a/homeassistant/components/light/nanoleaf_aurora.py +++ b/homeassistant/components/light/nanoleaf_aurora.py @@ -92,6 +92,16 @@ class AuroraLight(Light): """Return the list of supported effects.""" return self._effects_list + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 154 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 833 + @property def name(self): """Return the display name of this light.""" diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 04e9c34b0f6..a2cc4fd7aeb 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -16,8 +16,6 @@ from homeassistant.util.color import \ DEPENDENCIES = ['wink'] -SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Wink lights.""" @@ -78,7 +76,14 @@ class WinkLight(WinkDevice, Light): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_WINK + supports = SUPPORT_BRIGHTNESS + if self.wink.supports_temperature(): + supports = supports | SUPPORT_COLOR_TEMP + if self.wink.supports_xy_color(): + supports = supports | SUPPORT_COLOR + elif self.wink.supports_hue_saturation(): + supports = supports | SUPPORT_COLOR + return supports def turn_on(self, **kwargs): """Turn the switch on.""" diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index 8eb1b3dc9b6..b44bf820b23 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -6,7 +6,6 @@ at https://home-assistant.io/components/light.zha/ """ import logging from homeassistant.components import light, zha -from homeassistant.const import STATE_UNKNOWN import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -76,7 +75,7 @@ class Light(zha.Entity, light.Light): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state == STATE_UNKNOWN: + if self._state is None: return False return bool(self._state) diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index a5cd18454df..1c42e427a00 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -11,7 +11,8 @@ import voluptuous as vol from homeassistant.components.lock import LockDevice from homeassistant.components.wink import DOMAIN, WinkDevice -from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_CODE, ATTR_ENTITY_ID, ATTR_NAME, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['wink'] @@ -28,7 +29,6 @@ SERVICE_ADD_KEY = 'wink_add_new_lock_key_code' ATTR_ENABLED = 'enabled' ATTR_SENSITIVITY = 'sensitivity' ATTR_MODE = 'mode' -ATTR_NAME = 'name' ALARM_SENSITIVITY_MAP = { 'low': 0.2, diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 8bab6fe0440..1ea0b586d33 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -4,44 +4,49 @@ Event parser and human readable log generator. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logbook/ """ -import logging from datetime import timedelta from itertools import groupby +import logging import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components import sun from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, - STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST, - EVENT_LOGBOOK_ENTRY) -from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN - -DOMAIN = 'logbook' -DEPENDENCIES = ['recorder', 'frontend'] + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, CONF_EXCLUDE, + CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME, + STATE_OFF, STATE_ON) +from homeassistant.core import DOMAIN as HA_DOMAIN +from homeassistant.core import State, callback, split_entity_id +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -CONF_EXCLUDE = 'exclude' -CONF_INCLUDE = 'include' -CONF_ENTITIES = 'entities' +ATTR_MESSAGE = 'message' + CONF_DOMAINS = 'domains' +CONF_ENTITIES = 'entities' +CONTINUOUS_DOMAINS = ['proximity', 'sensor'] + +DEPENDENCIES = ['recorder', 'frontend'] + +DOMAIN = 'logbook' + +GROUP_BY_MINUTES = 15 CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ CONF_EXCLUDE: vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, - [cv.string]) + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) }), CONF_INCLUDE: vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, - [cv.string]) + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) }) }), }, extra=vol.ALLOW_EXTRA) @@ -51,15 +56,6 @@ ALL_EVENT_TYPES = [ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP ] -GROUP_BY_MINUTES = 15 - -CONTINUOUS_DOMAINS = ['proximity', 'sensor'] - -ATTR_NAME = 'name' -ATTR_MESSAGE = 'message' -ATTR_DOMAIN = 'domain' -ATTR_ENTITY_ID = 'entity_id' - LOG_MESSAGE_SCHEMA = vol.Schema({ vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_MESSAGE): cv.template, diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index fe6ebe8e618..89cc296111b 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.04.25'] +REQUIREMENTS = ['youtube_dl==2018.05.09'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index bcbee5c4ff7..0758b5f3058 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_PASSWORD) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pycmus==0.1.0'] +REQUIREMENTS = ['pycmus==0.1.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 39c278ff95d..71b74868544 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -24,7 +24,6 @@ _LOGGER = logging.getLogger(__name__) CONF_SOURCES = 'sources' CONF_MAX_VOLUME = 'max_volume' -CONF_ZONE2 = 'zone2' DEFAULT_NAME = 'Onkyo Receiver' SUPPORTED_MAX_VOLUME = 80 @@ -47,9 +46,36 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME)), vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, - vol.Optional(CONF_ZONE2, default=False): cv.boolean, }) +TIMEOUT_MESSAGE = 'Timeout waiting for response.' + + +def determine_zones(receiver): + """Determine what zones are available for the receiver.""" + out = { + "zone2": False, + "zone3": False, + } + try: + _LOGGER.debug("Checking for zone 2 capability") + receiver.raw("ZPW") + out["zone2"] = True + except ValueError as error: + if str(error) != TIMEOUT_MESSAGE: + raise error + _LOGGER.debug("Zone 2 timed out, assuming no functionality") + try: + _LOGGER.debug("Checking for zone 3 capability") + receiver.raw("PW3") + out["zone3"] = True + except ValueError as error: + if str(error) != TIMEOUT_MESSAGE: + raise error + _LOGGER.debug("Zone 3 timed out, assuming no functionality") + + return out + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Onkyo platform.""" @@ -61,20 +87,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if CONF_HOST in config and host not in KNOWN_HOSTS: try: + receiver = eiscp.eISCP(host) hosts.append(OnkyoDevice( - eiscp.eISCP(host), config.get(CONF_SOURCES), + receiver, + config.get(CONF_SOURCES), name=config.get(CONF_NAME), max_volume=config.get(CONF_MAX_VOLUME), )) KNOWN_HOSTS.append(host) - # Add Zone2 if configured - if config.get(CONF_ZONE2): + zones = determine_zones(receiver) + + # Add Zone2 if available + if zones["zone2"]: _LOGGER.debug("Setting up zone 2") - hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host), - config.get(CONF_SOURCES), - name=config.get(CONF_NAME) + - " Zone 2")) + hosts.append(OnkyoDeviceZone( + "2", receiver, + config.get(CONF_SOURCES), + name="{} Zone 2".format(config[CONF_NAME]))) + # Add Zone3 if available + if zones["zone3"]: + _LOGGER.debug("Setting up zone 3") + hosts.append(OnkyoDeviceZone( + "3", receiver, + config.get(CONF_SOURCES), + name="{} Zone 3".format(config[CONF_NAME]))) except OSError: _LOGGER.error("Unable to connect to receiver at %s", host) else: @@ -227,12 +264,17 @@ class OnkyoDevice(MediaPlayerDevice): self.command('input-selector {}'.format(source)) -class OnkyoDeviceZone2(OnkyoDevice): - """Representation of an Onkyo device's zone 2.""" +class OnkyoDeviceZone(OnkyoDevice): + """Representation of an Onkyo device's extra zone.""" + + def __init__(self, zone, receiver, sources, name=None): + """Initialize the Zone with the zone identifier.""" + self._zone = zone + super().__init__(receiver, sources, name) def update(self): """Get the latest state from the device.""" - status = self.command('zone2.power=query') + status = self.command('zone{}.power=query'.format(self._zone)) if not status: return @@ -242,9 +284,10 @@ class OnkyoDeviceZone2(OnkyoDevice): self._pwstate = STATE_OFF return - volume_raw = self.command('zone2.volume=query') - mute_raw = self.command('zone2.muting=query') - current_source_raw = self.command('zone2.selector=query') + volume_raw = self.command('zone{}.volume=query'.format(self._zone)) + mute_raw = self.command('zone{}.muting=query'.format(self._zone)) + current_source_raw = self.command( + 'zone{}.selector=query'.format(self._zone)) if not (volume_raw and mute_raw and current_source_raw): return @@ -268,33 +311,33 @@ class OnkyoDeviceZone2(OnkyoDevice): def turn_off(self): """Turn the media player off.""" - self.command('zone2.power=standby') + self.command('zone{}.power=standby'.format(self._zone)) def set_volume_level(self, volume): """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command('zone2.volume={}'.format(int(volume*80))) + self.command('zone{}.volume={}'.format(self._zone, int(volume*80))) def volume_up(self): """Increase volume by 1 step.""" - self.command('zone2.volume=level-up') + self.command('zone{}.volume=level-up'.format(self._zone)) def volume_down(self): """Decrease volume by 1 step.""" - self.command('zone2.volume=level-down') + self.command('zone{}.volume=level-down'.format(self._zone)) def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: - self.command('zone2.muting=on') + self.command('zone{}.muting=on'.format(self._zone)) else: - self.command('zone2.muting=off') + self.command('zone{}.muting=off'.format(self._zone)) def turn_on(self): """Turn the media player on.""" - self.command('zone2.power=on') + self.command('zone{}.power=on'.format(self._zone)) def select_source(self, source): """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] - self.command('zone2.selector={}'.format(source)) + self.command('zone{}.selector={}'.format(self._zone, source)) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 87129f30db5..a46e781de59 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -146,6 +146,11 @@ class RokuDevice(MediaPlayerDevice): """Flag media player features that are supported.""" return SUPPORT_ROKU + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return self.device_info.sernum + @property def media_content_type(self): """Content type of current playing media.""" diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index 955456f2465..5d0962775f0 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -151,8 +151,8 @@ class SongpalDevice(MediaPlayerDevice): return if len(volumes) > 1: - _LOGGER.warning("Got %s volume controls, using the first one", - volumes) + _LOGGER.debug("Got %s volume controls, using the first one", + volumes) volume = volumes[0] _LOGGER.debug("Current volume: %s", volume) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index cc10355abe8..06e5f3befe4 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -682,11 +682,15 @@ class SonosDevice(MediaPlayerDevice): if group: # New group information is pushed coordinator_uid, *slave_uids = group.split(',') - else: + elif self.soco.group: # Use SoCo cache for existing topology coordinator_uid = self.soco.group.coordinator.uid slave_uids = [p.uid for p in self.soco.group.members if p.uid != coordinator_uid] + else: + # Not yet in the cache, this can happen when a speaker boots + coordinator_uid = self.unique_id + slave_uids = [] if self.unique_id == coordinator_uid: sonos_group = [] diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index fa4f03f1179..03f847ae40c 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -30,7 +30,8 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, - SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP) + SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + SERVICE_MEDIA_STOP) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_call_from_config @@ -45,7 +46,7 @@ CONF_SERVICE_DATA = 'service_data' ATTR_DATA = 'data' CONF_STATE = 'state' -OFF_STATES = [STATE_IDLE, STATE_OFF] +OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE] REQUIREMENTS = [] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 5b8ac2ad236..bb7942a2545 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -222,7 +222,7 @@ class YamahaDevice(MediaPlayerDevice): @property def zone_id(self): - """Return an zone_id to ensure 1 media player per zone.""" + """Return a zone_id to ensure 1 media player per zone.""" return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone) @property diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index 7c167f93142..847f4131f43 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -1,5 +1,5 @@ """ -Support for microsoft face recognition. +Support for Microsoft face recognition. For more details about this component, please refer to the documentation at https://home-assistant.io/components/microsoft_face/ @@ -13,7 +13,7 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT +from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT, ATTR_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -22,28 +22,25 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -DOMAIN = 'microsoft_face' -DEPENDENCIES = ['camera'] - -FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" - -DATA_MICROSOFT_FACE = 'microsoft_face' +ATTR_CAMERA_ENTITY = 'camera_entity' +ATTR_GROUP = 'group' +ATTR_PERSON = 'person' CONF_AZURE_REGION = 'azure_region' +DATA_MICROSOFT_FACE = 'microsoft_face' +DEFAULT_TIMEOUT = 10 +DEPENDENCIES = ['camera'] +DOMAIN = 'microsoft_face' + +FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" + SERVICE_CREATE_GROUP = 'create_group' -SERVICE_DELETE_GROUP = 'delete_group' -SERVICE_TRAIN_GROUP = 'train_group' SERVICE_CREATE_PERSON = 'create_person' +SERVICE_DELETE_GROUP = 'delete_group' SERVICE_DELETE_PERSON = 'delete_person' SERVICE_FACE_PERSON = 'face_person' - -ATTR_GROUP = 'group' -ATTR_PERSON = 'person' -ATTR_CAMERA_ENTITY = 'camera_entity' -ATTR_NAME = 'name' - -DEFAULT_TIMEOUT = 10 +SERVICE_TRAIN_GROUP = 'train_group' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -111,7 +108,7 @@ def face_person(hass, group, person, camera_entity): @asyncio.coroutine def async_setup(hass, config): - """Set up microsoft face.""" + """Set up Microsoft Face.""" entities = {} face = MicrosoftFace( hass, diff --git a/homeassistant/components/mychevy.py b/homeassistant/components/mychevy.py index 678cdf10c56..3531c6b4919 100644 --- a/homeassistant/components/mychevy.py +++ b/homeassistant/components/mychevy.py @@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.util import Throttle -REQUIREMENTS = ["mychevy==0.1.1"] +REQUIREMENTS = ["mychevy==0.4.0"] DOMAIN = 'mychevy' UPDATE_TOPIC = DOMAIN @@ -73,9 +73,6 @@ def setup(hass, base_config): hass.data[DOMAIN] = MyChevyHub(mc.MyChevy(email, password), hass) hass.data[DOMAIN].start() - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - return True @@ -98,8 +95,9 @@ class MyChevyHub(threading.Thread): super().__init__() self._client = client self.hass = hass - self.car = None + self.cars = [] self.status = None + self.ready = False @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -109,7 +107,22 @@ class MyChevyHub(threading.Thread): (like 2 to 3 minutes long time) """ - self.car = self._client.data() + self._client.login() + self._client.get_cars() + self.cars = self._client.cars + if self.ready is not True: + discovery.load_platform(self.hass, 'sensor', DOMAIN, {}, {}) + discovery.load_platform(self.hass, 'binary_sensor', DOMAIN, {}, {}) + self.ready = True + self.cars = self._client.update_cars() + + def get_car(self, vid): + """Compatibility to work with one car.""" + if self.cars: + for car in self.cars: + if car.vid == vid: + return car + return None def run(self): """Thread run loop.""" diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 9b394457973..1e7e252bd9d 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -4,6 +4,7 @@ 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 @@ -11,22 +12,23 @@ 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_START, - EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) + 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, dispatcher_send) + async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component -REQUIREMENTS = ['pymysensors==0.11.1'] +REQUIREMENTS = ['pymysensors==0.14.0'] _LOGGER = logging.getLogger(__name__) @@ -56,9 +58,11 @@ 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_{}_{}_{}_{}' @@ -280,67 +284,62 @@ MYSENSORS_CONST_SCHEMA = { } -def setup(hass, config): +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) - def setup_gateway(device, persistence_file, baud_rate, tcp_port, in_prefix, - out_prefix): + 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 setup_component(hass, MQTT_COMPONENT, config): - return + 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.publish(topic, payload, qos, retain) + mqtt.async_publish(topic, payload, qos, retain) def sub_callback(topic, sub_cb, qos): """Call MQTT subscribe function.""" - mqtt.subscribe(topic, sub_cb, qos) - gateway = mysensors.MQTTGateway( - pub_callback, sub_callback, + @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, in_prefix=in_prefix, - out_prefix=out_prefix, retain=retain) + protocol_version=version) else: try: - is_serial_port(device) - gateway = mysensors.SerialGateway( - device, event_callback=None, persistence=persistence, + 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, baud=baud_rate) + protocol_version=version) except vol.Invalid: - try: - socket.getaddrinfo(device, None) - # valid ip address - gateway = mysensors.TCPGateway( - device, event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version, port=tcp_port) - except OSError: - # invalid ip address - return + 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) - - def gw_start(event): - """Trigger to start of the gateway and any persistence.""" - if persistence: - discover_persistent_devices(hass, gateway) - gateway.start() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: gateway.stop()) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start) + if persistence: + await gateway.start_persistence() return gateway @@ -357,12 +356,12 @@ def setup(hass, config): tcp_port = gway.get(CONF_TCP_PORT) in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - ready_gateway = setup_gateway( + gateway = await setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) - if ready_gateway is not None: - ready_gateway.nodes_config = gway.get(CONF_NODES) - gateways[id(ready_gateway)] = ready_gateway + if gateway is not None: + gateway.nodes_config = gway.get(CONF_NODES) + gateways[id(gateway)] = gateway if not gateways: _LOGGER.error( @@ -371,9 +370,65 @@ def setup(hass, config): 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. @@ -431,14 +486,18 @@ def validate_child(gateway, node_id, child): return validated +@callback def discover_mysensors_platform(hass, platform, new_devices): """Discover a MySensors platform.""" - discovery.load_platform( - hass, platform, DOMAIN, {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}) + task = hass.async_add_job(discovery.async_load_platform( + hass, platform, DOMAIN, + {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) + return task -def discover_persistent_devices(hass, gateway): +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] @@ -447,7 +506,9 @@ def discover_persistent_devices(hass, gateway): for platform, dev_ids in validated.items(): new_devices[platform].extend(dev_ids) for platform, dev_ids in new_devices.items(): - discover_mysensors_platform(hass, platform, dev_ids) + tasks.append(discover_mysensors_platform(hass, platform, dev_ids)) + if tasks: + await asyncio.wait(tasks, loop=hass.loop) def get_mysensors_devices(hass, domain): @@ -459,14 +520,18 @@ def get_mysensors_devices(hass, 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) - child = msg.gateway.sensors[msg.node_id].children.get(msg.child_id) - if child is None: + 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 @@ -489,7 +554,7 @@ def gw_callback_factory(hass): # 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. - dispatcher_send(hass, signal) + async_dispatcher_send(hass, signal) end = timer() if end - start > 0.1: _LOGGER.debug( diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index dcbd1ce1317..9cca81e1485 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -12,8 +12,8 @@ import voluptuous as vol from homeassistant.helpers.event import track_state_change from homeassistant.config import load_yaml_config_file from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN) -from homeassistant.const import CONF_NAME, CONF_PLATFORM + ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME, CONF_PLATFORM, ATTR_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template as template_helper @@ -27,9 +27,8 @@ DEVICE_TRACKER_DOMAIN = 'device_tracker' SERVICE_REGISTER = 'apns_register' ATTR_PUSH_ID = 'push_id' -ATTR_NAME = 'name' -PLATFORM_SCHEMA = vol.Schema({ +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PLATFORM): 'apns', vol.Required(CONF_NAME): cv.string, vol.Required(CONF_CERTFILE): cv.isfile, @@ -66,7 +65,7 @@ class ApnsDevice(object): """ def __init__(self, push_id, name, tracking_device_id=None, disabled=False): - """Initialize Apns Device.""" + """Initialize APNS Device.""" self.device_push_id = push_id self.device_name = name self.tracking_id = tracking_device_id @@ -104,7 +103,7 @@ class ApnsDevice(object): @property def disabled(self): - """Return the .""" + """Return the state of the service.""" return self.device_disabled def disable(self): diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py index 895ffd9db10..f6c3e152b0a 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/notify/lametric.py @@ -23,11 +23,16 @@ _LOGGER = logging.getLogger(__name__) CONF_LIFETIME = "lifetime" CONF_CYCLES = "cycles" +CONF_PRIORITY = "priority" + +AVAILABLE_PRIORITIES = ["info", "warning", "critical"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ICON, default="i555"): cv.string, vol.Optional(CONF_LIFETIME, default=10): cv.positive_int, vol.Optional(CONF_CYCLES, default=1): cv.positive_int, + vol.Optional(CONF_PRIORITY, default="warning"): + vol.In(AVAILABLE_PRIORITIES) }) @@ -38,18 +43,20 @@ def get_service(hass, config, discovery_info=None): return LaMetricNotificationService(hlmn, config[CONF_ICON], config[CONF_LIFETIME] * 1000, - config[CONF_CYCLES]) + config[CONF_CYCLES], + config[CONF_PRIORITY]) class LaMetricNotificationService(BaseNotificationService): """Implement the notification service for LaMetric.""" - def __init__(self, hasslametricmanager, icon, lifetime, cycles): + def __init__(self, hasslametricmanager, icon, lifetime, cycles, priority): """Initialize the service.""" self.hasslametricmanager = hasslametricmanager self._icon = icon self._lifetime = lifetime self._cycles = cycles + self._priority = priority self._devices = [] # pylint: disable=broad-except @@ -64,6 +71,7 @@ class LaMetricNotificationService(BaseNotificationService): icon = self._icon cycles = self._cycles sound = None + priority = self._priority # Additional data? if data is not None: @@ -78,6 +86,14 @@ class LaMetricNotificationService(BaseNotificationService): except AssertionError: _LOGGER.error("Sound ID %s unknown, ignoring", data["sound"]) + if "cycles" in data: + cycles = data['cycles'] + if "priority" in data: + if data['priority'] in AVAILABLE_PRIORITIES: + priority = data['priority'] + else: + _LOGGER.warning("Priority %s invalid, using default %s", + data['priority'], priority) text_frame = SimpleFrame(icon, message) _LOGGER.debug("Icon/Message/Cycles/Lifetime: %s, %s, %d, %d", @@ -100,7 +116,8 @@ class LaMetricNotificationService(BaseNotificationService): if targets is None or dev["name"] in targets: try: lmn.set_device(dev) - lmn.send_notification(model, lifetime=self._lifetime) + lmn.send_notification(model, lifetime=self._lifetime, + priority=priority) _LOGGER.debug("Sent notification to LaMetric %s", dev["name"]) except OSError: diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index 257b5995446..1374779c5f0 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -42,7 +42,7 @@ class MySensorsNotificationService(BaseNotificationService): """Initialize the service.""" self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) - def send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" target_devices = kwargs.get(ATTR_TARGET) devices = [device for device in self.devices.values() diff --git a/homeassistant/components/notify/simplepush.py b/homeassistant/components/notify/simplepush.py deleted file mode 100644 index 9d5c58fc5b1..00000000000 --- a/homeassistant/components/notify/simplepush.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Simplepush notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.simplepush/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import CONF_PASSWORD - -REQUIREMENTS = ['simplepush==1.1.4'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_ENCRYPTED = 'encrypted' - -CONF_DEVICE_KEY = 'device_key' -CONF_EVENT = 'event' -CONF_SALT = 'salt' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICE_KEY): cv.string, - vol.Optional(CONF_EVENT): cv.string, - vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): cv.string, - vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): cv.string, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the Simplepush notification service.""" - return SimplePushNotificationService(config) - - -class SimplePushNotificationService(BaseNotificationService): - """Implementation of the notification service for Simplepush.""" - - def __init__(self, config): - """Initialize the Simplepush notification service.""" - self._device_key = config.get(CONF_DEVICE_KEY) - self._event = config.get(CONF_EVENT) - self._password = config.get(CONF_PASSWORD) - self._salt = config.get(CONF_SALT) - - def send_message(self, message='', **kwargs): - """Send a message to a Simplepush user.""" - from simplepush import send, send_encrypted - - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - - if self._password: - send_encrypted(self._device_key, self._password, self._salt, title, - message, event=self._event) - else: - send(self._device_key, title, message, event=self._event) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index f26318fa7a9..63e30a9491e 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -150,8 +150,10 @@ async def async_setup(hass, config): comps = {'switch': [], 'light': [], 'sensor': [], 'binary_sensor': []} try: + sensor_ids = [] for sens in sensors: _, _type = SENSORS[sens['type']] + sensor_ids.append(sens['id']) if _type is bool: comps['binary_sensor'].append(sens) continue @@ -192,9 +194,7 @@ async def async_setup(hass, config): 'qwikswitch.button.{}'.format(qspacket[QS_ID]), qspacket) return - if qspacket[QS_ID] not in qsusb.devices: - # Not a standard device in, component can handle packet - # i.e. sensors + if qspacket[QS_ID] in sensor_ids: _LOGGER.debug("Dispatch %s ((%s))", qspacket[QS_ID], qspacket) hass.helpers.dispatcher.async_dispatcher_send( qspacket[QS_ID], qspacket) diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py index 99cec53c2ed..f2d5893d60b 100644 --- a/homeassistant/components/rainmachine.py +++ b/homeassistant/components/rainmachine.py @@ -5,13 +5,14 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/rainmachine/ """ import logging -from datetime import timedelta import voluptuous as vol -from homeassistant.helpers import config_validation as cv, discovery from homeassistant.const import ( - CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_SWITCHES) + ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_SWITCHES) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['regenmaschine==0.4.1'] @@ -26,11 +27,11 @@ NOTIFICATION_TITLE = 'RainMachine Component Setup' CONF_ZONE_RUN_TIME = 'zone_run_time' DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_ICON = 'mdi:water' DEFAULT_PORT = 8080 DEFAULT_SSL = True -MIN_SCAN_TIME = timedelta(seconds=1) -MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) +PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) SWITCH_SCHEMA = vol.Schema({ vol.Optional(CONF_ZONE_RUN_TIME): @@ -68,8 +69,7 @@ def setup(hass, config): auth = Authenticator.create_local( ip_address, password, port=port, https=ssl) client = Client(auth) - mac = client.provision.wifi()['macAddress'] - hass.data[DATA_RAINMACHINE] = (client, mac) + hass.data[DATA_RAINMACHINE] = RainMachine(client) except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: _LOGGER.error('An error occurred: %s', str(exc_info)) hass.components.persistent_notification.create( @@ -87,3 +87,46 @@ def setup(hass, config): _LOGGER.debug('Setup complete') return True + + +class RainMachine(object): + """Define a generic RainMachine object.""" + + def __init__(self, client): + """Initialize.""" + self.client = client + self.device_mac = self.client.provision.wifi()['macAddress'] + + +class RainMachineEntity(Entity): + """Define a generic RainMachine entity.""" + + def __init__(self, + rainmachine, + rainmachine_type, + rainmachine_entity_id, + icon=DEFAULT_ICON): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._icon = icon + self._rainmachine_type = rainmachine_type + self._rainmachine_entity_id = rainmachine_entity_id + self.rainmachine = rainmachine + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + return self._attrs + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}_{2}'.format( + self.rainmachine.device_mac.replace( + ':', ''), self._rainmachine_type, + self._rainmachine_entity_id) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 2e96ec64d97..2f170a20646 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -4,21 +4,18 @@ Support for RFXtrx components. For more details about this component, please refer to the documentation at https://home-assistant.io/components/rfxtrx/ """ - import asyncio -import logging from collections import OrderedDict +import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - ATTR_ENTITY_ID, TEMP_CELSIUS, - CONF_DEVICES -) + ATTR_ENTITY_ID, ATTR_NAME, ATTR_STATE, CONF_DEVICE, CONF_DEVICES, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify REQUIREMENTS = ['pyRFXtrx==0.22.1'] @@ -29,8 +26,6 @@ DEFAULT_SIGNAL_REPETITIONS = 1 ATTR_AUTOMATIC_ADD = 'automatic_add' ATTR_DEVICE = 'device' ATTR_DEBUG = 'debug' -ATTR_STATE = 'state' -ATTR_NAME = 'name' ATTR_FIRE_EVENT = 'fire_event' ATTR_DATA_TYPE = 'data_type' ATTR_DUMMY = 'dummy' @@ -40,7 +35,6 @@ CONF_DATA_TYPE = 'data_type' CONF_SIGNAL_REPETITIONS = 'signal_repetitions' CONF_FIRE_EVENT = 'fire_event' CONF_DUMMY = 'dummy' -CONF_DEVICE = 'device' CONF_DEBUG = 'debug' CONF_OFF_DELAY = 'off_delay' EVENT_BUTTON_PRESSED = 'button_pressed' diff --git a/homeassistant/components/sabnzbd.py b/homeassistant/components/sabnzbd.py new file mode 100644 index 00000000000..a7b33b4c697 --- /dev/null +++ b/homeassistant/components/sabnzbd.py @@ -0,0 +1,254 @@ +""" +Support for monitoring an SABnzbd NZB client. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sabnzbd/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.discovery import SERVICE_SABNZBD +from homeassistant.const import ( + CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_SSL) +from homeassistant.core import callback +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.json import load_json, save_json + +REQUIREMENTS = ['pysabnzbd==1.0.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'sabnzbd' +DATA_SABNZBD = 'sabznbd' + +_CONFIGURING = {} + +ATTR_SPEED = 'speed' +BASE_URL_FORMAT = '{}://{}:{}/' +CONFIG_FILE = 'sabnzbd.conf' +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'SABnzbd' +DEFAULT_PORT = 8080 +DEFAULT_SPEED_LIMIT = '100' +DEFAULT_SSL = False + +UPDATE_INTERVAL = timedelta(seconds=30) + +SERVICE_PAUSE = 'pause' +SERVICE_RESUME = 'resume' +SERVICE_SET_SPEED = 'set_speed' + +SIGNAL_SABNZBD_UPDATED = 'sabnzbd_updated' + +SENSOR_TYPES = { + 'current_status': ['Status', None, 'status'], + 'speed': ['Speed', 'MB/s', 'kbpersec'], + 'queue_size': ['Queue', 'MB', 'mb'], + 'queue_remaining': ['Left', 'MB', 'mbleft'], + 'disk_size': ['Disk', 'GB', 'diskspacetotal1'], + 'disk_free': ['Disk Free', 'GB', 'diskspace1'], + 'queue_count': ['Queue Count', None, 'noofslots_total'], + 'day_size': ['Daily Total', 'GB', 'day_size'], + 'week_size': ['Weekly Total', 'GB', 'week_size'], + 'month_size': ['Monthly Total', 'GB', 'month_size'], + 'total_size': ['Total', 'GB', 'total_size'], +} + +SPEED_LIMIT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_check_sabnzbd(sab_api): + """Check if we can reach SABnzbd.""" + from pysabnzbd import SabnzbdApiException + + try: + await sab_api.check_available() + return True + except SabnzbdApiException: + _LOGGER.error("Connection to SABnzbd API failed") + return False + + +async def async_configure_sabnzbd(hass, config, use_ssl, name=DEFAULT_NAME, + api_key=None): + """Try to configure Sabnzbd and request api key if configuration fails.""" + from pysabnzbd import SabnzbdApi + + host = config[CONF_HOST] + port = config[CONF_PORT] + uri_scheme = 'https' if use_ssl else 'http' + base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) + if api_key is None: + conf = await hass.async_add_job(load_json, + hass.config.path(CONFIG_FILE)) + api_key = conf.get(base_url, {}).get(CONF_API_KEY, '') + + sab_api = SabnzbdApi(base_url, api_key) + if await async_check_sabnzbd(sab_api): + async_setup_sabnzbd(hass, sab_api, config, name) + else: + async_request_configuration(hass, config, base_url) + + +async def async_setup(hass, config): + """Setup the SABnzbd component.""" + async def sabnzbd_discovered(service, info): + """Handle service discovery.""" + ssl = info.get('properties', {}).get('https', '0') == '1' + await async_configure_sabnzbd(hass, info, ssl) + + discovery.async_listen(hass, SERVICE_SABNZBD, sabnzbd_discovered) + + conf = config.get(DOMAIN) + if conf is not None: + use_ssl = conf.get(CONF_SSL) + name = conf.get(CONF_NAME) + api_key = conf.get(CONF_API_KEY) + await async_configure_sabnzbd(hass, conf, use_ssl, name, api_key) + return True + + +@callback +def async_setup_sabnzbd(hass, sab_api, config, name): + """Setup SABnzbd sensors and services.""" + sab_api_data = SabnzbdApiData(sab_api, name, config.get(CONF_SENSORS, {})) + + if config.get(CONF_SENSORS): + hass.data[DATA_SABNZBD] = sab_api_data + hass.async_add_job( + discovery.async_load_platform(hass, 'sensor', DOMAIN, {}, config)) + + async def async_service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_PAUSE: + await sab_api_data.async_pause_queue() + elif service.service == SERVICE_RESUME: + await sab_api_data.async_resume_queue() + elif service.service == SERVICE_SET_SPEED: + speed = service.data.get(ATTR_SPEED) + await sab_api_data.async_set_queue_speed(speed) + + hass.services.async_register(DOMAIN, SERVICE_PAUSE, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_RESUME, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_SET_SPEED, + async_service_handler, + schema=SPEED_LIMIT_SCHEMA) + + async def async_update_sabnzbd(now): + """Refresh SABnzbd queue data.""" + from pysabnzbd import SabnzbdApiException + try: + await sab_api.refresh_data() + async_dispatcher_send(hass, SIGNAL_SABNZBD_UPDATED, None) + except SabnzbdApiException as err: + _LOGGER.error(err) + + async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) + + +@callback +def async_request_configuration(hass, config, host): + """Request configuration steps from the user.""" + from pysabnzbd import SabnzbdApi + + configurator = hass.components.configurator + # We got an error if this method is called while we are configuring + if host in _CONFIGURING: + configurator.async_notify_errors( + _CONFIGURING[host], + 'Failed to register, please try again.') + + return + + async def async_configuration_callback(data): + """Handle configuration changes.""" + api_key = data.get(CONF_API_KEY) + sab_api = SabnzbdApi(host, api_key) + if not await async_check_sabnzbd(sab_api): + return + + def success(): + """Setup was successful.""" + conf = load_json(hass.config.path(CONFIG_FILE)) + conf[host] = {CONF_API_KEY: api_key} + save_json(hass.config.path(CONFIG_FILE), conf) + req_config = _CONFIGURING.pop(host) + configurator.request_done(req_config) + + hass.async_add_job(success) + async_setup_sabnzbd(hass, sab_api, config, + config.get(CONF_NAME, DEFAULT_NAME)) + + _CONFIGURING[host] = configurator.async_request_config( + DEFAULT_NAME, + async_configuration_callback, + description='Enter the API Key', + submit_caption='Confirm', + fields=[{'id': CONF_API_KEY, 'name': 'API Key', 'type': ''}] + ) + + +class SabnzbdApiData: + """Class for storing/refreshing sabnzbd api queue data.""" + + def __init__(self, sab_api, name, sensors): + """Initialize component.""" + self.sab_api = sab_api + self.name = name + self.sensors = sensors + + async def async_pause_queue(self): + """Pause Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.pause_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_resume_queue(self): + """Resume Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.resume_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_set_queue_speed(self, limit): + """Set speed limit for the Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.set_speed_limit(limit) + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + def get_queue_field(self, field): + """Return the value for the given field from the Sabnzbd queue.""" + return self.sab_api.queue.get(field) diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index ed75520c179..e3331cdc763 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -9,22 +9,23 @@ import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) -LENGTH_ATTRIBUTES = { - 'remaining_range_fuel': ['Range (fuel)', 'mdi:ruler'], - 'mileage': ['Mileage', 'mdi:speedometer'] +ATTR_TO_HA = { + 'mileage': ['mdi:speedometer', 'km'], + 'remaining_range_total': ['mdi:ruler', 'km'], + 'remaining_range_electric': ['mdi:ruler', 'km'], + 'remaining_range_fuel': ['mdi:ruler', 'km'], + 'max_range_electric': ['mdi:ruler', 'km'], + 'remaining_fuel': ['mdi:gas-station', 'l'], + 'charging_time_remaining': ['mdi:update', 'h'], + 'charging_status': ['mdi:battery-charging', None] } -VALID_ATTRIBUTES = { - 'remaining_fuel': ['Remaining Fuel', 'mdi:gas-station'] -} - -VALID_ATTRIBUTES.update(LENGTH_ATTRIBUTES) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BMW sensors.""" @@ -34,27 +35,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for account in accounts: for vehicle in account.account.vehicles: - for key, value in sorted(VALID_ATTRIBUTES.items()): - device = BMWConnectedDriveSensor(account, vehicle, key, - value[0], value[1]) + for attribute_name in vehicle.drive_train_attributes: + device = BMWConnectedDriveSensor(account, vehicle, + attribute_name) devices.append(device) + device = BMWConnectedDriveSensor(account, vehicle, 'mileage') + devices.append(device) add_devices(devices, True) class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str, sensor_name, icon): + def __init__(self, account, vehicle, attribute: str): """Constructor.""" self._vehicle = vehicle self._account = account self._attribute = attribute self._state = None - self._unit_of_measurement = None self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) - self._sensor_name = sensor_name - self._icon = icon @property def should_poll(self) -> bool: @@ -74,7 +74,17 @@ class BMWConnectedDriveSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return self._icon + from bimmer_connected.state import ChargingState + vehicle_state = self._vehicle.state + charging_state = vehicle_state.charging_status in \ + [ChargingState.CHARGING] + + if self._attribute == 'charging_level_hv': + return icon_for_battery_level( + battery_level=vehicle_state.charging_level_hv, + charging=charging_state) + icon, _ = ATTR_TO_HA.get(self._attribute, [None, None]) + return icon @property def state(self): @@ -88,7 +98,8 @@ class BMWConnectedDriveSensor(Entity): @property def unit_of_measurement(self) -> str: """Get the unit of measurement.""" - return self._unit_of_measurement + _, unit = ATTR_TO_HA.get(self._attribute, [None, None]) + return unit @property def device_state_attributes(self): @@ -101,14 +112,10 @@ class BMWConnectedDriveSensor(Entity): """Read new state data from the library.""" _LOGGER.debug('Updating %s', self._vehicle.name) vehicle_state = self._vehicle.state - self._state = getattr(vehicle_state, self._attribute) - - if self._attribute in LENGTH_ATTRIBUTES: - self._unit_of_measurement = 'km' - elif self._attribute == 'remaining_fuel': - self._unit_of_measurement = 'l' + if self._attribute == 'charging_status': + self._state = getattr(vehicle_state, self._attribute).value else: - self._unit_of_measurement = None + self._state = getattr(vehicle_state, self._attribute) def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index 128f532e459..d6764e5e994 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -19,8 +19,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, STATE_UNKNOWN, CONF_NAME, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE) + CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, CONF_NAME, ATTR_ATTRIBUTION, + CONF_LATITUDE, CONF_LONGITUDE) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -145,21 +145,18 @@ class BOMCurrentSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.bom_data.data and self._condition in self.bom_data.data: - return self.bom_data.data[self._condition] - - return STATE_UNKNOWN + return self.bom_data.get_reading(self._condition) @property def device_state_attributes(self): """Return the state attributes of the device.""" attr = {} attr['Sensor Id'] = self._condition - attr['Zone Id'] = self.bom_data.data['history_product'] - attr['Station Id'] = self.bom_data.data['wmo'] - attr['Station Name'] = self.bom_data.data['name'] + attr['Zone Id'] = self.bom_data.latest_data['history_product'] + attr['Station Id'] = self.bom_data.latest_data['wmo'] + attr['Station Name'] = self.bom_data.latest_data['name'] attr['Last Update'] = datetime.datetime.strptime(str( - self.bom_data.data['local_date_time_full']), '%Y%m%d%H%M%S') + self.bom_data.latest_data['local_date_time_full']), '%Y%m%d%H%M%S') attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION return attr @@ -180,22 +177,43 @@ class BOMCurrentData(object): """Initialize the data object.""" self._hass = hass self._zone_id, self._wmo_id = station_id.split('.') - self.data = None + self._data = None def _build_url(self): url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) _LOGGER.info("BOM URL %s", url) return url + @property + def latest_data(self): + """Return the latest data object.""" + if self._data: + return self._data[0] + return None + + def get_reading(self, condition): + """Return the value for the given condition. + + BOM weather publishes condition readings for weather (and a few other + conditions) at intervals throughout the day. To avoid a `-` value in + the frontend for these conditions, we traverse the historical data + for the latest value that is not `-`. + + Iterators are used in this method to avoid iterating needlessly + iterating through the entire BOM provided dataset + """ + condition_readings = (entry[condition] for entry in self._data) + return next((x for x in condition_readings if x != '-'), None) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from BOM.""" try: result = requests.get(self._build_url(), timeout=10).json() - self.data = result['observations']['data'][0] + self._data = result['observations']['data'] except ValueError as err: _LOGGER.error("Check BOM %s", err.args) - self.data = None + self._data = None raise diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 6eb67f7cbd8..590d5a8f1ce 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -197,7 +197,7 @@ class BrSensor(Entity): def uid(self, coordinates): """Generate a unique id using coordinates and sensor type.""" - # The combination of the location, name an sensor type is unique + # The combination of the location, name and sensor type is unique return "%2.6f%2.6f%s" % (coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE], self.type) diff --git a/homeassistant/components/sensor/citybikes.py b/homeassistant/components/sensor/citybikes.py index b7635f729e2..a8bc441b722 100644 --- a/homeassistant/components/sensor/citybikes.py +++ b/homeassistant/components/sensor/citybikes.py @@ -4,32 +4,31 @@ Sensor for the CityBikes data. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.citybikes/ """ -import logging -from datetime import timedelta - import asyncio +from datetime import timedelta +import logging + import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT +from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, - ATTR_ATTRIBUTION, ATTR_LOCATION, ATTR_LATITUDE, ATTR_LONGITUDE, - STATE_UNKNOWN, LENGTH_METERS, LENGTH_FEET, ATTR_ID) + ATTR_ATTRIBUTION, ATTR_ID, ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, + ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS, + LENGTH_FEET, LENGTH_METERS) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import location, distance +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import distance, location _LOGGER = logging.getLogger(__name__) ATTR_EMPTY_SLOTS = 'empty_slots' ATTR_EXTRA = 'extra' ATTR_FREE_BIKES = 'free_bikes' -ATTR_NAME = 'name' ATTR_NETWORK = 'network' ATTR_NETWORKS_LIST = 'networks' ATTR_STATIONS_LIST = 'stations' @@ -151,8 +150,7 @@ def async_setup_platform(hass, config, async_add_devices, network = CityBikesNetwork(hass, network_id) hass.data[PLATFORM][MONITORED_NETWORKS][network_id] = network hass.async_add_job(network.async_refresh) - async_track_time_interval(hass, network.async_refresh, - SCAN_INTERVAL) + async_track_time_interval(hass, network.async_refresh, SCAN_INTERVAL) else: network = hass.data[PLATFORM][MONITORED_NETWORKS][network_id] @@ -160,14 +158,14 @@ def async_setup_platform(hass, config, async_add_devices, devices = [] for station in network.stations: - dist = location.distance(latitude, longitude, - station[ATTR_LATITUDE], - station[ATTR_LONGITUDE]) + dist = location.distance( + latitude, longitude, station[ATTR_LATITUDE], + station[ATTR_LONGITUDE]) station_id = station[ATTR_ID] station_uid = str(station.get(ATTR_EXTRA, {}).get(ATTR_UID, '')) - if radius > dist or stations_list.intersection((station_id, - station_uid)): + if radius > dist or stations_list.intersection( + (station_id, station_uid)): devices.append(CityBikesStation(hass, network, station_id, name)) async_add_devices(devices, True) @@ -199,8 +197,8 @@ class CityBikesNetwork: for network in networks_list[1:]: network_latitude = network[ATTR_LOCATION][ATTR_LATITUDE] network_longitude = network[ATTR_LOCATION][ATTR_LONGITUDE] - dist = location.distance(latitude, longitude, - network_latitude, network_longitude) + dist = location.distance( + latitude, longitude, network_latitude, network_longitude) if dist < minimum_dist: minimum_dist = dist result = network[ATTR_ID] @@ -246,13 +244,13 @@ class CityBikesStation(Entity): uid = "_".join([network.network_id, base_name, station_id]) else: uid = "_".join([network.network_id, station_id]) - self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, - hass=hass) + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, uid, hass=hass) @property def state(self): """Return the state of the sensor.""" - return self._station_data.get(ATTR_FREE_BIKES, STATE_UNKNOWN) + return self._station_data.get(ATTR_FREE_BIKES, None) @property def name(self): diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index f8ada07eec6..849e21a0901 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -23,7 +23,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_24H_VOLUME = '24h_volume' ATTR_AVAILABLE_SUPPLY = 'available_supply' ATTR_MARKET_CAP = 'market_cap' -ATTR_NAME = 'name' ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' @@ -130,6 +129,4 @@ class CoinMarketCapData(object): """Get the latest data from blockchain.info.""" from coinmarketcap import Market self.ticker = Market().ticker( - self.currency, - limit=1, - convert=self.display_currency) + self.currency, limit=1, convert=self.display_currency) diff --git a/homeassistant/components/sensor/comed_hourly_pricing.py b/homeassistant/components/sensor/comed_hourly_pricing.py index 01e9f443e0e..c0c477ade0b 100644 --- a/homeassistant/components/sensor/comed_hourly_pricing.py +++ b/homeassistant/components/sensor/comed_hourly_pricing.py @@ -4,19 +4,21 @@ Support for ComEd Hourly Pricing data. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.comed_hourly_pricing/ """ -from datetime import timedelta -import logging import asyncio +from datetime import timedelta import json -import async_timeout +import logging + import aiohttp +import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN -from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET, STATE_UNKNOWN) from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://hourlypricing.comed.com/api' @@ -27,8 +29,6 @@ CONF_ATTRIBUTION = "Data provided by ComEd Hourly Pricing service" CONF_CURRENT_HOUR_AVERAGE = 'current_hour_average' CONF_FIVE_MINUTE = 'five_minute' CONF_MONITORED_FEEDS = 'monitored_feeds' -CONF_NAME = 'name' -CONF_OFFSET = 'offset' CONF_SENSOR_TYPE = 'type' SENSOR_TYPES = { @@ -40,12 +40,12 @@ TYPES_SCHEMA = vol.In(SENSOR_TYPES) SENSORS_SCHEMA = vol.Schema({ vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_OFFSET, default=0.0): vol.Coerce(float), - vol.Optional(CONF_NAME): cv.string }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA] + vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA], }) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index ac09de9c699..e75f36d59f7 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -33,6 +33,11 @@ DEFAULT_LANGUAGE = 'en' DEFAULT_NAME = 'Dark Sky' +DEPRECATED_SENSOR_TYPES = {'apparent_temperature_max', + 'apparent_temperature_min', + 'temperature_max', + 'temperature_min'} + # Sensor types are defined like so: # Name, si unit, us unit, ca unit, uk unit, uk2 unit SENSOR_TYPES = { @@ -90,16 +95,28 @@ SENSOR_TYPES = { '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly', 'daily']], + 'apparent_temperature_high': ["Daytime High Apparent Temperature", + '°C', '°F', '°C', '°C', '°C', + 'mdi:thermometer', ['daily']], 'apparent_temperature_min': ['Daily Low Apparent Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly', 'daily']], + 'apparent_temperature_low': ['Overnight Low Apparent Temperature', + '°C', '°F', '°C', '°C', '°C', + 'mdi:thermometer', ['daily']], 'temperature_max': ['Daily High Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['currently', 'hourly', 'daily']], + ['daily']], + 'temperature_high': ['Daytime High Temperature', + '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', + ['daily']], 'temperature_min': ['Daily Low Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['currently', 'hourly', 'daily']], + ['daily']], + 'temperature_low': ['Overnight Low Temperature', + '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', + ['daily']], 'precip_intensity_max': ['Daily Max Precip Intensity', 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', 'mdi:thermometer', @@ -185,6 +202,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): forecast = config.get(CONF_FORECAST) sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: + if variable in DEPRECATED_SENSOR_TYPES: + _LOGGER.warning("Monitored condition %s is deprecated.", + variable) sensors.append(DarkSkySensor(forecast_data, variable, name)) if forecast is not None and 'daily' in SENSOR_TYPES[variable][7]: for forecast_day in forecast: @@ -288,9 +308,13 @@ class DarkSkySensor(Entity): elif self.forecast_day > 0 or ( self.type in ['daily_summary', 'temperature_min', + 'temperature_low', 'temperature_max', + 'temperature_high', 'apparent_temperature_min', + 'apparent_temperature_low', 'apparent_temperature_max', + 'apparent_temperature_high', 'precip_intensity_max', 'precip_accumulation']): self.forecast_data.update_daily() diff --git a/homeassistant/components/sensor/fints.py b/homeassistant/components/sensor/fints.py new file mode 100644 index 00000000000..798f74bb654 --- /dev/null +++ b/homeassistant/components/sensor/fints.py @@ -0,0 +1,285 @@ +""" +Read the balance of your bank accounts via FinTS. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.fints/ +""" + +from collections import namedtuple +from datetime import timedelta +import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_USERNAME, CONF_PIN, CONF_URL, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['fints==0.2.1'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(hours=4) + +ICON = 'mdi:currency-eur' + +BankCredentials = namedtuple('BankCredentials', 'blz login pin url') + +CONF_BIN = 'bank_identification_number' +CONF_ACCOUNTS = 'accounts' +CONF_HOLDINGS = 'holdings' +CONF_ACCOUNT = 'account' + +ATTR_ACCOUNT = CONF_ACCOUNT +ATTR_BANK = 'bank' +ATTR_ACCOUNT_TYPE = 'account_type' + +SCHEMA_ACCOUNTS = vol.Schema({ + vol.Required(CONF_ACCOUNT): cv.string, + vol.Optional(CONF_NAME, default=None): vol.Any(None, cv.string), +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_BIN): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PIN): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACCOUNTS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), + vol.Optional(CONF_HOLDINGS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the sensors. + + Login to the bank and get a list of existing accounts. Create a + sensor for each account. + """ + credentials = BankCredentials(config[CONF_BIN], config[CONF_USERNAME], + config[CONF_PIN], config[CONF_URL]) + fints_name = config.get(CONF_NAME, config[CONF_BIN]) + + account_config = {acc[CONF_ACCOUNT]: acc[CONF_NAME] + for acc in config[CONF_ACCOUNTS]} + + holdings_config = {acc[CONF_ACCOUNT]: acc[CONF_NAME] + for acc in config[CONF_HOLDINGS]} + + client = FinTsClient(credentials, fints_name) + balance_accounts, holdings_accounts = client.detect_accounts() + accounts = [] + + for account in balance_accounts: + if config[CONF_ACCOUNTS] and account.iban not in account_config: + _LOGGER.info('skipping account %s for bank %s', + account.iban, fints_name) + continue + + account_name = account_config.get(account.iban) + if not account_name: + account_name = '{} - {}'.format(fints_name, account.iban) + accounts.append(FinTsAccount(client, account, account_name)) + _LOGGER.debug('Creating account %s for bank %s', + account.iban, fints_name) + + for account in holdings_accounts: + if config[CONF_HOLDINGS] and \ + account.accountnumber not in holdings_config: + _LOGGER.info('skipping holdings %s for bank %s', + account.accountnumber, fints_name) + continue + + account_name = holdings_config.get(account.accountnumber) + if not account_name: + account_name = '{} - {}'.format( + fints_name, account.accountnumber) + accounts.append(FinTsHoldingsAccount(client, account, account_name)) + _LOGGER.debug('Creating holdings %s for bank %s', + account.accountnumber, fints_name) + + add_devices(accounts, True) + + +class FinTsClient(object): + """Wrapper around the FinTS3PinTanClient. + + Use this class as Context Manager to get the FinTS3Client object. + """ + + def __init__(self, credentials: BankCredentials, name: str): + """Constructor for class FinTsClient.""" + self._credentials = credentials + self.name = name + + @property + def client(self): + """Get the client object. + + As the fints library is stateless, there is not benefit in caching + the client objects. If that ever changes, consider caching the client + object and also think about potential concurrency problems. + """ + from fints.client import FinTS3PinTanClient + return FinTS3PinTanClient( + self._credentials.blz, self._credentials.login, + self._credentials.pin, self._credentials.url) + + def detect_accounts(self): + """Identify the accounts of the bank.""" + from fints.dialog import FinTSDialogError + balance_accounts = [] + holdings_accounts = [] + for account in self.client.get_sepa_accounts(): + try: + self.client.get_balance(account) + balance_accounts.append(account) + except IndexError: + # account is not a balance account. + pass + except FinTSDialogError: + # account is not a balance account. + pass + try: + self.client.get_holdings(account) + holdings_accounts.append(account) + except FinTSDialogError: + # account is not a holdings account. + pass + + return balance_accounts, holdings_accounts + + +class FinTsAccount(Entity): + """Sensor for a FinTS balanc account. + + A balance account contains an amount of money (=balance). The amount may + also be negative. + """ + + def __init__(self, client: FinTsClient, account, name: str) -> None: + """Constructor for class FinTsAccount.""" + self._client = client # type: FinTsClient + self._account = account + self._name = name # type: str + self._balance = None # type: float + self._currency = None # type: str + + @property + def should_poll(self) -> bool: + """Data needs to be polled from the bank servers.""" + return True + + def update(self) -> None: + """Get the current balance and currency for the account.""" + bank = self._client.client + balance = bank.get_balance(self._account) + self._balance = balance.amount.amount + self._currency = balance.amount.currency + _LOGGER.debug('updated balance of account %s', self.name) + + @property + def name(self) -> str: + """Friendly name of the sensor.""" + return self._name + + @property + def state(self) -> float: + """Return the balance of the account as state.""" + return self._balance + + @property + def unit_of_measurement(self) -> str: + """Use the currency as unit of measurement.""" + return self._currency + + @property + def device_state_attributes(self) -> dict: + """Additional attributes of the sensor.""" + attributes = { + ATTR_ACCOUNT: self._account.iban, + ATTR_ACCOUNT_TYPE: 'balance', + } + if self._client.name: + attributes[ATTR_BANK] = self._client.name + return attributes + + @property + def icon(self) -> str: + """Set the icon for the sensor.""" + return ICON + + +class FinTsHoldingsAccount(Entity): + """Sensor for a FinTS holdings account. + + A holdings account does not contain money but rather some financial + instruments, e.g. stocks. + """ + + def __init__(self, client: FinTsClient, account, name: str) -> None: + """Constructor for class FinTsHoldingsAccount.""" + self._client = client # type: FinTsClient + self._name = name # type: str + self._account = account + self._holdings = [] + self._total = None # type: float + + @property + def should_poll(self) -> bool: + """Data needs to be polled from the bank servers.""" + return True + + def update(self) -> None: + """Get the current holdings for the account.""" + bank = self._client.client + self._holdings = bank.get_holdings(self._account) + self._total = sum(h.total_value for h in self._holdings) + + @property + def state(self) -> float: + """Return total market value as state.""" + return self._total + + @property + def icon(self) -> str: + """Set the icon for the sensor.""" + return ICON + + @property + def device_state_attributes(self) -> dict: + """Additional attributes of the sensor. + + Lists each holding of the account with the current value. + """ + attributes = { + ATTR_ACCOUNT: self._account.accountnumber, + ATTR_ACCOUNT_TYPE: 'holdings', + } + if self._client.name: + attributes[ATTR_BANK] = self._client.name + for holding in self._holdings: + total_name = '{} total'.format(holding.name) + attributes[total_name] = holding.total_value + pieces_name = '{} pieces'.format(holding.name) + attributes[pieces_name] = holding.pieces + price_name = '{} price'.format(holding.name) + attributes[price_name] = holding.market_value + + return attributes + + @property + def name(self) -> str: + """Friendly name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self) -> str: + """Get the unit of measurement. + + Hardcoded to EUR, as the library does not provide the currency for the + holdings. And as FinTS is only used in Germany, most accounts will be + in EUR anyways. + """ + return "EUR" diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py index 8f8ce2d1681..82816c83404 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -70,7 +70,7 @@ class HiveSensorEntity(Entity): return DEVICETYPE_ICONS.get(self.device_type) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" if self.session.core.update_data(self.node_id): for entity in self.session.entities: entity.handle_update(self.data_updatesource) diff --git a/homeassistant/components/sensor/iota.py b/homeassistant/components/sensor/iota.py index c973fa83148..2e3e58a18f3 100644 --- a/homeassistant/components/sensor/iota.py +++ b/homeassistant/components/sensor/iota.py @@ -7,10 +7,18 @@ https://home-assistant.io/components/iota import logging from datetime import timedelta -from homeassistant.components.iota import IotaDevice +from homeassistant.components.iota import IotaDevice, CONF_WALLETS +from homeassistant.const import CONF_NAME _LOGGER = logging.getLogger(__name__) +ATTR_TESTNET = 'testnet' +ATTR_URL = 'url' + +CONF_IRI = 'iri' +CONF_SEED = 'seed' +CONF_TESTNET = 'testnet' + DEPENDENCIES = ['iota'] SCAN_INTERVAL = timedelta(minutes=3) @@ -21,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Add sensors for wallet balance iota_config = discovery_info sensors = [IotaBalanceSensor(wallet, iota_config) - for wallet in iota_config['wallets']] + for wallet in iota_config[CONF_WALLETS]] # Add sensor for node information sensors.append(IotaNodeSensor(iota_config=iota_config)) @@ -34,10 +42,9 @@ class IotaBalanceSensor(IotaDevice): def __init__(self, wallet_config, iota_config): """Initialize the sensor.""" - super().__init__(name=wallet_config['name'], - seed=wallet_config['seed'], - iri=iota_config['iri'], - is_testnet=iota_config['testnet']) + super().__init__( + name=wallet_config[CONF_NAME], seed=wallet_config[CONF_SEED], + iri=iota_config[CONF_IRI], is_testnet=iota_config[CONF_TESTNET]) self._state = None @property @@ -65,10 +72,11 @@ class IotaNodeSensor(IotaDevice): def __init__(self, iota_config): """Initialize the sensor.""" - super().__init__(name='Node Info', seed=None, iri=iota_config['iri'], - is_testnet=iota_config['testnet']) + super().__init__( + name='Node Info', seed=None, iri=iota_config[CONF_IRI], + is_testnet=iota_config[CONF_TESTNET]) self._state = None - self._attr = {'url': self.iri, 'testnet': self.is_testnet} + self._attr = {ATTR_URL: self.iri, ATTR_TESTNET: self.is_testnet} @property def name(self): diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py index aad8c2f7a92..e7b8bf600a4 100644 --- a/homeassistant/components/sensor/linux_battery.py +++ b/homeassistant/components/sensor/linux_battery.py @@ -10,15 +10,14 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY -from homeassistant.helpers.entity import Entity +from homeassistant.const import ATTR_NAME, CONF_NAME, DEVICE_CLASS_BATTERY import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['batinfo==0.4.2'] _LOGGER = logging.getLogger(__name__) -ATTR_NAME = 'name' ATTR_PATH = 'path' ATTR_ALARM = 'alarm' ATTR_CAPACITY = 'capacity' diff --git a/homeassistant/components/sensor/mychevy.py b/homeassistant/components/sensor/mychevy.py index bdbffc46ca8..ef7c7ba8608 100644 --- a/homeassistant/components/sensor/mychevy.py +++ b/homeassistant/components/sensor/mychevy.py @@ -17,14 +17,15 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify -BATTERY_SENSOR = "percent" +BATTERY_SENSOR = "batteryLevel" SENSORS = [ - EVSensorConfig("Mileage", "mileage", "miles", "mdi:speedometer"), - EVSensorConfig("Range", "range", "miles", "mdi:speedometer"), - EVSensorConfig("Charging", "charging"), - EVSensorConfig("Charge Mode", "charge_mode"), - EVSensorConfig("EVCharge", BATTERY_SENSOR, "%", "mdi:battery") + EVSensorConfig("Mileage", "totalMiles", "miles", "mdi:speedometer"), + EVSensorConfig("Electric Range", "electricRange", "miles", + "mdi:speedometer"), + EVSensorConfig("Charged By", "estimatedFullChargeBy"), + EVSensorConfig("Charge Mode", "chargeMode"), + EVSensorConfig("Battery Level", BATTERY_SENSOR, "%", "mdi:battery") ] _LOGGER = logging.getLogger(__name__) @@ -38,7 +39,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hub = hass.data[MYCHEVY_DOMAIN] sensors = [MyChevyStatus()] for sconfig in SENSORS: - sensors.append(EVSensor(hub, sconfig)) + for car in hub.cars: + sensors.append(EVSensor(hub, sconfig, car.vid)) add_devices(sensors) @@ -112,7 +114,7 @@ class EVSensor(Entity): """ - def __init__(self, connection, config): + def __init__(self, connection, config, car_vid): """Initialize sensor with car connection.""" self._conn = connection self._name = config.name @@ -120,9 +122,12 @@ class EVSensor(Entity): self._unit_of_measurement = config.unit_of_measurement self._icon = config.icon self._state = None + self._car_vid = car_vid self.entity_id = ENTITY_ID_FORMAT.format( - '{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name))) + '{}_{}_{}'.format(MYCHEVY_DOMAIN, + slugify(self._car.name), + slugify(self._name))) @asyncio.coroutine def async_added_to_hass(self): @@ -130,6 +135,11 @@ class EVSensor(Entity): self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.async_update_callback) + @property + def _car(self): + """Return the car.""" + return self._conn.get_car(self._car_vid) + @property def icon(self): """Return the icon.""" @@ -145,8 +155,8 @@ class EVSensor(Entity): @callback def async_update_callback(self): """Update state.""" - if self._conn.car is not None: - self._state = getattr(self._conn.car, self._attr, None) + if self._car is not None: + self._state = getattr(self._car, self._attr, None) self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index b3ca054f88f..7dd795d8f8d 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.const import ( - CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, ATTR_NAME, CONF_VERIFY_SSL, CONF_TIMEOUT, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS) from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -29,7 +29,6 @@ ATTR_MASK = 'Mask' ATTR_MAX_SPEED = 'Max Speed' ATTR_MEMORY_SIZE = 'Memory Size' ATTR_MODEL = 'Model' -ATTR_NAME = 'Name' ATTR_PACKETS_TX = 'Packets (TX)' ATTR_PACKETS_RX = 'Packets (RX)' ATTR_PACKETS_ERR = 'Packets (Err)' diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 74bfaa38f02..75235bedaab 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -176,6 +176,7 @@ class RestData(object): self._request, timeout=10, verify=self._verify_ssl) self.data = response.text - except requests.exceptions.RequestException: - _LOGGER.error("Error fetching data: %s", self._request) + except requests.exceptions.RequestException as ex: + _LOGGER.error("Error fetching data: %s from %s failed with %s", + self._request, self._request.url, ex) self.data = None diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 4a555905d50..a5a6eb5f07b 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -10,10 +10,10 @@ import voluptuous as vol import homeassistant.components.rfxtrx as rfxtrx from homeassistant.components.rfxtrx import ( - ATTR_DATA_TYPE, ATTR_FIRE_EVENT, ATTR_NAME, CONF_AUTOMATIC_ADD, - CONF_DATA_TYPE, CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES) + ATTR_DATA_TYPE, ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, CONF_DATA_TYPE, + CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES) from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 194ff71222a..185f83c9405 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -4,216 +4,75 @@ Support for monitoring an SABnzbd NZB client. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sabnzbd/ """ -import asyncio import logging -from datetime import timedelta -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES, - CONF_SSL) +from homeassistant.components.sabnzbd import DATA_SABNZBD, \ + SIGNAL_SABNZBD_UPDATED, SENSOR_TYPES +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from homeassistant.util.json import load_json, save_json -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pysabnzbd==1.0.1'] +DEPENDENCIES = ['sabnzbd'] -_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -CONFIG_FILE = 'sabnzbd.conf' -DEFAULT_NAME = 'SABnzbd' -DEFAULT_PORT = 8080 -DEFAULT_SSL = False - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) - -SENSOR_TYPES = { - 'current_status': ['Status', None], - 'speed': ['Speed', 'MB/s'], - 'queue_size': ['Queue', 'MB'], - 'queue_remaining': ['Left', 'MB'], - 'disk_size': ['Disk', 'GB'], - 'disk_free': ['Disk Free', 'GB'], - 'queue_count': ['Queue Count', None], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=['current_status']): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, -}) - - -@asyncio.coroutine -def async_check_sabnzbd(sab_api, base_url, api_key): - """Check if we can reach SABnzbd.""" - from pysabnzbd import SabnzbdApiException - sab_api = sab_api(base_url, api_key) - - try: - yield from sab_api.check_available() - except SabnzbdApiException: - _LOGGER.error("Connection to SABnzbd API failed") - return False - return True - - -def setup_sabnzbd(base_url, apikey, name, config, - async_add_devices, sab_api): - """Set up polling from SABnzbd and sensors.""" - sab_api = sab_api(base_url, apikey) - monitored = config.get(CONF_MONITORED_VARIABLES) - async_add_devices([SabnzbdSensor(variable, sab_api, name) - for variable in monitored]) - - -@Throttle(MIN_TIME_BETWEEN_UPDATES) -async def async_update_queue(sab_api): - """ - Throttled function to update SABnzbd queue. - - This ensures that the queue info only gets updated once for all sensors - """ - await sab_api.refresh_data() - - -def request_configuration(host, name, hass, config, async_add_devices, - sab_api): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.notify_errors(_CONFIGURING[host], - 'Failed to register, please try again.') +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the SABnzbd sensors.""" + if discovery_info is None: return - @asyncio.coroutine - def async_configuration_callback(data): - """Handle configuration changes.""" - api_key = data.get('api_key') - if (yield from async_check_sabnzbd(sab_api, host, api_key)): - setup_sabnzbd(host, api_key, name, config, - async_add_devices, sab_api) - - def success(): - """Set up was successful.""" - conf = load_json(hass.config.path(CONFIG_FILE)) - conf[host] = {'api_key': api_key} - save_json(hass.config.path(CONFIG_FILE), conf) - req_config = _CONFIGURING.pop(host) - configurator.async_request_done(req_config) - - hass.async_add_job(success) - - _CONFIGURING[host] = configurator.async_request_config( - DEFAULT_NAME, - async_configuration_callback, - description='Enter the API Key', - submit_caption='Confirm', - fields=[{'id': 'api_key', 'name': 'API Key', 'type': ''}] - ) - - -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the SABnzbd platform.""" - from pysabnzbd import SabnzbdApi - - if discovery_info is not None: - host = discovery_info.get(CONF_HOST) - port = discovery_info.get(CONF_PORT) - name = DEFAULT_NAME - use_ssl = discovery_info.get('properties', {}).get('https', '0') == '1' - else: - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME, DEFAULT_NAME) - use_ssl = config.get(CONF_SSL) - - api_key = config.get(CONF_API_KEY) - - uri_scheme = 'https://' if use_ssl else 'http://' - base_url = "{}{}:{}/".format(uri_scheme, host, port) - - if not api_key: - conf = load_json(hass.config.path(CONFIG_FILE)) - if conf.get(base_url, {}).get('api_key'): - api_key = conf[base_url]['api_key'] - - if not (yield from async_check_sabnzbd(SabnzbdApi, base_url, api_key)): - request_configuration(base_url, name, hass, config, - async_add_devices, SabnzbdApi) - return - - setup_sabnzbd(base_url, api_key, name, config, - async_add_devices, SabnzbdApi) + sab_api_data = hass.data[DATA_SABNZBD] + sensors = sab_api_data.sensors + client_name = sab_api_data.name + async_add_devices([SabnzbdSensor(sensor, sab_api_data, client_name) + for sensor in sensors]) class SabnzbdSensor(Entity): """Representation of an SABnzbd sensor.""" - def __init__(self, sensor_type, sabnzbd_api, client_name): + def __init__(self, sensor_type, sabnzbd_api_data, client_name): """Initialize the sensor.""" + self._client_name = client_name + self._field_name = SENSOR_TYPES[sensor_type][2] self._name = SENSOR_TYPES[sensor_type][0] - self.sabnzbd_api = sabnzbd_api - self.type = sensor_type - self.client_name = client_name + self._sabnzbd_api = sabnzbd_api_data self._state = None + self._type = sensor_type self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + async_dispatcher_connect(self.hass, SIGNAL_SABNZBD_UPDATED, + self.update_state) + @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return '{} {}'.format(self._client_name, self._name) @property def state(self): """Return the state of the sensor.""" return self._state + def should_poll(self): + """Don't poll. Will be updated by dispatcher signal.""" + return False + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @asyncio.coroutine - def async_refresh_sabnzbd_data(self): - """Call the throttled SABnzbd refresh method.""" - from pysabnzbd import SabnzbdApiException - try: - yield from async_update_queue(self.sabnzbd_api) - except SabnzbdApiException: - _LOGGER.exception("Connection to SABnzbd API failed") - - @asyncio.coroutine - def async_update(self): + def update_state(self, args): """Get the latest data and updates the states.""" - yield from self.async_refresh_sabnzbd_data() + self._state = self._sabnzbd_api.get_queue_field(self._field_name) - if self.sabnzbd_api.queue: - if self.type == 'current_status': - self._state = self.sabnzbd_api.queue.get('status') - elif self.type == 'speed': - mb_spd = float(self.sabnzbd_api.queue.get('kbpersec')) / 1024 - self._state = round(mb_spd, 1) - elif self.type == 'queue_size': - self._state = self.sabnzbd_api.queue.get('mb') - elif self.type == 'queue_remaining': - self._state = self.sabnzbd_api.queue.get('mbleft') - elif self.type == 'disk_size': - self._state = self.sabnzbd_api.queue.get('diskspacetotal1') - elif self.type == 'disk_free': - self._state = self.sabnzbd_api.queue.get('diskspace1') - elif self.type == 'queue_count': - self._state = self.sabnzbd_api.queue.get('noofslots_total') - else: - self._state = 'Unknown' + if self._type == 'speed': + self._state = round(float(self._state) / 1024, 1) + elif 'size' in self._type: + self._state = round(float(self._state), 2) + + self.schedule_update_ha_state() diff --git a/homeassistant/components/sensor/sigfox.py b/homeassistant/components/sensor/sigfox.py index ef47132eefc..da8f3fcc639 100644 --- a/homeassistant/components/sensor/sigfox.py +++ b/homeassistant/components/sensor/sigfox.py @@ -66,7 +66,7 @@ class SigfoxAPI(object): self._devices = self.get_devices(device_types) def check_credentials(self): - """"Check API credentials are valid.""" + """Check API credentials are valid.""" url = urljoin(API_URL, 'devicetypes') response = requests.get(url, auth=self._auth, timeout=10) if response.status_code != 200: diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py index 7091146e3ac..ae2d4939eab 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -87,7 +87,7 @@ class SimulatedSensor(Entity): self._state = None def time_delta(self): - """"Return the time delta.""" + """Return the time delta.""" dt0 = self._start_time dt1 = dt_util.utcnow() return dt1 - dt0 diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index eabc33312b2..61933614a74 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -10,38 +10,38 @@ from uuid import UUID import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_MAC, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP) + CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity -# REQUIREMENTS = ['pygatt==3.1.1'] +REQUIREMENTS = ['pygatt==3.2.0'] _LOGGER = logging.getLogger(__name__) -CONNECT_LOCK = threading.Lock() - ATTR_DEVICE = 'device' ATTR_MODEL = 'model' +BLE_TEMP_HANDLE = 0x24 +BLE_TEMP_UUID = '0000ff92-0000-1000-8000-00805f9b34fb' + +CONNECT_LOCK = threading.Lock() +CONNECT_TIMEOUT = 30 + +DEFAULT_NAME = 'Skybeacon' + +SKIP_HANDLE_LOOKUP = True + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_NAME, default=""): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -BLE_TEMP_UUID = '0000ff92-0000-1000-8000-00805f9b34fb' -BLE_TEMP_HANDLE = 0x24 -SKIP_HANDLE_LOOKUP = True -CONNECT_TIMEOUT = 30 - # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Skybeacon sensor.""" - _LOGGER.warning("This platform has been disabled due to having a " - "requirement depending on enum34.") - return # pylint: disable=unreachable name = config.get(CONF_NAME) mac = config.get(CONF_MAC) @@ -150,7 +150,7 @@ class Monitor(threading.Thread): adapter = pygatt.backends.GATTToolBackend() while True: try: - _LOGGER.info("Connecting to %s", self.name) + _LOGGER.debug("Connecting to %s", self.name) # We need concurrent connect, so lets not reset the device adapter.start(reset_on_start=False) # Seems only one connection can be initiated at a time diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 7b2ae537d4b..a77509c18d4 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -156,7 +156,7 @@ class StatisticsSensor(Entity): ATTR_CHANGE: self.change, ATTR_AVERAGE_CHANGE: self.average_change, } - # Only return min/max age if we have a age span + # Only return min/max age if we have an age span if self._max_age: state.update({ ATTR_MAX_AGE: self.max_age, diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index 7acdc1a20bd..ff8ad7fe849 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -6,16 +6,14 @@ https://home-assistant.io/components/sensor.tado/ """ import logging -from homeassistant.const import TEMP_CELSIUS +from homeassistant.components.tado import DATA_TADO +from homeassistant.const import ATTR_ID, ATTR_NAME, TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from homeassistant.components.tado import (DATA_TADO) -from homeassistant.const import (ATTR_ID) _LOGGER = logging.getLogger(__name__) ATTR_DATA_ID = 'data_id' ATTR_DEVICE = 'device' -ATTR_NAME = 'name' ATTR_ZONE = 'zone' CLIMATE_SENSOR_TYPES = ['temperature', 'humidity', 'power', @@ -39,14 +37,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if zone['type'] == 'HEATING': for variable in CLIMATE_SENSOR_TYPES: sensor_items.append(create_zone_sensor( - tado, zone, zone['name'], zone['id'], - variable)) + tado, zone, zone['name'], zone['id'], variable)) elif zone['type'] == 'HOT_WATER': for variable in HOT_WATER_SENSOR_TYPES: sensor_items.append(create_zone_sensor( - tado, zone, zone['name'], zone['id'], - variable - )) + tado, zone, zone['name'], zone['id'], variable)) me_data = tado.get_me() sensor_items.append(create_device_sensor( diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index 47589f33530..dbcfcb9cc27 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -26,6 +26,8 @@ ATTR_ROUTE = 'route' CONF_ATTRIBUTION = "Data provided by the Waze.com" CONF_DESTINATION = 'destination' CONF_ORIGIN = 'origin' +CONF_INCL_FILTER = 'incl_filter' +CONF_EXCL_FILTER = 'excl_filter' DEFAULT_NAME = 'Waze Travel Time' @@ -40,6 +42,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DESTINATION): cv.string, vol.Required(CONF_REGION): vol.In(REGIONS), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_INCL_FILTER): cv.string, + vol.Optional(CONF_EXCL_FILTER): cv.string, }) @@ -49,9 +53,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) origin = config.get(CONF_ORIGIN) region = config.get(CONF_REGION) + incl_filter = config.get(CONF_INCL_FILTER) + excl_filter = config.get(CONF_EXCL_FILTER) try: - waze_data = WazeRouteData(origin, destination, region) + waze_data = WazeRouteData( + origin, destination, region, incl_filter, excl_filter) except requests.exceptions.HTTPError as error: _LOGGER.error("%s", error) return @@ -109,11 +116,13 @@ class WazeTravelTime(Entity): class WazeRouteData(object): """Get data from Waze.""" - def __init__(self, origin, destination, region): + def __init__(self, origin, destination, region, incl_filter, excl_filter): """Initialize the data object.""" self._destination = destination self._origin = origin self._region = region + self._incl_filter = incl_filter + self._excl_filter = excl_filter self.data = {} @Throttle(SCAN_INTERVAL) @@ -125,6 +134,12 @@ class WazeRouteData(object): params = WazeRouteCalculator.WazeRouteCalculator( self._origin, self._destination, self._region, None) results = params.calc_all_routes_info() + if self._incl_filter is not None: + results = {k: v for k, v in results.items() if + self._incl_filter.lower() in k.lower()} + if self._excl_filter is not None: + results = {k: v for k, v in results.items() if + self._excl_filter.lower() not in k.lower()} best_route = next(iter(results)) (duration, distance) = results[best_route] best_route_str = bytes(best_route, 'ISO-8859-1').decode('UTF-8') diff --git a/homeassistant/components/sensor/wsdot.py b/homeassistant/components/sensor/wsdot.py index fecff260716..0cd5ba44349 100644 --- a/homeassistant/components/sensor/wsdot.py +++ b/homeassistant/components/sensor/wsdot.py @@ -13,24 +13,27 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, CONF_NAME, ATTR_ATTRIBUTION, CONF_ID - ) + CONF_API_KEY, CONF_NAME, ATTR_ATTRIBUTION, CONF_ID, ATTR_NAME) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +ATTR_ACCESS_CODE = 'AccessCode' +ATTR_AVG_TIME = 'AverageTime' +ATTR_CURRENT_TIME = 'CurrentTime' +ATTR_DESCRIPTION = 'Description' +ATTR_TIME_UPDATED = 'TimeUpdated' +ATTR_TRAVEL_TIME_ID = 'TravelTimeID' + +CONF_ATTRIBUTION = "Data provided by WSDOT" + CONF_TRAVEL_TIMES = 'travel_time' -# API codes for travel time details -ATTR_ACCESS_CODE = 'AccessCode' -ATTR_TRAVEL_TIME_ID = 'TravelTimeID' -ATTR_CURRENT_TIME = 'CurrentTime' -ATTR_AVG_TIME = 'AverageTime' -ATTR_NAME = 'Name' -ATTR_TIME_UPDATED = 'TimeUpdated' -ATTR_DESCRIPTION = 'Description' -ATTRIBUTION = "Data provided by WSDOT" +ICON = 'mdi:car' + +RESOURCE = 'http://www.wsdot.wa.gov/Traffic/api/TravelTimes/' \ + 'TravelTimesREST.svc/GetTravelTimeAsJson' SCAN_INTERVAL = timedelta(minutes=3) @@ -43,16 +46,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Get the WSDOT sensor.""" + """Set up the WSDOT sensor.""" sensors = [] for travel_time in config.get(CONF_TRAVEL_TIMES): - name = (travel_time.get(CONF_NAME) or - travel_time.get(CONF_ID)) + name = (travel_time.get(CONF_NAME) or travel_time.get(CONF_ID)) sensors.append( WashingtonStateTravelTimeSensor( - name, - config.get(CONF_API_KEY), - travel_time.get(CONF_ID))) + name, config.get(CONF_API_KEY), travel_time.get(CONF_ID))) + add_devices(sensors, True) @@ -65,8 +66,6 @@ class WashingtonStateTransportSensor(Entity): can read them and make them available. """ - ICON = 'mdi:car' - def __init__(self, name, access_code): """Initialize the sensor.""" self._data = {} @@ -87,16 +86,12 @@ class WashingtonStateTransportSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return self.ICON + return ICON class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): """Travel time sensor from WSDOT.""" - RESOURCE = ('http://www.wsdot.wa.gov/Traffic/api/TravelTimes/' - 'TravelTimesREST.svc/GetTravelTimeAsJson') - ICON = 'mdi:car' - def __init__(self, name, access_code, travel_time_id): """Construct a travel time sensor.""" self._travel_time_id = travel_time_id @@ -104,10 +99,12 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): def update(self): """Get the latest data from WSDOT.""" - params = {ATTR_ACCESS_CODE: self._access_code, - ATTR_TRAVEL_TIME_ID: self._travel_time_id} + params = { + ATTR_ACCESS_CODE: self._access_code, + ATTR_TRAVEL_TIME_ID: self._travel_time_id, + } - response = requests.get(self.RESOURCE, params, timeout=10) + response = requests.get(RESOURCE, params, timeout=10) if response.status_code != 200: _LOGGER.warning("Invalid response from WSDOT API") else: @@ -118,7 +115,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): def device_state_attributes(self): """Return other details about the sensor state.""" if self._data is not None: - attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} for key in [ATTR_AVG_TIME, ATTR_NAME, ATTR_DESCRIPTION, ATTR_TRAVEL_TIME_ID]: attrs[key] = self._data.get(key) @@ -129,7 +126,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return "min" + return 'min' def _parse_wsdot_timestamp(timestamp): @@ -139,5 +136,5 @@ def _parse_wsdot_timestamp(timestamp): # ex: Date(1485040200000-0800) milliseconds, tzone = re.search( r'Date\((\d+)([+-]\d\d)\d\d\)', timestamp).groups() - return datetime.fromtimestamp(int(milliseconds) / 1000, - tz=timezone(timedelta(hours=int(tzone)))) + return datetime.fromtimestamp( + int(milliseconds) / 1000, tz=timezone(timedelta(hours=int(tzone)))) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index d856ed1a17e..3ca908a679d 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -25,20 +25,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return sensor = yield from make_sensor(discovery_info) - async_add_devices([sensor]) + async_add_devices([sensor], update_before_add=True) @asyncio.coroutine def make_sensor(discovery_info): """Create ZHA sensors factory.""" from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, TemperatureMeasurement + RelativeHumidity, TemperatureMeasurement, PressureMeasurement ) in_clusters = discovery_info['in_clusters'] if RelativeHumidity.cluster_id in in_clusters: sensor = RelativeHumiditySensor(**discovery_info) elif TemperatureMeasurement.cluster_id in in_clusters: sensor = TemperatureSensor(**discovery_info) + elif PressureMeasurement.cluster_id in in_clusters: + sensor = PressureSensor(**discovery_info) else: sensor = Sensor(**discovery_info) @@ -59,6 +61,11 @@ class Sensor(zha.Entity): value_attribute = 0 min_reportable_change = 1 + @property + def should_poll(self) -> bool: + """State gets pushed from device.""" + return False + @property def state(self) -> str: """Return the state of the entity.""" @@ -73,6 +80,14 @@ class Sensor(zha.Entity): self._state = value self.async_schedule_update_ha_state() + async def async_update(self): + """Retrieve latest state.""" + result = await zha.safe_read( + list(self._in_clusters.values())[0], + [self.value_attribute] + ) + self._state = result.get(self.value_attribute, self._state) + class TemperatureSensor(Sensor): """ZHA temperature sensor.""" @@ -87,8 +102,8 @@ class TemperatureSensor(Sensor): @property def state(self): """Return the state of the entity.""" - if self._state == 'unknown': - return 'unknown' + if self._state is None: + return None celsius = round(float(self._state) / 100, 1) return convert_temperature( celsius, TEMP_CELSIUS, self.unit_of_measurement) @@ -107,7 +122,24 @@ class RelativeHumiditySensor(Sensor): @property def state(self): """Return the state of the entity.""" - if self._state == 'unknown': - return 'unknown' + if self._state is None: + return None return round(float(self._state) / 100, 1) + + +class PressureSensor(Sensor): + """ZHA pressure sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'hPa' + + @property + def state(self): + """Return the state of the entity.""" + if self._state is None: + return None + + return round(float(self._state)) diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index ca33666d1f3..10a6c350b7c 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -68,8 +68,9 @@ def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: cmd, loop=hass.loop, stdin=None, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL) + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) else: # Template used. Break into list and use create_subprocess_exec # (which uses shell=False) for security @@ -80,12 +81,19 @@ def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: *shlexed_cmd, loop=hass.loop, stdin=None, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL) + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) process = yield from create_process - yield from process.communicate() + stdout_data, stderr_data = yield from process.communicate() + if stdout_data: + _LOGGER.debug("Stdout of command: `%s`, return code: %s:\n%s", + cmd, process.returncode, stdout_data) + if stderr_data: + _LOGGER.debug("Stderr of command: `%s`, return code: %s:\n%s", + cmd, process.returncode, stderr_data) if process.returncode != 0: _LOGGER.exception("Error running command: `%s`, return code: %s", cmd, process.returncode) diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index 812906e7be9..4f50c6beaaa 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -131,6 +131,8 @@ async def async_setup(hass, config): slots = {} for slot in request.get('slots', []): slots[slot['slotName']] = {'value': resolve_slot_values(slot)} + slots['site_id'] = {'value': request.get('siteId')} + slots['probability'] = {'value': request['intent']['probability']} try: intent_response = await intent.async_handle( diff --git a/homeassistant/components/spaceapi.py b/homeassistant/components/spaceapi.py new file mode 100644 index 00000000000..eaf1508071a --- /dev/null +++ b/homeassistant/components/spaceapi.py @@ -0,0 +1,175 @@ +""" +Support for the SpaceAPI. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/spaceapi/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_ICON, ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, + ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, CONF_ADDRESS, CONF_EMAIL, + CONF_ENTITY_ID, CONF_SENSORS, CONF_STATE, CONF_URL) +import homeassistant.core as ha +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +ATTR_ADDRESS = 'address' +ATTR_API = 'api' +ATTR_CLOSE = 'close' +ATTR_CONTACT = 'contact' +ATTR_ISSUE_REPORT_CHANNELS = 'issue_report_channels' +ATTR_LASTCHANGE = 'lastchange' +ATTR_LOGO = 'logo' +ATTR_NAME = 'name' +ATTR_OPEN = 'open' +ATTR_SENSORS = 'sensors' +ATTR_SPACE = 'space' +ATTR_UNIT = 'unit' +ATTR_URL = 'url' +ATTR_VALUE = 'value' + +CONF_CONTACT = 'contact' +CONF_HUMIDITY = 'humidity' +CONF_ICON_CLOSED = 'icon_closed' +CONF_ICON_OPEN = 'icon_open' +CONF_ICONS = 'icons' +CONF_IRC = 'irc' +CONF_ISSUE_REPORT_CHANNELS = 'issue_report_channels' +CONF_LOCATION = 'location' +CONF_LOGO = 'logo' +CONF_MAILING_LIST = 'mailing_list' +CONF_PHONE = 'phone' +CONF_SPACE = 'space' +CONF_TEMPERATURE = 'temperature' +CONF_TWITTER = 'twitter' + +DATA_SPACEAPI = 'data_spaceapi' +DEPENDENCIES = ['http'] +DOMAIN = 'spaceapi' + +ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_IRC, CONF_MAILING_LIST, CONF_TWITTER] + +SENSOR_TYPES = [CONF_HUMIDITY, CONF_TEMPERATURE] +SPACEAPI_VERSION = 0.13 + +URL_API_SPACEAPI = '/api/spaceapi' + +LOCATION_SCHEMA = vol.Schema({ + vol.Optional(CONF_ADDRESS): cv.string, +}, required=True) + +CONTACT_SCHEMA = vol.Schema({ + vol.Optional(CONF_EMAIL): cv.string, + vol.Optional(CONF_IRC): cv.string, + vol.Optional(CONF_MAILING_LIST): cv.string, + vol.Optional(CONF_PHONE): cv.string, + vol.Optional(CONF_TWITTER): cv.string, +}, required=False) + +STATE_SCHEMA = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Inclusive(CONF_ICON_CLOSED, CONF_ICONS): cv.url, + vol.Inclusive(CONF_ICON_OPEN, CONF_ICONS): cv.url, +}, required=False) + +SENSOR_SCHEMA = vol.Schema( + {vol.In(SENSOR_TYPES): [cv.entity_id]} +) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_CONTACT): CONTACT_SCHEMA, + vol.Required(CONF_ISSUE_REPORT_CHANNELS): + vol.All(cv.ensure_list, [vol.In(ISSUE_REPORT_CHANNELS)]), + vol.Required(CONF_LOCATION): LOCATION_SCHEMA, + vol.Required(CONF_LOGO): cv.url, + vol.Required(CONF_SPACE): cv.string, + vol.Required(CONF_STATE): STATE_SCHEMA, + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Register the SpaceAPI with the HTTP interface.""" + hass.data[DATA_SPACEAPI] = config[DOMAIN] + hass.http.register_view(APISpaceApiView) + + return True + + +class APISpaceApiView(HomeAssistantView): + """View to provide details according to the SpaceAPI.""" + + url = URL_API_SPACEAPI + name = 'api:spaceapi' + + @ha.callback + def get(self, request): + """Get SpaceAPI data.""" + hass = request.app['hass'] + spaceapi = dict(hass.data[DATA_SPACEAPI]) + is_sensors = spaceapi.get('sensors') + + location = { + ATTR_ADDRESS: spaceapi[ATTR_LOCATION][CONF_ADDRESS], + ATTR_LATITUDE: hass.config.latitude, + ATTR_LONGITUDE: hass.config.longitude, + } + + state_entity = spaceapi['state'][ATTR_ENTITY_ID] + space_state = hass.states.get(state_entity) + + if space_state is not None: + state = { + ATTR_OPEN: False if space_state.state == 'off' else True, + ATTR_LASTCHANGE: + dt_util.as_timestamp(space_state.last_updated), + } + else: + state = {ATTR_OPEN: 'null', ATTR_LASTCHANGE: 0} + + try: + state[ATTR_ICON] = { + ATTR_OPEN: spaceapi['state'][CONF_ICON_OPEN], + ATTR_CLOSE: spaceapi['state'][CONF_ICON_CLOSED], + } + except KeyError: + pass + + data = { + ATTR_API: SPACEAPI_VERSION, + ATTR_CONTACT: spaceapi[CONF_CONTACT], + ATTR_ISSUE_REPORT_CHANNELS: spaceapi[CONF_ISSUE_REPORT_CHANNELS], + ATTR_LOCATION: location, + ATTR_LOGO: spaceapi[CONF_LOGO], + ATTR_SPACE: spaceapi[CONF_SPACE], + ATTR_STATE: state, + ATTR_URL: spaceapi[CONF_URL], + } + + if is_sensors is not None: + sensors = {} + for sensor_type in is_sensors: + sensors[sensor_type] = [] + for sensor in spaceapi['sensors'][sensor_type]: + sensor_state = hass.states.get(sensor) + unit = sensor_state.attributes[ATTR_UNIT_OF_MEASUREMENT] + value = sensor_state.state + sensor_data = { + ATTR_LOCATION: spaceapi[CONF_SPACE], + ATTR_NAME: sensor_state.name, + ATTR_UNIT: unit, + ATTR_VALUE: value, + } + sensors[sensor_type].append(sensor_data) + data[ATTR_SENSORS] = sensors + + return self.json(data) diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py index 6e31694fd2d..fd72d0728a0 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -18,11 +18,13 @@ _LOGGER = logging.getLogger(__name__) CONF_FUNCTIONS = 'functions' CONF_PINS = 'pins' +CONF_INVERT = 'invert' DEFAULT_NAME = 'aREST switch' PIN_FUNCTION_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INVERT, default=False): cv.boolean, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -54,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for pinnum, pin in pins.items(): dev.append(ArestSwitchPin( resource, config.get(CONF_NAME, response.json()[CONF_NAME]), - pin.get(CONF_NAME), pinnum)) + pin.get(CONF_NAME), pinnum, pin.get(CONF_INVERT))) functions = config.get(CONF_FUNCTIONS) for funcname, func in functions.items(): @@ -152,10 +154,11 @@ class ArestSwitchFunction(ArestSwitchBase): class ArestSwitchPin(ArestSwitchBase): """Representation of an aREST switch. Based on digital I/O.""" - def __init__(self, resource, location, name, pin): + def __init__(self, resource, location, name, pin, invert): """Initialize the switch.""" super().__init__(resource, location, name) self._pin = pin + self.invert = invert request = requests.get( '{}/mode/{}/o'.format(self._resource, self._pin), timeout=10) @@ -165,8 +168,11 @@ class ArestSwitchPin(ArestSwitchBase): def turn_on(self, **kwargs): """Turn the device on.""" + turn_on_payload = int(not self.invert) request = requests.get( - '{}/digital/{}/1'.format(self._resource, self._pin), timeout=10) + '{}/digital/{}/{}'.format(self._resource, self._pin, + turn_on_payload), + timeout=10) if request.status_code == 200: self._state = True else: @@ -175,8 +181,11 @@ class ArestSwitchPin(ArestSwitchBase): def turn_off(self, **kwargs): """Turn the device off.""" + turn_off_payload = int(self.invert) request = requests.get( - '{}/digital/{}/0'.format(self._resource, self._pin), timeout=10) + '{}/digital/{}/{}'.format(self._resource, self._pin, + turn_off_payload), + timeout=10) if request.status_code == 200: self._state = False else: @@ -188,7 +197,8 @@ class ArestSwitchPin(ArestSwitchBase): try: request = requests.get( '{}/digital/{}'.format(self._resource, self._pin), timeout=10) - self._state = request.json()['return_value'] != 0 + status_value = int(self.invert) + self._state = request.json()['return_value'] != status_value self._available = True except requests.exceptions.ConnectionError: _LOGGER.warning("No route to device %s", self._resource) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index e0bfdeee030..21689dcca0f 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -72,7 +72,8 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness, transition): turn_on(hass, light, xy_color=[x_val, y_val], brightness=brightness, - transition=transition) + transition=transition, + white_value=brightness) def set_lights_temp(hass, lights, mired, brightness, transition): diff --git a/homeassistant/components/switch/homematicip_cloud.py b/homeassistant/components/switch/homematicip_cloud.py new file mode 100644 index 00000000000..9123d46c87b --- /dev/null +++ b/homeassistant/components/switch/homematicip_cloud.py @@ -0,0 +1,84 @@ +""" +Support for HomematicIP switch. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/switch.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_POWER_CONSUMPTION = 'power_consumption' +ATTR_ENERGIE_COUNTER = 'energie_counter' +ATTR_PROFILE_MODE = 'profile_mode' + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP switch devices.""" + from homematicip.device import ( + PlugableSwitch, PlugableSwitchMeasuring, + BrandSwitchMeasuring) + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [] + for device in home.devices: + if isinstance(device, BrandSwitchMeasuring): + # BrandSwitchMeasuring inherits PlugableSwitchMeasuring + # This device is implemented in the light platform and will + # not be added in the switch platform + pass + elif isinstance(device, PlugableSwitchMeasuring): + devices.append(HomematicipSwitchMeasuring(home, device)) + elif isinstance(device, PlugableSwitch): + devices.append(HomematicipSwitch(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): + """MomematicIP switch device.""" + + def __init__(self, home, device): + """Initialize the switch device.""" + super().__init__(home, device) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._device.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._device.turn_off() + + +class HomematicipSwitchMeasuring(HomematicipSwitch): + """MomematicIP measuring switch device.""" + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self._device.currentPowerConsumption + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + if self._device.energyCounter is None: + return 0 + return round(self._device.energyCounter) diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py new file mode 100644 index 00000000000..53c6406b28a --- /dev/null +++ b/homeassistant/components/switch/konnected.py @@ -0,0 +1,94 @@ +""" +Support for wired switches attached to a Konnected device. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.konnected/ +""" + +import logging + +from homeassistant.components.konnected import ( + DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, CONF_ACTIVATION, + STATE_LOW, STATE_HIGH) +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import (CONF_DEVICES, CONF_SWITCHES, ATTR_STATE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set switches attached to a Konnected device.""" + if discovery_info is None: + return + + data = hass.data[KONNECTED_DOMAIN] + device_id = discovery_info['device_id'] + client = data[CONF_DEVICES][device_id]['client'] + switches = [KonnectedSwitch(device_id, pin_num, pin_data, client) + for pin_num, pin_data in + data[CONF_DEVICES][device_id][CONF_SWITCHES].items()] + async_add_devices(switches) + + +class KonnectedSwitch(ToggleEntity): + """Representation of a Konnected switch.""" + + def __init__(self, device_id, pin_num, data, client): + """Initialize the switch.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._activation = self._data.get(CONF_ACTIVATION, STATE_HIGH) + self._state = self._boolean_state(self._data.get(ATTR_STATE)) + self._name = self._data.get( + 'name', 'Konnected {} Actuator {}'.format( + device_id, PIN_TO_ZONE[pin_num])) + self._client = client + _LOGGER.debug('Created new switch: %s', self._name) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + def turn_on(self, **kwargs): + """Send a command to turn on the switch.""" + resp = self._client.put_device( + self._pin_num, int(self._activation == STATE_HIGH)) + + if resp.get(ATTR_STATE) is not None: + self._set_state(self._boolean_state(resp.get(ATTR_STATE))) + + def turn_off(self, **kwargs): + """Send a command to turn off the switch.""" + resp = self._client.put_device( + self._pin_num, int(self._activation == STATE_LOW)) + + if resp.get(ATTR_STATE) is not None: + self._set_state(self._boolean_state(resp.get(ATTR_STATE))) + + def _boolean_state(self, int_state): + if int_state is None: + return False + if int_state == 0: + return self._activation == STATE_LOW + if int_state == 1: + return self._activation == STATE_HIGH + + def _set_state(self, state): + self._state = state + self.schedule_update_ha_state() + _LOGGER.debug('Setting status of %s actuator pin %s to %s', + self._device_id, self.name, state) + + async def async_added_to_hass(self): + """Store entity_id.""" + self._data['entity_id'] = self.entity_id diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 69f12536c5f..1075888e199 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.mqtt/ """ import logging +from typing import Optional import voluptuous as vol @@ -29,12 +30,14 @@ DEFAULT_NAME = 'MQTT Switch' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False +CONF_UNIQUE_ID = 'unique_id' PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -62,6 +65,7 @@ async def async_setup_platform(hass, config, async_add_devices, config.get(CONF_OPTIMISTIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + config.get(CONF_UNIQUE_ID), value_template, )]) @@ -72,7 +76,8 @@ class MqttSwitch(MqttAvailability, SwitchDevice): def __init__(self, name, icon, state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, - payload_available, payload_not_available, value_template): + payload_available, payload_not_available, + unique_id: Optional[str], value_template): """Initialize the MQTT switch.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -87,6 +92,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._payload_off = payload_off self._optimistic = optimistic self._template = value_template + self._unique_id = unique_id async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -139,6 +145,11 @@ class MqttSwitch(MqttAvailability, SwitchDevice): """Return true if we do optimistic updates.""" return self._optimistic + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def icon(self): """Return the icon.""" diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index c0f45cad861..a91ca6d11e7 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -42,7 +42,7 @@ async def async_setup_platform( hass, DOMAIN, discovery_info, device_class_map, async_add_devices=async_add_devices) - def send_ir_code_service(service): + async def async_send_ir_code_service(service): """Set IR code as device state attribute.""" entity_ids = service.data.get(ATTR_ENTITY_ID) ir_code = service.data.get(ATTR_IR_CODE) @@ -58,10 +58,10 @@ async def async_setup_platform( kwargs = {ATTR_IR_CODE: ir_code} for device in _devices: - device.turn_on(**kwargs) + await device.async_turn_on(**kwargs) hass.services.async_register( - DOMAIN, SERVICE_SEND_IR_CODE, send_ir_code_service, + DOMAIN, SERVICE_SEND_IR_CODE, async_send_ir_code_service, schema=SEND_IR_CODE_SERVICE_SCHEMA) @@ -84,23 +84,23 @@ class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): """Return True if switch is on.""" return self._values.get(self.value_type) == STATE_ON - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the switch on.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 1) if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the switch off.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 0) if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() class MySensorsIRSwitch(MySensorsSwitch): @@ -117,7 +117,7 @@ class MySensorsIRSwitch(MySensorsSwitch): set_req = self.gateway.const.SetReq return self._values.get(set_req.V_LIGHT) == STATE_ON - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the IR switch on.""" set_req = self.gateway.const.SetReq if ATTR_IR_CODE in kwargs: @@ -130,11 +130,11 @@ class MySensorsIRSwitch(MySensorsSwitch): # optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code self._values[set_req.V_LIGHT] = STATE_ON - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() # turn off switch after switch was turned on - self.turn_off() + await self.async_turn_off() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the IR switch off.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -142,7 +142,7 @@ class MySensorsIRSwitch(MySensorsSwitch): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 8306b323330..beb00eeca44 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -1,26 +1,118 @@ -"""Implements a RainMachine sprinkler controller for Home Assistant.""" +""" +This component provides support for RainMachine programs and zones. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/switch.rainmachine/ +""" from logging import getLogger from homeassistant.components.rainmachine import ( - CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, - MIN_SCAN_TIME_FORCED) + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, PROGRAM_UPDATE_TOPIC, + RainMachineEntity) +from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.util import Throttle +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) DEPENDENCIES = ['rainmachine'] _LOGGER = getLogger(__name__) +ATTR_AREA = 'area' +ATTR_CS_ON = 'cs_on' +ATTR_CURRENT_CYCLE = 'current_cycle' ATTR_CYCLES = 'cycles' -ATTR_TOTAL_DURATION = 'total_duration' +ATTR_DELAY = 'delay' +ATTR_DELAY_ON = 'delay_on' +ATTR_FIELD_CAPACITY = 'field_capacity' +ATTR_NO_CYCLES = 'number_of_cycles' +ATTR_PRECIP_RATE = 'sprinkler_head_precipitation_rate' +ATTR_RESTRICTIONS = 'restrictions' +ATTR_SLOPE = 'slope' +ATTR_SOAK = 'soak' +ATTR_SOIL_TYPE = 'soil_type' +ATTR_SPRINKLER_TYPE = 'sprinkler_head_type' +ATTR_STATUS = 'status' +ATTR_SUN_EXPOSURE = 'sun_exposure' +ATTR_VEGETATION_TYPE = 'vegetation_type' +ATTR_ZONES = 'zones' DEFAULT_ZONE_RUN = 60 * 10 +DAYS = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday' +] + +PROGRAM_STATUS_MAP = { + 0: 'Not Running', + 1: 'Running', + 2: 'Queued' +} + +SOIL_TYPE_MAP = { + 0: 'Not Set', + 1: 'Clay Loam', + 2: 'Silty Clay', + 3: 'Clay', + 4: 'Loam', + 5: 'Sandy Loam', + 6: 'Loamy Sand', + 7: 'Sand', + 8: 'Sandy Clay', + 9: 'Silt Loam', + 10: 'Silt', + 99: 'Other' +} + +SLOPE_TYPE_MAP = { + 0: 'Not Set', + 1: 'Flat', + 2: 'Moderate', + 3: 'High', + 4: 'Very High', + 99: 'Other' +} + +SPRINKLER_TYPE_MAP = { + 0: 'Not Set', + 1: 'Popup Spray', + 2: 'Rotors', + 3: 'Surface Drip', + 4: 'Bubblers', + 99: 'Other' +} + +SUN_EXPOSURE_MAP = { + 0: 'Not Set', + 1: 'Full Sun', + 2: 'Partial Shade', + 3: 'Full Shade' +} + +VEGETATION_MAP = { + 0: 'Not Set', + 1: 'Not Set', + 2: 'Grass', + 3: 'Fruit Trees', + 4: 'Flowers', + 5: 'Vegetables', + 6: 'Citrus', + 7: 'Bushes', + 8: 'Xeriscape', + 99: 'Other' +} + def setup_platform(hass, config, add_devices, discovery_info=None): - """Set this component up under its platform.""" + """Set up the RainMachine Switch platform.""" if discovery_info is None: return @@ -28,181 +120,196 @@ def setup_platform(hass, config, add_devices, discovery_info=None): zone_run_time = discovery_info.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN) - client, device_mac = hass.data.get(DATA_RAINMACHINE) + rainmachine = hass.data[DATA_RAINMACHINE] entities = [] - for program in client.programs.all().get('programs', {}): + for program in rainmachine.client.programs.all().get('programs', {}): if not program.get('active'): continue _LOGGER.debug('Adding program: %s', program) - entities.append( - RainMachineProgram(client, device_mac, program)) + entities.append(RainMachineProgram(rainmachine, program)) - for zone in client.zones.all().get('zones', {}): + for zone in rainmachine.client.zones.all().get('zones', {}): if not zone.get('active'): continue _LOGGER.debug('Adding zone: %s', zone) - entities.append( - RainMachineZone(client, device_mac, zone, - zone_run_time)) + entities.append(RainMachineZone(rainmachine, zone, zone_run_time)) add_devices(entities, True) -class RainMachineEntity(SwitchDevice): +class RainMachineSwitch(RainMachineEntity, SwitchDevice): """A class to represent a generic RainMachine entity.""" - def __init__(self, client, device_mac, entity_json): + def __init__(self, rainmachine, rainmachine_type, obj): """Initialize a generic RainMachine entity.""" - self._api_type = 'remote' if client.auth.using_remote_api else 'local' - self._client = client - self._entity_json = entity_json + self._obj = obj + self._type = rainmachine_type - self.device_mac = device_mac - - self._attrs = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION - } - - @property - def device_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def icon(self) -> str: - """Return the icon.""" - return 'mdi:water' + super().__init__(rainmachine, rainmachine_type, obj.get('uid')) @property def is_enabled(self) -> bool: """Return whether the entity is enabled.""" - return self._entity_json.get('active') - - @property - def rainmachine_entity_id(self) -> int: - """Return the RainMachine ID for this entity.""" - return self._entity_json.get('uid') + return self._obj.get('active') -class RainMachineProgram(RainMachineEntity): +class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" + def __init__(self, rainmachine, obj): + """Initialize.""" + super().__init__(rainmachine, 'program', obj) + @property def is_on(self) -> bool: """Return whether the program is running.""" - return bool(self._entity_json.get('status')) + return bool(self._obj.get('status')) @property def name(self) -> str: """Return the name of the program.""" - return 'Program: {0}'.format(self._entity_json.get('name')) + return 'Program: {0}'.format(self._obj.get('name')) @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_program_{1}'.format( - self.device_mac.replace(':', ''), self.rainmachine_entity_id) + def zones(self) -> list: + """Return a list of active zones associated with this program.""" + return [z for z in self._obj['wateringTimes'] if z['active']] def turn_off(self, **kwargs) -> None: """Turn the program off.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.programs.stop(self.rainmachine_entity_id) - except exceptions.BrokenAPICall: - _LOGGER.error('programs.stop currently broken in remote API') - except exceptions.HTTPError as exc_info: + self.rainmachine.client.programs.stop(self._rainmachine_entity_id) + dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except HTTPError as exc_info: _LOGGER.error('Unable to turn off program "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the program on.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.programs.start(self.rainmachine_entity_id) - except exceptions.BrokenAPICall: - _LOGGER.error('programs.start currently broken in remote API') - except exceptions.HTTPError as exc_info: + self.rainmachine.client.programs.start(self._rainmachine_entity_id) + dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except HTTPError as exc_info: _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _LOGGER.debug(exc_info) - @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) def update(self) -> None: """Update info for the program.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._entity_json = self._client.programs.get( - self.rainmachine_entity_id) - except exceptions.HTTPError as exc_info: + self._obj = self.rainmachine.client.programs.get( + self._rainmachine_entity_id) + + self._attrs.update({ + ATTR_ID: self._obj['uid'], + ATTR_CS_ON: self._obj.get('cs_on'), + ATTR_CYCLES: self._obj.get('cycles'), + ATTR_DELAY: self._obj.get('delay'), + ATTR_DELAY_ON: self._obj.get('delay_on'), + ATTR_SOAK: self._obj.get('soak'), + ATTR_STATUS: + PROGRAM_STATUS_MAP[self._obj.get('status')], + ATTR_ZONES: ', '.join(z['name'] for z in self.zones) + }) + except HTTPError as exc_info: _LOGGER.error('Unable to update info for program "%s"', self.unique_id) _LOGGER.debug(exc_info) -class RainMachineZone(RainMachineEntity): +class RainMachineZone(RainMachineSwitch): """A RainMachine zone.""" - def __init__(self, client, device_mac, zone_json, - zone_run_time): + def __init__(self, rainmachine, obj, zone_run_time): """Initialize a RainMachine zone.""" - super().__init__(client, device_mac, zone_json) + super().__init__(rainmachine, 'zone', obj) + + self._properties_json = {} self._run_time = zone_run_time - self._attrs.update({ - ATTR_CYCLES: self._entity_json.get('noOfCycles'), - ATTR_TOTAL_DURATION: self._entity_json.get('userDuration') - }) @property def is_on(self) -> bool: """Return whether the zone is running.""" - return bool(self._entity_json.get('state')) + return bool(self._obj.get('state')) @property def name(self) -> str: """Return the name of the zone.""" - return 'Zone: {0}'.format(self._entity_json.get('name')) + return 'Zone: {0}'.format(self._obj.get('name')) - @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_zone_{1}'.format( - self.device_mac.replace(':', ''), self.rainmachine_entity_id) + @callback + def _program_updated(self): + """Update state, trigger updates.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, PROGRAM_UPDATE_TOPIC, + self._program_updated) def turn_off(self, **kwargs) -> None: """Turn the zone off.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.zones.stop(self.rainmachine_entity_id) - except exceptions.HTTPError as exc_info: + self.rainmachine.client.zones.stop(self._rainmachine_entity_id) + except HTTPError as exc_info: _LOGGER.error('Unable to turn off zone "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the zone on.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.zones.start(self.rainmachine_entity_id, - self._run_time) - except exceptions.HTTPError as exc_info: + self.rainmachine.client.zones.start(self._rainmachine_entity_id, + self._run_time) + except HTTPError as exc_info: _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _LOGGER.debug(exc_info) - @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) def update(self) -> None: """Update info for the zone.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._entity_json = self._client.zones.get( - self.rainmachine_entity_id) - except exceptions.HTTPError as exc_info: + self._obj = self.rainmachine.client.zones.get( + self._rainmachine_entity_id) + + self._properties_json = self.rainmachine.client.zones.get( + self._rainmachine_entity_id, properties=True) + + self._attrs.update({ + ATTR_ID: self._obj['uid'], + ATTR_AREA: self._properties_json.get('waterSense').get('area'), + ATTR_CURRENT_CYCLE: self._obj.get('cycle'), + ATTR_FIELD_CAPACITY: + self._properties_json.get( + 'waterSense').get('fieldCapacity'), + ATTR_NO_CYCLES: self._obj.get('noOfCycles'), + ATTR_PRECIP_RATE: + self._properties_json.get( + 'waterSense').get('precipitationRate'), + ATTR_RESTRICTIONS: self._obj.get('restriction'), + ATTR_SLOPE: SLOPE_TYPE_MAP[self._properties_json.get('slope')], + ATTR_SOIL_TYPE: + SOIL_TYPE_MAP[self._properties_json.get('sun')], + ATTR_SPRINKLER_TYPE: + SPRINKLER_TYPE_MAP[self._properties_json.get('group_id')], + ATTR_SUN_EXPOSURE: + SUN_EXPOSURE_MAP[self._properties_json.get('sun')], + ATTR_VEGETATION_TYPE: + VEGETATION_MAP[self._obj.get('type')], + }) + except HTTPError as exc_info: _LOGGER.error('Unable to update info for zone "%s"', self.unique_id) _LOGGER.debug(exc_info) diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 7dd1d25ad94..68e91612008 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -10,11 +10,11 @@ import voluptuous as vol import homeassistant.components.rfxtrx as rfxtrx from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME from homeassistant.components.rfxtrx import ( CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, CONF_SIGNAL_REPETITIONS, CONF_DEVICES) from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_NAME DEPENDENCIES = ['rfxtrx'] @@ -24,7 +24,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema({ vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, }) }, vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, diff --git a/homeassistant/components/switch/rpi_pfio.py b/homeassistant/components/switch/rpi_pfio.py index c10f417ba49..3031b1e0290 100644 --- a/homeassistant/components/switch/rpi_pfio.py +++ b/homeassistant/components/switch/rpi_pfio.py @@ -10,7 +10,7 @@ import voluptuous as vol import homeassistant.components.rpi_pfio as rpi_pfio from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.const import ATTR_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity @@ -19,7 +19,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['rpi_pfio'] ATTR_INVERT_LOGIC = 'invert_logic' -ATTR_NAME = 'name' CONF_PORTS = 'ports' diff --git a/homeassistant/components/switch/tahoma.py b/homeassistant/components/switch/tahoma.py index 339a0c39386..aa3554a494c 100644 --- a/homeassistant/components/switch/tahoma.py +++ b/homeassistant/components/switch/tahoma.py @@ -1,7 +1,7 @@ """ Support for Tahoma Switch - those are push buttons for garage door etc. -Those buttons are implemented as switchs that are never on. They only +Those buttons are implemented as switches that are never on. They only receive the turn_on action, perform the relay click, and stay in OFF state For more details about this platform, please refer to the documentation at @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Tahoma switchs.""" + """Set up Tahoma switches.""" controller = hass.data[TAHOMA_DOMAIN]['controller'] devices = [] for switch in hass.data[TAHOMA_DOMAIN]['devices']['switch']: diff --git a/homeassistant/components/switch/zha.py b/homeassistant/components/switch/zha.py index 22eb50be86b..6109dc192f3 100644 --- a/homeassistant/components/switch/zha.py +++ b/homeassistant/components/switch/zha.py @@ -51,7 +51,7 @@ class Switch(zha.Entity, SwitchDevice): @property def is_on(self) -> bool: """Return if the switch is on based on the statemachine.""" - if self._state == 'unknown': + if self._state is None: return False return bool(self._state) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 5994184d815..2a2a19aa2f5 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -126,11 +126,7 @@ class LogErrorHandler(logging.Handler): if record.levelno >= logging.WARN: stack = [] if not record.exc_info: - try: - stack = [f for f, _, _, _ in traceback.extract_stack()] - except ValueError: - # On Python 3.4 under py.test getting the stack might fail. - pass + stack = [f for f, _, _, _ in traceback.extract_stack()] entry = self._create_entry(record, stack) self.records.appendleft(entry) diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 9848d20094c..84edd9afd40 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -40,6 +40,8 @@ TAHOMA_TYPES = { 'rts:CurtainRTSComponent': 'cover', 'rts:BlindRTSComponent': 'cover', 'rts:VenetianBlindRTSComponent': 'cover', + 'io:ExteriorVenetianBlindIOComponent': 'cover', + 'io:RollerShutterUnoIOComponent': 'cover', 'io:RollerShutterWithLowSpeedManagementIOComponent': 'cover', 'io:RollerShutterVeluxIOComponent': 'cover', 'io:RollerShutterGenericIOComponent': 'cover', diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index af0fe5bd572..b9329a46b72 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==10.0.2'] +REQUIREMENTS = ['python-telegram-bot==10.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index bf03ec1adad..cb05795c445 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -29,7 +29,7 @@ SUPPORT_LANGUAGES = [ 'hr', 'cs', 'da', 'nl', 'en', 'en-au', 'en-uk', 'en-us', 'eo', 'fi', 'fr', 'de', 'el', 'hi', 'hu', 'is', 'id', 'it', 'ja', 'ko', 'la', 'lv', 'mk', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru', 'sr', 'sk', 'es', 'es-es', - 'es-us', 'sw', 'sv', 'ta', 'th', 'tr', 'vi', 'cy', 'uk', + 'es-us', 'sw', 'sv', 'ta', 'th', 'tr', 'vi', 'cy', 'uk', 'bg-BG' ] DEFAULT_LANG = 'en' diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index 9ccf280ed04..0cb22bd98dc 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -25,7 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['distro==1.2.0'] +REQUIREMENTS = ['distro==1.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/bom.py b/homeassistant/components/weather/bom.py index 236aeb2fa2e..ad74bb4fb77 100644 --- a/homeassistant/components/weather/bom.py +++ b/homeassistant/components/weather/bom.py @@ -48,7 +48,7 @@ class BOMWeather(WeatherEntity): def __init__(self, bom_data, stationname=None): """Initialise the platform with a data instance and station name.""" self.bom_data = bom_data - self.stationname = stationname or self.bom_data.data.get('name') + self.stationname = stationname or self.bom_data.latest_data.get('name') def update(self): """Update current conditions.""" @@ -62,14 +62,14 @@ class BOMWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - return self.bom_data.data.get('weather') + return self.bom_data.get_reading('weather') # Now implement the WeatherEntity interface @property def temperature(self): """Return the platform temperature.""" - return self.bom_data.data.get('air_temp') + return self.bom_data.get_reading('air_temp') @property def temperature_unit(self): @@ -79,17 +79,17 @@ class BOMWeather(WeatherEntity): @property def pressure(self): """Return the mean sea-level pressure.""" - return self.bom_data.data.get('press_msl') + return self.bom_data.get_reading('press_msl') @property def humidity(self): """Return the relative humidity.""" - return self.bom_data.data.get('rel_hum') + return self.bom_data.get_reading('rel_hum') @property def wind_speed(self): """Return the wind speed.""" - return self.bom_data.data.get('wind_spd_kmh') + return self.bom_data.get_reading('wind_spd_kmh') @property def wind_bearing(self): @@ -99,7 +99,7 @@ class BOMWeather(WeatherEntity): 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] wind = {name: idx * 360 / 16 for idx, name in enumerate(directions)} - return wind.get(self.bom_data.data.get('wind_dir')) + return wind.get(self.bom_data.get_reading('wind_dir')) @property def attribution(self): diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 4989f4f0db2..11094acd3e2 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -60,7 +60,8 @@ JSON_DUMP = partial(json.dumps, cls=JSONEncoder) AUTH_MESSAGE_SCHEMA = vol.Schema({ vol.Required('type'): TYPE_AUTH, - vol.Required('api_password'): str, + vol.Exclusive('api_password', 'auth'): str, + vol.Exclusive('access_token', 'auth'): str, }) # Minimal requirements of a message @@ -318,15 +319,18 @@ class ActiveConnection: msg = await wsock.receive_json() msg = AUTH_MESSAGE_SCHEMA(msg) - if validate_password(request, msg['api_password']): - authenticated = True + if 'api_password' in msg: + authenticated = validate_password( + request, msg['api_password']) - else: - self.debug("Invalid password") - await self.wsock.send_json( - auth_invalid_message('Invalid password')) + elif 'access_token' in msg: + authenticated = \ + msg['access_token'] in self.hass.auth.access_tokens if not authenticated: + self.debug("Invalid password") + await self.wsock.send_json( + auth_invalid_message('Invalid password')) await process_wrong_login(request) return wsock diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index eab67c18aed..042943f7a3f 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, CONF_EMAIL, CONF_PASSWORD, + ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_NAME, CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON, __version__) from homeassistant.core import callback @@ -45,7 +45,6 @@ ATTR_ACCESS_TOKEN = 'access_token' ATTR_REFRESH_TOKEN = 'refresh_token' ATTR_CLIENT_ID = 'client_id' ATTR_CLIENT_SECRET = 'client_secret' -ATTR_NAME = 'name' ATTR_PAIRING_MODE = 'pairing_mode' ATTR_KIDDE_RADIO_CODE = 'kidde_radio_code' ATTR_HUB_NAME = 'hub_name' @@ -53,7 +52,8 @@ ATTR_HUB_NAME = 'hub_name' WINK_AUTH_CALLBACK_PATH = '/auth/wink/callback' WINK_AUTH_START = '/auth/wink' WINK_CONFIG_FILE = '.wink.conf' -USER_AGENT = "Manufacturer/Home-Assistant%s python/3 Wink/3" % __version__ +USER_AGENT = "Manufacturer/Home-Assistant{} python/3 Wink/3".format( + __version__) DEFAULT_CONFIG = { 'client_id': 'CLIENT_ID_HERE', diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 48c54cdecff..2cbf977443c 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.9.0'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 9b66c4c6ded..3ea95ff1dd1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -16,9 +16,9 @@ from homeassistant.helpers import discovery, entity from homeassistant.util import slugify REQUIREMENTS = [ - 'bellows==0.5.2', - 'zigpy==0.0.3', - 'zigpy-xbee==0.0.2', + 'bellows==0.6.0', + 'zigpy==0.1.0', + 'zigpy-xbee==0.1.0', ] DOMAIN = 'zha' @@ -256,11 +256,16 @@ class ApplicationListener: """Try to set up an entity from a "bare" cluster.""" if cluster.cluster_id in profile_clusters: return - # pylint: disable=unidiomatic-typecheck - if type(cluster) not in device_classes: + + component = None + for cluster_type, candidate_component in device_classes.items(): + if isinstance(cluster, cluster_type): + component = candidate_component + break + + if component is None: return - component = device_classes[type(cluster)] cluster_key = "{}-{}".format(device_key, cluster.cluster_id) discovery_info = { 'application_listener': self, @@ -319,7 +324,7 @@ class Entity(entity.Entity): self._endpoint = endpoint self._in_clusters = in_clusters self._out_clusters = out_clusters - self._state = ha_const.STATE_UNKNOWN + self._state = None self._unique_id = unique_id # Normally the entity itself is the listener. Sub-classes may set this @@ -410,7 +415,7 @@ def get_discovery_info(hass, discovery_info): return all_discovery_info.get(discovery_key, None) -async def safe_read(cluster, attributes): +async def safe_read(cluster, attributes, allow_cache=True): """Swallow all exceptions from network read. If we throw during initialization, setup fails. Rather have an entity that @@ -420,7 +425,7 @@ async def safe_read(cluster, attributes): try: result, _ = await cluster.read_attributes( attributes, - allow_cache=True, + allow_cache=allow_cache, ) return result except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 36eb4d55c97..1c083c3ca93 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -47,6 +47,7 @@ def populate_data(): zcl.clusters.general.OnOff: 'switch', zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', + zcl.clusters.measurement.PressureMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 01b17023c12..a8ba5e4a6d3 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -11,15 +11,16 @@ from pprint import pprint import voluptuous as vol -from homeassistant.core import CoreState +from homeassistant.core import callback, CoreState from homeassistant.loader import get_platform from homeassistant.helpers import discovery from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers.event import track_time_change +from homeassistant.helpers.event import async_track_time_change from homeassistant.util import convert import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv @@ -31,7 +32,8 @@ from .const import DOMAIN, DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS -from .util import check_node_schema, check_value_schema, node_name +from .util import (check_node_schema, check_value_schema, node_name, + check_has_unique_id, is_node_parsed) REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3'] @@ -217,7 +219,7 @@ async def async_setup_platform(hass, config, async_add_devices, # pylint: disable=R0914 -def setup(hass, config): +async def async_setup(hass, config): """Set up Z-Wave. Will automatically load components to support devices found on the network. @@ -285,7 +287,7 @@ def setup(hass, config): continue values = ZWaveDeviceEntityValues( - hass, schema, value, config, device_config) + hass, schema, value, config, device_config, registry) # We create a new list and update the reference here so that # the list can be safely iterated over in the main thread @@ -293,6 +295,7 @@ def setup(hass, config): hass.data[DATA_ENTITY_VALUES] = new_values component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = await async_get_registry(hass) def node_added(node): """Handle a new node on the network.""" @@ -313,30 +316,22 @@ def setup(hass, config): _add_node_to_component() return - async def _check_node_ready(): - """Wait for node to be parsed.""" - start_time = dt_util.utcnow() - while True: - waited = int((dt_util.utcnow()-start_time).total_seconds()) - - if entity.unique_id: - _LOGGER.info("Z-Wave node %d ready after %d seconds", - entity.node_id, waited) - break - elif waited >= const.NODE_READY_WAIT_SECS: - # Wait up to NODE_READY_WAIT_SECS seconds for the Z-Wave - # node to be ready. - _LOGGER.warning( - "Z-Wave node %d not ready after %d seconds, " - "continuing anyway", - entity.node_id, waited) - break - else: - await asyncio.sleep(1, loop=hass.loop) - + @callback + def _on_ready(sec): + _LOGGER.info("Z-Wave node %d ready after %d seconds", + entity.node_id, sec) hass.async_add_job(_add_node_to_component) - hass.add_job(_check_node_ready) + @callback + def _on_timeout(sec): + _LOGGER.warning( + "Z-Wave node %d not ready after %d seconds, " + "continuing anyway", + entity.node_id, sec) + hass.async_add_job(_add_node_to_component) + + hass.add_job(check_has_unique_id, entity, _on_ready, _on_timeout, + hass.loop) def network_ready(): """Handle the query of all awake nodes.""" @@ -709,9 +704,9 @@ def setup(hass, config): # Setup autoheal if autoheal: _LOGGER.info("Z-Wave network autoheal is enabled") - track_time_change(hass, heal_network, hour=0, minute=0, second=0) + async_track_time_change(hass, heal_network, hour=0, minute=0, second=0) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave) return True @@ -720,7 +715,7 @@ class ZWaveDeviceEntityValues(): """Manages entity access to the underlying zwave value objects.""" def __init__(self, hass, schema, primary_value, zwave_config, - device_config): + device_config, registry): """Initialize the values object with the passed entity schema.""" self._hass = hass self._zwave_config = zwave_config @@ -729,6 +724,7 @@ class ZWaveDeviceEntityValues(): self._values = {} self._entity = None self._workaround_ignore = False + self._registry = registry for name in self._schema[const.DISC_VALUES].keys(): self._values[name] = None @@ -801,9 +797,13 @@ class ZWaveDeviceEntityValues(): workaround_component, component) component = workaround_component - value_name = _value_name(self.primary) - generated_id = generate_entity_id(component + '.{}', value_name, []) - node_config = self._device_config.get(generated_id) + entity_id = self._registry.async_get_entity_id( + component, DOMAIN, + compute_value_unique_id(self._node, self.primary)) + if entity_id is None: + value_name = _value_name(self.primary) + entity_id = generate_entity_id(component + '.{}', value_name, []) + node_config = self._device_config.get(entity_id) # Configure node _LOGGER.debug("Adding Node_id=%s Generic_command_class=%s, " @@ -816,7 +816,7 @@ class ZWaveDeviceEntityValues(): if node_config.get(CONF_IGNORED): _LOGGER.info( - "Ignoring entity %s due to device settings", generated_id) + "Ignoring entity %s due to device settings", entity_id) # No entity will be created for this value self._workaround_ignore = True return @@ -839,13 +839,35 @@ class ZWaveDeviceEntityValues(): dict_id = id(self) + @callback + def _on_ready(sec): + _LOGGER.info( + "Z-Wave entity %s (node_id: %d) ready after %d seconds", + device.name, self._node.node_id, sec) + self._hass.async_add_job(discover_device, component, device, + dict_id) + + @callback + def _on_timeout(sec): + _LOGGER.warning( + "Z-Wave entity %s (node_id: %d) not ready after %d seconds, " + "continuing anyway", + device.name, self._node.node_id, sec) + self._hass.async_add_job(discover_device, component, device, + dict_id) + async def discover_device(component, device, dict_id): """Put device in a dictionary and call discovery on it.""" self._hass.data[DATA_DEVICES][dict_id] = device await discovery.async_load_platform( self._hass, component, DOMAIN, {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) - self._hass.add_job(discover_device, component, device, dict_id) + + if device.unique_id: + self._hass.add_job(discover_device, component, device, dict_id) + else: + self._hass.add_job(check_has_unique_id, device, _on_ready, + _on_timeout, self._hass.loop) class ZWaveDeviceEntity(ZWaveBaseEntity): @@ -862,8 +884,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.values.primary.set_change_verified(False) self._name = _value_name(self.values.primary) - self._unique_id = "{}-{}".format(self.node.node_id, - self.values.primary.object_id) + self._unique_id = self._compute_unique_id() self._update_attributes() dispatcher.connect( @@ -894,6 +915,11 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): def _update_attributes(self): """Update the node attributes. May only be used inside callback.""" self.node_id = self.node.node_id + self._name = _value_name(self.values.primary) + if not self._unique_id: + self._unique_id = self._compute_unique_id() + if self._unique_id: + self.try_remove_and_add() if self.values.power: self.power_consumption = round( @@ -940,3 +966,15 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): for value in self.values: if value is not None: self.node.refresh_value(value.value_id) + + def _compute_unique_id(self): + if (is_node_parsed(self.node) and + self.values.primary.label != "Unknown") or \ + self.node.is_ready: + return compute_value_unique_id(self.node, self.values.primary) + return None + + +def compute_value_unique_id(node, value): + """Compute unique_id a value would get if it were to get one.""" + return "{}-{}".format(node.node_id, value.object_id) diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index bcddcb0b800..2c6d26802bd 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -9,7 +9,7 @@ from .const import ( ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA, ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, COMMAND_CLASS_CENTRAL_SCENE) -from .util import node_name +from .util import node_name, is_node_parsed _LOGGER = logging.getLogger(__name__) @@ -65,6 +65,15 @@ class ZWaveBaseEntity(Entity): self._update_scheduled = True self.hass.loop.call_later(0.1, do_update) + def try_remove_and_add(self): + """Remove this entity and add it back.""" + async def _async_remove_and_add(): + await self.async_remove() + self.entity_id = None + await self.platform.async_add_entities([self]) + if self.hass and self.platform: + self.hass.add_job(_async_remove_and_add) + class ZWaveNodeEntity(ZWaveBaseEntity): """Representation of a Z-Wave node.""" @@ -151,6 +160,9 @@ class ZWaveNodeEntity(ZWaveBaseEntity): if not self._unique_id: self._unique_id = self._compute_unique_id() + if self._unique_id: + # Node info parsed. Remove and re-add + self.try_remove_and_add() self.maybe_schedule_update() @@ -243,6 +255,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity): return attrs def _compute_unique_id(self): - if self._manufacturer_name and self._product_name: + if is_node_parsed(self.node) or self.node.is_ready: return 'node-{}'.format(self.node_id) return None diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 8c74b731ad6..1c0bb14f7e5 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -1,6 +1,9 @@ """Zwave util methods.""" +import asyncio import logging +import homeassistant.util.dt as dt_util + from . import const _LOGGER = logging.getLogger(__name__) @@ -67,3 +70,23 @@ def node_name(node): """Return the name of the node.""" return node.name or '{} {}'.format( node.manufacturer_name, node.product_name) + + +async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): + """Wait for entity to have unique_id.""" + start_time = dt_util.utcnow() + while True: + waited = int((dt_util.utcnow()-start_time).total_seconds()) + if entity.unique_id: + ready_callback(waited) + return + elif waited >= const.NODE_READY_WAIT_SECS: + # Wait up to NODE_READY_WAIT_SECS seconds for unique_id to appear. + timeout_callback(waited) + return + await asyncio.sleep(1, loop=loop) + + +def is_node_parsed(node): + """Check whether the node has been parsed or still waiting to be parsed.""" + return node.manufacturer_name and node.product_name diff --git a/homeassistant/config.py b/homeassistant/config.py index 5c432490f6a..2f916e69b76 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,7 +7,7 @@ import os import re import shutil # pylint: disable=unused-import -from typing import Any, List, Tuple # NOQA +from typing import Any, List, Tuple, Optional # NOQA import voluptuous as vol from voluptuous.humanize import humanize_error @@ -60,7 +60,7 @@ DEFAULT_CORE_CONFIG = ( (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), (CONF_CUSTOMIZE, '!include customize.yaml', None, 'Customization file'), -) # type: Tuple[Tuple[str, Any, Any, str], ...] +) # type: Tuple[Tuple[str, Any, Any, Optional[str]], ...] DEFAULT_CONFIG = """ # Show links to resources in log and frontend introduction: @@ -167,7 +167,7 @@ def get_default_config_dir() -> str: """Put together the default configuration directory based on the OS.""" data_dir = os.getenv('APPDATA') if os.name == "nt" \ else os.path.expanduser('~') - return os.path.join(data_dir, CONFIG_DIR_NAME) + return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore def ensure_config_exists(config_dir: str, detect_location: bool = True) -> str: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1350cd7d76a..8a73e424fb5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -347,6 +347,15 @@ class ConfigEntries: async def _async_finish_flow(self, result): """Finish a config flow and add an entry.""" + # If no discovery config entries in progress, remove notification. + if not any(ent['source'] in DISCOVERY_SOURCES for ent + in self.hass.config_entries.flow.async_progress()): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID) + + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + entry = ConfigEntry( version=result['version'], domain=result['handler'], @@ -370,12 +379,6 @@ class ConfigEntries: if result['source'] not in DISCOVERY_SOURCES: return entry - # If no discovery config entries in progress, remove notification. - if not any(ent['source'] in DISCOVERY_SOURCES for ent - in self.hass.config_entries.flow.async_progress()): - self.hass.components.persistent_notification.async_dismiss( - DISCOVERY_NOTIFICATION_ID) - return entry async def _async_create_flow(self, handler, *, source, data): diff --git a/homeassistant/const.py b/homeassistant/const.py index 8de8922e4e0..84088c4511c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 69 -PATCH_VERSION = '1' +MINOR_VERSION = 70 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) @@ -221,6 +221,9 @@ ATTR_SERVICE_DATA = 'service_data' # IDs ATTR_ID = 'id' +# Name +ATTR_NAME = 'name' + # Data for a SERVICE_EXECUTED event ATTR_SERVICE_CALL_ID = 'service_call_id' diff --git a/homeassistant/core.py b/homeassistant/core.py index feb8d331ae8..bc3b598180c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -17,7 +17,7 @@ import threading from time import monotonic from types import MappingProxyType -from typing import Optional, Any, Callable, List # NOQA +from typing import Optional, Any, Callable, List, TypeVar, Dict # NOQA from async_timeout import timeout import voluptuous as vol @@ -41,6 +41,8 @@ import homeassistant.util.dt as dt_util import homeassistant.util.location as location from homeassistant.util.unit_system import UnitSystem, METRIC_SYSTEM # NOQA +T = TypeVar('T') + DOMAIN = 'homeassistant' # How long we wait for the result of a service call @@ -70,16 +72,15 @@ def valid_state(state: str) -> bool: return len(state) < 256 -def callback(func: Callable[..., None]) -> Callable[..., None]: +def callback(func: Callable[..., T]) -> Callable[..., T]: """Annotation to mark method as safe to call from within the event loop.""" - # pylint: disable=protected-access - func._hass_callback = True + setattr(func, '_hass_callback', True) return func def is_callback(func: Callable[..., Any]) -> bool: """Check if function is safe to be called in the event loop.""" - return '_hass_callback' in getattr(func, '__dict__', {}) + return getattr(func, '_hass_callback', False) is True @callback @@ -136,13 +137,14 @@ class HomeAssistant(object): self.data = {} self.state = CoreState.not_running self.exit_code = None + self.config_entries = None @property def is_running(self) -> bool: """Return if Home Assistant is running.""" return self.state in (CoreState.starting, CoreState.running) - def start(self) -> None: + def start(self) -> int: """Start home assistant.""" # Register the async start fire_coroutine_threadsafe(self.async_start(), self.loop) @@ -152,13 +154,13 @@ class HomeAssistant(object): # Block until stopped _LOGGER.info("Starting Home Assistant core loop") self.loop.run_forever() - return self.exit_code except KeyboardInterrupt: self.loop.call_soon_threadsafe( self.loop.create_task, self.async_stop()) self.loop.run_forever() finally: self.loop.close() + return self.exit_code async def async_start(self): """Finalize startup from inside the event loop. @@ -200,7 +202,10 @@ class HomeAssistant(object): self.loop.call_soon_threadsafe(self.async_add_job, target, *args) @callback - def async_add_job(self, target: Callable[..., None], *args: Any) -> None: + def async_add_job( + self, + target: Callable[..., Any], + *args: Any) -> Optional[asyncio.tasks.Task]: """Add a job from within the eventloop. This method must be run in the event loop. @@ -354,7 +359,7 @@ class EventBus(object): def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" - self._listeners = {} + self._listeners = {} # type: Dict[str, List[Callable]] self._hass = hass @callback @@ -1039,7 +1044,7 @@ class Config(object): # List of allowed external dirs to access self.whitelist_external_dirs = set() - def distance(self: object, lat: float, lon: float) -> float: + def distance(self, lat: float, lon: float) -> float: """Calculate distance from Home Assistant. Async friendly. diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e9580aba273..5095297e795 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -110,11 +110,11 @@ class FlowManager: # Abort and Success results both finish the flow self._progress.pop(flow.flow_id) - if result['type'] == RESULT_TYPE_ABORT: - return result - # We pass a copy of the result because we're mutating our version - result['result'] = await self._async_finish_flow(dict(result)) + entry = await self._async_finish_flow(dict(result)) + + if result['type'] == RESULT_TYPE_CREATE_ENTRY: + result['result'] = entry return result diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index cb8a3c87820..73bd2377950 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,4 +1,5 @@ """The exceptions used by Home Assistant.""" +import jinja2 class HomeAssistantError(Exception): @@ -22,7 +23,7 @@ class NoEntitySpecifiedError(HomeAssistantError): class TemplateError(HomeAssistantError): """Error during template rendering.""" - def __init__(self, exception): + def __init__(self, exception: jinja2.TemplateError) -> None: """Init the error.""" super().__init__('{}: {}'.format(exception.__class__.__name__, exception)) diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 913e90a859d..5a0b2ca56ea 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -44,7 +44,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): @RequestDataValidator(vol.Schema({ vol.Required('handler'): vol.Any(str, list), - })) + }, extra=vol.ALLOW_EXTRA)) async def post(self, request, data): """Handle a POST request.""" if isinstance(data['handler'], list): diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b5a9c309119..35cc1015aaf 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -83,6 +83,15 @@ class EntityRegistry: """Check if an entity_id is currently registered.""" return entity_id in self.entities + @callback + def async_get_entity_id(self, domain: str, platform: str, unique_id: str): + """Check if an entity_id is currently registered.""" + for entity in self.entities.values(): + if entity.domain == domain and entity.platform == platform and \ + entity.unique_id == unique_id: + return entity.entity_id + return None + @callback def async_generate_entity_id(self, domain, suggested_object_id): """Generate an entity ID that does not conflict. @@ -99,10 +108,9 @@ class EntityRegistry: def async_get_or_create(self, domain, platform, unique_id, *, suggested_object_id=None): """Get entity. Create if it doesn't exist.""" - for entity in self.entities.values(): - if entity.domain == domain and entity.platform == platform and \ - entity.unique_id == unique_id: - return entity + entity_id = self.async_get_entity_id(domain, platform, unique_id) + if entity_id: + return self.entities[entity_id] entity_id = self.async_generate_entity_id( domain, suggested_object_id or '{}_{}'.format(platform, unique_id)) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 67647a323c9..ce93c8705b5 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -93,7 +93,7 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: # This prevents that when only # custom_components/switch/some_platform.py exists, # the import custom_components.switch would succeed. - if module.__spec__.origin == 'namespace': + if module.__spec__ and module.__spec__.origin == 'namespace': continue _LOGGER.info("Loaded %s from %s", comp_or_platform, path) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f6666c829e0..4a7df44ee5e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ requests==2.18.4 pyyaml>=3.11,<4 -pytz>=2017.02 +pytz>=2018.04 pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 @@ -8,7 +8,7 @@ typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6.1 -certifi>=2017.4.17 +certifi>=2018.04.16 attrs==18.1.0 # Breaks Python 3.6 and is not needed for our supported Python versions diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py new file mode 100644 index 00000000000..b4f1ddd2f11 --- /dev/null +++ b/homeassistant/scripts/auth.py @@ -0,0 +1,78 @@ +"""Script to manage users for the Home Assistant auth provider.""" +import argparse +import os + +from homeassistant.config import get_default_config_dir +from homeassistant.auth_providers import homeassistant as hass_auth + + +def run(args): + """Handle Home Assistant auth provider script.""" + parser = argparse.ArgumentParser( + description=("Manage Home Assistant users")) + parser.add_argument( + '--script', choices=['auth']) + parser.add_argument( + '-c', '--config', + default=get_default_config_dir(), + help="Directory that contains the Home Assistant configuration") + + subparsers = parser.add_subparsers() + parser_list = subparsers.add_parser('list') + parser_list.set_defaults(func=list_users) + + parser_add = subparsers.add_parser('add') + parser_add.add_argument('username', type=str) + parser_add.add_argument('password', type=str) + parser_add.set_defaults(func=add_user) + + parser_validate_login = subparsers.add_parser('validate') + parser_validate_login.add_argument('username', type=str) + parser_validate_login.add_argument('password', type=str) + parser_validate_login.set_defaults(func=validate_login) + + parser_change_pw = subparsers.add_parser('change_password') + parser_change_pw.add_argument('username', type=str) + parser_change_pw.add_argument('new_password', type=str) + 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) + + +def list_users(data, args): + """List the users.""" + count = 0 + for user in data.users: + count += 1 + print(user['username']) + + print() + print("Total users:", count) + + +def add_user(data, args): + """Create a user.""" + data.add_user(args.username, args.password) + data.save() + print("User created") + + +def validate_login(data, args): + """Validate a login.""" + try: + data.validate_login(args.username, args.password) + print("Auth valid") + except hass_auth.InvalidAuth: + print("Auth invalid") + + +def change_password(data, args): + """Change password.""" + try: + data.change_password(args.username, args.new_password) + data.save() + print("Password changed") + except hass_auth.InvalidUser: + print("User not found") diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 82a57c90263..11e337a76b5 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==12.0.0', 'keyrings.alt==3.0'] +REQUIREMENTS = ['keyring==12.2.0', 'keyrings.alt==3.1'] def run(args): diff --git a/homeassistant/setup.py b/homeassistant/setup.py index f26aa9b61f1..1664653f2a7 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -139,10 +139,11 @@ async def _async_setup_component(hass: core.HomeAssistant, try: if hasattr(component, 'async_setup'): - result = await component.async_setup(hass, processed_config) + result = await component.async_setup( # type: ignore + hass, processed_config) else: result = await hass.async_add_job( - component.setup, hass, processed_config) + component.setup, hass, processed_config) # type: ignore except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) async_notify_setup_error(hass, domain, True) @@ -165,14 +166,15 @@ async def _async_setup_component(hass: core.HomeAssistant, for entry in hass.config_entries.async_entries(domain): await entry.async_setup(hass, component=component) - hass.config.components.add(component.DOMAIN) + hass.config.components.add(component.DOMAIN) # type: ignore # Cleanup if domain in hass.data[DATA_SETUP]: hass.data[DATA_SETUP].pop(domain) hass.bus.async_fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} + EVENT_COMPONENT_LOADED, + {ATTR_COMPONENT: component.DOMAIN} # type: ignore ) return True diff --git a/requirements_all.txt b/requirements_all.txt index 0887ff98996..ae16651d8e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,7 +1,7 @@ # Home Assistant core requests==2.18.4 pyyaml>=3.11,<4 -pytz>=2017.02 +pytz>=2018.04 pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 @@ -9,7 +9,7 @@ typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6.1 -certifi>=2017.4.17 +certifi>=2018.04.16 attrs==18.1.0 # homeassistant.components.nuimo_controller @@ -28,7 +28,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==2.0.0 +HAP-python==2.1.0 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.0 +PyXiaomiGateway==0.9.3 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 @@ -146,10 +146,10 @@ batinfo==0.4.2 beautifulsoup4==4.6.0 # homeassistant.components.zha -bellows==0.5.2 +bellows==0.6.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.0 +bimmer_connected==0.5.1 # homeassistant.components.blink blinkpy==0.6.0 @@ -258,7 +258,7 @@ discogs_client==2.2.1 discord.py==0.16.12 # homeassistant.components.updater -distro==1.2.0 +distro==1.3.0 # homeassistant.components.switch.digitalloggers dlipower==0.7.165 @@ -308,6 +308,9 @@ fedexdeliverymanager==1.0.6 # homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 +# homeassistant.components.sensor.fints +fints==0.2.1 + # homeassistant.components.sensor.fitbit fitbit==0.3.0 @@ -386,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180509.0 +home-assistant-frontend==20180526.4 # homeassistant.components.homekit_controller # homekit==0.6 @@ -446,7 +449,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.9.1 +insteonplm==0.9.2 # homeassistant.components.verisure jsonpath==0.75 @@ -459,13 +462,16 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==12.0.0 +keyring==12.2.0 # homeassistant.scripts.keyring -keyrings.alt==3.0 +keyrings.alt==3.1 + +# homeassistant.components.konnected +konnected==0.1.2 # homeassistant.components.eufy -lakeside==0.5 +lakeside==0.6 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http @@ -503,7 +509,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==1.2.2 +locationsharinglib==2.0.2 # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 @@ -537,7 +543,7 @@ motorparts==1.0.2 mutagen==1.40.0 # homeassistant.components.mychevy -mychevy==0.1.1 +mychevy==0.4.0 # homeassistant.components.mycroft mycroftapi==2.0 @@ -729,7 +735,7 @@ pychannels==1.0.0 pychromecast==2.1.0 # homeassistant.components.media_player.cmus -pycmus==0.1.0 +pycmus==0.1.1 # homeassistant.components.comfoconnect pycomfoconnect==0.3 @@ -745,7 +751,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==37 +pydeconz==38 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -783,8 +789,11 @@ pyfritzhome==0.3.7 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.sensor.skybeacon +pygatt==3.2.0 + # homeassistant.components.cover.gogogate2 -pygogogate2==0.0.7 +pygogogate2==0.1.1 # homeassistant.components.remote.harmony pyharmony==1.0.20 @@ -869,7 +878,7 @@ pymusiccast==0.1.6 pymyq==0.0.8 # homeassistant.components.mysensors -pymysensors==0.11.1 +pymysensors==0.14.0 # homeassistant.components.lock.nello pynello==1.5.1 @@ -891,7 +900,7 @@ pynut2==2.1.2 pynx584==0.4 # homeassistant.components.iota -pyota==2.0.4 +pyota==2.0.5 # homeassistant.components.sensor.otp pyotp==2.2.6 @@ -909,11 +918,11 @@ pyqwikswitch==0.8 # homeassistant.components.rainbird pyrainbird==0.1.3 -# homeassistant.components.sensor.sabnzbd +# homeassistant.components.sabnzbd pysabnzbd==1.0.1 # homeassistant.components.climate.sensibo -pysensibo==1.0.2 +pysensibo==1.0.3 # homeassistant.components.sensor.serial pyserial-asyncio==0.4 @@ -966,6 +975,9 @@ python-ecobee-api==0.0.18 # homeassistant.components.sensor.etherscan python-etherscan-api==0.0.3 +# homeassistant.components.camera.familyhub +python-family-hub-local==0.0.2 + # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky python-forecastio==1.4.0 @@ -1030,7 +1042,7 @@ python-synology==0.1.0 python-tado==0.2.3 # homeassistant.components.telegram_bot -python-telegram-bot==10.0.2 +python-telegram-bot==10.1.0 # homeassistant.components.sensor.twitch python-twitch==1.3.0 @@ -1174,9 +1186,6 @@ sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan shodan==1.7.7 -# homeassistant.components.notify.simplepush -simplepush==1.1.4 - # homeassistant.components.alarm_control_panel.simplisafe simplisafe-python==1.0.5 @@ -1367,7 +1376,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.04.25 +youtube_dl==2018.05.09 # homeassistant.components.light.zengge zengge==0.2 @@ -1379,7 +1388,7 @@ zeroconf==0.20.0 ziggo-mediabox-xl==1.0.0 # homeassistant.components.zha -zigpy-xbee==0.0.2 +zigpy-xbee==0.1.0 # homeassistant.components.zha -zigpy==0.0.3 +zigpy==0.1.0 diff --git a/requirements_docs.txt b/requirements_docs.txt index bb0d30462ce..5ef38e1537e 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.1 -sphinx-autodoc-typehints==1.2.5 +Sphinx==1.7.4 +sphinx-autodoc-typehints==1.3.0 sphinx-autodoc-annotation==1.0.post1 diff --git a/requirements_test.txt b/requirements_test.txt index 6d5f68615be..0a4a0bcb5b0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,10 +8,10 @@ flake8==3.5 mock-open==1.3.1 mypy==0.590 pydocstyle==1.1.1 -pylint==1.8.3 +pylint==1.8.4 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout>=1.2.1 pytest==3.4.2 -requests_mock==1.4 +requests_mock==1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a25f36a8195..ce458995d2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,17 +9,17 @@ flake8==3.5 mock-open==1.3.1 mypy==0.590 pydocstyle==1.1.1 -pylint==1.8.3 +pylint==1.8.4 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout>=1.2.1 pytest==3.4.2 -requests_mock==1.4 +requests_mock==1.5 # homeassistant.components.homekit -HAP-python==2.0.0 +HAP-python==2.1.0 # homeassistant.components.notify.html5 PyJWT==1.6.0 @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180509.0 +home-assistant-frontend==20180526.4 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==37 +pydeconz==38 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/script/lint b/script/lint index dc6884f4882..8ba14d8939e 100755 --- a/script/lint +++ b/script/lint @@ -8,7 +8,7 @@ echo '=================================================' echo '= FILES CHANGED =' echo '=================================================' if [ -z "$files" ] ; then - echo "No python file changed. Rather use: tox -e lint" + echo "No python file changed. Rather use: tox -e lint\n" exit fi printf "%s\n" $files @@ -19,5 +19,10 @@ flake8 --doctests $files echo "================" echo "LINT with pylint" echo "================" -pylint $(echo "$files" | grep -v '^tests.*') +pylint_files=$(echo "$files" | grep -v '^tests.*') +if [ -z "$pylint_files" ] ; then + echo "Only test files changed. Skipping\n" + exit +fi +pylint $pylint_files echo diff --git a/script/release b/script/release index dc3e208bc1a..cf4f808377e 100755 --- a/script/release +++ b/script/release @@ -27,5 +27,6 @@ then exit 1 fi +rm -rf dist python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* --skip-existing diff --git a/setup.cfg b/setup.cfg index d6dfdfe0ea5..8b17da455dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[wheel] -universal = 1 - [tool:pytest] testpaths = tests norecursedirs = .git testing_config diff --git a/setup.py b/setup.py index 8a68617afd9..2469f32d77e 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ 'requests==2.18.4', 'pyyaml>=3.11,<4', - 'pytz>=2017.02', + 'pytz>=2018.04', 'pip>=8.0.3', 'jinja2>=2.10', 'voluptuous==0.11.1', @@ -52,7 +52,7 @@ REQUIRES = [ 'aiohttp==3.1.3', 'async_timeout==2.0.1', 'astral==1.6.1', - 'certifi>=2017.4.17', + 'certifi>=2018.04.16', 'attrs==18.1.0', ] diff --git a/tests/auth_providers/test_homeassistant.py b/tests/auth_providers/test_homeassistant.py new file mode 100644 index 00000000000..8b12e682865 --- /dev/null +++ b/tests/auth_providers/test_homeassistant.py @@ -0,0 +1,124 @@ +"""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 + + +def test_adding_user(): + """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(): + """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(): + """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(): + """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(): + """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') + + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login(user, 'test-pass') + + data.validate_login(user, 'new-pass') + + +def test_changing_password_raises_invalid_user(): + """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): + """Test login flow.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + 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') + + with patch(JSON__OPEN_PATH, mock_open(), create=True) as mock_write: + await hass.async_add_job(data.save) + + # 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) + + 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 92fc2974e27..0b481f93099 100644 --- a/tests/auth_providers/test_insecure_example.py +++ b/tests/auth_providers/test_insecure_example.py @@ -19,7 +19,7 @@ def store(): @pytest.fixture def provider(store): """Mock provider.""" - return insecure_example.ExampleAuthProvider(store, { + return insecure_example.ExampleAuthProvider(None, store, { 'type': 'insecure_example', 'users': [ { @@ -64,20 +64,16 @@ async def test_match_existing_credentials(store, provider): async def test_verify_username(provider): """Test we raise if incorrect user specified.""" - with pytest.raises(auth.InvalidUser): - await provider.async_get_or_create_credentials({ - 'username': 'non-existing-user', - 'password': 'password-test', - }) + with pytest.raises(insecure_example.InvalidAuthError): + await provider.async_validate_login( + 'non-existing-user', 'password-test') async def test_verify_password(provider): """Test we raise if incorrect user specified.""" - with pytest.raises(auth.InvalidPassword): - await provider.async_get_or_create_credentials({ - 'username': 'user-test', - 'password': 'incorrect-password', - }) + with pytest.raises(insecure_example.InvalidAuthError): + await provider.async_validate_login( + 'user-test', 'incorrect-password') async def test_utf_8_username_password(provider): diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index 3e5a59e8386..f0b205ff5ce 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -19,6 +19,7 @@ BASE_CONFIG = [{ CLIENT_ID = 'test-id' CLIENT_SECRET = 'test-secret' CLIENT_AUTH = BasicAuth(CLIENT_ID, CLIENT_SECRET) +CLIENT_REDIRECT_URI = 'http://example.com/callback' async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, @@ -31,7 +32,8 @@ async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, 'api_password': 'bla' } }) - client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET) + client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET, + redirect_uris=[CLIENT_REDIRECT_URI]) hass.auth._store.clients[client.id] = client if setup_api: await async_setup_component(hass, 'api', {}) diff --git a/tests/components/auth/test_client.py b/tests/components/auth/test_client.py index 2995a6ac81a..65ad22efae2 100644 --- a/tests/components/auth/test_client.py +++ b/tests/components/auth/test_client.py @@ -21,9 +21,9 @@ def mock_view(hass): name = 'bla' @verify_client - async def get(self, request, client_id): + async def get(self, request, client): """Handle GET request.""" - clients.append(client_id) + clients.append(client) hass.http.register_view(ClientView) return clients @@ -36,7 +36,7 @@ async def test_verify_client(hass, aiohttp_client, mock_view): resp = await http_client.get('/', auth=BasicAuth(client.id, client.secret)) assert resp.status == 200 - assert mock_view == [client.id] + assert mock_view[0] is client async def test_verify_client_no_auth_header(hass, aiohttp_client, mock_view): diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 5d9bf6b98cc..7cff04327b8 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,12 +1,13 @@ """Integration tests for the auth component.""" -from . import async_setup_auth, CLIENT_AUTH +from . import async_setup_auth, CLIENT_AUTH, CLIENT_REDIRECT_URI async def test_login_new_user_and_refresh_token(hass, aiohttp_client): """Test logging in with new user and refreshing tokens.""" client = await async_setup_auth(hass, aiohttp_client, setup_api=True) resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', None] + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI, }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 44695bce202..853c002ba46 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -1,5 +1,5 @@ """Tests for the link user flow.""" -from . import async_setup_auth, CLIENT_AUTH, CLIENT_ID +from . import async_setup_auth, CLIENT_AUTH, CLIENT_ID, CLIENT_REDIRECT_URI async def async_get_code(hass, aiohttp_client): @@ -25,7 +25,8 @@ async def async_get_code(hass, aiohttp_client): client = await async_setup_auth(hass, aiohttp_client, config) resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', None] + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI, }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() @@ -56,7 +57,8 @@ async def async_get_code(hass, aiohttp_client): # Now authenticate with the 2nd flow resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', '2nd auth'] + 'handler': ['insecure_example', '2nd auth'], + 'redirect_uri': CLIENT_REDIRECT_URI, }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() diff --git a/tests/components/auth/test_init_login_flow.py b/tests/components/auth/test_init_login_flow.py index 96fece6506b..ad39fba3997 100644 --- a/tests/components/auth/test_init_login_flow.py +++ b/tests/components/auth/test_init_login_flow.py @@ -1,7 +1,7 @@ """Tests for the login flow.""" from aiohttp.helpers import BasicAuth -from . import async_setup_auth, CLIENT_AUTH +from . import async_setup_auth, CLIENT_AUTH, CLIENT_REDIRECT_URI async def test_fetch_auth_providers(hass, aiohttp_client): @@ -34,7 +34,8 @@ async def test_invalid_username_password(hass, aiohttp_client): """Test we cannot get flows in progress.""" client = await async_setup_auth(hass, aiohttp_client) resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', None] + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index df9ab69e7e8..aea6e517e38 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -26,7 +26,7 @@ class TestAutomationEvent(unittest.TestCase): self.hass.services.register('test', 'automation', record_call) def tearDown(self): - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_if_fires_on_event(self): diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 63ca4b5cd1a..de453675a57 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -35,7 +35,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.stop() def test_if_fires_on_entity_change_below(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -62,7 +62,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_over_to_below(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -85,7 +85,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entities_change_over_to_below(self): - """"Test the firing with changed entities.""" + """Test the firing with changed entities.""" self.hass.states.set('test.entity_1', 11) self.hass.states.set('test.entity_2', 11) self.hass.block_till_done() @@ -115,7 +115,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(2, len(self.calls)) def test_if_not_fires_on_entity_change_below_to_below(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -148,7 +148,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_below_fires_on_entity_change_to_equal(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -171,7 +171,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_initial_entity_below(self): - """"Test the firing when starting with a match.""" + """Test the firing when starting with a match.""" self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -194,7 +194,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_initial_entity_above(self): - """"Test the firing when starting with a match.""" + """Test the firing when starting with a match.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -217,7 +217,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_above(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -236,7 +236,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_below_to_above(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" # set initial state self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -260,7 +260,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_fires_on_entity_change_above_to_above(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" # set initial state self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -289,7 +289,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_above_fires_on_entity_change_to_equal(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" # set initial state self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -313,7 +313,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_below_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -333,7 +333,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_below_above_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -353,7 +353,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_over_to_below_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -377,7 +377,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_over_to_below_above_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -401,7 +401,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_not_fires_if_entity_not_match(self): - """"Test if not fired with non matching entity.""" + """Test if not fired with non matching entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -420,7 +420,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_below_with_attribute(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -439,7 +439,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_fires_on_entity_change_not_below_with_attribute(self): - """"Test attributes.""" + """Test attributes.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -458,7 +458,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_attribute_change_with_attribute_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -478,7 +478,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_fires_on_attribute_change_with_attribute_not_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -498,7 +498,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_not_fires_on_entity_change_with_attribute_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -518,7 +518,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_not_fires_on_entity_change_with_not_attribute_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -538,7 +538,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -559,7 +559,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_template_list(self): - """"Test template list.""" + """Test template list.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -581,7 +581,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_template_string(self): - """"Test template string.""" + """Test template string.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -614,7 +614,7 @@ class TestAutomationNumericState(unittest.TestCase): self.calls[0].data['some']) def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(self): - """"Test if not fired changed attributes.""" + """Test if not fired changed attributes.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -635,7 +635,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_action(self): - """"Test if action.""" + """Test if action.""" entity_id = 'domain.test_entity' assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { diff --git a/tests/components/binary_sensor/test_bayesian.py b/tests/components/binary_sensor/test_bayesian.py index 3b403c3702f..c3242e09e78 100644 --- a/tests/components/binary_sensor/test_bayesian.py +++ b/tests/components/binary_sensor/test_bayesian.py @@ -154,6 +154,37 @@ class TestBayesianBinarySensor(unittest.TestCase): assert state.state == 'off' + def test_threshold(self): + """Test sensor on probabilty threshold limits.""" + config = { + 'binary_sensor': { + 'name': + 'Test_Binary', + 'platform': + 'bayesian', + 'observations': [{ + 'platform': 'state', + 'entity_id': 'sensor.test_monitored', + 'to_state': 'on', + 'prob_given_true': 1.0, + }], + 'prior': + 0.5, + 'probability_threshold': + 1.0, + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 'on') + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_binary') + self.assertAlmostEqual(1.0, state.attributes.get('probability')) + + assert state.state == 'on' + def test_multiple_observations(self): """Test sensor with multiple observations of same entity.""" config = { diff --git a/tests/components/binary_sensor/test_nx584.py b/tests/components/binary_sensor/test_nx584.py index d94d887c641..4d1d85d30fb 100644 --- a/tests/components/binary_sensor/test_nx584.py +++ b/tests/components/binary_sensor/test_nx584.py @@ -113,7 +113,7 @@ class TestNX584SensorSetup(unittest.TestCase): self._test_assert_graceful_fail({}) def test_setup_version_too_old(self): - """"Test if version is too old.""" + """Test if version is too old.""" nx584_client.Client.return_value.get_version.return_value = '1.0' self._test_assert_graceful_fail({}) diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index 18c095f4bc1..62623a04f3c 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -31,7 +31,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.hass.stop() def test_setup(self): - """"Test the setup.""" + """Test the setup.""" config = { 'binary_sensor': { 'platform': 'template', @@ -49,7 +49,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.hass, 'binary_sensor', config) def test_setup_no_sensors(self): - """"Test setup with no sensors.""" + """Test setup with no sensors.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -58,7 +58,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }) def test_setup_invalid_device(self): - """"Test the setup with invalid devices.""" + """Test the setup with invalid devices.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -70,7 +70,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }) def test_setup_invalid_device_class(self): - """"Test setup with invalid sensor class.""" + """Test setup with invalid sensor class.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -85,7 +85,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }) def test_setup_invalid_missing_template(self): - """"Test setup with invalid and missing template.""" + """Test setup with invalid and missing template.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -161,7 +161,7 @@ class TestBinarySensorTemplate(unittest.TestCase): assert state.attributes['entity_picture'] == '/local/sensor.png' def test_attributes(self): - """"Test the attributes.""" + """Test the attributes.""" vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', @@ -182,7 +182,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.assertTrue(vs.is_on) def test_event(self): - """"Test the event.""" + """Test the event.""" config = { 'binary_sensor': { 'platform': 'template', @@ -214,7 +214,7 @@ class TestBinarySensorTemplate(unittest.TestCase): @mock.patch('homeassistant.helpers.template.Template.render') def test_update_template_error(self, mock_render): - """"Test the template update error.""" + """Test the template update error.""" vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 40517ea1298..0a57512aabd 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -2,10 +2,6 @@ import asyncio from unittest import mock -# Using third party package because of a bug reading binary data in Python 3.4 -# https://bugs.python.org/issue23004 -from mock_open import MockOpen - from homeassistant.components.camera import DOMAIN from homeassistant.components.camera.local_file import ( SERVICE_UPDATE_FILE_PATH) @@ -30,7 +26,7 @@ def test_loading_file(hass, aiohttp_client): client = yield from aiohttp_client(hass.http.app) - m_open = MockOpen(read_data=b'hello') + m_open = mock.mock_open(read_data=b'hello') with mock.patch( 'homeassistant.components.camera.local_file.open', m_open, create=True @@ -90,7 +86,7 @@ def test_camera_content_type(hass, aiohttp_client): client = yield from aiohttp_client(hass.http.app) image = 'hello' - m_open = MockOpen(read_data=image.encode()) + m_open = mock.mock_open(read_data=image.encode()) with mock.patch('homeassistant.components.camera.local_file.open', m_open, create=True): resp_1 = yield from client.get('/api/camera_proxy/camera.test_jpg') diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index 40b4fb2d8e2..dabad953bea 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -26,7 +26,7 @@ class TestUVCSetup(unittest.TestCase): @mock.patch('uvcclient.nvr.UVCRemote') @mock.patch.object(uvc, 'UnifiVideoCamera') def test_setup_full_config(self, mock_uvc, mock_remote): - """"Test the setup with full configuration.""" + """Test the setup with full configuration.""" config = { 'platform': 'uvc', 'nvr': 'foo', @@ -41,7 +41,7 @@ class TestUVCSetup(unittest.TestCase): ] def fake_get_camera(uuid): - """"Create a fake camera.""" + """Create a fake camera.""" if uuid == 'id3': return {'model': 'airCam'} else: @@ -65,7 +65,7 @@ class TestUVCSetup(unittest.TestCase): @mock.patch('uvcclient.nvr.UVCRemote') @mock.patch.object(uvc, 'UnifiVideoCamera') def test_setup_partial_config(self, mock_uvc, mock_remote): - """"Test the setup with partial configuration.""" + """Test the setup with partial configuration.""" config = { 'platform': 'uvc', 'nvr': 'foo', @@ -152,7 +152,7 @@ class TestUVC(unittest.TestCase): """Test class for UVC.""" def setup_method(self, method): - """"Setup the mock camera.""" + """Setup the mock camera.""" self.nvr = mock.MagicMock() self.uuid = 'uuid' self.name = 'name' @@ -171,7 +171,7 @@ class TestUVC(unittest.TestCase): self.nvr.server_version = (3, 2, 0) def test_properties(self): - """"Test the properties.""" + """Test the properties.""" self.assertEqual(self.name, self.uvc.name) self.assertTrue(self.uvc.is_recording) self.assertEqual('Ubiquiti', self.uvc.brand) @@ -180,7 +180,7 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login(self, mock_camera, mock_store): - """"Test the login.""" + """Test the login.""" self.uvc._login() self.assertEqual(mock_camera.call_count, 1) self.assertEqual( @@ -205,7 +205,7 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store): - """"Test the login tries.""" + """Test the login tries.""" responses = [0] def fake_login(*a): @@ -234,13 +234,13 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login_fails_both_properly(self, mock_camera, mock_store): - """"Test if login fails properly.""" + """Test if login fails properly.""" mock_camera.return_value.login.side_effect = socket.error self.assertEqual(None, self.uvc._login()) self.assertEqual(None, self.uvc._connect_addr) def test_camera_image_tries_login_bails_on_failure(self): - """"Test retrieving failure.""" + """Test retrieving failure.""" with mock.patch.object(self.uvc, '_login') as mock_login: mock_login.return_value = False self.assertEqual(None, self.uvc.camera_image()) @@ -248,19 +248,19 @@ class TestUVC(unittest.TestCase): self.assertEqual(mock_login.call_args, mock.call()) def test_camera_image_logged_in(self): - """"Test the login state.""" + """Test the login state.""" self.uvc._camera = mock.MagicMock() self.assertEqual(self.uvc._camera.get_snapshot.return_value, self.uvc.camera_image()) def test_camera_image_error(self): - """"Test the camera image error.""" + """Test the camera image error.""" self.uvc._camera = mock.MagicMock() self.uvc._camera.get_snapshot.side_effect = camera.CameraConnectError self.assertEqual(None, self.uvc.camera_image()) def test_camera_image_reauths(self): - """"Test the re-authentication.""" + """Test the re-authentication.""" responses = [0] def fake_snapshot(): @@ -281,7 +281,7 @@ class TestUVC(unittest.TestCase): self.assertEqual([], responses) def test_camera_image_reauths_only_once(self): - """"Test if the re-authentication only happens once.""" + """Test if the re-authentication only happens once.""" self.uvc._camera = mock.MagicMock() self.uvc._camera.get_snapshot.side_effect = camera.CameraAuthError with mock.patch.object(self.uvc, '_login') as mock_login: diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 53caeb80783..8a1b934ab76 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -3,6 +3,8 @@ import pytest from homeassistant.setup import async_setup_component +from tests.common import MockUser + @pytest.fixture def hass_ws_client(aiohttp_client): @@ -20,3 +22,17 @@ def hass_ws_client(aiohttp_client): return websocket return create_client + + +@pytest.fixture +def hass_access_token(hass): + """Return an access token to access Home Assistant.""" + user = MockUser().add_to_hass(hass) + client = hass.loop.run_until_complete(hass.auth.async_create_client( + 'Access Token Fixture', + redirect_uris=['/'], + no_secret=True, + )) + refresh_token = hass.loop.run_until_complete( + hass.auth.async_create_refresh_token(user, client.id)) + yield hass.auth.async_create_access_token(refresh_token) diff --git a/tests/components/device_tracker/test_unifi_direct.py b/tests/components/device_tracker/test_unifi_direct.py index 8bc3a60146c..ccfa59404a1 100644 --- a/tests/components/device_tracker/test_unifi_direct.py +++ b/tests/components/device_tracker/test_unifi_direct.py @@ -71,7 +71,7 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): @patch('pexpect.pxssh.pxssh') def test_get_device_name(self, mock_ssh): - """"Testing MAC matching.""" + """Testing MAC matching.""" conf_dict = { DOMAIN: { CONF_PLATFORM: 'unifi_direct', @@ -95,7 +95,7 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): @patch('pexpect.pxssh.pxssh.logout') @patch('pexpect.pxssh.pxssh.login') def test_failed_to_log_in(self, mock_login, mock_logout): - """"Testing exception at login results in False.""" + """Testing exception at login results in False.""" from pexpect import exceptions conf_dict = { @@ -120,7 +120,7 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): @patch('pexpect.pxssh.pxssh.sendline') def test_to_get_update(self, mock_sendline, mock_prompt, mock_login, mock_logout): - """"Testing exception in get_update matching.""" + """Testing exception in get_update matching.""" conf_dict = { DOMAIN: { CONF_PLATFORM: 'unifi_direct', diff --git a/tests/components/device_tracker/test_xiaomi.py b/tests/components/device_tracker/test_xiaomi.py index 19f25b514db..bdd921f395f 100644 --- a/tests/components/device_tracker/test_xiaomi.py +++ b/tests/components/device_tracker/test_xiaomi.py @@ -210,7 +210,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): @patch('requests.get', side_effect=mocked_requests) @patch('requests.post', side_effect=mocked_requests) def test_invalid_credential(self, mock_get, mock_post): - """"Testing invalid credential handling.""" + """Testing invalid credential handling.""" config = { DOMAIN: xiaomi.PLATFORM_SCHEMA({ CONF_PLATFORM: xiaomi.DOMAIN, @@ -224,7 +224,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): @patch('requests.get', side_effect=mocked_requests) @patch('requests.post', side_effect=mocked_requests) def test_valid_credential(self, mock_get, mock_post): - """"Testing valid refresh.""" + """Testing valid refresh.""" config = { DOMAIN: xiaomi.PLATFORM_SCHEMA({ CONF_PLATFORM: xiaomi.DOMAIN, @@ -244,7 +244,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): @patch('requests.get', side_effect=mocked_requests) @patch('requests.post', side_effect=mocked_requests) def test_token_timed_out(self, mock_get, mock_post): - """"Testing refresh with a timed out token. + """Testing refresh with a timed out token. New token is requested and list is downloaded a second time. """ diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py index ec68492ed1e..9060d7b9986 100644 --- a/tests/components/fan/test_mqtt.py +++ b/tests/components/fan/test_mqtt.py @@ -18,7 +18,7 @@ class TestMqttFan(unittest.TestCase): self.mock_publish = mock_mqtt_component(self.hass) def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_default_availability_payload(self): diff --git a/tests/components/fan/test_template.py b/tests/components/fan/test_template.py index 719a3f96aed..53eb9e8e2d4 100644 --- a/tests/components/fan/test_template.py +++ b/tests/components/fan/test_template.py @@ -6,7 +6,8 @@ from homeassistant import setup import homeassistant.components as components from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.fan import ( - ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) + ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + ATTR_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE) from tests.common import ( get_test_home_assistant, assert_setup_component) @@ -20,6 +21,8 @@ _STATE_INPUT_BOOLEAN = 'input_boolean.state' _SPEED_INPUT_SELECT = 'input_select.speed' # Represent for fan's oscillating _OSC_INPUT = 'input_select.osc' +# Represent for fan's direction +_DIRECTION_INPUT_SELECT = 'input_select.direction' class TestTemplateFan: @@ -71,7 +74,7 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) def test_missing_value_template_config(self): """Test: missing 'value_template' will fail.""" @@ -185,6 +188,8 @@ class TestTemplateFan: "{{ states('input_select.speed') }}", 'oscillating_template': "{{ states('input_select.osc') }}", + 'direction_template': + "{{ states('input_select.direction') }}", 'turn_on': { 'service': 'script.fan_on' }, @@ -199,14 +204,15 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_OFF, None, None) + self._verify(STATE_OFF, None, None, None) self.hass.states.set(_STATE_INPUT_BOOLEAN, True) self.hass.states.set(_SPEED_INPUT_SELECT, SPEED_MEDIUM) self.hass.states.set(_OSC_INPUT, 'True') + self.hass.states.set(_DIRECTION_INPUT_SELECT, DIRECTION_FORWARD) self.hass.block_till_done() - self._verify(STATE_ON, SPEED_MEDIUM, True) + self._verify(STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) def test_templates_with_valid_values(self): """Test templates with valid values.""" @@ -222,6 +228,8 @@ class TestTemplateFan: "{{ 'medium' }}", 'oscillating_template': "{{ 1 == 1 }}", + 'direction_template': + "{{ 'forward' }}", 'turn_on': { 'service': 'script.fan_on' @@ -237,7 +245,7 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_ON, SPEED_MEDIUM, True) + self._verify(STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) def test_templates_invalid_values(self): """Test templates with invalid values.""" @@ -253,6 +261,8 @@ class TestTemplateFan: "{{ '0' }}", 'oscillating_template': "{{ 'xyz' }}", + 'direction_template': + "{{ 'right' }}", 'turn_on': { 'service': 'script.fan_on' @@ -268,7 +278,7 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_OFF, None, None) + self._verify(STATE_OFF, None, None, None) # End of template tests # @@ -283,7 +293,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) # Turn off fan components.fan.turn_off(self.hass, _TEST_FAN) @@ -291,7 +301,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_OFF - self._verify(STATE_OFF, None, None) + self._verify(STATE_OFF, None, None, None) def test_on_with_speed(self): """Test turn on with speed.""" @@ -304,7 +314,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) def test_set_speed(self): """Test set valid speed.""" @@ -320,7 +330,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to medium components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) @@ -328,7 +338,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_MEDIUM - self._verify(STATE_ON, SPEED_MEDIUM, None) + self._verify(STATE_ON, SPEED_MEDIUM, None, None) def test_set_invalid_speed_from_initial_stage(self): """Test set invalid speed when fan is in initial state.""" @@ -344,7 +354,7 @@ class TestTemplateFan: # verify speed is unchanged assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '' - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) def test_set_invalid_speed(self): """Test set invalid speed when fan has valid speed.""" @@ -360,7 +370,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to 'invalid' components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') @@ -368,7 +378,7 @@ class TestTemplateFan: # verify speed is unchanged assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) def test_custom_speed_list(self): """Test set custom speed list.""" @@ -384,7 +394,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' - self._verify(STATE_ON, '1', None) + self._verify(STATE_ON, '1', None, None) # Set fan's speed to 'medium' which is invalid components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) @@ -392,7 +402,7 @@ class TestTemplateFan: # verify that speed is unchanged assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' - self._verify(STATE_ON, '1', None) + self._verify(STATE_ON, '1', None, None) def test_set_osc(self): """Test set oscillating.""" @@ -408,7 +418,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == 'True' - self._verify(STATE_ON, None, True) + self._verify(STATE_ON, None, True, None) # Set fan's osc to False components.fan.oscillate(self.hass, _TEST_FAN, False) @@ -416,7 +426,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == 'False' - self._verify(STATE_ON, None, False) + self._verify(STATE_ON, None, False, None) def test_set_invalid_osc_from_initial_state(self): """Test set invalid oscillating when fan is in initial state.""" @@ -432,7 +442,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == '' - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) def test_set_invalid_osc(self): """Test set invalid oscillating when fan has valid osc.""" @@ -448,7 +458,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == 'True' - self._verify(STATE_ON, None, True) + self._verify(STATE_ON, None, True, None) # Set fan's osc to False components.fan.oscillate(self.hass, _TEST_FAN, None) @@ -456,15 +466,85 @@ class TestTemplateFan: # verify osc is unchanged assert self.hass.states.get(_OSC_INPUT).state == 'True' - self._verify(STATE_ON, None, True) + self._verify(STATE_ON, None, True, None) - def _verify(self, expected_state, expected_speed, expected_oscillating): + def test_set_direction(self): + """Test set valid direction.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's direction to forward + components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state \ + == DIRECTION_FORWARD + self._verify(STATE_ON, None, None, DIRECTION_FORWARD) + + # Set fan's direction to reverse + components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_REVERSE) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state \ + == DIRECTION_REVERSE + self._verify(STATE_ON, None, None, DIRECTION_REVERSE) + + def test_set_invalid_direction_from_initial_stage(self): + """Test set invalid direction when fan is in initial state.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's direction to 'invalid' + components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify direction is unchanged + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state == '' + self._verify(STATE_ON, None, None, None) + + def test_set_invalid_direction(self): + """Test set invalid direction when fan has valid direction.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's direction to forward + components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state == \ + DIRECTION_FORWARD + self._verify(STATE_ON, None, None, DIRECTION_FORWARD) + + # Set fan's direction to 'invalid' + components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify direction is unchanged + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state == \ + DIRECTION_FORWARD + self._verify(STATE_ON, None, None, DIRECTION_FORWARD) + + def _verify(self, expected_state, expected_speed, expected_oscillating, + expected_direction): """Verify fan's state, speed and osc.""" state = self.hass.states.get(_TEST_FAN) attributes = state.attributes assert state.state == expected_state assert attributes.get(ATTR_SPEED, None) == expected_speed assert attributes.get(ATTR_OSCILLATING, None) == expected_oscillating + assert attributes.get(ATTR_DIRECTION, None) == expected_direction def _register_components(self, speed_list=None): """Register basic components for testing.""" @@ -475,7 +555,7 @@ class TestTemplateFan: {'input_boolean': {'state': None}} ) - with assert_setup_component(2, 'input_select'): + with assert_setup_component(3, 'input_select'): assert setup.setup_component(self.hass, 'input_select', { 'input_select': { 'speed': { @@ -488,6 +568,11 @@ class TestTemplateFan: 'name': 'oscillating', 'options': ['', 'True', 'False'] }, + + 'direction': { + 'name': 'Direction', + 'options': ['', DIRECTION_FORWARD, DIRECTION_REVERSE] + }, } }) @@ -506,6 +591,8 @@ class TestTemplateFan: "{{ states('input_select.speed') }}", 'oscillating_template': "{{ states('input_select.osc') }}", + 'direction_template': + "{{ states('input_select.direction') }}", 'turn_on': { 'service': 'input_boolean.turn_on', @@ -530,6 +617,14 @@ class TestTemplateFan: 'entity_id': _OSC_INPUT, 'option': '{{ oscillating }}' } + }, + 'set_direction': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _DIRECTION_INPUT_SELECT, + 'option': '{{ direction }}' + } } } diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ed425ad8cca..ac90deb9f73 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -48,7 +48,7 @@ def test_auth_required_forward_request(hassio_client): @pytest.mark.parametrize( 'build_type', [ 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html', - 'latest/hassio-app.html' + 'latest/hassio-app.html', 'es5/some-chunk.js', 'es5/app.js', ]) def test_forward_request_no_auth_for_panel(hassio_client, build_type): """Test no auth needed for .""" diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py new file mode 100644 index 00000000000..915759f22d6 --- /dev/null +++ b/tests/components/homekit/common.py @@ -0,0 +1,8 @@ +"""Collection of fixtures and functions for the HomeKit tests.""" +from unittest.mock import patch + + +def patch_debounce(): + """Return patch for debounce method.""" + return patch('homeassistant.components.homekit.accessories.debounce', + lambda f: lambda *args, **kwargs: f(*args, **kwargs)) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index faa982f62f3..3d1c335f8ae 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -3,162 +3,149 @@ This includes tests for all mock object types. """ from datetime import datetime, timedelta -import unittest -from unittest.mock import call, patch, Mock +from unittest.mock import patch, Mock + +import pytest from homeassistant.components.homekit.accessories import ( debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, SERV_ACCESSORY_INFO, - CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, - CHAR_SERIAL_NUMBER, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, + MANUFACTURER, SERV_ACCESSORY_INFO) from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant + +async def test_debounce(hass): + """Test add_timeout decorator function.""" + def demo_func(*args): + nonlocal arguments, counter + counter += 1 + arguments = args + + arguments = None + counter = 0 + mock = Mock(hass=hass) + + debounce_demo = debounce(demo_func) + assert debounce_demo.__name__ == 'demo_func' + now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC) + + with patch('homeassistant.util.dt.utcnow', return_value=now): + await hass.async_add_job(debounce_demo, mock, 'value') + hass.bus.async_fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + await hass.async_block_till_done() + assert counter == 1 + assert len(arguments) == 2 + + with patch('homeassistant.util.dt.utcnow', return_value=now): + await hass.async_add_job(debounce_demo, mock, 'value') + await hass.async_add_job(debounce_demo, mock, 'value') + + hass.bus.async_fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + await hass.async_block_till_done() + assert counter == 2 -def patch_debounce(): - """Return patch for debounce method.""" - return patch('homeassistant.components.homekit.accessories.debounce', - lambda f: lambda *args, **kwargs: f(*args, **kwargs)) +async def test_home_accessory(hass): + """Test HomeAccessory class.""" + entity_id = 'homekit.accessory' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, 'Home Accessory', entity_id, 2, None) + assert acc.hass == hass + assert acc.display_name == 'Home Accessory' + assert acc.aid == 2 + assert acc.category == 1 # Category.OTHER + assert len(acc.services) == 1 + serv = acc.services[0] # SERV_ACCESSORY_INFO + assert serv.display_name == SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_NAME).value == 'Home Accessory' + assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER + assert serv.get_characteristic(CHAR_MODEL).value == 'Homekit' + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ + 'homekit.accessory' + + hass.states.async_set(entity_id, 'on') + await hass.async_block_till_done() + with patch('homeassistant.components.homekit.accessories.' + 'HomeAccessory.update_state') as mock_update_state: + await hass.async_add_job(acc.run) + state = hass.states.get(entity_id) + mock_update_state.assert_called_with(state) + + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert mock_update_state.call_count == 1 + + with pytest.raises(NotImplementedError): + acc.update_state('new_state') + + # Test model name from domain + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) + serv = acc.services[0] # SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' -class TestAccessories(unittest.TestCase): - """Test pyhap adapter methods.""" +def test_home_bridge(): + """Test HomeBridge class.""" + bridge = HomeBridge('hass') + assert bridge.hass == 'hass' + assert bridge.display_name == BRIDGE_NAME + assert bridge.category == 2 # Category.BRIDGE + assert len(bridge.services) == 1 + serv = bridge.services[0] # SERV_ACCESSORY_INFO + assert serv.display_name == SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == __version__ + assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER + assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ + BRIDGE_SERIAL_NUMBER - def test_debounce(self): - """Test add_timeout decorator function.""" - def demo_func(*args): - nonlocal arguments, counter - counter += 1 - arguments = args + bridge = HomeBridge('hass', 'test_name') + assert bridge.display_name == 'test_name' + assert len(bridge.services) == 1 + serv = bridge.services[0] # SERV_ACCESSORY_INFO - arguments = None - counter = 0 - hass = get_test_home_assistant() - mock = Mock(hass=hass) + # setup_message + bridge.setup_message() - debounce_demo = debounce(demo_func) - self.assertEqual(debounce_demo.__name__, 'demo_func') - now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.utcnow', return_value=now): - debounce_demo(mock, 'value') - hass.bus.fire( - EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) - hass.block_till_done() - assert counter == 1 - assert len(arguments) == 2 +def test_home_driver(): + """Test HomeDriver class.""" + bridge = HomeBridge('hass') + ip_address = '127.0.0.1' + port = 51826 + path = '.homekit.state' + pin = b'123-45-678' - with patch('homeassistant.util.dt.utcnow', return_value=now): - debounce_demo(mock, 'value') - debounce_demo(mock, 'value') + with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ + as mock_driver: + driver = HomeDriver('hass', bridge, ip_address, port, path) - hass.bus.fire( - EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) - hass.block_till_done() - assert counter == 2 + mock_driver.assert_called_with(bridge, ip_address, port, path) + driver.state = Mock(pincode=pin) - hass.stop() + # pair + with patch('pyhap.accessory_driver.AccessoryDriver.pair') as mock_pair, \ + patch('homeassistant.components.homekit.accessories.' + 'dismiss_setup_message') as mock_dissmiss_msg: + driver.pair('client_uuid', 'client_public') - def test_home_accessory(self): - """Test HomeAccessory class.""" - hass = get_test_home_assistant() + mock_pair.assert_called_with('client_uuid', 'client_public') + mock_dissmiss_msg.assert_called_with('hass') - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) - self.assertEqual(acc.hass, hass) - self.assertEqual(acc.display_name, 'Home Accessory') - self.assertEqual(acc.category, 1) # Category.OTHER - self.assertEqual(len(acc.services), 1) - serv = acc.services[0] # SERV_ACCESSORY_INFO - self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) - self.assertEqual( - serv.get_characteristic(CHAR_NAME).value, 'Home Accessory') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'Homekit') - self.assertEqual(serv.get_characteristic(CHAR_SERIAL_NUMBER).value, - 'homekit.accessory') + # unpair + with patch('pyhap.accessory_driver.AccessoryDriver.unpair') \ + as mock_unpair, \ + patch('homeassistant.components.homekit.accessories.' + 'show_setup_message') as mock_show_msg: + driver.unpair('client_uuid') - hass.states.set('homekit.accessory', 'on') - hass.block_till_done() - acc.run() - hass.states.set('homekit.accessory', 'off') - hass.block_till_done() - - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) - self.assertEqual(acc.display_name, 'test_name') - self.assertEqual(acc.aid, 2) - self.assertEqual(len(acc.services), 1) - serv = acc.services[0] # SERV_ACCESSORY_INFO - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'Test Model') - - hass.stop() - - def test_home_bridge(self): - """Test HomeBridge class.""" - bridge = HomeBridge('hass') - self.assertEqual(bridge.hass, 'hass') - self.assertEqual(bridge.display_name, BRIDGE_NAME) - self.assertEqual(bridge.category, 2) # Category.BRIDGE - self.assertEqual(len(bridge.services), 1) - serv = bridge.services[0] # SERV_ACCESSORY_INFO - self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) - self.assertEqual( - serv.get_characteristic(CHAR_NAME).value, BRIDGE_NAME) - self.assertEqual( - serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, __version__) - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) - self.assertEqual( - serv.get_characteristic(CHAR_SERIAL_NUMBER).value, - BRIDGE_SERIAL_NUMBER) - - bridge = HomeBridge('hass', 'test_name') - self.assertEqual(bridge.display_name, 'test_name') - self.assertEqual(len(bridge.services), 1) - serv = bridge.services[0] # SERV_ACCESSORY_INFO - - # setup_message - bridge.setup_message() - - # add_paired_client - with patch('pyhap.accessory.Accessory.add_paired_client') \ - as mock_add_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'dismiss_setup_message') as mock_dissmiss_msg: - bridge.add_paired_client('client_uuid', 'client_public') - - self.assertEqual(mock_add_paired_client.call_args, - call('client_uuid', 'client_public')) - self.assertEqual(mock_dissmiss_msg.call_args, call('hass')) - - # remove_paired_client - with patch('pyhap.accessory.Accessory.remove_paired_client') \ - as mock_remove_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'show_setup_message') as mock_show_msg: - bridge.remove_paired_client('client_uuid') - - self.assertEqual( - mock_remove_paired_client.call_args, call('client_uuid')) - self.assertEqual(mock_show_msg.call_args, call('hass', bridge)) - - def test_home_driver(self): - """Test HomeDriver class.""" - bridge = HomeBridge('hass') - ip_address = '127.0.0.1' - port = 51826 - path = '.homekit.state' - - with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ - as mock_driver: - HomeDriver(bridge, ip_address, port, path) - - self.assertEqual( - mock_driver.call_args, call(bridge, ip_address, port, path)) + mock_unpair.assert_called_with('client_uuid') + mock_show_msg.assert_called_with('hass', pin) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index cff52b2ff20..25a0dd3f1cb 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,213 +1,122 @@ """Package to test the get_accessory method.""" -import logging -import unittest from unittest.mock import patch, Mock +import pytest + from homeassistant.core import State -from homeassistant.components.cover import ( - SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( - ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_DEVICE_CLASS) - -_LOGGER = logging.getLogger(__name__) - -CONFIG = {} + ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) -def test_get_accessory_invalid_aid(caplog): - """Test with unsupported component.""" - assert get_accessory(None, State('light.demo', 'on'), - None, config=None) is None +def test_not_supported(caplog): + """Test if none is returned if entity isn't supported.""" + # not supported entity + assert get_accessory(None, State('demo.demo', 'on'), 2, {}) is None + + # invalid aid + assert get_accessory(None, State('light.demo', 'on'), None, None) is None assert caplog.records[0].levelname == 'WARNING' assert 'invalid aid' in caplog.records[0].msg -def test_not_supported(): - """Test if none is returned if entity isn't supported.""" - assert get_accessory(None, State('demo.demo', 'on'), 2, config=None) \ - is None +@pytest.mark.parametrize('config, name', [ + ({CONF_NAME: 'Customize Name'}, 'Customize Name'), +]) +def test_customize_options(config, name): + """Test with customized options.""" + mock_type = Mock() + with patch.dict(TYPES, {'Light': mock_type}): + entity_state = State('light.demo', 'on') + get_accessory(None, entity_state, 2, config) + mock_type.assert_called_with(None, name, 'light.demo', 2, config) -class TestGetAccessories(unittest.TestCase): - """Methods to test the get_accessory method.""" +@pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ + ('Fan', 'fan.test', 'on', {}, {}), + ('Light', 'light.test', 'on', {}, {}), + ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), + ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, + {ATTR_CODE: '1234'}), + ('Thermostat', 'climate.test', 'auto', {}, {}), + ('Thermostat', 'climate.test', 'auto', + {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | + SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), +]) +def test_types(type_name, entity_id, state, attrs, config): + """Test if types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, config) + assert mock_type.called - def setUp(self): - """Setup Mock type.""" - self.mock_type = Mock() + if config: + assert mock_type.call_args[0][-1] == config - def tearDown(self): - """Test if mock type was called.""" - self.assertTrue(self.mock_type.called) - def test_sensor_temperature(self): - """Test temperature sensor with device class temperature.""" - with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): - state = State('sensor.temperature', '23', - {ATTR_DEVICE_CLASS: 'temperature'}) - get_accessory(None, state, 2, {}) +@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ + ('GarageDoorOpener', 'cover.garage_door', 'open', + {ATTR_DEVICE_CLASS: 'garage', + ATTR_SUPPORTED_FEATURES: SUPPORT_OPEN | SUPPORT_CLOSE}), + ('WindowCovering', 'cover.set_position', 'open', + {ATTR_SUPPORTED_FEATURES: 4}), + ('WindowCoveringBasic', 'cover.open_window', 'open', + {ATTR_SUPPORTED_FEATURES: 3}), +]) +def test_type_covers(type_name, entity_id, state, attrs): + """Test if cover types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, {}) + assert mock_type.called - def test_sensor_temperature_celsius(self): - """Test temperature sensor with Celsius as unit.""" - with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): - state = State('sensor.temperature', '23', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - get_accessory(None, state, 2, {}) - def test_sensor_temperature_fahrenheit(self): - """Test temperature sensor with Fahrenheit as unit.""" - with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): - state = State('sensor.temperature', '74', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - get_accessory(None, state, 2, {}) +@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ + ('BinarySensor', 'binary_sensor.opening', 'on', + {ATTR_DEVICE_CLASS: 'opening'}), + ('BinarySensor', 'device_tracker.someone', 'not_home', {}), + ('AirQualitySensor', 'sensor.air_quality_pm25', '40', {}), + ('AirQualitySensor', 'sensor.air_quality', '40', + {ATTR_DEVICE_CLASS: 'pm25'}), + ('CarbonDioxideSensor', 'sensor.airmeter_co2', '500', {}), + ('CarbonDioxideSensor', 'sensor.airmeter', '500', + {ATTR_DEVICE_CLASS: 'co2'}), + ('HumiditySensor', 'sensor.humidity', '20', + {ATTR_DEVICE_CLASS: 'humidity', ATTR_UNIT_OF_MEASUREMENT: '%'}), + ('LightSensor', 'sensor.light', '900', {ATTR_DEVICE_CLASS: 'illuminance'}), + ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lm'}), + ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lx'}), + ('TemperatureSensor', 'sensor.temperature', '23', + {ATTR_DEVICE_CLASS: 'temperature'}), + ('TemperatureSensor', 'sensor.temperature', '23', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}), + ('TemperatureSensor', 'sensor.temperature', '74', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}), +]) +def test_type_sensors(type_name, entity_id, state, attrs): + """Test if sensor types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, {}) + assert mock_type.called - def test_sensor_humidity(self): - """Test humidity sensor with device class humidity.""" - with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): - state = State('sensor.humidity', '20', - {ATTR_DEVICE_CLASS: 'humidity', - ATTR_UNIT_OF_MEASUREMENT: '%'}) - get_accessory(None, state, 2, {}) - def test_air_quality_sensor(self): - """Test air quality sensor with pm25 class.""" - with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}): - state = State('sensor.air_quality', '40', - {ATTR_DEVICE_CLASS: 'pm25'}) - get_accessory(None, state, 2, {}) - - def test_air_quality_sensor_entity_id(self): - """Test air quality sensor with entity_id contains pm25.""" - with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}): - state = State('sensor.air_quality_pm25', '40', {}) - get_accessory(None, state, 2, {}) - - def test_co2_sensor(self): - """Test co2 sensor with device class co2.""" - with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}): - state = State('sensor.airmeter', '500', - {ATTR_DEVICE_CLASS: 'co2'}) - get_accessory(None, state, 2, {}) - - def test_co2_sensor_entity_id(self): - """Test co2 sensor with entity_id contains co2.""" - with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}): - state = State('sensor.airmeter_co2', '500', {}) - get_accessory(None, state, 2, {}) - - def test_light_sensor(self): - """Test light sensor with device class illuminance.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_DEVICE_CLASS: 'illuminance'}) - get_accessory(None, state, 2, {}) - - def test_light_sensor_unit_lm(self): - """Test light sensor with lm as unit.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_UNIT_OF_MEASUREMENT: 'lm'}) - get_accessory(None, state, 2, {}) - - def test_light_sensor_unit_lx(self): - """Test light sensor with lx as unit.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_UNIT_OF_MEASUREMENT: 'lx'}) - get_accessory(None, state, 2, {}) - - def test_binary_sensor(self): - """Test binary sensor with opening class.""" - with patch.dict(TYPES, {'BinarySensor': self.mock_type}): - state = State('binary_sensor.opening', 'on', - {ATTR_DEVICE_CLASS: 'opening'}) - get_accessory(None, state, 2, {}) - - def test_device_tracker(self): - """Test binary sensor with opening class.""" - with patch.dict(TYPES, {'BinarySensor': self.mock_type}): - state = State('device_tracker.someone', 'not_home', {}) - get_accessory(None, state, 2, {}) - - def test_garage_door(self): - """Test cover with device_class: 'garage' and required features.""" - with patch.dict(TYPES, {'GarageDoorOpener': self.mock_type}): - state = State('cover.garage_door', 'open', { - ATTR_DEVICE_CLASS: 'garage', - ATTR_SUPPORTED_FEATURES: - SUPPORT_OPEN | SUPPORT_CLOSE}) - get_accessory(None, state, 2, {}) - - def test_cover_set_position(self): - """Test cover with support for set_cover_position.""" - with patch.dict(TYPES, {'WindowCovering': self.mock_type}): - state = State('cover.set_position', 'open', - {ATTR_SUPPORTED_FEATURES: 4}) - get_accessory(None, state, 2, {}) - - def test_cover_open_close(self): - """Test cover with support for open and close.""" - with patch.dict(TYPES, {'WindowCoveringBasic': self.mock_type}): - state = State('cover.open_window', 'open', - {ATTR_SUPPORTED_FEATURES: 3}) - get_accessory(None, state, 2, {}) - - def test_alarm_control_panel(self): - """Test alarm control panel.""" - config = {ATTR_CODE: '1234'} - with patch.dict(TYPES, {'SecuritySystem': self.mock_type}): - state = State('alarm_control_panel.test', 'armed') - get_accessory(None, state, 2, config) - - # pylint: disable=unsubscriptable-object - print(self.mock_type.call_args[1]) - self.assertEqual( - self.mock_type.call_args[1]['config'][ATTR_CODE], '1234') - - def test_climate(self): - """Test climate devices.""" - with patch.dict(TYPES, {'Thermostat': self.mock_type}): - state = State('climate.test', 'auto') - get_accessory(None, state, 2, {}) - - def test_light(self): - """Test light devices.""" - with patch.dict(TYPES, {'Light': self.mock_type}): - state = State('light.test', 'on') - get_accessory(None, state, 2, {}) - - def test_climate_support_auto(self): - """Test climate devices with support for auto mode.""" - with patch.dict(TYPES, {'Thermostat': self.mock_type}): - state = State('climate.test', 'auto', { - ATTR_SUPPORTED_FEATURES: - SUPPORT_TARGET_TEMPERATURE_LOW | - SUPPORT_TARGET_TEMPERATURE_HIGH}) - get_accessory(None, state, 2, {}) - - def test_switch(self): - """Test switch.""" - with patch.dict(TYPES, {'Switch': self.mock_type}): - state = State('switch.test', 'on') - get_accessory(None, state, 2, {}) - - def test_remote(self): - """Test remote.""" - with patch.dict(TYPES, {'Switch': self.mock_type}): - state = State('remote.test', 'on') - get_accessory(None, state, 2, {}) - - def test_input_boolean(self): - """Test input_boolean.""" - with patch.dict(TYPES, {'Switch': self.mock_type}): - state = State('input_boolean.test', 'on') - get_accessory(None, state, 2, {}) - - def test_lock(self): - """Test lock.""" - with patch.dict(TYPES, {'Lock': self.mock_type}): - state = State('lock.test', 'locked') - get_accessory(None, state, 2, {}) +@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ + ('Switch', 'switch.test', 'on', {}), + ('Switch', 'remote.test', 'on', {}), + ('Switch', 'input_boolean.test', 'on', {}), +]) +def test_type_switches(type_name, entity_id, state, attrs): + """Test if switch types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, {}) + assert mock_type.called diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 082953038b5..31337088b33 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,223 +1,209 @@ """Tests for the HomeKit component.""" -import unittest -from unittest.mock import call, patch, ANY, Mock +from unittest.mock import patch, ANY, Mock + +import pytest from homeassistant import setup from homeassistant.core import State from homeassistant.components.homekit import ( - HomeKit, generate_aid, - STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) + generate_aid, HomeKit, STATUS_READY, STATUS_RUNNING, + STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( - DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, - DEFAULT_PORT, SERVICE_HOMEKIT_START) + CONF_AUTO_START, DEFAULT_PORT, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from tests.common import get_test_home_assistant -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' -class TestHomeKit(unittest.TestCase): - """Test setup of HomeKit component and HomeKit class.""" +@pytest.fixture(scope='module') +def debounce_patcher(): + """Patch debounce method.""" + patcher = patch_debounce() + yield patcher.start() + patcher.stop() - @classmethod - def setUpClass(cls): - """Setup debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +def test_generate_aid(): + """Test generate aid method.""" + aid = generate_aid('demo.entity') + assert isinstance(aid, int) + assert aid >= 2 and aid <= 18446744073709551615 - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() + with patch(PATH_HOMEKIT + '.adler32') as mock_adler32: + mock_adler32.side_effect = [0, 1] + assert generate_aid('demo.entity') is None - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - def test_generate_aid(self): - """Test generate aid method.""" - aid = generate_aid('demo.entity') - self.assertIsInstance(aid, int) - self.assertTrue(aid >= 2 and aid <= 18446744073709551615) +async def test_setup_min(hass): + """Test async_setup with min config options.""" + with patch(PATH_HOMEKIT + '.HomeKit') as mock_homekit: + assert await setup.async_setup_component( + hass, DOMAIN, {DOMAIN: {}}) - with patch(PATH_HOMEKIT + '.adler32') as mock_adler32: - mock_adler32.side_effect = [0, 1] - self.assertIsNone(generate_aid('demo.entity')) + mock_homekit.assert_any_call(hass, DEFAULT_PORT, None, ANY, {}) + assert mock_homekit().setup.called is True - @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_min(self, mock_homekit): - """Test async_setup with min config options.""" - self.assertTrue(setup.setup_component( - self.hass, DOMAIN, {DOMAIN: {}})) + # Test auto start enabled + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() - self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, DEFAULT_PORT, None, ANY, {}), - call().setup()]) + mock_homekit().start.assert_called_with(ANY) - # Test auto start enabled - mock_homekit.reset_mock() - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) - self.hass.block_till_done() - self.assertEqual(mock_homekit.mock_calls, [call().start(ANY)]) +async def test_setup_auto_start_disabled(hass): + """Test async_setup with auto start disabled and test service calls.""" + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, + CONF_IP_ADDRESS: '172.0.0.0'}} - @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_auto_start_disabled(self, mock_homekit): - """Test async_setup with auto start disabled and test service calls.""" + with patch(PATH_HOMEKIT + '.HomeKit') as mock_homekit: mock_homekit.return_value = homekit = Mock() + assert await setup.async_setup_component( + hass, DOMAIN, config) - config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, - CONF_IP_ADDRESS: '172.0.0.0'}} - self.assertTrue(setup.setup_component( - self.hass, DOMAIN, config)) - self.hass.block_till_done() + mock_homekit.assert_any_call(hass, 11111, '172.0.0.0', ANY, {}) + assert mock_homekit().setup.called is True - self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, 11111, '172.0.0.0', ANY, {}), - call().setup()]) + # Test auto_start disabled + homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert homekit.start.called is False - # Test auto_start disabled - homekit.reset_mock() - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) - self.hass.block_till_done() - self.assertEqual(homekit.mock_calls, []) + # Test start call with driver is ready + homekit.reset_mock() + homekit.status = STATUS_READY - # Test start call with driver is ready - homekit.reset_mock() - homekit.status = STATUS_READY + await hass.services.async_call( + DOMAIN, SERVICE_HOMEKIT_START, blocking=True) + assert homekit.start.called is True - self.hass.services.call('homekit', 'start') - self.assertEqual(homekit.mock_calls, [call.start()]) + # Test start call with driver started + homekit.reset_mock() + homekit.status = STATUS_STOPPED - # Test start call with driver started - homekit.reset_mock() - homekit.status = STATUS_STOPPED + await hass.services.async_call( + DOMAIN, SERVICE_HOMEKIT_START, blocking=True) + assert homekit.start.called is False - self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START) - self.assertEqual(homekit.mock_calls, []) - def test_homekit_setup(self): - """Test setup of bridge and driver.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, None, {}, {}) +async def test_homekit_setup(hass): + """Test setup of bridge and driver.""" + homekit = HomeKit(hass, DEFAULT_PORT, None, {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ - patch('homeassistant.util.get_local_ip') as mock_ip: - mock_ip.return_value = IP_ADDRESS - homekit.setup() + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ + patch('homeassistant.util.get_local_ip') as mock_ip: + mock_ip.return_value = IP_ADDRESS + await hass.async_add_job(homekit.setup) - path = self.hass.config.path(HOMEKIT_FILE) - self.assertTrue(isinstance(homekit.bridge, HomeBridge)) - self.assertEqual(mock_driver.mock_calls, [ - call(homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path)]) + path = hass.config.path(HOMEKIT_FILE) + assert isinstance(homekit.bridge, HomeBridge) + mock_driver.assert_called_with( + hass, homekit.bridge, port=DEFAULT_PORT, + address=IP_ADDRESS, persist_file=path) - # Test if stop listener is setup - self.assertEqual( - self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) + # Test if stop listener is setup + assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 - def test_homekit_setup_ip_address(self): - """Test setup with given IP address.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, '172.0.0.0', {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: - homekit.setup() - mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) +async def test_homekit_setup_ip_address(hass): + """Test setup with given IP address.""" + homekit = HomeKit(hass, DEFAULT_PORT, '172.0.0.0', {}, {}) - def test_homekit_add_accessory(self): - """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(self.hass, None, None, lambda entity_id: True, {}) - homekit.bridge = HomeBridge(self.hass) + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + await hass.async_add_job(homekit.setup) + mock_driver.assert_called_with( + hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) - with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ - as mock_add_acc, \ - patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: - mock_get_acc.side_effect = [None, 'acc', None] - homekit.add_bridge_accessory(State('light.demo', 'on')) - self.assertEqual(mock_get_acc.call_args, - call(self.hass, ANY, 363398124, {})) - self.assertFalse(mock_add_acc.called) - homekit.add_bridge_accessory(State('demo.test', 'on')) - self.assertEqual(mock_get_acc.call_args, - call(self.hass, ANY, 294192020, {})) - self.assertTrue(mock_add_acc.called) - homekit.add_bridge_accessory(State('demo.test_2', 'on')) - self.assertEqual(mock_get_acc.call_args, - call(self.hass, ANY, 429982757, {})) - self.assertEqual(mock_add_acc.mock_calls, [call('acc')]) - def test_homekit_entity_filter(self): - """Test the entity filter.""" - entity_filter = generate_filter(['cover'], ['demo.test'], [], []) - homekit = HomeKit(self.hass, None, None, entity_filter, {}) +async def test_homekit_add_accessory(): + """Add accessory if config exists and get_acc returns an accessory.""" + homekit = HomeKit('hass', None, None, lambda entity_id: True, {}) + homekit.bridge = mock_bridge = Mock() - with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: - mock_get_acc.return_value = None + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: - homekit.add_bridge_accessory(State('cover.test', 'open')) - self.assertTrue(mock_get_acc.called) - mock_get_acc.reset_mock() + mock_get_acc.side_effect = [None, 'acc', None] + homekit.add_bridge_accessory(State('light.demo', 'on')) + mock_get_acc.assert_called_with('hass', ANY, 363398124, {}) + assert not mock_bridge.add_accessory.called - homekit.add_bridge_accessory(State('demo.test', 'on')) - self.assertTrue(mock_get_acc.called) - mock_get_acc.reset_mock() + homekit.add_bridge_accessory(State('demo.test', 'on')) + mock_get_acc.assert_called_with('hass', ANY, 294192020, {}) + assert mock_bridge.add_accessory.called - homekit.add_bridge_accessory(State('light.demo', 'light')) - self.assertFalse(mock_get_acc.called) + homekit.add_bridge_accessory(State('demo.test_2', 'on')) + mock_get_acc.assert_called_with('hass', ANY, 429982757, {}) + mock_bridge.add_accessory.assert_called_with('acc') - @patch(PATH_HOMEKIT + '.show_setup_message') - @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') - def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): - """Test HomeKit start method.""" - homekit = HomeKit(self.hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = HomeBridge(self.hass) - homekit.driver = Mock() - self.hass.states.set('light.demo', 'on') - state = self.hass.states.all()[0] +async def test_homekit_entity_filter(hass): + """Test the entity filter.""" + entity_filter = generate_filter(['cover'], ['demo.test'], [], []) + homekit = HomeKit(hass, None, None, entity_filter, {}) - homekit.start() - self.hass.block_till_done() + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + mock_get_acc.return_value = None - self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) - self.assertEqual(mock_show_setup_msg.mock_calls, [ - call(self.hass, homekit.bridge)]) - self.assertEqual(homekit.driver.mock_calls, [call.start()]) - self.assertEqual(homekit.status, STATUS_RUNNING) + homekit.add_bridge_accessory(State('cover.test', 'open')) + assert mock_get_acc.called is True + mock_get_acc.reset_mock() - # Test start() if already started - homekit.driver.reset_mock() - homekit.start() - self.hass.block_till_done() - self.assertEqual(homekit.driver.mock_calls, []) + homekit.add_bridge_accessory(State('demo.test', 'on')) + assert mock_get_acc.called is True + mock_get_acc.reset_mock() - def test_homekit_stop(self): - """Test HomeKit stop method.""" - homekit = HomeKit(self.hass, None, None, None, None) - homekit.driver = Mock() + homekit.add_bridge_accessory(State('light.demo', 'light')) + assert mock_get_acc.called is False - self.assertEqual(homekit.status, STATUS_READY) - homekit.stop() - self.hass.block_till_done() - homekit.status = STATUS_WAIT - homekit.stop() - self.hass.block_till_done() - homekit.status = STATUS_STOPPED - homekit.stop() - self.hass.block_till_done() - self.assertFalse(homekit.driver.stop.called) - # Test if driver is started - homekit.status = STATUS_RUNNING - homekit.stop() - self.hass.block_till_done() - self.assertTrue(homekit.driver.stop.called) +async def test_homekit_start(hass, debounce_patcher): + """Test HomeKit start method.""" + pin = b'123-45-678' + homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) + homekit.bridge = Mock() + homekit.driver = mock_driver = Mock(state=Mock(paired=False, pincode=pin)) + + hass.states.async_set('light.demo', 'on') + state = hass.states.async_all()[0] + + with patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') as \ + mock_add_acc, \ + patch(PATH_HOMEKIT + '.show_setup_message') as mock_setup_msg: + await hass.async_add_job(homekit.start) + + mock_add_acc.assert_called_with(state) + mock_setup_msg.assert_called_with(hass, pin) + assert mock_driver.start.called is True + assert homekit.status == STATUS_RUNNING + + # Test start() if already started + mock_driver.reset_mock() + await hass.async_add_job(homekit.start) + assert mock_driver.start.called is False + + +async def test_homekit_stop(hass): + """Test HomeKit stop method.""" + homekit = HomeKit(hass, None, None, None, None) + homekit.driver = Mock() + + assert homekit.status == STATUS_READY + await hass.async_add_job(homekit.stop) + homekit.status = STATUS_WAIT + await hass.async_add_job(homekit.stop) + homekit.status = STATUS_STOPPED + await hass.async_add_job(homekit.stop) + assert homekit.driver.stop.called is False + + # Test if driver is started + homekit.status = STATUS_RUNNING + await hass.async_add_job(homekit.stop) + assert homekit.driver.stop.called is True diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 313d58e78fd..8138d1c506b 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -1,265 +1,235 @@ """Test different accessory types: Covers.""" -import unittest +from collections import namedtuple + +import pytest -from homeassistant.core import callback from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP) + ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( - STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, - ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, - ATTR_SUPPORTED_FEATURES) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) -from tests.common import get_test_home_assistant -from tests.components.homekit.test_accessories import patch_debounce +from tests.common import async_mock_service +from tests.components.homekit.common import patch_debounce -class TestHomekitCovers(unittest.TestCase): - """Test class for all accessory types regarding covers.""" +@pytest.fixture(scope='module') +def cls(): + """Patch debounce decorator during import of type_covers.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_covers', + fromlist=['GarageDoorOpener', 'WindowCovering,', + 'WindowCoveringBasic']) + patcher_tuple = namedtuple('Cls', ['window', 'window_basic', 'garage']) + yield patcher_tuple(window=_import.WindowCovering, + window_basic=_import.WindowCoveringBasic, + garage=_import.GarageDoorOpener) + patcher.stop() - @classmethod - def setUpClass(cls): - """Setup Light class import and debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - _import = __import__('homeassistant.components.homekit.type_covers', - fromlist=['GarageDoorOpener', 'WindowCovering,', - 'WindowCoveringBasic']) - cls.garage_cls = _import.GarageDoorOpener - cls.window_cls = _import.WindowCovering - cls.window_basic_cls = _import.WindowCoveringBasic - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +async def test_garage_door_open_close(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.garage_door' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = cls.garage(hass, 'Garage Door', entity_id, 2, None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 4 # GarageDoorOpener - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 1 + assert acc.char_target_state.value == 1 - def test_garage_door_open_close(self): - """Test if accessory and HA are updated accordingly.""" - garage_door = 'cover.garage_door' + hass.states.async_set(entity_id, STATE_OPEN) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - acc = self.garage_cls(self.hass, 'Cover', garage_door, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 4) # GarageDoorOpener + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) + # Set from HomeKit + call_close_cover = async_mock_service(hass, DOMAIN, 'close_cover') + call_open_cover = async_mock_service(hass, DOMAIN, 'open_cover') - self.hass.states.set(garage_door, STATE_CLOSED) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_close_cover + assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_state.value == 2 + assert acc.char_target_state.value == 1 - self.assertEqual(acc.char_current_state.value, 1) - self.assertEqual(acc.char_target_state.value, 1) + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() - self.hass.states.set(garage_door, STATE_OPEN) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_open_cover + assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) - self.hass.states.set(garage_door, STATE_UNAVAILABLE) - self.hass.block_till_done() +async def test_window_set_cover_position(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.window' - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = cls.window(hass, 'Cover', entity_id, 2, None) + await hass.async_add_job(acc.run) - self.hass.states.set(garage_door, STATE_UNKNOWN) - self.hass.block_till_done() + assert acc.aid == 2 + assert acc.category == 14 # WindowCovering - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 - # Set closed from HomeKit - acc.char_target_state.client_update_value(1) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_CURRENT_POSITION: None}) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 - self.assertEqual(acc.char_current_state.value, 2) - self.assertEqual(acc.char_target_state.value, 1) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'close_cover') + hass.states.async_set(entity_id, STATE_OPEN, + {ATTR_CURRENT_POSITION: 50}) + await hass.async_block_till_done() + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 - self.hass.states.set(garage_door, STATE_CLOSED) - self.hass.block_till_done() + # Set from HomeKit + call_set_cover_position = async_mock_service(hass, DOMAIN, + 'set_cover_position') - # Set open from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_position.client_update_value, 25) + await hass.async_block_till_done() + assert call_set_cover_position[0] + assert call_set_cover_position[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_cover_position[0].data[ATTR_POSITION] == 25 + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 25 - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 0) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'open_cover') + await hass.async_add_job(acc.char_target_position.client_update_value, 75) + await hass.async_block_till_done() + assert call_set_cover_position[1] + assert call_set_cover_position[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_cover_position[1].data[ATTR_POSITION] == 75 + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 75 - def test_window_set_cover_position(self): - """Test if accessory and HA are updated accordingly.""" - window_cover = 'cover.window' - acc = self.window_cls(self.hass, 'Cover', window_cover, 2, config=None) - acc.run() +async def test_window_open_close(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.window' - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 14) # WindowCovering + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: 0}) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) + assert acc.aid == 2 + assert acc.category == 14 # WindowCovering - self.hass.states.set(window_cover, STATE_UNKNOWN, - {ATTR_CURRENT_POSITION: None}) - self.hass.block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - self.hass.states.set(window_cover, STATE_OPEN, - {ATTR_CURRENT_POSITION: 50}) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_OPEN) + await hass.async_block_till_done() + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 50) + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - # Set from HomeKit - acc.char_target_position.client_update_value(25) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_cover_position') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_POSITION], 25) + # Set from HomeKit + call_close_cover = async_mock_service(hass, DOMAIN, 'close_cover') + call_open_cover = async_mock_service(hass, DOMAIN, 'open_cover') - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 25) + await hass.async_add_job(acc.char_target_position.client_update_value, 25) + await hass.async_block_till_done() + assert call_close_cover + assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - # Set from HomeKit - acc.char_target_position.client_update_value(75) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_cover_position') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75) + await hass.async_add_job(acc.char_target_position.client_update_value, 90) + await hass.async_block_till_done() + assert call_open_cover[0] + assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 75) + await hass.async_add_job(acc.char_target_position.client_update_value, 55) + await hass.async_block_till_done() + assert call_open_cover[1] + assert call_open_cover[1].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - def test_window_open_close(self): - """Test if accessory and HA are updated accordingly.""" - window_cover = 'cover.window' - self.hass.states.set(window_cover, STATE_UNKNOWN, - {ATTR_SUPPORTED_FEATURES: 0}) - acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, - config=None) - acc.run() +async def test_window_open_close_stop(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.window' - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 14) # WindowCovering + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) + # Set from HomeKit + call_close_cover = async_mock_service(hass, DOMAIN, 'close_cover') + call_open_cover = async_mock_service(hass, DOMAIN, 'open_cover') + call_stop_cover = async_mock_service(hass, DOMAIN, 'stop_cover') - self.hass.states.set(window_cover, STATE_UNKNOWN) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_position.client_update_value, 25) + await hass.async_block_till_done() + assert call_close_cover + assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) + await hass.async_add_job(acc.char_target_position.client_update_value, 90) + await hass.async_block_till_done() + assert call_open_cover + assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - self.hass.states.set(window_cover, STATE_OPEN) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - self.hass.states.set(window_cover, STATE_CLOSED) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(25) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'close_cover') - - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(90) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'open_cover') - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(55) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'open_cover') - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - def test_window_open_close_stop(self): - """Test if accessory and HA are updated accordingly.""" - window_cover = 'cover.window' - - self.hass.states.set(window_cover, STATE_UNKNOWN, - {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, - config=None) - acc.run() - - # Set from HomeKit - acc.char_target_position.client_update_value(25) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'close_cover') - - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(90) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'open_cover') - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(55) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'stop_cover') - - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 50) - self.assertEqual(acc.char_position_state.value, 2) + await hass.async_add_job(acc.char_target_position.client_update_value, 55) + await hass.async_block_till_done() + assert call_stop_cover + assert call_stop_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 + assert acc.char_position_state.value == 2 diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py new file mode 100644 index 00000000000..f96fe19d603 --- /dev/null +++ b/tests/components/homekit/test_type_fans.py @@ -0,0 +1,149 @@ +"""Test different accessory types: Fans.""" +from collections import namedtuple + +import pytest + +from homeassistant.components.fan import ( + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, + SUPPORT_DIRECTION, SUPPORT_OSCILLATE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, + STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) + +from tests.common import async_mock_service +from tests.components.homekit.common import patch_debounce + + +@pytest.fixture(scope='module') +def cls(): + """Patch debounce decorator during import of type_fans.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_fans', + fromlist=['Fan']) + patcher_tuple = namedtuple('Cls', ['fan']) + yield patcher_tuple(fan=_import.Fan) + patcher.stop() + + +async def test_fan_basic(hass, cls): + """Test fan with char state.""" + entity_id = 'fan.demo' + + hass.states.async_set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + acc = cls.fan(hass, 'Fan', entity_id, 2, None) + + assert acc.aid == 2 + assert acc.category == 3 # Fan + assert acc.char_active.value == 0 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_active.value == 1 + + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + call_turn_off = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + + await hass.async_add_job(acc.char_active.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + + await hass.async_add_job(acc.char_active.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + +async def test_fan_direction(hass, cls): + """Test fan with direction.""" + entity_id = 'fan.demo' + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, + ATTR_DIRECTION: DIRECTION_FORWARD}) + await hass.async_block_till_done() + acc = cls.fan(hass, 'Fan', entity_id, 2, None) + + assert acc.char_direction.value == 0 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_direction.value == 0 + + hass.states.async_set(entity_id, STATE_ON, + {ATTR_DIRECTION: DIRECTION_REVERSE}) + await hass.async_block_till_done() + assert acc.char_direction.value == 1 + + # Set from HomeKit + call_set_direction = async_mock_service(hass, DOMAIN, + SERVICE_SET_DIRECTION) + + await hass.async_add_job(acc.char_direction.client_update_value, 0) + await hass.async_block_till_done() + assert call_set_direction[0] + assert call_set_direction[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_direction[0].data[ATTR_DIRECTION] == DIRECTION_FORWARD + + await hass.async_add_job(acc.char_direction.client_update_value, 1) + await hass.async_block_till_done() + assert call_set_direction[1] + assert call_set_direction[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_direction[1].data[ATTR_DIRECTION] == DIRECTION_REVERSE + + +async def test_fan_oscillate(hass, cls): + """Test fan with oscillate.""" + entity_id = 'fan.demo' + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False}) + await hass.async_block_till_done() + acc = cls.fan(hass, 'Fan', entity_id, 2, None) + + assert acc.char_swing.value == 0 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_swing.value == 0 + + hass.states.async_set(entity_id, STATE_ON, + {ATTR_OSCILLATING: True}) + await hass.async_block_till_done() + assert acc.char_swing.value == 1 + + # Set from HomeKit + call_oscillate = async_mock_service(hass, DOMAIN, SERVICE_OSCILLATE) + + await hass.async_add_job(acc.char_swing.client_update_value, 0) + await hass.async_block_till_done() + assert call_oscillate[0] + assert call_oscillate[0].data[ATTR_ENTITY_ID] == entity_id + assert call_oscillate[0].data[ATTR_OSCILLATING] is False + + await hass.async_add_job(acc.char_swing.client_update_value, 1) + await hass.async_block_till_done() + assert call_oscillate[1] + assert call_oscillate[1].data[ATTR_ENTITY_ID] == entity_id + assert call_oscillate[1].data[ATTR_OSCILLATING] is True diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 10bf469c08d..7a1db7b3f71 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,188 +1,174 @@ """Test different accessory types: Lights.""" -import unittest +from collections import namedtuple + +import pytest -from homeassistant.core import callback from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.const import ( - ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, - ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, - SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_ON, STATE_OFF, STATE_UNKNOWN) -from tests.common import get_test_home_assistant -from tests.components.homekit.test_accessories import patch_debounce +from tests.common import async_mock_service +from tests.components.homekit.common import patch_debounce -class TestHomekitLights(unittest.TestCase): - """Test class for all accessory types regarding lights.""" +@pytest.fixture(scope='module') +def cls(): + """Patch debounce decorator during import of type_lights.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_lights', + fromlist=['Light']) + patcher_tuple = namedtuple('Cls', ['light']) + yield patcher_tuple(light=_import.Light) + patcher.stop() - @classmethod - def setUpClass(cls): - """Setup Light class import and debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - _import = __import__('homeassistant.components.homekit.type_lights', - fromlist=['Light']) - cls.light_cls = _import.Light - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +async def test_light_basic(hass, cls): + """Test light with char state.""" + entity_id = 'light.demo' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, None) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 5 # Lightbulb + assert acc.char_on.value == 0 - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_on.value == 1 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + assert acc.char_on.value == 0 - def test_light_basic(self): - """Test light with char state.""" - entity_id = 'light.demo' + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_on.value == 0 - self.hass.states.set(entity_id, STATE_ON, - {ATTR_SUPPORTED_FEATURES: 0}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 5) # Lightbulb - self.assertEqual(acc.char_on.value, 0) + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_on.value == 0 - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, 1) + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') - self.hass.states.set(entity_id, STATE_OFF, - {ATTR_SUPPORTED_FEATURES: 0}) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, 0) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, 0) + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() - # Set from HomeKit - acc.char_on.client_update_value(1) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + await hass.async_add_job(acc.char_on.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id - self.hass.states.set(entity_id, STATE_ON) - self.hass.block_till_done() - acc.char_on.client_update_value(0) - self.hass.block_till_done() - self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) +async def test_light_brightness(hass, cls): + """Test light with brightness.""" + entity_id = 'light.demo' - self.hass.states.set(entity_id, STATE_OFF) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, None) - # Remove entity - self.hass.states.remove(entity_id) - self.hass.block_till_done() + assert acc.char_brightness.value == 0 - def test_light_brightness(self): - """Test light with brightness.""" - entity_id = 'light.demo' + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_brightness.value == 100 - self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.char_brightness.value, 0) + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 40 - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_brightness.value, 100) + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') - self.hass.states.set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) - self.hass.block_till_done() - self.assertEqual(acc.char_brightness.value, 40) + await hass.async_add_job(acc.char_brightness.client_update_value, 20) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 - # Set from HomeKit - acc.char_brightness.client_update_value(20) - acc.char_on.client_update_value(1) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_add_job(acc.char_brightness.client_update_value, 40) + await hass.async_block_till_done() + assert call_turn_on[1] + assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 - acc.char_on.client_update_value(1) - acc.char_brightness.client_update_value(40) - self.hass.block_till_done() - self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_add_job(acc.char_brightness.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id - acc.char_on.client_update_value(1) - acc.char_brightness.client_update_value(0) - self.hass.block_till_done() - self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) - def test_light_color_temperature(self): - """Test light with color temperature.""" - entity_id = 'light.demo' +async def test_light_color_temperature(hass, cls): + """Test light with color temperature.""" + entity_id = 'light.demo' - self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, - ATTR_COLOR_TEMP: 190}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.char_color_temperature.value, 153) + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, + ATTR_COLOR_TEMP: 190}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, None) - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_color_temperature.value, 190) + assert acc.char_color_temperature.value == 153 - # Set from HomeKit - acc.char_color_temperature.client_update_value(250) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 250}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_color_temperature.value == 190 - def test_light_rgb_color(self): - """Test light with rgb_color.""" - entity_id = 'light.demo' + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') - self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, - ATTR_HS_COLOR: (260, 90)}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.char_hue.value, 0) - self.assertEqual(acc.char_saturation.value, 75) + await hass.async_add_job( + acc.char_color_temperature.client_update_value, 250) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_hue.value, 260) - self.assertEqual(acc.char_saturation.value, 90) - # Set from HomeKit - acc.char_hue.client_update_value(145) - acc.char_saturation.client_update_value(75) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) +async def test_light_rgb_color(hass, cls): + """Test light with rgb_color.""" + entity_id = 'light.demo' + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, + ATTR_HS_COLOR: (260, 90)}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, None) + + assert acc.char_hue.value == 0 + assert acc.char_saturation.value == 75 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_hue.value == 260 + assert acc.char_saturation.value == 90 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + + await hass.async_add_job(acc.char_hue.client_update_value, 145) + await hass.async_add_job(acc.char_saturation.client_update_value, 75) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index b2053116060..f4698b1380b 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -1,77 +1,85 @@ """Test different accessory types: Locks.""" -import unittest +import pytest -from homeassistant.core import callback from homeassistant.components.homekit.type_locks import Lock +from homeassistant.components.lock import DOMAIN from homeassistant.const import ( - STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED, - ATTR_SERVICE, EVENT_CALL_SERVICE) + ATTR_CODE, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service -class TestHomekitSensors(unittest.TestCase): - """Test class for all accessory types regarding covers.""" +async def test_lock_unlock(hass): + """Test if accessory and HA are updated accordingly.""" + code = '1234' + config = {ATTR_CODE: code} + entity_id = 'lock.kitchen_door' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Lock(hass, 'Lock', entity_id, 2, config) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 6 # DoorLock - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 1 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_LOCKED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 1 + assert acc.char_target_state.value == 1 - def test_lock_unlock(self): - """Test if accessory and HA are updated accordingly.""" - kitchen_lock = 'lock.kitchen_door' + hass.states.async_set(entity_id, STATE_UNLOCKED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - acc = Lock(self.hass, 'Lock', kitchen_lock, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 6) # DoorLock + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 1) + # Set from HomeKit + call_lock = async_mock_service(hass, DOMAIN, 'lock') + call_unlock = async_mock_service(hass, DOMAIN, 'unlock') - self.hass.states.set(kitchen_lock, STATE_LOCKED) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_lock + assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id + assert call_lock[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 1 - self.assertEqual(acc.char_current_state.value, 1) - self.assertEqual(acc.char_target_state.value, 1) + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_unlock + assert call_unlock[0].data[ATTR_ENTITY_ID] == entity_id + assert call_unlock[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 0 - self.hass.states.set(kitchen_lock, STATE_UNLOCKED) - self.hass.block_till_done() - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) +@pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) +async def test_no_code(hass, config): + """Test accessory if lock doesn't require a code.""" + entity_id = 'lock.kitchen_door' - self.hass.states.set(kitchen_lock, STATE_UNKNOWN) - self.hass.block_till_done() + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Lock(hass, 'Lock', entity_id, 2, config) - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 0) + # Set from HomeKit + call_lock = async_mock_service(hass, DOMAIN, 'lock') - # Set from HomeKit - acc.char_target_state.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'lock') - self.assertEqual(acc.char_target_state.value, 1) - - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'unlock') - self.assertEqual(acc.char_target_state.value, 0) - - self.hass.states.remove(kitchen_lock) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_lock + assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id + assert ATTR_CODE not in call_lock[0].data + assert acc.char_target_state.value == 1 diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index baa461af772..7b72404cdaa 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -1,134 +1,114 @@ """Test different accessory types: Security Systems.""" -import unittest +import pytest -from homeassistant.core import callback -from homeassistant.components.homekit.type_security_systems import ( - SecuritySystem) +from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.components.homekit.type_security_systems import \ + SecuritySystem from homeassistant.const import ( - ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service -class TestHomekitSecuritySystems(unittest.TestCase): - """Test class for all accessory types regarding security systems.""" +async def test_switch_set_state(hass): + """Test if accessory and HA are updated accordingly.""" + code = '1234' + config = {ATTR_CODE: code} + entity_id = 'alarm_control_panel.test' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 11 # AlarmSystem - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 3 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 1 - def test_switch_set_state(self): - """Test if accessory and HA are updated accordingly.""" - acp = 'alarm_control_panel.test' + hass.states.async_set(entity_id, STATE_ALARM_ARMED_HOME) + await hass.async_block_till_done() + assert acc.char_target_state.value == 0 + assert acc.char_current_state.value == 0 - acc = SecuritySystem(self.hass, 'SecuritySystem', acp, - 2, config={ATTR_CODE: '1234'}) - acc.run() + hass.states.async_set(entity_id, STATE_ALARM_ARMED_NIGHT) + await hass.async_block_till_done() + assert acc.char_target_state.value == 2 + assert acc.char_current_state.value == 2 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 11) # AlarmSystem + hass.states.async_set(entity_id, STATE_ALARM_DISARMED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 3 - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 3) + hass.states.async_set(entity_id, STATE_ALARM_TRIGGERED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 4 - self.hass.states.set(acp, STATE_ALARM_ARMED_AWAY) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 1) - self.assertEqual(acc.char_current_state.value, 1) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 4 - self.hass.states.set(acp, STATE_ALARM_ARMED_HOME) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 0) - self.assertEqual(acc.char_current_state.value, 0) + # Set from HomeKit + call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') + call_arm_away = async_mock_service(hass, DOMAIN, 'alarm_arm_away') + call_arm_night = async_mock_service(hass, DOMAIN, 'alarm_arm_night') + call_disarm = async_mock_service(hass, DOMAIN, 'alarm_disarm') - self.hass.states.set(acp, STATE_ALARM_ARMED_NIGHT) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 2) - self.assertEqual(acc.char_current_state.value, 2) + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_arm_home + assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id + assert call_arm_home[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 0 - self.hass.states.set(acp, STATE_ALARM_DISARMED) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 3) + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_arm_away + assert call_arm_away[0].data[ATTR_ENTITY_ID] == entity_id + assert call_arm_away[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 1 - self.hass.states.set(acp, STATE_ALARM_TRIGGERED) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 4) + await hass.async_add_job(acc.char_target_state.client_update_value, 2) + await hass.async_block_till_done() + assert call_arm_night + assert call_arm_night[0].data[ATTR_ENTITY_ID] == entity_id + assert call_arm_night[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 2 - self.hass.states.set(acp, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 4) + await hass.async_add_job(acc.char_target_state.client_update_value, 3) + await hass.async_block_till_done() + assert call_disarm + assert call_disarm[0].data[ATTR_ENTITY_ID] == entity_id + assert call_disarm[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 3 - # Set from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 0) - acc.char_target_state.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'alarm_arm_away') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 1) +@pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) +async def test_no_alarm_code(hass, config): + """Test accessory if security_system doesn't require an alarm_code.""" + entity_id = 'alarm_control_panel.test' - acc.char_target_state.client_update_value(2) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'alarm_arm_night') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 2) + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) - acc.char_target_state.client_update_value(3) - self.hass.block_till_done() - self.assertEqual( - self.events[3].data[ATTR_SERVICE], 'alarm_disarm') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 3) + # Set from HomeKit + call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') - def test_no_alarm_code(self): - """Test accessory if security_system doesn't require a alarm_code.""" - acp = 'alarm_control_panel.test' - - acc = SecuritySystem(self.hass, 'SecuritySystem', acp, - 2, config={ATTR_CODE: None}) - # Set from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') - self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) - self.assertEqual(acc.char_target_state.value, 0) - - acc = SecuritySystem(self.hass, 'SecuritySystem', acp, - 2, config={}) - # Set from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') - self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) - self.assertEqual(acc.char_target_state.value, 0) + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_arm_home + assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id + assert ATTR_CODE not in call_arm_home[0].data + assert acc.char_target_state.value == 0 diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 77bfc0c8901..e36ae67da12 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,209 +1,207 @@ """Test different accessory types: Sensors.""" -import unittest - from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, AirQualitySensor, CarbonDioxideSensor, - LightSensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP) + AirQualitySensor, BinarySensor, CarbonDioxideSensor, HumiditySensor, + LightSensor, TemperatureSensor, BINARY_SENSOR_SERVICE_MAP) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, - STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) - -from tests.common import get_test_home_assistant + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_HOME, STATE_NOT_HOME, + STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) -class TestHomekitSensors(unittest.TestCase): - """Test class for all accessory types regarding sensors.""" +async def test_temperature(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.temperature' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, None) + await hass.async_add_job(acc.run) - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + assert acc.aid == 2 + assert acc.category == 10 # Sensor - def test_temperature(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.temperature' + assert acc.char_temp.value == 0.0 + for key, value in PROP_CELSIUS.items(): + assert acc.char_temp.properties[key] == value - acc = TemperatureSensor(self.hass, 'Temperature', entity_id, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_temp.value == 0.0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_set(entity_id, '20', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_temp.value == 20 - self.assertEqual(acc.char_temp.value, 0.0) - for key, value in PROP_CELSIUS.items(): - self.assertEqual(acc.char_temp.properties[key], value) + hass.states.async_set(entity_id, '75.2', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + await hass.async_block_till_done() + assert acc.char_temp.value == 24 - self.hass.states.set(entity_id, STATE_UNKNOWN, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 0.0) - self.hass.states.set(entity_id, '20', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 20) +async def test_humidity(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.humidity' - self.hass.states.set(entity_id, '75.2', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 24) + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = HumiditySensor(hass, 'Humidity', entity_id, 2, None) + await hass.async_add_job(acc.run) - def test_humidity(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.humidity' + assert acc.aid == 2 + assert acc.category == 10 # Sensor - acc = HumiditySensor(self.hass, 'Humidity', entity_id, 2, config=None) - acc.run() + assert acc.char_humidity.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_humidity.value == 0 - self.assertEqual(acc.char_humidity.value, 0) + hass.states.async_set(entity_id, '20') + await hass.async_block_till_done() + assert acc.char_humidity.value == 20 - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_humidity.value, 0) - self.hass.states.set(entity_id, '20') - self.hass.block_till_done() - self.assertEqual(acc.char_humidity.value, 20) +async def test_air_quality(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.air_quality' - def test_air_quality(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.air_quality' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, None) + await hass.async_add_job(acc.run) - acc = AirQualitySensor(self.hass, 'Air Quality', entity_id, - 2, config=None) - acc.run() + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 - self.assertEqual(acc.char_density.value, 0) - self.assertEqual(acc.char_quality.value, 0) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_density.value, 0) - self.assertEqual(acc.char_quality.value, 0) + hass.states.async_set(entity_id, '34') + await hass.async_block_till_done() + assert acc.char_density.value == 34 + assert acc.char_quality.value == 1 - self.hass.states.set(entity_id, '34') - self.hass.block_till_done() - self.assertEqual(acc.char_density.value, 34) - self.assertEqual(acc.char_quality.value, 1) + hass.states.async_set(entity_id, '200') + await hass.async_block_till_done() + assert acc.char_density.value == 200 + assert acc.char_quality.value == 5 - self.hass.states.set(entity_id, '200') - self.hass.block_till_done() - self.assertEqual(acc.char_density.value, 200) - self.assertEqual(acc.char_quality.value, 5) - def test_co2(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.co2' +async def test_co2(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.co2' - acc = CarbonDioxideSensor(self.hass, 'CO2', entity_id, 2, config=None) - acc.run() + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.assertEqual(acc.char_co2.value, 0) - self.assertEqual(acc.char_peak.value, 0) - self.assertEqual(acc.char_detected.value, 0) + assert acc.char_co2.value == 0 + assert acc.char_peak.value == 0 + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_co2.value, 0) - self.assertEqual(acc.char_peak.value, 0) - self.assertEqual(acc.char_detected.value, 0) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_co2.value == 0 + assert acc.char_peak.value == 0 + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, '1100') - self.hass.block_till_done() - self.assertEqual(acc.char_co2.value, 1100) - self.assertEqual(acc.char_peak.value, 1100) - self.assertEqual(acc.char_detected.value, 1) + hass.states.async_set(entity_id, '1100') + await hass.async_block_till_done() + assert acc.char_co2.value == 1100 + assert acc.char_peak.value == 1100 + assert acc.char_detected.value == 1 - self.hass.states.set(entity_id, '800') - self.hass.block_till_done() - self.assertEqual(acc.char_co2.value, 800) - self.assertEqual(acc.char_peak.value, 1100) - self.assertEqual(acc.char_detected.value, 0) + hass.states.async_set(entity_id, '800') + await hass.async_block_till_done() + assert acc.char_co2.value == 800 + assert acc.char_peak.value == 1100 + assert acc.char_detected.value == 0 - def test_light(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.light' - acc = LightSensor(self.hass, 'Light', entity_id, 2, config=None) - acc.run() +async def test_light(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.light' - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = LightSensor(hass, 'Light', entity_id, 2, None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.char_light.value, 0.0001) + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_light.value, 0.0001) + assert acc.char_light.value == 0.0001 - self.hass.states.set(entity_id, '300') - self.hass.block_till_done() - self.assertEqual(acc.char_light.value, 300) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_light.value == 0.0001 - def test_binary(self): - """Test if accessory is updated after state change.""" - entity_id = 'binary_sensor.opening' + hass.states.async_set(entity_id, '300') + await hass.async_block_till_done() + assert acc.char_light.value == 300 - self.hass.states.set(entity_id, STATE_UNKNOWN, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - acc = BinarySensor(self.hass, 'Window Opening', entity_id, - 2, config=None) - acc.run() +async def test_binary(hass): + """Test if accessory is updated after state change.""" + entity_id = 'binary_sensor.opening' - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() - self.assertEqual(acc.char_detected.value, 0) + acc = BinarySensor(hass, 'Window Opening', entity_id, 2, None) + await hass.async_add_job(acc.run) - self.hass.states.set(entity_id, STATE_ON, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 1) + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.hass.states.set(entity_id, STATE_OFF, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 0) + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, STATE_HOME, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 1) + hass.states.async_set(entity_id, STATE_ON, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 1 - self.hass.states.set(entity_id, STATE_NOT_HOME, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 0) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 0 - self.hass.states.remove(entity_id) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_HOME, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 1 - def test_binary_device_classes(self): - """Test if services and characteristics are assigned correctly.""" - entity_id = 'binary_sensor.demo' + hass.states.async_set(entity_id, STATE_NOT_HOME, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 0 - for device_class, (service, char) in BINARY_SENSOR_SERVICE_MAP.items(): - self.hass.states.set(entity_id, STATE_OFF, - {ATTR_DEVICE_CLASS: device_class}) - self.hass.block_till_done() + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_detected.value == 0 - acc = BinarySensor(self.hass, 'Binary Sensor', entity_id, - 2, config=None) - self.assertEqual(acc.get_service(service).display_name, service) - self.assertEqual(acc.char_detected.display_name, char) + +async def test_binary_device_classes(hass): + """Test if services and characteristics are assigned correctly.""" + entity_id = 'binary_sensor.demo' + + for device_class, (service, char) in BINARY_SENSOR_SERVICE_MAP.items(): + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: device_class}) + await hass.async_block_till_done() + + acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, None) + assert acc.get_service(service).display_name == service + assert acc.char_detected.display_name == char diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 65b107e24cd..5fc0b6ce1b9 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -1,104 +1,47 @@ """Test different accessory types: Switches.""" -import unittest +import pytest -from homeassistant.core import callback, split_entity_id +from homeassistant.core import split_entity_id from homeassistant.components.homekit.type_switches import Switch -from homeassistant.const import ( - ATTR_DOMAIN, ATTR_SERVICE, EVENT_CALL_SERVICE, - SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON -from tests.common import get_test_home_assistant +from tests.common import async_mock_service -class TestHomekitSwitches(unittest.TestCase): - """Test class for all accessory types regarding switches.""" +@pytest.mark.parametrize('entity_id', [ + 'switch.test', 'remote.test', 'input_boolean.test']) +async def test_switch_set_state(hass, entity_id): + """Test if accessory and HA are updated accordingly.""" + domain = split_entity_id(entity_id)[0] - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Switch(hass, 'Switch', entity_id, 2, None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 8 # Switch - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_on.value is False - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_on.value is True - def test_switch_set_state(self): - """Test if accessory and HA are updated accordingly.""" - entity_id = 'switch.test' - domain = split_entity_id(entity_id)[0] + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_on.value is False - acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) - acc.run() + # Set from HomeKit + call_turn_on = async_mock_service(hass, domain, 'turn_on') + call_turn_off = async_mock_service(hass, domain, 'turn_off') - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 8) # Switch + await hass.async_add_job(acc.char_on.client_update_value, True) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id - self.assertEqual(acc.char_on.value, False) - - self.hass.states.set(entity_id, STATE_ON) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, True) - - self.hass.states.set(entity_id, STATE_OFF) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.client_update_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - - acc.char_on.client_update_value(False) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) - - def test_remote_set_state(self): - """Test service call for remote as domain.""" - entity_id = 'remote.test' - domain = split_entity_id(entity_id)[0] - - acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) - acc.run() - - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.client_update_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual(acc.char_on.value, True) - - def test_input_boolean_set_state(self): - """Test service call for remote as domain.""" - entity_id = 'input_boolean.test' - domain = split_entity_id(entity_id)[0] - - acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) - acc.run() - - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.client_update_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual(acc.char_on.value, True) + await hass.async_add_job(acc.char_on.client_update_value, False) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index fe2a7f6cd02..337ad23ad05 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,364 +1,347 @@ """Test different accessory types: Thermostats.""" -import unittest +from collections import namedtuple + +import pytest -from homeassistant.core import callback from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, - ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) + ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, + DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, EVENT_CALL_SERVICE, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from tests.common import get_test_home_assistant -from tests.components.homekit.test_accessories import patch_debounce +from tests.common import async_mock_service +from tests.components.homekit.common import patch_debounce -class TestHomekitThermostats(unittest.TestCase): - """Test class for all accessory types regarding thermostats.""" +@pytest.fixture(scope='module') +def cls(): + """Patch debounce decorator during import of type_thermostats.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_thermostats', + fromlist=['Thermostat']) + patcher_tuple = namedtuple('Cls', ['thermostat']) + yield patcher_tuple(thermostat=_import.Thermostat) + patcher.stop() - @classmethod - def setUpClass(cls): - """Setup Thermostat class import and debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - _import = __import__( - 'homeassistant.components.homekit.type_thermostats', - fromlist=['Thermostat']) - cls.thermostat_cls = _import.Thermostat - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +async def test_default_thermostat(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 9 # Thermostat - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 + assert acc.char_current_temp.value == 21.0 + assert acc.char_target_temp.value == 21.0 + assert acc.char_display_units.value == 0 + assert acc.char_cooling_thresh_temp is None + assert acc.char_heating_thresh_temp is None - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - def test_default_thermostat(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 23.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_temp.value == 23.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 20.0 + assert acc.char_current_heat_cool.value == 2 + assert acc.char_target_heat_cool.value == 2 + assert acc.char_current_temp.value == 25.0 + assert acc.char_display_units.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 9) # Thermostat + hass.states.async_set(entity_id, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 19.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 20.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 2 + assert acc.char_current_temp.value == 19.0 + assert acc.char_display_units.value == 0 - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) - self.assertEqual(acc.char_current_temp.value, 21.0) - self.assertEqual(acc.char_target_temp.value, 21.0) - self.assertEqual(acc.char_display_units.value, 0) - self.assertEqual(acc.char_cooling_thresh_temp, None) - self.assertEqual(acc.char_heating_thresh_temp, None) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_OFF, + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_HEAT, - {ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 1) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_HEAT, - {ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 23.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 1) - self.assertEqual(acc.char_current_temp.value, 23.0) - self.assertEqual(acc.char_display_units.value, 0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 2 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 25.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_COOL, - {ATTR_OPERATION_MODE: STATE_COOL, - ATTR_TEMPERATURE: 20.0, - ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 20.0) - self.assertEqual(acc.char_current_heat_cool.value, 2) - self.assertEqual(acc.char_target_heat_cool.value, 2) - self.assertEqual(acc.char_current_temp.value, 25.0) - self.assertEqual(acc.char_display_units.value, 0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 22.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 22.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_COOL, - {ATTR_OPERATION_MODE: STATE_COOL, - ATTR_TEMPERATURE: 20.0, - ATTR_CURRENT_TEMPERATURE: 19.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 20.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 2) - self.assertEqual(acc.char_current_temp.value, 19.0) - self.assertEqual(acc.char_display_units.value, 0) + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') + call_set_operation_mode = async_mock_service(hass, DOMAIN, + 'set_operation_mode') - self.hass.states.set(climate, STATE_OFF, - {ATTR_OPERATION_MODE: STATE_OFF, - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job(acc.char_target_temp.client_update_value, 19.0) + await hass.async_block_till_done() + assert call_set_temperature + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0 + assert acc.char_target_temp.value == 19.0 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_block_till_done() + assert call_set_operation_mode + assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT + assert acc.char_target_heat_cool.value == 1 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 2) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 25.0) - self.assertEqual(acc.char_display_units.value, 0) - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 22.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 22.0) - self.assertEqual(acc.char_display_units.value, 0) +async def test_auto_thermostat(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - # Set from HomeKit - acc.char_target_temp.client_update_value(19.0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_temperature') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_TEMPERATURE], 19.0) - self.assertEqual(acc.char_target_temp.value, 19.0) + # support_auto = True + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + await hass.async_add_job(acc.run) - acc.char_target_heat_cool.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_operation_mode') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE], - STATE_HEAT) - self.assertEqual(acc.char_target_heat_cool.value, 1) + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 - def test_auto_thermostat(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 22.0, + ATTR_TARGET_TEMP_LOW: 20.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_cooling_thresh_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - # support_auto = True - self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 23.0, + ATTR_TARGET_TEMP_LOW: 19.0, + ATTR_CURRENT_TEMPERATURE: 24.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_current_heat_cool.value == 2 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 24.0 + assert acc.char_display_units.value == 0 - self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) - self.assertEqual(acc.char_heating_thresh_temp.value, 19.0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 23.0, + ATTR_TARGET_TEMP_LOW: 19.0, + ATTR_CURRENT_TEMPERATURE: 21.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 21.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 22.0, - ATTR_TARGET_TEMP_LOW: 20.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 23.0, - ATTR_TARGET_TEMP_LOW: 19.0, - ATTR_CURRENT_TEMPERATURE: 24.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 19.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) - self.assertEqual(acc.char_current_heat_cool.value, 2) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 24.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job( + acc.char_heating_thresh_temp.client_update_value, 20.0) + await hass.async_block_till_done() + assert call_set_temperature[0] + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0 + assert acc.char_heating_thresh_temp.value == 20.0 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 23.0, - ATTR_TARGET_TEMP_LOW: 19.0, - ATTR_CURRENT_TEMPERATURE: 21.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 19.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 21.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job( + acc.char_cooling_thresh_temp.client_update_value, 25.0) + await hass.async_block_till_done() + assert call_set_temperature[1] + assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 25.0 + assert acc.char_cooling_thresh_temp.value == 25.0 - # Set from HomeKit - acc.char_heating_thresh_temp.client_update_value(20.0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_temperature') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_LOW], 20.0) - self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - acc.char_cooling_thresh_temp.client_update_value(25.0) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_temperature') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH], - 25.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) +async def test_power_state(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - def test_power_state(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' + # SUPPORT_ON_OFF = True + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_SUPPORTED_FEATURES: 4096, + ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + await hass.async_add_job(acc.run) + assert acc.support_power_state is True - # SUPPORT_ON_OFF = True - self.hass.states.set(climate, STATE_HEAT, - {ATTR_SUPPORTED_FEATURES: 4096, - ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 23.0, - ATTR_CURRENT_TEMPERATURE: 18.0}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() - self.assertTrue(acc.support_power_state) + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 1 - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 1) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + await hass.async_block_till_done() + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 - self.hass.states.set(climate, STATE_OFF, - {ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 23.0, - ATTR_CURRENT_TEMPERATURE: 18.0}) - self.hass.block_till_done() - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_OFF, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + await hass.async_block_till_done() + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 - self.hass.states.set(climate, STATE_OFF, - {ATTR_OPERATION_MODE: STATE_OFF, - ATTR_TEMPERATURE: 23.0, - ATTR_CURRENT_TEMPERATURE: 18.0}) - self.hass.block_till_done() - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') + call_set_operation_mode = async_mock_service(hass, DOMAIN, + 'set_operation_mode') - # Set from HomeKit - acc.char_target_heat_cool.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'turn_on') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], - climate) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_operation_mode') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE], - STATE_HEAT) - self.assertEqual(acc.char_target_heat_cool.value, 1) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_operation_mode + assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT + assert acc.char_target_heat_cool.value == 1 - acc.char_target_heat_cool.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'turn_off') - self.assertEqual( - self.events[2].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], - climate) - self.assertEqual(acc.char_target_heat_cool.value, 0) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_target_heat_cool.value == 0 - def test_thermostat_fahrenheit(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' - # support_auto = True - self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() +async def test_thermostat_fahrenheit(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 75.2, - ATTR_TARGET_TEMP_LOW: 68, - ATTR_TEMPERATURE: 71.6, - ATTR_CURRENT_TEMPERATURE: 73.4, - ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 24.0) - self.assertEqual(acc.char_current_temp.value, 23.0) - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_display_units.value, 1) + # support_auto = True + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + await hass.async_add_job(acc.run) - # Set from HomeKit - acc.char_cooling_thresh_temp.client_update_value(23) - self.hass.block_till_done() - service_data = self.events[-1].data[ATTR_SERVICE_DATA] - self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) - self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 75.2, + ATTR_TARGET_TEMP_LOW: 68, + ATTR_TEMPERATURE: 71.6, + ATTR_CURRENT_TEMPERATURE: 73.4, + ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_cooling_thresh_temp.value == 24.0 + assert acc.char_current_temp.value == 23.0 + assert acc.char_target_temp.value == 22.0 + assert acc.char_display_units.value == 1 - acc.char_heating_thresh_temp.client_update_value(22) - self.hass.block_till_done() - service_data = self.events[-1].data[ATTR_SERVICE_DATA] - self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) - self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') - acc.char_target_temp.client_update_value(24.0) - self.hass.block_till_done() - service_data = self.events[-1].data[ATTR_SERVICE_DATA] - self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) + await hass.async_add_job( + acc.char_cooling_thresh_temp.client_update_value, 23) + await hass.async_block_till_done() + assert call_set_temperature[0] + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.4 + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68 + + await hass.async_add_job( + acc.char_heating_thresh_temp.client_update_value, 22) + await hass.async_block_till_done() + assert call_set_temperature[1] + assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.4 + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.6 + + await hass.async_add_job(acc.char_target_temp.client_update_value, 24.0) + await hass.async_block_till_done() + assert call_set_temperature[2] + assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 4a9521384bd..0755e8f54d4 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -1,41 +1,44 @@ """Test HomeKit util module.""" -import unittest - -import voluptuous as vol import pytest +import voluptuous as vol -from homeassistant.core import callback -from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( - show_setup_message, dismiss_setup_message, convert_to_float, - temperature_to_homekit, temperature_to_states, ATTR_CODE, - density_to_air_quality) + convert_to_float, density_to_air_quality, dismiss_setup_message, + show_setup_message, temperature_to_homekit, temperature_to_states) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( - SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) + ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, - TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) + ATTR_CODE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service def test_validate_entity_config(): """Test validate entities.""" configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, - {'demo.test': None}] + {'demo.test': None}, {'demo.test': {CONF_NAME: None}}] for conf in configs: with pytest.raises(vol.Invalid): vec(conf) assert vec({}) == {} + assert vec({'demo.test': {CONF_NAME: 'Name'}}) == \ + {'demo.test': {CONF_NAME: 'Name'}} + + assert vec({'alarm_control_panel.demo': {}}) == \ + {'alarm_control_panel.demo': {ATTR_CODE: None}} assert vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) == \ {'alarm_control_panel.demo': {ATTR_CODE: '1234'}} + assert vec({'lock.demo': {}}) == {'lock.demo': {ATTR_CODE: None}} + assert vec({'lock.demo': {ATTR_CODE: '1234'}}) == \ + {'lock.demo': {ATTR_CODE: '1234'}} + def test_convert_to_float(): """Test convert_to_float method.""" @@ -68,51 +71,28 @@ def test_density_to_air_quality(): assert density_to_air_quality(300) == 5 -class TestUtil(unittest.TestCase): - """Test all HomeKit util methods.""" +async def test_show_setup_msg(hass): + """Test show setup message as persistence notification.""" + pincode = b'123-45-678' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + call_create_notification = async_mock_service(hass, DOMAIN, 'create') - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + await hass.async_add_job(show_setup_message, hass, pincode) + await hass.async_block_till_done() - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert call_create_notification + assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == \ + HOMEKIT_NOTIFY_ID + assert pincode.decode() in call_create_notification[0].data[ATTR_MESSAGE] - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - def test_show_setup_msg(self): - """Test show setup message as persistence notification.""" - bridge = HomeBridge(self.hass) +async def test_dismiss_setup_msg(hass): + """Test dismiss setup message.""" + call_dismiss_notification = async_mock_service(hass, DOMAIN, 'dismiss') - show_setup_message(self.hass, bridge) - self.hass.block_till_done() + await hass.async_add_job(dismiss_setup_message, hass) + await hass.async_block_till_done() - data = self.events[0].data - self.assertEqual( - data.get(ATTR_DOMAIN, None), 'persistent_notification') - self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_CREATE) - self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) - self.assertEqual( - data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), - HOMEKIT_NOTIFY_ID) - - def test_dismiss_setup_msg(self): - """Test dismiss setup message.""" - dismiss_setup_message(self.hass) - self.hass.block_till_done() - - data = self.events[0].data - self.assertEqual( - data.get(ATTR_DOMAIN, None), 'persistent_notification') - self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_DISMISS) - self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) - self.assertEqual( - data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), - HOMEKIT_NOTIFY_ID) + assert call_dismiss_notification + assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == \ + HOMEKIT_NOTIFY_ID diff --git a/tests/components/image_processing/test_facebox.py b/tests/components/image_processing/test_facebox.py new file mode 100644 index 00000000000..cdc19a3d8d1 --- /dev/null +++ b/tests/components/image_processing/test_facebox.py @@ -0,0 +1,139 @@ +"""The tests for the facebox component.""" +from unittest.mock import patch + +import pytest +import requests +import requests_mock + +from homeassistant.core import callback +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, + CONF_IP_ADDRESS, CONF_PORT, STATE_UNKNOWN) +from homeassistant.setup import async_setup_component +import homeassistant.components.image_processing as ip +import homeassistant.components.image_processing.facebox as fb + +MOCK_IP = '192.168.0.1' +MOCK_PORT = '8080' + +MOCK_FACE = {'confidence': 0.5812028911604818, + 'id': 'john.jpg', + 'matched': True, + 'name': 'John Lennon', + 'rect': {'height': 75, 'left': 63, 'top': 262, 'width': 74} + } + +MOCK_JSON = {"facesCount": 1, + "success": True, + "faces": [MOCK_FACE] + } + +VALID_ENTITY_ID = 'image_processing.facebox_demo_camera' +VALID_CONFIG = { + ip.DOMAIN: { + 'platform': 'facebox', + CONF_IP_ADDRESS: MOCK_IP, + CONF_PORT: MOCK_PORT, + ip.CONF_SOURCE: { + ip.CONF_ENTITY_ID: 'camera.demo_camera'} + }, + 'camera': { + 'platform': 'demo' + } + } + + +def test_encode_image(): + """Test that binary data is encoded correctly.""" + assert fb.encode_image(b'test')["base64"] == 'dGVzdA==' + + +def test_get_matched_faces(): + """Test that matched faces are parsed correctly.""" + assert fb.get_matched_faces([MOCK_FACE]) == {MOCK_FACE['name']: 0.58} + + +@pytest.fixture +def mock_image(): + """Return a mock camera image.""" + with patch('homeassistant.components.camera.demo.DemoCamera.camera_image', + return_value=b'Test') as image: + yield image + + +async def test_setup_platform(hass): + """Setup platform with one entity.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + +async def test_process_image(hass, mock_image): + """Test processing of an image.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + face_events = [] + + @callback + def mock_face_event(event): + """Mock event.""" + face_events.append(event) + + hass.bus.async_listen('image_processing.detect_face', mock_face_event) + + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) + mock_req.post(url, json=MOCK_JSON) + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} + await hass.services.async_call(ip.DOMAIN, + ip.SERVICE_SCAN, + service_data=data) + await hass.async_block_till_done() + + state = hass.states.get(VALID_ENTITY_ID) + assert state.state == '1' + assert state.attributes.get('matched_faces') == {MOCK_FACE['name']: 0.58} + + MOCK_FACE[ATTR_ENTITY_ID] = VALID_ENTITY_ID # Update. + assert state.attributes.get('faces') == [MOCK_FACE] + assert state.attributes.get(CONF_FRIENDLY_NAME) == 'facebox demo_camera' + + assert len(face_events) == 1 + assert face_events[0].data['name'] == MOCK_FACE['name'] + assert face_events[0].data['confidence'] == MOCK_FACE['confidence'] + assert face_events[0].data['entity_id'] == VALID_ENTITY_ID + + +async def test_connection_error(hass, mock_image): + """Test connection error.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) + mock_req.register_uri( + 'POST', url, exc=requests.exceptions.ConnectTimeout) + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} + await hass.services.async_call(ip.DOMAIN, + ip.SERVICE_SCAN, + service_data=data) + await hass.async_block_till_done() + + state = hass.states.get(VALID_ENTITY_ID) + assert state.state == STATE_UNKNOWN + assert state.attributes.get('faces') == [] + assert state.attributes.get('matched_faces') == {} + + +async def test_setup_platform_with_name(hass): + """Setup platform with one entity and a name.""" + MOCK_NAME = 'mock_name' + NAMED_ENTITY_ID = 'image_processing.{}'.format(MOCK_NAME) + + VALID_CONFIG_NAMED = VALID_CONFIG.copy() + VALID_CONFIG_NAMED[ip.DOMAIN][ip.CONF_SOURCE][ip.CONF_NAME] = MOCK_NAME + + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG_NAMED) + assert hass.states.get(NAMED_ENTITY_ID) + state = hass.states.get(NAMED_ENTITY_ID) + assert state.attributes.get(CONF_FRIENDLY_NAME) == MOCK_NAME diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 7f7841b1a69..8b51adb2187 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -140,14 +140,16 @@ light: """ import unittest from unittest import mock +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light +import homeassistant.core as ha from tests.common import ( assert_setup_component, get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message) + fire_mqtt_message, mock_coro) class TestLightMQTT(unittest.TestCase): @@ -481,12 +483,23 @@ class TestLightMQTT(unittest.TestCase): 'payload_on': 'on', 'payload_off': 'off' }} - - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, config) + fake_state = ha.State('light.test', 'on', {'brightness': 95, + 'hs_color': [100, 100], + 'effect': 'random', + 'color_temp': 100, + 'white_value': 50}) + with patch('homeassistant.components.light.mqtt.async_get_last_state', + return_value=mock_coro(fake_state)): + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, config) state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) + self.assertEqual(95, state.attributes.get('brightness')) + self.assertEqual((100, 100), state.attributes.get('hs_color')) + self.assertEqual('random', state.attributes.get('effect')) + self.assertEqual(100, state.attributes.get('color_temp')) + self.assertEqual(50, state.attributes.get('white_value')) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) light.turn_on(self.hass, 'light.test') diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 5bae1061b7f..275fb42ede9 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -90,15 +90,17 @@ light: import json import unittest +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES) import homeassistant.components.light as light +import homeassistant.core as ha from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) + assert_setup_component, mock_coro) class TestLightMQTTJSON(unittest.TestCase): @@ -284,22 +286,36 @@ class TestLightMQTTJSON(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): \ # pylint: disable=invalid-name """Test the sending of command in optimistic mode.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'brightness': True, - 'color_temp': True, - 'effect': True, - 'rgb': True, - 'white_value': True, - 'qos': 2 - } - }) + fake_state = ha.State('light.test', 'on', {'brightness': 95, + 'hs_color': [100, 100], + 'effect': 'random', + 'color_temp': 100, + 'white_value': 50}) + + with patch('homeassistant.components.light.mqtt_json' + '.async_get_last_state', + return_value=mock_coro(fake_state)): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'brightness': True, + 'color_temp': True, + 'effect': True, + 'rgb': True, + 'white_value': True, + 'qos': 2 + } + }) state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) + self.assertEqual(95, state.attributes.get('brightness')) + self.assertEqual((100, 100), state.attributes.get('hs_color')) + self.assertEqual('random', state.attributes.get('effect')) + self.assertEqual(100, state.attributes.get('color_temp')) + self.assertEqual(50, state.attributes.get('white_value')) self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 90d68dd10d2..1440a73f98e 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -27,14 +27,16 @@ If your light doesn't support white value feature, omit `white_value_template`. If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ import unittest +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light +import homeassistant.core as ha from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) + assert_setup_component, mock_coro) class TestLightMQTTTemplate(unittest.TestCase): @@ -207,26 +209,40 @@ class TestLightMQTTTemplate(unittest.TestCase): def test_optimistic(self): \ # pylint: disable=invalid-name """Test optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ white_value|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }}', - 'command_off_template': 'off', - 'qos': 2 - } - }) + fake_state = ha.State('light.test', 'on', {'brightness': 95, + 'hs_color': [100, 100], + 'effect': 'random', + 'color_temp': 100, + 'white_value': 50}) + + with patch('homeassistant.components.light.mqtt_template' + '.async_get_last_state', + return_value=mock_coro(fake_state)): + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ white_value|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'qos': 2 + } + }) state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) + self.assertEqual(95, state.attributes.get('brightness')) + self.assertEqual((100, 100), state.attributes.get('hs_color')) + self.assertEqual('random', state.attributes.get('effect')) + self.assertEqual(100, state.attributes.get('color_temp')) + self.assertEqual(50, state.attributes.get('white_value')) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) # turn on the light diff --git a/tests/components/light/test_template.py b/tests/components/light/test_template.py index 2d45ad1bf94..962760672f1 100644 --- a/tests/components/light/test_template.py +++ b/tests/components/light/test_template.py @@ -36,7 +36,7 @@ class TestTemplateLight: self.hass.stop() def test_template_state_text(self): - """"Test the state text of a template.""" + """Test the state text of a template.""" with assert_setup_component(1, 'light'): assert setup.setup_component(self.hass, 'light', { 'light': { diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py index eea6295b79e..7c85775949c 100644 --- a/tests/components/media_player/test_blackbird.py +++ b/tests/components/media_player/test_blackbird.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player.blackbird import ( class AttrDict(dict): - """Helper clas for mocking attributes.""" + """Helper class for mocking attributes.""" def __setattr__(self, name, value): """Set attribute.""" diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index 5bd3270b922..71b472afe74 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -33,7 +33,7 @@ class TestNotifyDemo(unittest.TestCase): self.hass.bus.listen(demo.EVENT_NOTIFY, record_event) def tearDown(self): # pylint: disable=invalid-name - """"Stop down everything that was started.""" + """Stop down everything that was started.""" self.hass.stop() def _setup_notify(self): diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index c5064fca851..d59bbe4d720 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -20,7 +20,7 @@ class TestNotifyFile(unittest.TestCase): self.hass = get_test_home_assistant() def tearDown(self): # pylint: disable=invalid-name - """"Stop down everything that was started.""" + """Stop down everything that was started.""" self.hass.stop() def test_bad_config(self): diff --git a/tests/components/notify/test_group.py b/tests/components/notify/test_group.py index c96a49d7cb3..a847de51142 100644 --- a/tests/components/notify/test_group.py +++ b/tests/components/notify/test_group.py @@ -53,7 +53,7 @@ class TestNotifyGroup(unittest.TestCase): assert self.service is not None def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_send_message_with_data(self): diff --git a/tests/components/notify/test_smtp.py b/tests/components/notify/test_smtp.py index 127eecae2b7..29e34974c6c 100644 --- a/tests/components/notify/test_smtp.py +++ b/tests/components/notify/test_smtp.py @@ -27,7 +27,7 @@ class TestNotifySmtp(unittest.TestCase): 'HomeAssistant', 0) def tearDown(self): # pylint: disable=invalid-name - """"Stop down everything that was started.""" + """Stop down everything that was started.""" self.hass.stop() @patch('email.utils.make_msgid', return_value='') diff --git a/tests/components/sensor/test_bom.py b/tests/components/sensor/test_bom.py new file mode 100644 index 00000000000..06a7089e052 --- /dev/null +++ b/tests/components/sensor/test_bom.py @@ -0,0 +1,97 @@ +"""The tests for the BOM Weather sensor platform.""" +import re +import unittest +import json +import requests +from unittest.mock import patch +from urllib.parse import urlparse + +from homeassistant.setup import setup_component +from homeassistant.components import sensor + +from tests.common import ( + get_test_home_assistant, assert_setup_component, load_fixture) + +VALID_CONFIG = { + 'platform': 'bom', + 'station': 'IDN60901.94767', + 'name': 'Fake', + 'monitored_conditions': [ + 'apparent_t', + 'press', + 'weather' + ] +} + + +def mocked_requests(*args, **kwargs): + """Mock requests.get invocations.""" + class MockResponse: + """Class to represent a mocked response.""" + + def __init__(self, json_data, status_code): + """Initialize the mock response class.""" + self.json_data = json_data + self.status_code = status_code + + def json(self): + """Return the json of the response.""" + return self.json_data + + @property + def content(self): + """Return the content of the response.""" + return self.json() + + def raise_for_status(self): + """Raise an HTTPError if status is not 200.""" + if self.status_code != 200: + raise requests.HTTPError(self.status_code) + + url = urlparse(args[0]) + if re.match(r'^/fwo/[\w]+/[\w.]+\.json', url.path): + return MockResponse(json.loads(load_fixture('bom_weather.json')), 200) + + raise NotImplementedError('Unknown route {}'.format(url.path)) + + +class TestBOMWeatherSensor(unittest.TestCase): + """Test the BOM Weather sensor.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('requests.get', side_effect=mocked_requests) + def test_setup(self, mock_get): + """Test the setup with custom settings.""" + with assert_setup_component(1, sensor.DOMAIN): + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { + 'sensor': VALID_CONFIG})) + + fake_entities = [ + 'bom_fake_feels_like_c', + 'bom_fake_pressure_mb', + 'bom_fake_weather'] + + for entity_id in fake_entities: + state = self.hass.states.get('sensor.{}'.format(entity_id)) + self.assertIsNotNone(state) + + @patch('requests.get', side_effect=mocked_requests) + def test_sensor_values(self, mock_get): + """Test retrieval of sensor values.""" + self.assertTrue(setup_component( + self.hass, sensor.DOMAIN, {'sensor': VALID_CONFIG})) + + self.assertEqual('Fine', self.hass.states.get( + 'sensor.bom_fake_weather').state) + self.assertEqual('1021.7', self.hass.states.get( + 'sensor.bom_fake_pressure_mb').state) + self.assertEqual('25.0', self.hass.states.get( + 'sensor.bom_fake_feels_like_c').state) diff --git a/tests/components/sensor/test_sigfox.py b/tests/components/sensor/test_sigfox.py index dcdeef56b98..569fab584ad 100644 --- a/tests/components/sensor/test_sigfox.py +++ b/tests/components/sensor/test_sigfox.py @@ -38,7 +38,7 @@ class TestSigfoxSensor(unittest.TestCase): self.hass.stop() def test_invalid_credentials(self): - """Test for a invalid credentials.""" + """Test for invalid credentials.""" with requests_mock.Mocker() as mock_req: url = re.compile(API_URL + 'devicetypes') mock_req.get(url, text='{}', status_code=401) @@ -47,7 +47,7 @@ class TestSigfoxSensor(unittest.TestCase): assert len(self.hass.states.entity_ids()) == 0 def test_valid_credentials(self): - """Test for a valid credentials.""" + """Test for valid credentials.""" with requests_mock.Mocker() as mock_req: url1 = re.compile(API_URL + 'devicetypes') mock_req.get(url1, text='{"data":[{"id":"fake_type"}]}', diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index f8d912f24dd..6861d3a5070 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -269,7 +269,7 @@ class TestTemplateSensor: assert self.hass.states.all() == [] def test_setup_invalid_device_class(self): - """"Test setup with invalid device_class.""" + """Test setup with invalid device_class.""" with assert_setup_component(0): assert setup_component(self.hass, 'sensor', { 'sensor': { @@ -284,7 +284,7 @@ class TestTemplateSensor: }) def test_setup_valid_device_class(self): - """"Test setup with valid device_class.""" + """Test setup with valid device_class.""" with assert_setup_component(1): assert setup_component(self.hass, 'sensor', { 'sensor': { diff --git a/tests/components/sensor/test_wsdot.py b/tests/components/sensor/test_wsdot.py index ee2cec3bb2a..8eb542b2b68 100644 --- a/tests/components/sensor/test_wsdot.py +++ b/tests/components/sensor/test_wsdot.py @@ -1,17 +1,16 @@ """The tests for the WSDOT platform.""" +from datetime import datetime, timedelta, timezone import re import unittest -from datetime import timedelta, datetime, timezone import requests_mock +from tests.common import get_test_home_assistant, load_fixture from homeassistant.components.sensor import wsdot from homeassistant.components.sensor.wsdot import ( - WashingtonStateTravelTimeSensor, ATTR_DESCRIPTION, - ATTR_TIME_UPDATED, CONF_API_KEY, CONF_NAME, - CONF_ID, CONF_TRAVEL_TIMES, SCAN_INTERVAL) + ATTR_DESCRIPTION, ATTR_TIME_UPDATED, CONF_API_KEY, CONF_ID, CONF_NAME, + CONF_TRAVEL_TIMES, RESOURCE, SCAN_INTERVAL) from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant class TestWSDOT(unittest.TestCase): @@ -50,7 +49,7 @@ class TestWSDOT(unittest.TestCase): @requests_mock.Mocker() def test_setup(self, mock_req): """Test for operational WSDOT sensor with proper attributes.""" - uri = re.compile(WashingtonStateTravelTimeSensor.RESOURCE + '*') + uri = re.compile(RESOURCE + '*') mock_req.get(uri, text=load_fixture('wsdot.json')) wsdot.setup_platform(self.hass, self.config, self.add_entities) self.assertEqual(len(self.entities), 1) diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index b5e2a0b0395..31f9a729c53 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -20,7 +20,7 @@ class TestSwitchMQTT(unittest.TestCase): self.mock_publish = mock_mqtt_component(self.hass) def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_controlling_state_via_topic(self): @@ -248,3 +248,26 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) + + def test_unique_id(self): + """Test unique id option only creates one switch per unique_id.""" + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + fire_mqtt_message(self.hass, 'test-topic', 'payload') + self.hass.block_till_done() + assert len(self.hass.states.async_entity_ids()) == 2 + # all switches group is 1, unique id created is 1 diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index 7456ae11a0d..8f7bbda8e98 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -32,7 +32,7 @@ class TestTemplateSwitch: self.hass.stop() def test_template_state_text(self): - """"Test the state text of a template.""" + """Test the state text of a template.""" with assert_setup_component(1, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { diff --git a/tests/components/test_api.py b/tests/components/test_api.py index c9dae27d14c..f53010ef27f 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -12,8 +12,6 @@ from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component -from tests.common import mock_coro - @pytest.fixture def mock_api_client(hass, aiohttp_client): @@ -420,14 +418,14 @@ async def test_api_error_log(hass, aiohttp_client): assert resp.status == 401 with patch( - 'homeassistant.components.http.view.HomeAssistantView.file', - return_value=mock_coro(web.Response(status=200, text='Hello')) + 'aiohttp.web.FileResponse', + return_value=web.Response(status=200, text='Hello') ) as mock_file: resp = await client.get(const.URL_API_ERROR_LOG, headers={ 'x-ha-access': 'yolo' }) assert len(mock_file.mock_calls) == 1 - assert mock_file.mock_calls[0][1][1] == hass.data[DATA_LOGGING] + assert mock_file.mock_calls[0][1][0] == hass.data[DATA_LOGGING] assert resp.status == 200 assert await resp.text() == 'Hello' diff --git a/tests/components/test_feedreader.py b/tests/components/test_feedreader.py new file mode 100644 index 00000000000..c20b297017c --- /dev/null +++ b/tests/components/test_feedreader.py @@ -0,0 +1,186 @@ +"""The tests for the feedreader component.""" +import time +from datetime import datetime, timedelta + +import unittest +from genericpath import exists +from logging import getLogger +from os import remove +from unittest import mock +from unittest.mock import patch + +from homeassistant.components import feedreader +from homeassistant.components.feedreader import CONF_URLS, FeedManager, \ + StoredData, EVENT_FEEDREADER, DEFAULT_SCAN_INTERVAL, CONF_MAX_ENTRIES, \ + DEFAULT_MAX_ENTRIES +from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL +from homeassistant.core import callback +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant, load_fixture + +_LOGGER = getLogger(__name__) + +URL = 'http://some.rss.local/rss_feed.xml' +VALID_CONFIG_1 = { + feedreader.DOMAIN: { + CONF_URLS: [URL] + } +} +VALID_CONFIG_2 = { + feedreader.DOMAIN: { + CONF_URLS: [URL], + CONF_SCAN_INTERVAL: 60 + } +} +VALID_CONFIG_3 = { + feedreader.DOMAIN: { + CONF_URLS: [URL], + CONF_MAX_ENTRIES: 100 + } +} + + +class TestFeedreaderComponent(unittest.TestCase): + """Test the feedreader component.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + # Delete any previously stored data + data_file = self.hass.config.path("{}.pickle".format('feedreader')) + if exists(data_file): + remove(data_file) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_one_feed(self): + """Test the general setup of this component.""" + with patch("homeassistant.components.feedreader." + "track_time_interval") as track_method: + self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, + VALID_CONFIG_1)) + track_method.assert_called_once_with(self.hass, mock.ANY, + DEFAULT_SCAN_INTERVAL) + + def test_setup_scan_interval(self): + """Test the setup of this component with scan interval.""" + with patch("homeassistant.components.feedreader." + "track_time_interval") as track_method: + self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, + VALID_CONFIG_2)) + track_method.assert_called_once_with(self.hass, mock.ANY, + timedelta(seconds=60)) + + def test_setup_max_entries(self): + """Test the setup of this component with max entries.""" + self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, + VALID_CONFIG_3)) + + def setup_manager(self, feed_data, max_entries=DEFAULT_MAX_ENTRIES): + """Generic test setup method.""" + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(EVENT_FEEDREADER, record_event) + + # Loading raw data from fixture and plug in to data object as URL + # works since the third-party feedparser library accepts a URL + # as well as the actual data. + data_file = self.hass.config.path("{}.pickle".format( + feedreader.DOMAIN)) + storage = StoredData(data_file) + with patch("homeassistant.components.feedreader." + "track_time_interval") as track_method: + manager = FeedManager(feed_data, DEFAULT_SCAN_INTERVAL, + max_entries, self.hass, storage) + # Can't use 'assert_called_once' here because it's not available + # in Python 3.5 yet. + track_method.assert_called_once_with(self.hass, mock.ANY, + DEFAULT_SCAN_INTERVAL) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + return manager, events + + def test_feed(self): + """Test simple feed with valid data.""" + feed_data = load_fixture('feedreader.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 1 + assert events[0].data.title == "Title 1" + assert events[0].data.description == "Description 1" + assert events[0].data.link == "http://www.example.com/link/1" + assert events[0].data.id == "GUID 1" + assert datetime.fromtimestamp( + time.mktime(events[0].data.published_parsed)) == \ + datetime(2018, 4, 30, 5, 10, 0) + assert manager.last_update_successful is True + + def test_feed_updates(self): + """Test feed updates.""" + # 1. Run + feed_data = load_fixture('feedreader.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 1 + # 2. Run + feed_data2 = load_fixture('feedreader1.xml') + # Must patch 'get_timestamp' method because the timestamp is stored + # with the URL which in these tests is the raw XML data. + with patch("homeassistant.components.feedreader.StoredData." + "get_timestamp", return_value=time.struct_time( + (2018, 4, 30, 5, 10, 0, 0, 120, 0))): + manager2, events2 = self.setup_manager(feed_data2) + assert len(events2) == 1 + # 3. Run + feed_data3 = load_fixture('feedreader1.xml') + with patch("homeassistant.components.feedreader.StoredData." + "get_timestamp", return_value=time.struct_time( + (2018, 4, 30, 5, 11, 0, 0, 120, 0))): + manager3, events3 = self.setup_manager(feed_data3) + assert len(events3) == 0 + + def test_feed_default_max_length(self): + """Test long feed beyond the default 20 entry limit.""" + feed_data = load_fixture('feedreader2.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 20 + + def test_feed_max_length(self): + """Test long feed beyond a configured 5 entry limit.""" + feed_data = load_fixture('feedreader2.xml') + manager, events = self.setup_manager(feed_data, max_entries=5) + assert len(events) == 5 + + def test_feed_without_publication_date(self): + """Test simple feed with entry without publication date.""" + feed_data = load_fixture('feedreader3.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 2 + + def test_feed_invalid_data(self): + """Test feed with invalid data.""" + feed_data = "INVALID DATA" + manager, events = self.setup_manager(feed_data) + assert len(events) == 0 + assert manager.last_update_successful is True + + @mock.patch('feedparser.parse', return_value=None) + def test_feed_parsing_failed(self, mock_parse): + """Test feed where parsing fails.""" + data_file = self.hass.config.path("{}.pickle".format( + feedreader.DOMAIN)) + storage = StoredData(data_file) + manager = FeedManager("FEED DATA", DEFAULT_SCAN_INTERVAL, + DEFAULT_MAX_ENTRIES, self.hass, storage) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + assert manager.last_update_successful is False diff --git a/tests/components/test_folder_watcher.py b/tests/components/test_folder_watcher.py index 16ec7a58a02..b5ac9cca9d9 100644 --- a/tests/components/test_folder_watcher.py +++ b/tests/components/test_folder_watcher.py @@ -8,7 +8,7 @@ from tests.common import MockDependency async def test_invalid_path_setup(hass): - """Test that a invalid path is not setup.""" + """Test that an invalid path is not setup.""" assert not await async_setup_component( hass, folder_watcher.DOMAIN, { folder_watcher.DOMAIN: { diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 973544495d7..657497b868b 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -57,7 +57,7 @@ def test_frontend_and_static(mock_http_client): # Test we can retrieve frontend.js frontendjs = re.search( - r'(?P\/frontend_es5\/frontend-[A-Za-z0-9]{32}.html)', text) + r'(?P\/frontend_es5\/app-[A-Za-z0-9]{32}.js)', text) assert frontendjs is not None resp = yield from mock_http_client.get(frontendjs.groups(0)[0]) diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/test_mqtt_eventstream.py index f4fc3e89ee0..48bc04d46ed 100644 --- a/tests/components/test_mqtt_eventstream.py +++ b/tests/components/test_mqtt_eventstream.py @@ -44,11 +44,11 @@ class TestMqttEventStream(object): eventstream.DOMAIN: config}) def test_setup_succeeds(self): - """"Test the success of the setup.""" + """Test the success of the setup.""" assert self.add_eventstream() def test_setup_with_pub(self): - """"Test the setup with subscription.""" + """Test the setup with subscription.""" # Should start off with no listeners for all events assert self.hass.bus.listeners.get('*') is None @@ -60,7 +60,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_subscribe') def test_subscribe(self, mock_sub): - """"Test the subscription.""" + """Test the subscription.""" sub_topic = 'foo' assert self.add_eventstream(sub_topic=sub_topic) self.hass.block_till_done() @@ -71,7 +71,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_sends_message(self, mock_utcnow, mock_pub): - """"Test the sending of a new message if event changed.""" + """Test the sending of a new message if event changed.""" now = dt_util.as_utc(dt_util.now()) e_id = 'fake.entity' pub_topic = 'bar' @@ -113,7 +113,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') def test_time_event_does_not_send_message(self, mock_pub): - """"Test the sending of a new message if time event.""" + """Test the sending of a new message if time event.""" assert self.add_eventstream(pub_topic='bar') self.hass.block_till_done() @@ -125,7 +125,7 @@ class TestMqttEventStream(object): assert not mock_pub.called def test_receiving_remote_event_fires_hass_event(self): - """"Test the receiving of the remotely fired event.""" + """Test the receiving of the remotely fired event.""" sub_topic = 'foo' assert self.add_eventstream(sub_topic=sub_topic) self.hass.block_till_done() @@ -150,7 +150,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') def test_ignored_event_doesnt_send_over_stream(self, mock_pub): - """"Test the ignoring of sending events if defined.""" + """Test the ignoring of sending events if defined.""" assert self.add_eventstream(pub_topic='bar', ignore_event=['state_changed']) self.hass.block_till_done() @@ -177,7 +177,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') def test_wrong_ignored_event_sends_over_stream(self, mock_pub): - """"Test the ignoring of sending events if defined.""" + """Test the ignoring of sending events if defined.""" assert self.add_eventstream(pub_topic='bar', ignore_event=['statee_changed']) self.hass.block_till_done() diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/test_mqtt_statestream.py index e120c3a7dd2..2ed2f4487ea 100644 --- a/tests/components/test_mqtt_statestream.py +++ b/tests/components/test_mqtt_statestream.py @@ -47,17 +47,17 @@ class TestMqttStateStream(object): assert self.add_statestream() is False def test_setup_succeeds_without_attributes(self): - """"Test the success of the setup with a valid base_topic.""" + """Test the success of the setup with a valid base_topic.""" assert self.add_statestream(base_topic='pub') def test_setup_succeeds_with_attributes(self): - """"Test setup with a valid base_topic and publish_attributes.""" + """Test setup with a valid base_topic and publish_attributes.""" assert self.add_statestream(base_topic='pub', publish_attributes=True) @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_sends_message(self, mock_utcnow, mock_pub): - """"Test the sending of a new message if event changed.""" + """Test the sending of a new message if event changed.""" e_id = 'fake.entity' base_topic = 'pub' @@ -84,7 +84,7 @@ class TestMqttStateStream(object): self, mock_utcnow, mock_pub): - """"Test the sending of a message and timestamps if event changed.""" + """Test the sending of a message and timestamps if event changed.""" e_id = 'another.entity' base_topic = 'pub' @@ -118,7 +118,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_attr_sends_message(self, mock_utcnow, mock_pub): - """"Test the sending of a new message if attribute changed.""" + """Test the sending of a new message if attribute changed.""" e_id = 'fake.entity' base_topic = 'pub' @@ -160,7 +160,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_include_domain(self, mock_utcnow, mock_pub): - """"Test that filtering on included domain works as expected.""" + """Test that filtering on included domain works as expected.""" base_topic = 'pub' incl = { @@ -198,7 +198,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_include_entity(self, mock_utcnow, mock_pub): - """"Test that filtering on included entity works as expected.""" + """Test that filtering on included entity works as expected.""" base_topic = 'pub' incl = { @@ -236,7 +236,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_exclude_domain(self, mock_utcnow, mock_pub): - """"Test that filtering on excluded domain works as expected.""" + """Test that filtering on excluded domain works as expected.""" base_topic = 'pub' incl = {} @@ -274,7 +274,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_exclude_entity(self, mock_utcnow, mock_pub): - """"Test that filtering on excluded entity works as expected.""" + """Test that filtering on excluded entity works as expected.""" base_topic = 'pub' incl = {} @@ -313,7 +313,7 @@ class TestMqttStateStream(object): @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_exclude_domain_include_entity( self, mock_utcnow, mock_pub): - """"Test filtering with excluded domain and included entity.""" + """Test filtering with excluded domain and included entity.""" base_topic = 'pub' incl = { @@ -354,7 +354,7 @@ class TestMqttStateStream(object): @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_include_domain_exclude_entity( self, mock_utcnow, mock_pub): - """"Test filtering with included domain and excluded entity.""" + """Test filtering with included domain and excluded entity.""" base_topic = 'pub' incl = { diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py index 91a07511787..214eda04ad8 100644 --- a/tests/components/test_panel_iframe.py +++ b/tests/components/test_panel_iframe.py @@ -1,6 +1,5 @@ """The tests for the panel_iframe component.""" import unittest -from unittest.mock import patch from homeassistant import setup from homeassistant.components import frontend @@ -33,8 +32,6 @@ class TestPanelIframe(unittest.TestCase): 'panel_iframe': conf }) - @patch.dict('hass_frontend_es5.FINGERPRINTS', - {'iframe': 'md5md5'}) def test_correct_config(self): """Test correct config.""" assert setup.setup_component( @@ -70,7 +67,6 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'http://192.168.1.1'}, 'icon': 'mdi:network-wireless', 'title': 'Router', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'router' } @@ -79,7 +75,6 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'}, 'icon': 'mdi:weather', 'title': 'Weather', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'weather', } @@ -88,7 +83,6 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': '/api'}, 'icon': 'mdi:weather', 'title': 'Api', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'api', } @@ -97,6 +91,5 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'ftp://some/ftp'}, 'icon': 'mdi:weather', 'title': 'FTP', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'ftp', } diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index 6cc0e4fcada..e336a28eb03 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -8,7 +8,7 @@ import homeassistant.components.prometheus as prometheus @pytest.fixture def prometheus_client(loop, hass, aiohttp_client): - """Initialize a aiohttp_client with Prometheus component.""" + """Initialize an aiohttp_client with Prometheus component.""" assert loop.run_until_complete(async_setup_component( hass, prometheus.DOMAIN, diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py index 6f993732c38..a1acffd62e5 100644 --- a/tests/components/test_shell_command.py +++ b/tests/components/test_shell_command.py @@ -19,8 +19,7 @@ def mock_process_creator(error: bool = False) -> asyncio.coroutine: def communicate() -> Tuple[bytes, bytes]: """Mock a coroutine that runs a process when yielded. - Returns: - a tuple of (stdout, stderr). + Returns a tuple of (stdout, stderr). """ return b"I am stdout", b"I am stderr" @@ -149,3 +148,41 @@ class TestShellCommand(unittest.TestCase): self.assertEqual(1, mock_call.call_count) self.assertEqual(1, mock_error.call_count) self.assertFalse(os.path.isfile(path)) + + @patch('homeassistant.components.shell_command._LOGGER.debug') + def test_stdout_captured(self, mock_output): + """Test subprocess that has stdout.""" + test_phrase = "I have output" + self.assertTrue( + setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { + 'test_service': "echo {}".format(test_phrase) + } + })) + + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + self.hass.block_till_done() + self.assertEqual(1, mock_output.call_count) + self.assertEqual(test_phrase.encode() + b'\n', + mock_output.call_args_list[0][0][-1]) + + @patch('homeassistant.components.shell_command._LOGGER.debug') + def test_stderr_captured(self, mock_output): + """Test subprocess that has stderr.""" + test_phrase = "I have error" + self.assertTrue( + setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { + 'test_service': ">&2 echo {}".format(test_phrase) + } + })) + + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + self.hass.block_till_done() + self.assertEqual(1, mock_output.call_count) + self.assertEqual(test_phrase.encode() + b'\n', + mock_output.call_args_list[0][0][-1]) diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index 2342e897708..d9238336768 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -118,7 +118,9 @@ async def test_snips_intent(hass, mqtt_mock): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'Lights' - assert intent.slots == {'light_color': {'value': 'green'}} + assert intent.slots == {'light_color': {'value': 'green'}, + 'probability': {'value': 1}, + 'site_id': {'value': None}} assert intent.text_input == 'turn the lights green' @@ -169,7 +171,9 @@ async def test_snips_intent_with_duration(hass, mqtt_mock): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'SetTimer' - assert intent.slots == {'timer_duration': {'value': 300}} + assert intent.slots == {'probability': {'value': 1}, + 'site_id': {'value': None}, + 'timer_duration': {'value': 300}} async def test_intent_speech_response(hass, mqtt_mock): @@ -318,11 +322,51 @@ async def test_snips_low_probability(hass, mqtt_mock, caplog): assert 'Intent below probaility threshold 0.49 < 0.5' in caplog.text +async def test_intent_special_slots(hass, mqtt_mock): + """Test intent special slot values via Snips.""" + calls = async_mock_service(hass, 'light', 'turn_on') + result = await async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + result = await async_setup_component(hass, "intent_script", { + "intent_script": { + "Lights": { + "action": { + "service": "light.turn_on", + "data_template": { + "probability": "{{ probability }}", + "site_id": "{{ site_id }}" + } + } + } + } + }) + assert result + payload = """ + { + "input": "turn the light on", + "intent": { + "intentName": "Lights", + "probability": 0.85 + }, + "siteId": "default", + "slots": [] + } + """ + 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['probability'] == '0.85' + assert calls[0].data['site_id'] == 'default' + + async def test_snips_say(hass, caplog): """Test snips say with invalid config.""" - calls = async_mock_service(hass, 'snips', 'say', - snips.SERVICE_SCHEMA_SAY) - + calls = async_mock_service(hass, 'snips', 'say', snips.SERVICE_SCHEMA_SAY) data = {'text': 'Hello'} await hass.services.async_call('snips', 'say', data) await hass.async_block_till_done() diff --git a/tests/components/test_spaceapi.py b/tests/components/test_spaceapi.py new file mode 100644 index 00000000000..e7e7d158a31 --- /dev/null +++ b/tests/components/test_spaceapi.py @@ -0,0 +1,113 @@ +"""The tests for the Home Assistant SpaceAPI component.""" +# pylint: disable=protected-access +from unittest.mock import patch + +import pytest +from tests.common import mock_coro + +from homeassistant.components.spaceapi import ( + DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI) +from homeassistant.setup import async_setup_component + +CONFIG = { + DOMAIN: { + 'space': 'Home', + 'logo': 'https://home-assistant.io/logo.png', + 'url': 'https://home-assistant.io', + 'location': {'address': 'In your Home'}, + 'contact': {'email': 'hello@home-assistant.io'}, + 'issue_report_channels': ['email'], + 'state': { + 'entity_id': 'test.test_door', + 'icon_open': 'https://home-assistant.io/open.png', + 'icon_closed': 'https://home-assistant.io/close.png', + }, + 'sensors': { + 'temperature': ['test.temp1', 'test.temp2'], + 'humidity': ['test.hum1'], + } + } +} + +SENSOR_OUTPUT = { + 'temperature': [ + { + 'location': 'Home', + 'name': 'temp1', + 'unit': '°C', + 'value': '25' + }, + { + 'location': 'Home', + 'name': 'temp2', + 'unit': '°C', + 'value': '23' + }, + ], + 'humidity': [ + { + 'location': 'Home', + 'name': 'hum1', + 'unit': '%', + 'value': '88' + }, + ] +} + + +@pytest.fixture +def mock_client(hass, aiohttp_client): + """Start the Home Assistant HTTP component.""" + with patch('homeassistant.components.spaceapi', + return_value=mock_coro(True)): + hass.loop.run_until_complete( + async_setup_component(hass, 'spaceapi', CONFIG)) + + hass.states.async_set('test.temp1', 25, + attributes={'unit_of_measurement': '°C'}) + hass.states.async_set('test.temp2', 23, + attributes={'unit_of_measurement': '°C'}) + hass.states.async_set('test.hum1', 88, + attributes={'unit_of_measurement': '%'}) + + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + +async def test_spaceapi_get(hass, mock_client): + """Test response after start-up Home Assistant.""" + resp = await mock_client.get(URL_API_SPACEAPI) + assert resp.status == 200 + + data = await resp.json() + + assert data['api'] == SPACEAPI_VERSION + assert data['space'] == 'Home' + assert data['contact']['email'] == 'hello@home-assistant.io' + assert data['location']['address'] == 'In your Home' + assert data['location']['latitude'] == 32.87336 + assert data['location']['longitude'] == -117.22743 + assert data['state']['open'] == 'null' + assert data['state']['icon']['open'] == \ + 'https://home-assistant.io/open.png' + assert data['state']['icon']['close'] == \ + 'https://home-assistant.io/close.png' + + +async def test_spaceapi_state_get(hass, mock_client): + """Test response if the state entity was set.""" + hass.states.async_set('test.test_door', True) + + resp = await mock_client.get(URL_API_SPACEAPI) + assert resp.status == 200 + + data = await resp.json() + assert data['state']['open'] == bool(1) + + +async def test_spaceapi_sensors_get(hass, mock_client): + """Test the response for the sensors.""" + resp = await mock_client.get(URL_API_SPACEAPI) + assert resp.status == 200 + + data = await resp.json() + assert data['sensors'] == SENSOR_OUTPUT diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 0a130e507d4..cff103142b0 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -313,3 +313,49 @@ def test_unknown_command(websocket_client): msg = yield from websocket_client.receive() assert msg.type == WSMsgType.close + + +async def test_auth_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 + } + }) + + 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 + + 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 + + +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: + 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/zwave/test_init.py b/tests/components/zwave/test_init.py index faa7357bd8a..a25b725e500 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor.zwave import get_device from homeassistant.components.zwave import ( const, CONFIG_SCHEMA, CONF_DEVICE_CONFIG_GLOB, DATA_NETWORK) from homeassistant.setup import setup_component +from tests.common import mock_registry import pytest @@ -237,7 +238,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_receivers) == 1 - node = MockNode(node_id=14, manufacturer_name=None) + node = MockNode(node_id=14, manufacturer_name=None, is_ready=False) sleeps = [] @@ -468,6 +469,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() self.hass.start() + self.registry = mock_registry(self.hass) setup_component(self.hass, 'zwave', {'zwave': {}}) self.hass.block_till_done() @@ -487,7 +489,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): const.DISC_OPTIONAL: True, }}} self.primary = MockValue( - command_class='mock_primary_class', node=self.node) + command_class='mock_primary_class', node=self.node, value_id=1000) self.secondary = MockValue( command_class='mock_secondary_class', node=self.node) self.duplicate_secondary = MockValue( @@ -521,6 +523,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) assert values.primary is self.primary @@ -592,6 +595,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) self.hass.block_till_done() @@ -630,6 +634,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() @@ -639,7 +644,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'get_platform') @patch.object(zwave, 'discovery') def test_entity_workaround_component(self, discovery, get_platform): - """Test ignore workaround.""" + """Test component workaround.""" discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform @@ -666,6 +671,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() @@ -697,6 +703,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() @@ -720,12 +727,42 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() assert not discovery.async_load_platform.called + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_entity_config_ignore_with_registry(self, discovery, get_platform): + """Test ignore config. + + The case when the device is in entity registry. + """ + self.node.values = { + self.primary.value_id: self.primary, + self.secondary.value_id: self.secondary, + } + self.device_config = {'mock_component.registry_id': { + zwave.CONF_IGNORED: True + }} + self.registry.async_get_or_create( + 'mock_component', zwave.DOMAIN, '567-1000', + suggested_object_id='registry_id') + zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + registry=self.registry + ) + self.hass.block_till_done() + + assert not discovery.async_load_platform.called + @patch.object(zwave, 'get_platform') @patch.object(zwave, 'discovery') def test_entity_platform_ignore(self, discovery, get_platform): @@ -743,6 +780,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) self.hass.block_till_done() @@ -770,6 +808,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index 73e69605eae..4d619c5ef61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ logging.basicConfig(level=logging.INFO) logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) -def test_real(func): +def check_real(func): """Force a function to require a keyword _test_real to be passed in.""" @functools.wraps(func) def guard_func(*args, **kwargs): @@ -40,8 +40,8 @@ def test_real(func): # Guard a few functions that would make network connections -location.detect_location_info = test_real(location.detect_location_info) -location.elevation = test_real(location.elevation) +location.detect_location_info = check_real(location.detect_location_info) +location.elevation = check_real(location.elevation) util.get_local_ip = lambda: '127.0.0.1' diff --git a/tests/fixtures/bom_weather.json b/tests/fixtures/bom_weather.json new file mode 100644 index 00000000000..d40ea6fb21a --- /dev/null +++ b/tests/fixtures/bom_weather.json @@ -0,0 +1,42 @@ +{ + "observations": { + "data": [ + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 25.0, + "press": 1021.7, + "weather": "-" + }, + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 22.0, + "press": 1019.7, + "weather": "-" + }, + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 20.0, + "press": 1011.7, + "weather": "Fine" + }, + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 18.0, + "press": 1010.0, + "weather": "-" + } + ] + } +} diff --git a/tests/fixtures/feedreader.xml b/tests/fixtures/feedreader.xml new file mode 100644 index 00000000000..8c85a4975ee --- /dev/null +++ b/tests/fixtures/feedreader.xml @@ -0,0 +1,20 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + + + + diff --git a/tests/fixtures/feedreader1.xml b/tests/fixtures/feedreader1.xml new file mode 100644 index 00000000000..ff856125779 --- /dev/null +++ b/tests/fixtures/feedreader1.xml @@ -0,0 +1,27 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + + + Title 2 + Description 2 + http://www.example.com/link/2 + GUID 2 + Mon, 30 Apr 2018 15:11:00 +1000 + + + + diff --git a/tests/fixtures/feedreader2.xml b/tests/fixtures/feedreader2.xml new file mode 100644 index 00000000000..653a16e4561 --- /dev/null +++ b/tests/fixtures/feedreader2.xml @@ -0,0 +1,97 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Mon, 30 Apr 2018 15:00:00 +1000 + + + Title 2 + Mon, 30 Apr 2018 15:01:00 +1000 + + + Title 3 + Mon, 30 Apr 2018 15:02:00 +1000 + + + Title 4 + Mon, 30 Apr 2018 15:03:00 +1000 + + + Title 5 + Mon, 30 Apr 2018 15:04:00 +1000 + + + Title 6 + Mon, 30 Apr 2018 15:05:00 +1000 + + + Title 7 + Mon, 30 Apr 2018 15:06:00 +1000 + + + Title 8 + Mon, 30 Apr 2018 15:07:00 +1000 + + + Title 9 + Mon, 30 Apr 2018 15:08:00 +1000 + + + Title 10 + Mon, 30 Apr 2018 15:09:00 +1000 + + + Title 11 + Mon, 30 Apr 2018 15:10:00 +1000 + + + Title 12 + Mon, 30 Apr 2018 15:11:00 +1000 + + + Title 13 + Mon, 30 Apr 2018 15:12:00 +1000 + + + Title 14 + Mon, 30 Apr 2018 15:13:00 +1000 + + + Title 15 + Mon, 30 Apr 2018 15:14:00 +1000 + + + Title 16 + Mon, 30 Apr 2018 15:15:00 +1000 + + + Title 17 + Mon, 30 Apr 2018 15:16:00 +1000 + + + Title 18 + Mon, 30 Apr 2018 15:17:00 +1000 + + + Title 19 + Mon, 30 Apr 2018 15:18:00 +1000 + + + Title 20 + Mon, 30 Apr 2018 15:19:00 +1000 + + + Title 21 + Mon, 30 Apr 2018 15:20:00 +1000 + + + + diff --git a/tests/fixtures/feedreader3.xml b/tests/fixtures/feedreader3.xml new file mode 100644 index 00000000000..7b28e067cfe --- /dev/null +++ b/tests/fixtures/feedreader3.xml @@ -0,0 +1,26 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + + + Title 2 + Description 2 + http://www.example.com/link/2 + GUID 2 + + + + diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index cb8703d1fe6..492b97f6387 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -180,3 +180,13 @@ test.disabled_hass: assert entry_disabled_hass.disabled_by == entity_registry.DISABLED_HASS assert entry_disabled_user.disabled assert entry_disabled_user.disabled_by == entity_registry.DISABLED_USER + + +@asyncio.coroutine +def test_async_get_entity_id(registry): + """Test that entity_id is returned.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + assert entry.entity_id == 'light.hue_1234' + assert registry.async_get_entity_id( + 'light', 'hue', '1234') == 'light.hue_1234' + assert registry.async_get_entity_id('light', 'hue', '123') is None diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 67bfb590c3f..59d97ddb621 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -178,6 +178,7 @@ class MockValue(MagicMock): MockValue._mock_value_id += 1 value_id = MockValue._mock_value_id self.value_id = value_id + self.object_id = value_id for attr_name in kwargs: setattr(self, attr_name, kwargs[attr_name]) diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py new file mode 100644 index 00000000000..2e837b06b58 --- /dev/null +++ b/tests/scripts/test_auth.py @@ -0,0 +1,100 @@ +"""Test the auth script to manage local users.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.scripts import auth as script_auth +from homeassistant.auth_providers import homeassistant as hass_auth + +MOCK_PATH = '/bla/users.json' + + +def test_list_user(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) + + captured = capsys.readouterr() + + assert captured.out == '\n'.join([ + 'test-user', + 'second-user', + '', + 'Total users: 2', + '' + ]) + + +def test_add_user(capsys): + """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')) + + assert len(mock_save.mock_calls) == 1 + + captured = capsys.readouterr() + assert captured.out == 'User created\n' + + assert len(data.users) == 1 + data.validate_login('paulus', 'test-pass') + + +def test_validate_login(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( + data, Mock(username='test-user', password='test-pass')) + captured = capsys.readouterr() + assert captured.out == 'Auth valid\n' + + 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( + data, Mock(username='invalid-user', password='test-pass')) + captured = capsys.readouterr() + assert captured.out == 'Auth invalid\n' + + +def test_change_password(capsys): + """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')) + + assert len(mock_save.mock_calls) == 1 + captured = capsys.readouterr() + assert captured.out == 'Password changed\n' + data.validate_login('test-user', 'new-pass') + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('test-user', 'test-pass') + + +def test_change_password_invalid_user(capsys): + """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')) + + assert len(mock_save.mock_calls) == 0 + 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') diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1518706db55..84bd0771542 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -284,3 +284,23 @@ async def test_discovery_notification(hass): await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') assert state is None + + +async def test_discovery_notification_not_created(hass): + """Test that we not create a notification when discovery is aborted.""" + loader.set_component(hass, 'test', MockModule('test')) + await async_setup_component(hass, 'persistent_notification', {}) + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + return self.async_abort(reason='test') + + with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): + await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY) + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is None diff --git a/tests/test_core.py b/tests/test_core.py index 1fcd9416f36..4abce180093 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -375,7 +375,7 @@ class TestEventBus(unittest.TestCase): self.assertEqual(1, len(runs)) def test_thread_event_listener(self): - """Test a event listener listeners.""" + """Test thread event listener.""" thread_calls = [] def thread_listener(event): @@ -387,7 +387,7 @@ class TestEventBus(unittest.TestCase): assert len(thread_calls) == 1 def test_callback_event_listener(self): - """Test a event listener listeners.""" + """Test callback event listener.""" callback_calls = [] @ha.callback @@ -400,7 +400,7 @@ class TestEventBus(unittest.TestCase): assert len(callback_calls) == 1 def test_coroutine_event_listener(self): - """Test a event listener listeners.""" + """Test coroutine event listener.""" coroutine_calls = [] @asyncio.coroutine diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 6d3e41436c5..894fd4d7194 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -21,7 +21,8 @@ def manager(): return handler() async def async_add_entry(result): - entries.append(result) + if (result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY): + entries.append(result) manager = data_entry_flow.FlowManager( None, async_create_flow, async_add_entry) diff --git a/tox.ini b/tox.ini index 86acefe9b3f..8b034346475 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35, py36, lint, requirements, typing +envlist = py35, py36, lint, pylint, typing skip_missing_interpreters = True [testenv] @@ -12,7 +12,7 @@ setenv = whitelist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = - py.test --timeout=9 --duration=10 --cov --cov-report= {posargs} + pytest --timeout=9 --duration=10 --cov --cov-report= {posargs} deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt @@ -38,7 +38,8 @@ commands = [testenv:typing] basepython = {env:PYTHON3_PATH:python3} +whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt commands = - mypy --ignore-missing-imports --follow-imports=skip homeassistant + /bin/bash -c 'mypy --ignore-missing-imports --follow-imports=silent homeassistant/*.py' diff --git a/virtualization/Docker/scripts/ffmpeg b/virtualization/Docker/scripts/ffmpeg index 81b9ce694f9..914c2648e56 100755 --- a/virtualization/Docker/scripts/ffmpeg +++ b/virtualization/Docker/scripts/ffmpeg @@ -8,9 +8,4 @@ PACKAGES=( ffmpeg ) -# Add jessie-backports -echo "Adding jessie-backports" -echo "deb http://deb.debian.org/debian jessie-backports main" >> /etc/apt/sources.list -apt-get update - -apt-get install -y --no-install-recommends -t jessie-backports ${PACKAGES[@]} \ No newline at end of file +apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 302dfba2e1d..23f55eea13f 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -22,7 +22,7 @@ PACKAGES=( # homeassistant.components.device_tracker.bluetooth_tracker bluetooth libglib2.0-dev libbluetooth-dev # homeassistant.components.device_tracker.owntracks - libsodium13 + libsodium18 # homeassistant.components.zwave libudev-dev # homeassistant.components.homekit_controller