diff --git a/.coveragerc b/.coveragerc index 4287c8e1fce..ea9f302fbb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -15,6 +15,10 @@ omit = homeassistant/components/*/modbus.py homeassistant/components/*/tellstick.py + + homeassistant/components/tellduslive.py + homeassistant/components/*/tellduslive.py + homeassistant/components/*/vera.py homeassistant/components/ecobee.py @@ -41,7 +45,6 @@ omit = homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py - homeassistant/components/device_tracker/geofancy.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py @@ -98,6 +101,7 @@ omit = homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/temper.py homeassistant/components/sensor/time_date.py + homeassistant/components/sensor/torque.py homeassistant/components/sensor/transmission.py homeassistant/components/sensor/twitch.py homeassistant/components/sensor/worldclock.py diff --git a/.gitignore b/.gitignore index 8935ffedc17..3ee71808ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ Icon dist build eggs +.eggs parts bin var diff --git a/.travis.yml b/.travis.yml index a75cf6685d3..4383d49f548 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,8 @@ python: - 3.4 - 3.5 install: - # Validate requirements_all.txt on Python 3.5 - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py develop; script/gen_requirements_all.py validate; fi + # Validate requirements_all.txt on Python 3.4 + - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; tput sgr0; fi - script/bootstrap_server script: - script/cibuild diff --git a/LICENSE b/LICENSE index b3c5e1df750..42a425b4118 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Paulus Schoutsen +Copyright (c) 2016 Paulus Schoutsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a7507fd12b8..b704fc082ac 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -275,7 +275,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None): datefmt='%y-%m-%d %H:%M:%S')) logger = logging.getLogger('') logger.addHandler(err_handler) - logger.setLevel(logging.NOTSET) # this sets the minimum log level + logger.setLevel(logging.INFO) else: _LOGGER.error( diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index e4c498a5044..cc9f8dde69d 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): alarms.extend([ VerisureAlarm(value) - for value in verisure.get_alarm_status().values() + for value in verisure.ALARM_STATUS.values() if verisure.SHOW_ALARM ]) @@ -42,7 +42,6 @@ class VerisureAlarm(alarm.AlarmControlPanel): def __init__(self, alarm_status): self._id = alarm_status.id - self._device = verisure.MY_PAGES.DEVICE_ALARM self._state = STATE_UNKNOWN @property @@ -62,36 +61,36 @@ class VerisureAlarm(alarm.AlarmControlPanel): def update(self): """ Update alarm status """ - verisure.update() + verisure.update_alarm() - if verisure.STATUS[self._device][self._id].status == 'unarmed': + if verisure.ALARM_STATUS[self._id].status == 'unarmed': self._state = STATE_ALARM_DISARMED - elif verisure.STATUS[self._device][self._id].status == 'armedhome': + elif verisure.ALARM_STATUS[self._id].status == 'armedhome': self._state = STATE_ALARM_ARMED_HOME - elif verisure.STATUS[self._device][self._id].status == 'armedaway': + elif verisure.ALARM_STATUS[self._id].status == 'armedaway': self._state = STATE_ALARM_ARMED_AWAY - elif verisure.STATUS[self._device][self._id].status != 'pending': + elif verisure.ALARM_STATUS[self._id].status != 'pending': _LOGGER.error( 'Unknown alarm state %s', - verisure.STATUS[self._device][self._id].status) + verisure.ALARM_STATUS[self._id].status) def alarm_disarm(self, code=None): """ Send disarm command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_DISARMED) - _LOGGER.warning('disarming') + verisure.MY_PAGES.alarm.set(code, 'DISARMED') + _LOGGER.info('verisure alarm disarming') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() def alarm_arm_home(self, code=None): """ Send arm home command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_ARMED_HOME) - _LOGGER.warning('arming home') + verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME') + _LOGGER.info('verisure alarm arming home') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() def alarm_arm_away(self, code=None): """ Send arm away command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_ARMED_AWAY) - _LOGGER.warning('arming away') + verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY') + _LOGGER.info('verisure alarm arming away') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index c261cfd3f6a..0b06f3c9a79 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -116,7 +116,7 @@ class AlexaResponse(object): self.should_end_session = True if intent is not None and 'slots' in intent: self.variables = {key: value['value'] for key, value - in intent['slots'].items()} + in intent['slots'].items() if 'value' in value} else: self.variables = {} diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 84334493d0f..394dc904be1 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -17,6 +17,10 @@ DEPENDENCIES = ['sun'] CONF_OFFSET = 'offset' CONF_EVENT = 'event' +CONF_BEFORE = "before" +CONF_BEFORE_OFFSET = "before_offset" +CONF_AFTER = "after" +CONF_AFTER_OFFSET = "after_offset" EVENT_SUNSET = 'sunset' EVENT_SUNRISE = 'sunrise' @@ -37,26 +41,9 @@ def trigger(hass, config, action): _LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event) return False - if CONF_OFFSET in config: - raw_offset = config.get(CONF_OFFSET) - - negative_offset = False - if raw_offset.startswith('-'): - negative_offset = True - raw_offset = raw_offset[1:] - - try: - (hour, minute, second) = [int(x) for x in raw_offset.split(':')] - except ValueError: - _LOGGER.error('Could not parse offset %s', raw_offset) - return False - - offset = timedelta(hours=hour, minutes=minute, seconds=second) - - if negative_offset: - offset *= -1 - else: - offset = timedelta(0) + offset = _parse_offset(config.get(CONF_OFFSET)) + if offset is False: + return False # Do something to call action if event == EVENT_SUNRISE: @@ -67,6 +54,65 @@ def trigger(hass, config, action): return True +def if_action(hass, config): + """ Wraps action method with sun based condition. """ + before = config.get(CONF_BEFORE) + after = config.get(CONF_AFTER) + + # Make sure required configuration keys are present + if before is None and after is None: + logging.getLogger(__name__).error( + "Missing if-condition configuration key %s or %s", + CONF_BEFORE, CONF_AFTER) + return None + + # Make sure configuration keys have the right value + if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \ + after not in (None, EVENT_SUNRISE, EVENT_SUNSET): + logging.getLogger(__name__).error( + "%s and %s can only be set to %s or %s", + CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET) + return None + + before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET)) + after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET)) + if before_offset is False or after_offset is False: + return None + + if before is None: + before_func = lambda: None + elif before == EVENT_SUNRISE: + before_func = lambda: sun.next_rising_utc(hass) + before_offset + else: + before_func = lambda: sun.next_setting_utc(hass) + before_offset + + if after is None: + after_func = lambda: None + elif after == EVENT_SUNRISE: + after_func = lambda: sun.next_rising_utc(hass) + after_offset + else: + after_func = lambda: sun.next_setting_utc(hass) + after_offset + + def time_if(): + """ Validate time based if-condition """ + + now = dt_util.utcnow() + before = before_func() + after = after_func() + + if before is not None and now > now.replace(hour=before.hour, + minute=before.minute): + return False + + if after is not None and now < now.replace(hour=after.hour, + minute=after.minute): + return False + + return True + + return time_if + + def trigger_sunrise(hass, action, offset): """ Trigger action at next sun rise. """ def next_rise(): @@ -103,3 +149,26 @@ def trigger_sunset(hass, action, offset): action() track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + + +def _parse_offset(raw_offset): + if raw_offset is None: + return timedelta(0) + + negative_offset = False + if raw_offset.startswith('-'): + negative_offset = True + raw_offset = raw_offset[1:] + + try: + (hour, minute, second) = [int(x) for x in raw_offset.split(':')] + except ValueError: + _LOGGER.error('Could not parse offset %s', raw_offset) + return False + + offset = timedelta(hours=hour, minutes=minute, seconds=second) + + if negative_offset: + offset *= -1 + + return offset diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 60963988f39..6cb6ede5e50 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -47,15 +47,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): response = requests.post(resource, data=payload, timeout=10, verify=verify_ssl) if not response.ok: - _LOGGER.error('Response status is "%s"', response.status_code) + _LOGGER.error("Response status is '%s'", response.status_code) return False except requests.exceptions.MissingSchema: - _LOGGER.error('Missing resource or schema in configuration. ' - 'Add http:// to your URL.') + _LOGGER.error("Missing resource or schema in configuration. " + "Add http:// or https:// to your URL") return False except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint: %s', - resource) + _LOGGER.error('No route to resource/endpoint: %s', resource) return False if use_get: diff --git a/homeassistant/components/device_tracker/geofancy.py b/homeassistant/components/device_tracker/geofancy.py deleted file mode 100644 index a5e6edee71a..00000000000 --- a/homeassistant/components/device_tracker/geofancy.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -homeassistant.components.device_tracker.geofancy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Geofancy platform for the device tracker. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.geofancy/ -""" -from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR) - -DEPENDENCIES = ['http'] - -_SEE = 0 - -URL_API_GEOFANCY_ENDPOINT = "/api/geofancy" - - -def setup_scanner(hass, config, see): - """ Set up an endpoint for the Geofancy app. """ - - # Use a global variable to keep setup_scanner compact when using a callback - global _SEE - _SEE = see - - # POST would be semantically better, but that currently does not work - # since Geofancy sends the data as key1=value1&key2=value2 - # in the request body, while Home Assistant expects json there. - - hass.http.register_path( - 'GET', URL_API_GEOFANCY_ENDPOINT, _handle_get_api_geofancy) - - return True - - -def _handle_get_api_geofancy(handler, path_match, data): - """ Geofancy message received. """ - - if not isinstance(data, dict): - handler.write_json_message( - "Error while parsing Geofancy message.", - HTTP_INTERNAL_SERVER_ERROR) - return - if 'latitude' not in data or 'longitude' not in data: - handler.write_json_message( - "Location not specified.", - HTTP_UNPROCESSABLE_ENTITY) - return - if 'device' not in data or 'id' not in data: - handler.write_json_message( - "Device id or location id not specified.", - HTTP_UNPROCESSABLE_ENTITY) - return - - try: - gps_coords = (float(data['latitude']), float(data['longitude'])) - except ValueError: - # If invalid latitude / longitude format - handler.write_json_message( - "Invalid latitude / longitude format.", - HTTP_UNPROCESSABLE_ENTITY) - return - - # entity id's in Home Assistant must be alphanumerical - device_uuid = data['device'] - device_entity_id = device_uuid.replace('-', '') - - _SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id']) - - handler.write_json_message("Geofancy message processed") diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py new file mode 100644 index 00000000000..e7532d1075d --- /dev/null +++ b/homeassistant/components/device_tracker/locative.py @@ -0,0 +1,105 @@ +""" +homeassistant.components.device_tracker.locative +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Locative platform for the device tracker. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.locative/ +""" +import logging +from functools import partial + +from homeassistant.const import ( + HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) +from homeassistant.components.device_tracker import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['http'] + +URL_API_LOCATIVE_ENDPOINT = "/api/locative" + + +def setup_scanner(hass, config, see): + """ Set up an endpoint for the Locative app. """ + + # POST would be semantically better, but that currently does not work + # since Locative sends the data as key1=value1&key2=value2 + # in the request body, while Home Assistant expects json there. + + hass.http.register_path( + 'GET', URL_API_LOCATIVE_ENDPOINT, + partial(_handle_get_api_locative, hass, see)) + + return True + + +def _handle_get_api_locative(hass, see, handler, path_match, data): + """ Locative message received. """ + + if not _check_data(handler, data): + return + + device = data['device'].replace('-', '') + location_name = data['id'].lower() + direction = data['trigger'] + + if direction == 'enter': + see(dev_id=device, location_name=location_name) + handler.write_text("Setting location to {}".format(location_name)) + + elif direction == 'exit': + current_state = hass.states.get( + "{}.{}".format(DOMAIN, device)).state + + if current_state == location_name: + see(dev_id=device, location_name=STATE_NOT_HOME) + handler.write_text("Setting location to not home") + else: + # Ignore the message if it is telling us to exit a zone that we + # aren't currently in. This occurs when a zone is entered before + # the previous zone was exited. The enter message will be sent + # first, then the exit message will be sent second. + handler.write_text( + 'Ignoring exit from {} (already in {})'.format( + location_name, current_state)) + + elif direction == 'test': + # In the app, a test message can be sent. Just return something to + # the user to let them know that it works. + handler.write_text("Received test message.") + + else: + handler.write_text( + "Received unidentified message: {}".format(direction), + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Received unidentified message from Locative: %s", + direction) + + +def _check_data(handler, data): + if 'latitude' not in data or 'longitude' not in data: + handler.write_text("Latitude and longitude not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Latitude and longitude not specified.") + return False + + if 'device' not in data: + handler.write_text("Device id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Device id not specified.") + return False + + if 'id' not in data: + handler.write_text("Location id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Location id not specified.") + return False + + if 'trigger' not in data: + handler.write_text("Trigger is not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Trigger is not specified.") + return False + + return True diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 7a4e87de5a8..b7f57b0157e 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -21,7 +21,7 @@ from urllib.parse import urlparse, parse_qs import homeassistant.core as ha from homeassistant.const import ( - SERVER_PORT, CONTENT_TYPE_JSON, + SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING, HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED, @@ -293,6 +293,17 @@ class RequestHandler(SimpleHTTPRequestHandler): json.dumps(data, indent=4, sort_keys=True, cls=rem.JSONEncoder).encode("UTF-8")) + def write_text(self, message, status_code=HTTP_OK): + """ Helper method to return a text message to the caller. """ + self.send_response(status_code) + self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN) + + self.set_session_cookie_header() + + self.end_headers() + + self.wfile.write(message.encode("UTF-8")) + def write_file(self, path, cache_headers=True): """ Returns a file to the user. """ try: diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 1b80035fb0d..93321b5fd10 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -50,6 +50,7 @@ FLASH_LONG = "long" # Apply an effect to the light, can be EFFECT_COLORLOOP ATTR_EFFECT = "effect" EFFECT_COLORLOOP = "colorloop" +EFFECT_RANDOM = "random" EFFECT_WHITE = "white" LIGHT_PROFILES_FILE = "light_profiles.csv" @@ -228,7 +229,8 @@ def setup(hass, config): if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG): params[ATTR_FLASH] = dat[ATTR_FLASH] - if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE): + if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE, + EFFECT_RANDOM): params[ATTR_EFFECT] = dat[ATTR_EFFECT] for light in target_lights: diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 7c3af9f968d..61bff75bbd3 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -10,6 +10,7 @@ import json import logging import os import socket +import random from datetime import timedelta from urllib.parse import urlparse @@ -20,7 +21,7 @@ from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT, - ATTR_EFFECT, EFFECT_COLORLOOP, ATTR_RGB_COLOR) + ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, ATTR_RGB_COLOR) REQUIREMENTS = ['phue==0.8'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) @@ -120,10 +121,17 @@ def setup_bridge(host, hass, add_devices_callback): new_lights = [] + api_name = api.get('config').get('name') + if api_name == 'RaspBee-GW': + bridge_type = 'deconz' + else: + bridge_type = 'hue' + for light_id, info in api_states.items(): if light_id not in lights: lights[light_id] = HueLight(int(light_id), info, - bridge, update_lights) + bridge, update_lights, + bridge_type=bridge_type) new_lights.append(lights[light_id]) else: lights[light_id].info = info @@ -162,11 +170,14 @@ def request_configuration(host, hass, add_devices_callback): class HueLight(Light): """ Represents a Hue light """ - def __init__(self, light_id, info, bridge, update_lights): + # pylint: disable=too-many-arguments + def __init__(self, light_id, info, bridge, update_lights, + bridge_type='hue'): self.light_id = light_id self.info = info self.bridge = bridge self.update_lights = update_lights + self.bridge_type = bridge_type @property def unique_id(self): @@ -226,14 +237,17 @@ class HueLight(Light): command['alert'] = 'lselect' elif flash == FLASH_SHORT: command['alert'] = 'select' - else: + elif self.bridge_type == 'hue': command['alert'] = 'none' effect = kwargs.get(ATTR_EFFECT) if effect == EFFECT_COLORLOOP: command['effect'] = 'colorloop' - else: + elif effect == EFFECT_RANDOM: + command['hue'] = random.randrange(0, 65535) + command['sat'] = random.randrange(150, 254) + elif self.bridge_type == 'hue': command['effect'] = 'none' self.bridge.set_light(self.light_id, command) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 8a0c5b8fded..9908737b7b1 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -42,6 +42,7 @@ turn_on: description: Light effect values: - colorloop + - random turn_off: description: Turn a light off diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 829d3cfccdb..9135323fb1f 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -14,9 +14,9 @@ from homeassistant.components.switch.vera import VeraSwitch from homeassistant.components.light import ATTR_BRIGHTNESS -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) @@ -36,10 +36,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device_data = config.get('device_data', {}) - controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: - devices = controller.get_devices([ + devices = vera_controller.get_devices([ 'Switch', 'On/Off Switch', 'Dimmable Switch']) @@ -54,7 +63,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): exclude = extra_data.get('exclude', False) if exclude is not True: - lights.append(VeraLight(device, extra_data)) + lights.append(VeraLight(device, vera_controller, extra_data)) add_devices_callback(lights) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 5c2e7076955..16159404dec 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -28,7 +28,7 @@ QUERY_EVENTS_BETWEEN = """ SELECT * FROM events WHERE time_fired > ? AND time_fired < ? """ -EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY' +EVENT_LOGBOOK_ENTRY = 'logbook_entry' GROUP_BY_MINUTES = 15 @@ -204,7 +204,7 @@ def humanify(events): event.time_fired, "Home Assistant", action, domain=HA_DOMAIN) - elif event.event_type == EVENT_LOGBOOK_ENTRY: + elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY: domain = event.data.get(ATTR_DOMAIN) entity_id = event.data.get(ATTR_ENTITY_ID) if domain is None and entity_id is not None: diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 9a5d1c59d1a..a0d769e3d82 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -76,8 +76,12 @@ def setup(hass, config=None): logfilter[LOGGER_LOGS] = logs + logger = logging.getLogger('') + logger.setLevel(logging.NOTSET) + # Set log filter for all log handler for handler in logging.root.handlers: + handler.setLevel(logging.NOTSET) handler.addFilter(HomeAssistantLogFilter(logfilter)) return True diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index a61dac88150..27b5aa3863c 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -142,10 +142,14 @@ class MpdDevice(MediaPlayerDevice): def media_title(self): """ Title of current playing media. """ name = self.currentsong.get('name', None) - title = self.currentsong['title'] + title = self.currentsong.get('title', None) - if name is None: + if name is None and title is None: + return "None" + elif name is None: return title + elif title is None: + return name else: return '{}: {}'.format(name, title) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 37a7a63c72b..b5ea258c5cc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -30,7 +30,7 @@ DEFAULT_QOS = 0 DEFAULT_RETAIN = False SERVICE_PUBLISH = 'publish' -EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED' +EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received' REQUIREMENTS = ['paho-mqtt==1.1'] diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 126d8c9f40e..802634715e9 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -16,7 +16,7 @@ import json import atexit from homeassistant.core import Event, EventOrigin, State -import homeassistant.util.dt as date_util +import homeassistant.util.dt as dt_util from homeassistant.remote import JSONEncoder from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, @@ -62,8 +62,8 @@ def row_to_state(row): try: return State( row[1], row[2], json.loads(row[3]), - date_util.utc_from_timestamp(row[4]), - date_util.utc_from_timestamp(row[5])) + dt_util.utc_from_timestamp(row[4]), + dt_util.utc_from_timestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to state: %s", row) @@ -74,7 +74,7 @@ def row_to_event(row): """ Convert a databse row to an event. """ try: return Event(row[1], json.loads(row[2]), EventOrigin(row[3]), - date_util.utc_from_timestamp(row[5])) + dt_util.utc_from_timestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to event: %s", row) @@ -116,10 +116,10 @@ class RecorderRun(object): self.start = _INSTANCE.recording_start self.closed_incorrect = False else: - self.start = date_util.utc_from_timestamp(row[1]) + self.start = dt_util.utc_from_timestamp(row[1]) if row[2] is not None: - self.end = date_util.utc_from_timestamp(row[2]) + self.end = dt_util.utc_from_timestamp(row[2]) self.closed_incorrect = bool(row[3]) @@ -169,8 +169,8 @@ class Recorder(threading.Thread): self.queue = queue.Queue() self.quit_object = object() self.lock = threading.Lock() - self.recording_start = date_util.utcnow() - self.utc_offset = date_util.now().utcoffset().total_seconds() + self.recording_start = dt_util.utcnow() + self.utc_offset = dt_util.now().utcoffset().total_seconds() def start_recording(event): """ Start recording. """ @@ -217,10 +217,11 @@ class Recorder(threading.Thread): def shutdown(self, event): """ Tells the recorder to shut down. """ self.queue.put(self.quit_object) + self.block_till_done() def record_state(self, entity_id, state, event_id): """ Save a state to the database. """ - now = date_util.utcnow() + now = dt_util.utcnow() # State got deleted if state is None: @@ -247,7 +248,7 @@ class Recorder(threading.Thread): """ Save an event to the database. """ info = ( event.event_type, json.dumps(event.data, cls=JSONEncoder), - str(event.origin), date_util.utcnow(), event.time_fired, + str(event.origin), dt_util.utcnow(), event.time_fired, self.utc_offset ) @@ -307,7 +308,7 @@ class Recorder(threading.Thread): def save_migration(migration_id): """ Save and commit a migration to the database. """ cur.execute('INSERT INTO schema_version VALUES (?, ?)', - (migration_id, date_util.utcnow())) + (migration_id, dt_util.utcnow())) self.conn.commit() _LOGGER.info("Database migrated to version %d", migration_id) @@ -420,18 +421,18 @@ class Recorder(threading.Thread): self.query( """INSERT INTO recorder_runs (start, created, utc_offset) VALUES (?, ?, ?)""", - (self.recording_start, date_util.utcnow(), self.utc_offset)) + (self.recording_start, dt_util.utcnow(), self.utc_offset)) def _close_run(self): """ Save end time for current run. """ self.query( "UPDATE recorder_runs SET end=? WHERE start=?", - (date_util.utcnow(), self.recording_start)) + (dt_util.utcnow(), self.recording_start)) def _adapt_datetime(datetimestamp): """ Turn a datetime into an integer for in the DB. """ - return date_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp() + return dt_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp() def _verify_instance(): diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index 7c96230ccd4..ce1a3242542 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -73,8 +73,9 @@ def _process_config(scene_config): for entity_id in c_entities: if isinstance(c_entities[entity_id], dict): - state = c_entities[entity_id].pop('state', None) - attributes = c_entities[entity_id] + entity_attrs = c_entities[entity_id].copy() + state = entity_attrs.pop('state', None) + attributes = entity_attrs else: state = c_entities[entity_id] attributes = {} diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 04770ced241..9a6456857b8 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -9,7 +9,8 @@ https://home-assistant.io/components/sensor/ import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.components import wink, zwave, isy994, verisure, ecobee +from homeassistant.components import (wink, zwave, isy994, + verisure, ecobee, tellduslive) DOMAIN = 'sensor' SCAN_INTERVAL = 30 @@ -22,7 +23,8 @@ DISCOVERY_PLATFORMS = { zwave.DISCOVER_SENSORS: 'zwave', isy994.DISCOVER_SENSORS: 'isy994', verisure.DISCOVER_SENSORS: 'verisure', - ecobee.DISCOVER_SENSORS: 'ecobee' + ecobee.DISCOVER_SENSORS: 'ecobee', + tellduslive.DISCOVER_SENSORS: 'tellduslive', } diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 608dc2f19fd..151b679b10e 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -53,6 +53,11 @@ class EliqSensor(Entity): """ Returns the name. """ return self._name + @property + def icon(self): + """ Returns icon. """ + return "mdi:speedometer" + @property def unit_of_measurement(self): """ Unit of measurement of this entity, if any. """ diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4dcd036df5e..f6a56d3a99e 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -10,7 +10,7 @@ from datetime import timedelta import logging import requests -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import (CONF_VALUE_TEMPLATE, STATE_UNKNOWN) from homeassistant.util import template, Throttle from homeassistant.helpers.entity import Entity @@ -47,15 +47,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): response = requests.post(resource, data=payload, timeout=10, verify=verify_ssl) if not response.ok: - _LOGGER.error('Response status is "%s"', response.status_code) + _LOGGER.error("Response status is '%s'", response.status_code) return False except requests.exceptions.MissingSchema: - _LOGGER.error('Missing resource or schema in configuration. ' - 'Add http:// to your URL.') + _LOGGER.error("Missing resource or schema in configuration. " + "Add http:// or https:// to your URL") return False except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint. ' - 'Please check the URL in the configuration file.') + _LOGGER.error("No route to resource/endpoint: %s", resource) return False if use_get: @@ -78,7 +77,7 @@ class RestSensor(Entity): self._hass = hass self.rest = rest self._name = name - self._state = 'n/a' + self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement self._value_template = value_template self.update() @@ -103,13 +102,13 @@ class RestSensor(Entity): self.rest.update() value = self.rest.data - if 'error' in value: - self._state = value['error'] - else: - if self._value_template is not None: - value = template.render_with_possible_json_value( - self._hass, self._value_template, value, 'N/A') - self._state = value + if value is None: + value = STATE_UNKNOWN + elif self._value_template is not None: + value = template.render_with_possible_json_value( + self._hass, self._value_template, value, STATE_UNKNOWN) + + self._state = value # pylint: disable=too-few-public-methods @@ -119,7 +118,7 @@ class RestDataGet(object): def __init__(self, resource, verify_ssl): self._resource = resource self._verify_ssl = verify_ssl - self.data = dict() + self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -127,12 +126,10 @@ class RestDataGet(object): try: response = requests.get(self._resource, timeout=10, verify=self._verify_ssl) - if 'error' in self.data: - del self.data['error'] self.data = response.text except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint.") - self.data['error'] = 'N/A' + _LOGGER.error("No route to resource/endpoint: %s", self._resource) + self.data = None # pylint: disable=too-few-public-methods @@ -143,7 +140,7 @@ class RestDataPost(object): self._resource = resource self._payload = payload self._verify_ssl = verify_ssl - self.data = dict() + self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -151,9 +148,7 @@ class RestDataPost(object): try: response = requests.post(self._resource, data=self._payload, timeout=10, verify=self._verify_ssl) - if 'error' in self.data: - del self.data['error'] self.data = response.text except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint.") - self.data['error'] = 'N/A' + _LOGGER.error("No route to resource/endpoint: %s", self._resource) + self.data = None diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 110d58283c3..ecd56ad05d7 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -14,25 +14,25 @@ from homeassistant.const import STATE_ON, STATE_OFF REQUIREMENTS = ['psutil==3.2.2'] SENSOR_TYPES = { - 'disk_use_percent': ['Disk Use', '%'], - 'disk_use': ['Disk Use', 'GiB'], - 'disk_free': ['Disk Free', 'GiB'], - 'memory_use_percent': ['RAM Use', '%'], - 'memory_use': ['RAM Use', 'MiB'], - 'memory_free': ['RAM Free', 'MiB'], - 'processor_use': ['CPU Use', '%'], - 'process': ['Process', ''], - 'swap_use_percent': ['Swap Use', '%'], - 'swap_use': ['Swap Use', 'GiB'], - 'swap_free': ['Swap Free', 'GiB'], - 'network_out': ['Sent', 'MiB'], - 'network_in': ['Recieved', 'MiB'], - 'packets_out': ['Packets sent', ''], - 'packets_in': ['Packets recieved', ''], - 'ipv4_address': ['IPv4 address', ''], - 'ipv6_address': ['IPv6 address', ''], - 'last_boot': ['Last Boot', ''], - 'since_last_boot': ['Since Last Boot', ''] + 'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'], + 'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'], + 'disk_free': ['Disk Free', 'GiB', 'mdi:harddisk'], + 'memory_use_percent': ['RAM Use', '%', 'mdi:memory'], + 'memory_use': ['RAM Use', 'MiB', 'mdi:memory'], + 'memory_free': ['RAM Free', 'MiB', 'mdi:memory'], + 'processor_use': ['CPU Use', '%', 'mdi:memory'], + 'process': ['Process', '', 'mdi:memory'], + 'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'], + 'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'], + 'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'], + 'network_out': ['Sent', 'MiB', 'mdi:server-network'], + 'network_in': ['Recieved', 'MiB', 'mdi:server-network'], + 'packets_out': ['Packets sent', '', 'mdi:server-network'], + 'packets_in': ['Packets recieved', '', 'mdi:server-network'], + 'ipv4_address': ['IPv4 address', '', 'mdi:server-network'], + 'ipv6_address': ['IPv6 address', '', 'mdi:server-network'], + 'last_boot': ['Last Boot', '', 'mdi:clock'], + 'since_last_boot': ['Since Last Boot', '', 'mdi:clock'] } _LOGGER = logging.getLogger(__name__) @@ -69,6 +69,10 @@ class SystemMonitorSensor(Entity): def name(self): return self._name.rstrip() + @property + def icon(self): + return SENSOR_TYPES[self.type][2] + @property def state(self): """ Returns the state of the device. """ diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py new file mode 100644 index 00000000000..7cc49e3c611 --- /dev/null +++ b/homeassistant/components/sensor/tellduslive.py @@ -0,0 +1,99 @@ +""" +homeassistant.components.sensor.tellduslive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Shows sensor values from Tellstick Net/Telstick Live. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.tellduslive/ + +""" +import logging + +from datetime import datetime + +from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL +from homeassistant.helpers.entity import Entity +from homeassistant.components import tellduslive + +ATTR_LAST_UPDATED = "time_last_updated" + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['tellduslive'] + +SENSOR_TYPE_TEMP = "temp" +SENSOR_TYPE_HUMIDITY = "humidity" + +SENSOR_TYPES = { + SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], + SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up Tellstick sensors. """ + sensors = tellduslive.NETWORK.get_sensors() + devices = [] + + for component in sensors: + for sensor in component["data"]: + # one component can have more than one sensor + # (e.g. both humidity and temperature) + devices.append(TelldusLiveSensor(component["id"], + component["name"], + sensor["name"])) + add_devices(devices) + + +class TelldusLiveSensor(Entity): + """ Represents a Telldus Live sensor. """ + + def __init__(self, sensor_id, sensor_name, sensor_type): + self._sensor_id = sensor_id + self._sensor_type = sensor_type + self._state = None + self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0] + self._last_update = None + self._battery_level = None + self.update() + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def state_attributes(self): + attrs = dict() + if self._battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = self._battery_level + if self._last_update is not None: + attrs[ATTR_LAST_UPDATED] = self._last_update + return attrs + + @property + def unit_of_measurement(self): + return SENSOR_TYPES[self._sensor_type][1] + + @property + def icon(self): + return SENSOR_TYPES[self._sensor_type][2] + + def update(self): + values = tellduslive.NETWORK.get_sensor_value(self._sensor_id, + self._sensor_type) + self._state, self._battery_level, self._last_update = values + + self._state = float(self._state) + if self._sensor_type == SENSOR_TYPE_TEMP: + self._state = round(self._state, 1) + elif self._sensor_type == SENSOR_TYPE_HUMIDITY: + self._state = int(round(self._state)) + + self._battery_level = round(self._battery_level * 100 / 255) # percent + + self._last_update = str(datetime.fromtimestamp(self._last_update)) diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py new file mode 100644 index 00000000000..e123aa2d18c --- /dev/null +++ b/homeassistant/components/sensor/torque.py @@ -0,0 +1,117 @@ +""" +homeassistant.components.sensor.torque +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get data from the Torque OBD application. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.torque/ +""" + +import re +from homeassistant.const import HTTP_OK +from homeassistant.helpers.entity import Entity + + +DOMAIN = 'torque' +DEPENDENCIES = ['http'] +SENSOR_EMAIL_FIELD = 'eml' +DEFAULT_NAME = 'vehicle' +ENTITY_NAME_FORMAT = '{0} {1}' + +API_PATH = '/api/torque' +SENSOR_NAME_KEY = r'userFullName(\w+)' +SENSOR_UNIT_KEY = r'userUnit(\w+)' +SENSOR_VALUE_KEY = r'k(\w+)' + +NAME_KEY = re.compile(SENSOR_NAME_KEY) +UNIT_KEY = re.compile(SENSOR_UNIT_KEY) +VALUE_KEY = re.compile(SENSOR_VALUE_KEY) + + +def decode(value): + """ Double-decode required. """ + return value.encode('raw_unicode_escape').decode('utf-8') + + +def convert_pid(value): + """ Convert pid from hex string to integer. """ + return int(value, 16) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Set up Torque platform. """ + + vehicle = config.get('name', DEFAULT_NAME) + email = config.get('email', None) + sensors = {} + + def _receive_data(handler, path_match, data): + """ Received data from Torque. """ + handler.send_response(HTTP_OK) + handler.end_headers() + + if email is not None and email != data[SENSOR_EMAIL_FIELD]: + return + + names = {} + units = {} + for key in data: + is_name = NAME_KEY.match(key) + is_unit = UNIT_KEY.match(key) + is_value = VALUE_KEY.match(key) + + if is_name: + pid = convert_pid(is_name.group(1)) + names[pid] = decode(data[key]) + elif is_unit: + pid = convert_pid(is_unit.group(1)) + units[pid] = decode(data[key]) + elif is_value: + pid = convert_pid(is_value.group(1)) + if pid in sensors: + sensors[pid].on_update(data[key]) + + for pid in names: + if pid not in sensors: + sensors[pid] = TorqueSensor( + ENTITY_NAME_FORMAT.format(vehicle, names[pid]), + units.get(pid, None)) + add_devices([sensors[pid]]) + + hass.http.register_path('GET', API_PATH, _receive_data) + return True + + +class TorqueSensor(Entity): + """ Represents a Torque sensor. """ + + def __init__(self, name, unit): + self._name = name + self._unit = unit + self._state = None + + @property + def name(self): + """ Returns the name of the sensor. """ + return self._name + + @property + def unit_of_measurement(self): + """ Returns the unit of measurement. """ + return self._unit + + @property + def state(self): + """ State of the sensor. """ + return self._state + + @property + def icon(self): + """ Sensor default icon. """ + return 'mdi:car' + + def on_update(self, value): + """ Receive an update. """ + self._state = value + self.update_ha_state() diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 7fb72fd91b7..03b8d05d2f5 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -13,11 +13,9 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, - TEMP_CELCIUS, TEMP_FAHRENHEIT) + TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) @@ -37,7 +35,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + categories = ['Temperature Sensor', 'Light Sensor', 'Sensor'] devices = [] try: @@ -53,7 +60,8 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_sensors.append(VeraSensor(device, extra_data)) + vera_sensors.append( + VeraSensor(device, vera_controller, extra_data)) return vera_sensors @@ -66,8 +74,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSensor(Entity): """ Represents a Vera Sensor. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device + self.controller = controller self.extra_data = extra_data if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') @@ -76,6 +85,16 @@ class VeraSensor(Entity): self.current_value = '' self._temperature_units = None + self.controller.register(vera_device) + self.controller.on( + vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s', self.name) + self.update_ha_state(True) + def __str__(self): return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state) @@ -117,6 +136,11 @@ class VeraSensor(Entity): attr['Vera Device Id'] = self.vera_device.vera_device_id return attr + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + def update(self): if self.vera_device.category == "Temperature Sensor": self.vera_device.refresh_value('CurrentTemperature') diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py index e946be9a3f4..e7c6a30b558 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/sensor/verisure.py @@ -27,14 +27,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors.extend([ VerisureThermometer(value) - for value in verisure.get_climate_status().values() + for value in verisure.CLIMATE_STATUS.values() if verisure.SHOW_THERMOMETERS and hasattr(value, 'temperature') and value.temperature ]) sensors.extend([ VerisureHygrometer(value) - for value in verisure.get_climate_status().values() + for value in verisure.CLIMATE_STATUS.values() if verisure.SHOW_HYGROMETERS and hasattr(value, 'humidity') and value.humidity ]) @@ -47,20 +47,19 @@ class VerisureThermometer(Entity): def __init__(self, climate_status): self._id = climate_status.id - self._device = verisure.MY_PAGES.DEVICE_CLIMATE @property def name(self): """ Returns the name of the device. """ return '{} {}'.format( - verisure.STATUS[self._device][self._id].location, + verisure.CLIMATE_STATUS[self._id].location, "Temperature") @property def state(self): """ Returns the state of the device. """ # remove ° character - return verisure.STATUS[self._device][self._id].temperature[:-1] + return verisure.CLIMATE_STATUS[self._id].temperature[:-1] @property def unit_of_measurement(self): @@ -69,7 +68,7 @@ class VerisureThermometer(Entity): def update(self): ''' update sensor ''' - verisure.update() + verisure.update_climate() class VerisureHygrometer(Entity): @@ -77,20 +76,19 @@ class VerisureHygrometer(Entity): def __init__(self, climate_status): self._id = climate_status.id - self._device = verisure.MY_PAGES.DEVICE_CLIMATE @property def name(self): """ Returns the name of the device. """ return '{} {}'.format( - verisure.STATUS[self._device][self._id].location, + verisure.CLIMATE_STATUS[self._id].location, "Humidity") @property def state(self): """ Returns the state of the device. """ # remove % character - return verisure.STATUS[self._device][self._id].humidity[:-1] + return verisure.CLIMATE_STATUS[self._id].humidity[:-1] @property def unit_of_measurement(self): @@ -99,4 +97,4 @@ class VerisureHygrometer(Entity): def update(self): ''' update sensor ''' - verisure.update() + verisure.update_climate() diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py new file mode 100644 index 00000000000..cda9ba1b78f --- /dev/null +++ b/homeassistant/components/sensor/yr.py @@ -0,0 +1,223 @@ +""" +homeassistant.components.sensor.yr +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Yr.no weather service. + +Configuration: + +Will show a symbol for the current weather as default: +sensor: + platform: yr + +Will show temperatue and wind direction: +sensor: + platform: yr + monitored_conditions: + - temperature + - windDirection + +Will show all available sensors: +sensor: + platform: yr + monitored_conditions: + - temperature + - symbol + - precipitation + - windSpeed + - pressure + - windDirection + - humidity + - fog + - cloudiness + - lowClouds + - mediumClouds + - highClouds + - dewpointTemperature + +""" +import logging + +import requests + +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.helpers.entity import Entity +from homeassistant.util import location, dt as dt_util + +_LOGGER = logging.getLogger(__name__) + + +REQUIREMENTS = ['xmltodict'] + +# Sensor types are defined like so: +SENSOR_TYPES = { + 'symbol': ['Symbol', ''], + 'precipitation': ['Condition', 'mm'], + 'temperature': ['Temperature', '°C'], + 'windSpeed': ['Wind speed', 'm/s'], + 'pressure': ['Pressure', 'hPa'], + 'windDirection': ['Wind direction', '°'], + 'humidity': ['Humidity', '%'], + 'fog': ['Fog', '%'], + 'cloudiness': ['Cloudiness', '%'], + 'lowClouds': ['Low clouds', '%'], + 'mediumClouds': ['Medium clouds', '%'], + 'highClouds': ['High clouds', '%'], + 'dewpointTemperature': ['Dewpoint temperature', '°C'], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the yr.no sensor. """ + + if None in (hass.config.latitude, hass.config.longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return False + + elevation = config.get('elevation') + + if elevation is None: + elevation = location.elevation(hass.config.latitude, + hass.config.longitude) + + coordinates = dict(lat=hass.config.latitude, + lon=hass.config.longitude, msl=elevation) + + weather = YrData(coordinates) + + dev = [] + if 'monitored_conditions' in config: + for variable in config['monitored_conditions']: + if variable not in SENSOR_TYPES: + _LOGGER.error('Sensor type: "%s" does not exist', variable) + else: + dev.append(YrSensor(variable, weather)) + + # add symbol as default sensor + if len(dev) == 0: + dev.append(YrSensor("symbol", weather)) + add_devices(dev) + + +# pylint: disable=too-many-instance-attributes +class YrSensor(Entity): + """ Implements an Yr.no sensor. """ + + def __init__(self, sensor_type, weather): + self.client_name = 'yr' + self._name = SENSOR_TYPES[sensor_type][0] + self.type = sensor_type + self._state = None + self._weather = weather + self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self._update = None + + self.update() + + @property + def name(self): + return '{} {}'.format(self.client_name, self._name) + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def state_attributes(self): + """ Returns state attributes. """ + data = { + 'about': "Weather forecast from yr.no, delivered by the" + " Norwegian Meteorological Institute and the NRK" + } + if self.type == 'symbol': + symbol_nr = self._state + data[ATTR_ENTITY_PICTURE] = \ + "http://api.met.no/weatherapi/weathericon/1.1/" \ + "?symbol={0};content_type=image/png".format(symbol_nr) + + return data + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + return self._unit_of_measurement + + def update(self): + """ Gets the latest data from yr.no and updates the states. """ + + now = dt_util.utcnow() + # check if data should be updated + if self._update is not None and now <= self._update: + return + + self._weather.update() + + # find sensor + for time_entry in self._weather.data['product']['time']: + valid_from = dt_util.str_to_datetime( + time_entry['@from'], "%Y-%m-%dT%H:%M:%SZ") + valid_to = dt_util.str_to_datetime( + time_entry['@to'], "%Y-%m-%dT%H:%M:%SZ") + + loc_data = time_entry['location'] + + if self.type not in loc_data or now >= valid_to: + continue + + self._update = valid_to + + if self.type == 'precipitation' and valid_from < now: + self._state = loc_data[self.type]['@value'] + break + elif self.type == 'symbol' and valid_from < now: + self._state = loc_data[self.type]['@number'] + break + elif self.type == ('temperature', 'pressure', 'humidity', + 'dewpointTemperature'): + self._state = loc_data[self.type]['@value'] + break + elif self.type == 'windSpeed': + self._state = loc_data[self.type]['@mps'] + break + elif self.type == 'windDirection': + self._state = float(loc_data[self.type]['@deg']) + break + elif self.type in ('fog', 'cloudiness', 'lowClouds', + 'mediumClouds', 'highClouds'): + self._state = loc_data[self.type]['@percent'] + break + + +# pylint: disable=too-few-public-methods +class YrData(object): + """ Gets the latest data and updates the states. """ + + def __init__(self, coordinates): + self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ + 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) + + self._nextrun = None + self.data = {} + self.update() + + def update(self): + """ Gets the latest data from yr.no """ + # check if new will be available + if self._nextrun is not None and dt_util.utcnow() <= self._nextrun: + return + try: + with requests.Session() as sess: + response = sess.get(self._url) + except requests.RequestException: + return + if response.status_code != 200: + return + data = response.text + + import xmltodict + self.data = xmltodict.parse(data)['weatherdata'] + model = self.data['meta']['model'] + if '@nextrun' not in model: + model = model[0] + self._nextrun = dt_util.str_to_datetime(model['@nextrun'], + "%Y-%m-%dT%H:%M:%SZ") diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 2e1c0c9b377..fc08a4c09d8 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -8,10 +8,9 @@ https://home-assistant.io/components/sun/ """ import logging from datetime import timedelta -import urllib import homeassistant.util as util -import homeassistant.util.dt as dt_util +from homeassistant.util import location as location_util, dt as dt_util from homeassistant.helpers.event import ( track_point_in_utc_time, track_utc_time_change) from homeassistant.helpers.entity import Entity @@ -111,21 +110,13 @@ def setup(hass, config): platform_config = config.get(DOMAIN, {}) elevation = platform_config.get(CONF_ELEVATION) + if elevation is None: + elevation = location_util.elevation(latitude, longitude) - from astral import Location, GoogleGeocoder + from astral import Location location = Location(('', '', latitude, longitude, hass.config.time_zone, - elevation or 0)) - - if elevation is None: - google = GoogleGeocoder() - try: - google._get_elevation(location) # pylint: disable=protected-access - _LOGGER.info( - 'Retrieved elevation from Google: %s', location.elevation) - except urllib.error.URLError: - # If no internet connection available etc. - pass + elevation)) sun = Sun(hass, location) sun.point_in_time_listener(dt_util.utcnow()) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index e7b3c629f39..e2fbb256fb5 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.components import ( - group, discovery, wink, isy994, verisure, zwave) + group, discovery, wink, isy994, verisure, zwave, tellduslive) DOMAIN = 'switch' SCAN_INTERVAL = 30 @@ -40,6 +40,7 @@ DISCOVERY_PLATFORMS = { isy994.DISCOVER_SWITCHES: 'isy994', verisure.DISCOVER_SWITCHES: 'verisure', zwave.DISCOVER_SWITCHES: 'zwave', + tellduslive.DISCOVER_SWITCHES: 'tellduslive', } PROP_TO_ATTR = { diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 91171be3680..a90ed61c3e2 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -10,6 +10,8 @@ import logging import subprocess from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.util import template _LOGGER = logging.getLogger(__name__) @@ -24,20 +26,30 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): for dev_name, properties in switches.items(): devices.append( CommandSwitch( + hass, properties.get('name', dev_name), properties.get('oncmd', 'true'), - properties.get('offcmd', 'true'))) + properties.get('offcmd', 'true'), + properties.get('statecmd', False), + properties.get(CONF_VALUE_TEMPLATE, False))) add_devices_callback(devices) class CommandSwitch(SwitchDevice): """ Represents a switch that can be togggled using shell commands. """ - def __init__(self, name, command_on, command_off): + + # pylint: disable=too-many-arguments + def __init__(self, hass, name, command_on, command_off, + command_state, value_template): + + self._hass = hass self._name = name self._state = False self._command_on = command_on self._command_off = command_off + self._command_state = command_state + self._value_template = value_template @staticmethod def _switch(command): @@ -51,10 +63,27 @@ class CommandSwitch(SwitchDevice): return success + @staticmethod + def _query_state_value(command): + """ Execute state command for return value. """ + _LOGGER.info('Running state command: %s', command) + + try: + return_value = subprocess.check_output(command, shell=True) + return return_value.strip().decode('utf-8') + except subprocess.CalledProcessError: + _LOGGER.error('Command failed: %s', command) + + @staticmethod + def _query_state_code(command): + """ Execute state command for return code. """ + _LOGGER.info('Running state command: %s', command) + return subprocess.call(command, shell=True) == 0 + @property def should_poll(self): - """ No polling needed. """ - return False + """ Only poll if we have statecmd. """ + return self._command_state is not None @property def name(self): @@ -66,14 +95,34 @@ class CommandSwitch(SwitchDevice): """ True if device is on. """ return self._state + def _query_state(self): + """ Query for state. """ + if not self._command_state: + _LOGGER.error('No state command specified') + return + if self._value_template: + return CommandSwitch._query_state_value(self._command_state) + return CommandSwitch._query_state_code(self._command_state) + + def update(self): + """ Update device state. """ + if self._command_state: + payload = str(self._query_state()) + if self._value_template: + payload = template.render_with_possible_json_value( + self._hass, self._value_template, payload) + self._state = (payload.lower() == "true") + def turn_on(self, **kwargs): """ Turn the device on. """ - if CommandSwitch._switch(self._command_on): + if (CommandSwitch._switch(self._command_on) and + not self._command_state): self._state = True - self.update_ha_state() + self.update_ha_state() def turn_off(self, **kwargs): """ Turn the device off. """ - if CommandSwitch._switch(self._command_off): + if (CommandSwitch._switch(self._command_off) and + not self._command_state): self._state = False - self.update_ha_state() + self.update_ha_state() diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 2435829637e..5c4b9b37e1e 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -18,7 +18,7 @@ DEFAULT_BODY_ON = "ON" DEFAULT_BODY_OFF = "OFF" -# pylint: disable=unused-argument +# pylint: disable=unused-argument, def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Get REST switch. """ @@ -32,11 +32,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): requests.get(resource, timeout=10) except requests.exceptions.MissingSchema: _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// to your URL.") + "Add http:// or https:// to your URL") return False except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint. " - "Please check the IP address in the configuration file.") + _LOGGER.error("No route to resource/endpoint: %s", resource) return False add_devices_callback([RestSwitch( diff --git a/homeassistant/components/switch/tellduslive.py b/homeassistant/components/switch/tellduslive.py new file mode 100644 index 00000000000..d515dcb50a2 --- /dev/null +++ b/homeassistant/components/switch/tellduslive.py @@ -0,0 +1,73 @@ +""" +homeassistant.components.switch.tellduslive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Tellstick switches using Tellstick Net and +the Telldus Live online service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.tellduslive/ + +""" +import logging + +from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN) +from homeassistant.components import tellduslive +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['tellduslive'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Find and return Tellstick switches. """ + switches = tellduslive.NETWORK.get_switches() + add_devices([TelldusLiveSwitch(switch["name"], + switch["id"]) + for switch in switches if switch["type"] == "device"]) + + +class TelldusLiveSwitch(ToggleEntity): + """ Represents a Tellstick switch. """ + + def __init__(self, name, switch_id): + self._name = name + self._id = switch_id + self._state = STATE_UNKNOWN + self.update() + + @property + def should_poll(self): + """ Tells Home Assistant to poll this entity. """ + return False + + @property + def name(self): + """ Returns the name of the switch if any. """ + return self._name + + def update(self): + from tellive.live import const + state = tellduslive.NETWORK.get_switch_state(self._id) + if state == const.TELLSTICK_TURNON: + self._state = STATE_ON + elif state == const.TELLSTICK_TURNOFF: + self._state = STATE_OFF + else: + self._state = STATE_UNKNOWN + + @property + def is_on(self): + """ True if switch is on. """ + return self._state == STATE_ON + + def turn_on(self, **kwargs): + """ Turns the switch on. """ + if tellduslive.NETWORK.turn_switch_on(self._id): + self._state = STATE_ON + self.update_ha_state() + + def turn_off(self, **kwargs): + """ Turns the switch off. """ + if tellduslive.NETWORK.turn_switch_off(self._id): + self._state = STATE_OFF + self.update_ha_state() diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 14983919c64..614b588f36f 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -13,11 +13,13 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) + ATTR_BATTERY_LEVEL, + ATTR_TRIPPED, + ATTR_ARMED, + ATTR_LAST_TRIP_TIME, + EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) @@ -37,7 +39,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: devices = vera_controller.get_devices([ @@ -53,7 +64,8 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_switches.append(VeraSwitch(device, extra_data)) + vera_switches.append( + VeraSwitch(device, vera_controller, extra_data)) return vera_switches @@ -66,9 +78,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSwitch(ToggleEntity): """ Represents a Vera Switch. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device self.extra_data = extra_data + self.controller = controller if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') else: @@ -77,6 +90,16 @@ class VeraSwitch(ToggleEntity): # for debouncing status check after command is sent self.last_command_send = 0 + self.controller.register(vera_device) + self.controller.on( + vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s', self.name) + self.update_ha_state(True) + @property def name(self): """ Get the mame of the switch. """ @@ -118,6 +141,11 @@ class VeraSwitch(ToggleEntity): self.vera_device.switch_off() self.is_on_status = False + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + @property def is_on(self): """ True if device is on. """ diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py index a2893df76dd..c698a33ce18 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/switch/verisure.py @@ -25,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): switches.extend([ VerisureSmartplug(value) - for value in verisure.get_smartplug_status().values() + for value in verisure.SMARTPLUG_STATUS.values() if verisure.SHOW_SMARTPLUGS ]) @@ -36,31 +36,29 @@ class VerisureSmartplug(SwitchDevice): """ Represents a Verisure smartplug. """ def __init__(self, smartplug_status): self._id = smartplug_status.id - self.status_on = verisure.MY_PAGES.SMARTPLUG_ON - self.status_off = verisure.MY_PAGES.SMARTPLUG_OFF @property def name(self): """ Get the name (location) of the smartplug. """ - return verisure.get_smartplug_status()[self._id].location + return verisure.SMARTPLUG_STATUS[self._id].location @property def is_on(self): """ Returns True if on """ - plug_status = verisure.get_smartplug_status()[self._id].status - return plug_status == self.status_on + plug_status = verisure.SMARTPLUG_STATUS[self._id].status + return plug_status == 'on' def turn_on(self): """ Set smartplug status on. """ - verisure.MY_PAGES.set_smartplug_status( - self._id, - self.status_on) + verisure.MY_PAGES.smartplug.set(self._id, 'on') + verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'on') + verisure.update_smartplug() def turn_off(self): """ Set smartplug status off. """ - verisure.MY_PAGES.set_smartplug_status( - self._id, - self.status_off) + verisure.MY_PAGES.smartplug.set(self._id, 'off') + verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'off') + verisure.update_smartplug() def update(self): - verisure.update() + verisure.update_smartplug() diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index bad471ce437..ad21463ea17 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -9,11 +9,14 @@ https://home-assistant.io/components/switch.wemo/ import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['pywemo==0.3.3'] +REQUIREMENTS = ['pywemo==0.3.7'] _LOGGER = logging.getLogger(__name__) +_WEMO_SUBSCRIPTION_REGISTRY = None + # pylint: disable=unused-argument, too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -21,6 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import pywemo import pywemo.discovery as discovery + global _WEMO_SUBSCRIPTION_REGISTRY + if _WEMO_SUBSCRIPTION_REGISTRY is None: + _WEMO_SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry() + _WEMO_SUBSCRIPTION_REGISTRY.start() + + def stop_wemo(event): + """ Shutdown Wemo subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + _WEMO_SUBSCRIPTION_REGISTRY.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo) + if discovery_info is not None: location = discovery_info[2] mac = discovery_info[3] @@ -47,6 +62,23 @@ class WemoSwitch(SwitchDevice): self.insight_params = None self.maker_params = None + _WEMO_SUBSCRIPTION_REGISTRY.register(wemo) + _WEMO_SUBSCRIPTION_REGISTRY.on( + wemo, None, self._update_callback) + + def _update_callback(self, _device, _params): + """ Called by the wemo device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s, sevice=%s', + self.name, _device) + self.update_ha_state(True) + + @property + def should_poll(self): + """ No polling should be needed with subscriptions """ + # but leave in for initial version in case of issues. + return True + @property def unique_id(self): """ Returns the id of this WeMo switch """ diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py new file mode 100644 index 00000000000..5c314032b27 --- /dev/null +++ b/homeassistant/components/tellduslive.py @@ -0,0 +1,209 @@ +""" +homeassistant.components.tellduslive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tellduslive Component + +This component adds support for the Telldus Live service. +Telldus Live is the online service used with Tellstick Net devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.tellduslive/ + +Developer access to the Telldus Live service is neccessary +API keys can be aquired from https://api.telldus.com/keys/index + +Tellstick Net devices can be auto discovered using the method described in: +https://developer.telldus.com/doxygen/html/TellStickNet.html + +It might be possible to communicate with the Tellstick Net device +directly, bypassing the Tellstick Live service. +This however is poorly documented and yet not fully supported (?) according to +http://developer.telldus.se/ticket/114 and +https://developer.telldus.com/doxygen/html/TellStickNet.html + +API requests to certain methods, as described in +https://api.telldus.com/explore/sensor/info +are limited to one request every 10 minutes + +""" + +from datetime import timedelta +import logging + +from homeassistant.loader import get_component +from homeassistant import bootstrap +from homeassistant.util import Throttle +from homeassistant.helpers import validate_config +from homeassistant.const import ( + EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED) + + +DOMAIN = "tellduslive" +DISCOVER_SWITCHES = "tellduslive.switches" +DISCOVER_SENSORS = "tellduslive.sensors" + +CONF_PUBLIC_KEY = "public_key" +CONF_PRIVATE_KEY = "private_key" +CONF_TOKEN = "token" +CONF_TOKEN_SECRET = "token_secret" + +REQUIREMENTS = ['tellive-py==0.5.2'] +_LOGGER = logging.getLogger(__name__) + +NETWORK = None + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) + + +class TelldusLiveData(object): + """ Gets the latest data and update the states. """ + + def __init__(self, hass, config): + + public_key = config[DOMAIN].get(CONF_PUBLIC_KEY) + private_key = config[DOMAIN].get(CONF_PRIVATE_KEY) + token = config[DOMAIN].get(CONF_TOKEN) + token_secret = config[DOMAIN].get(CONF_TOKEN_SECRET) + + from tellive.client import LiveClient + from tellive.live import TelldusLive + + self._sensors = [] + self._switches = [] + + self._client = LiveClient(public_key=public_key, + private_key=private_key, + access_token=token, + access_secret=token_secret) + self._api = TelldusLive(self._client) + + def update(self, hass, config): + """ Send discovery event if component not yet discovered """ + self._update_sensors() + self._update_switches() + for component_name, found_devices, discovery_type in \ + (('sensor', self._sensors, DISCOVER_SENSORS), + ('switch', self._switches, DISCOVER_SWITCHES)): + if len(found_devices): + component = get_component(component_name) + bootstrap.setup_component(hass, component.DOMAIN, config) + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, + {ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: {}}) + + def _request(self, what, **params): + """ Sends a request to the tellstick live API """ + + from tellive.live import const + + supported_methods = const.TELLSTICK_TURNON \ + | const.TELLSTICK_TURNOFF \ + | const.TELLSTICK_TOGGLE + + default_params = {'supportedMethods': supported_methods, + "includeValues": 1, + "includeScale": 1} + + params.update(default_params) + + # room for improvement: the telllive library doesn't seem to + # re-use sessions, instead it opens a new session for each request + # this needs to be fixed + response = self._client.request(what, params) + return response + + def check_request(self, what, **params): + """ Make request, check result if successful """ + response = self._request(what, **params) + return response['status'] == "success" + + def validate_session(self): + """ Make a dummy request to see if the session is valid """ + try: + response = self._request("user/profile") + return 'email' in response + except RuntimeError: + return False + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def _update_sensors(self): + """ Get the latest sensor data from Telldus Live """ + _LOGGER.info("Updating sensors from Telldus Live") + self._sensors = self._request("sensors/list")["sensor"] + + def _update_switches(self): + """ Get the configured switches from Telldus Live""" + _LOGGER.info("Updating switches from Telldus Live") + self._switches = self._request("devices/list")["device"] + # filter out any group of switches + self._switches = [switch for switch in self._switches + if switch["type"] == "device"] + + def get_sensors(self): + """ Get the configured sensors """ + self._update_sensors() + return self._sensors + + def get_switches(self): + """ Get the configured switches """ + self._update_switches() + return self._switches + + def get_sensor_value(self, sensor_id, sensor_name): + """ Get the latest (possibly cached) sensor value """ + self._update_sensors() + for component in self._sensors: + if component["id"] == sensor_id: + for sensor in component["data"]: + if sensor["name"] == sensor_name: + return (sensor["value"], + component["battery"], + component["lastUpdated"]) + + def get_switch_state(self, switch_id): + """ returns state of switch. """ + _LOGGER.info("Updating switch state from Telldus Live") + response = self._request("device/info", id=switch_id)["state"] + return int(response) + + def turn_switch_on(self, switch_id): + """ turn switch off """ + return self.check_request("device/turnOn", id=switch_id) + + def turn_switch_off(self, switch_id): + """ turn switch on """ + return self.check_request("device/turnOff", id=switch_id) + + +def setup(hass, config): + """ Setup the tellduslive component """ + + # fixme: aquire app key and provide authentication + # using username + password + if not validate_config(config, + {DOMAIN: [CONF_PUBLIC_KEY, + CONF_PRIVATE_KEY, + CONF_TOKEN, + CONF_TOKEN_SECRET]}, + _LOGGER): + _LOGGER.error( + "Configuration Error: " + "Please make sure you have configured your keys " + "that can be aquired from https://api.telldus.com/keys/index") + return False + + global NETWORK + NETWORK = TelldusLiveData(hass, config) + + if not NETWORK.validate_session(): + _LOGGER.error( + "Authentication Error: " + "Please make sure you have configured your keys " + "that can be aquired from https://api.telldus.com/keys/index") + return False + + NETWORK.update(hass, config) + + return True diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 837acbd18ae..5a4d7c7ea99 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -7,6 +7,8 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/verisure/ """ import logging +import time + from datetime import timedelta from homeassistant import bootstrap @@ -26,15 +28,14 @@ DISCOVER_SWITCHES = 'verisure.switches' DISCOVER_ALARMS = 'verisure.alarm_control_panel' DEPENDENCIES = ['alarm_control_panel'] -REQUIREMENTS = [ - 'https://github.com/persandstrom/python-verisure/archive/' - '9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6' -] +REQUIREMENTS = ['vsure==0.4.3'] _LOGGER = logging.getLogger(__name__) MY_PAGES = None -STATUS = {} +ALARM_STATUS = {} +SMARTPLUG_STATUS = {} +CLIMATE_STATUS = {} VERISURE_LOGIN_ERROR = None VERISURE_ERROR = None @@ -47,7 +48,7 @@ SHOW_SMARTPLUGS = True # if wrong password was given don't try again WRONG_PASSWORD_GIVEN = False -MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=5) +MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=1) def setup(hass, config): @@ -60,10 +61,6 @@ def setup(hass, config): from verisure import MyPages, LoginError, Error - STATUS[MyPages.DEVICE_ALARM] = {} - STATUS[MyPages.DEVICE_CLIMATE] = {} - STATUS[MyPages.DEVICE_SMARTPLUG] = {} - global SHOW_THERMOMETERS, SHOW_HYGROMETERS, SHOW_ALARM, SHOW_SMARTPLUGS SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1')) SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1')) @@ -84,7 +81,9 @@ def setup(hass, config): _LOGGER.error('Could not log in to verisure mypages, %s', ex) return False - update() + update_alarm() + update_climate() + update_smartplug() # Load components for the devices in the ISY controller that we support for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), @@ -101,24 +100,10 @@ def setup(hass, config): return True -def get_alarm_status(): - """ Return a list of status overviews for alarm components. """ - return STATUS[MY_PAGES.DEVICE_ALARM] - - -def get_climate_status(): - """ Return a list of status overviews for alarm components. """ - return STATUS[MY_PAGES.DEVICE_CLIMATE] - - -def get_smartplug_status(): - """ Return a list of status overviews for alarm components. """ - return STATUS[MY_PAGES.DEVICE_SMARTPLUG] - - def reconnect(): """ Reconnect to verisure mypages. """ try: + time.sleep(1) MY_PAGES.login() except VERISURE_LOGIN_ERROR as ex: _LOGGER.error("Could not login to Verisure mypages, %s", ex) @@ -129,19 +114,31 @@ def reconnect(): @Throttle(MIN_TIME_BETWEEN_REQUESTS) -def update(): +def update_alarm(): + """ Updates the status of alarms. """ + update_component(MY_PAGES.alarm.get, ALARM_STATUS) + + +@Throttle(MIN_TIME_BETWEEN_REQUESTS) +def update_climate(): + """ Updates the status of climate sensors. """ + update_component(MY_PAGES.climate.get, CLIMATE_STATUS) + + +@Throttle(MIN_TIME_BETWEEN_REQUESTS) +def update_smartplug(): + """ Updates the status of smartplugs. """ + update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS) + + +def update_component(get_function, status): """ Updates the status of verisure components. """ if WRONG_PASSWORD_GIVEN: _LOGGER.error('Wrong password') return - try: - for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_ALARM): - STATUS[MY_PAGES.DEVICE_ALARM][overview.id] = overview - for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_CLIMATE): - STATUS[MY_PAGES.DEVICE_CLIMATE][overview.id] = overview - for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_SMARTPLUG): - STATUS[MY_PAGES.DEVICE_SMARTPLUG][overview.id] = overview - except ConnectionError as ex: + for overview in get_function(): + status[overview.id] = overview + except (ConnectionError, VERISURE_ERROR) as ex: _LOGGER.error('Caught connection error %s, tries to reconnect', ex) reconnect() diff --git a/homeassistant/const.py b/homeassistant/const.py index 287ae7998d2..82276d81b48 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """ Constants used by Home Assistant components. """ -__version__ = "0.10.0.dev0" +__version__ = "0.11.0.dev0" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' diff --git a/homeassistant/core.py b/homeassistant/core.py index 8ea55c653e3..55ceddb37c7 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1,6 +1,5 @@ """ -homeassistant -~~~~~~~~~~~~~ +Core components of Home Assistant. Home Assistant is a Home Automation framework for observing the state of entities and react to changes. @@ -53,9 +52,10 @@ _MockHA = namedtuple("MockHomeAssistant", ['bus']) class HomeAssistant(object): - """ Core class to route all communication to right components. """ + """Root object of the Home Assistant home automation.""" def __init__(self): + """Initialize new Home Assistant object.""" self.pool = pool = create_worker_pool() self.bus = EventBus(pool) self.services = ServiceRegistry(self.bus, pool) @@ -63,7 +63,7 @@ class HomeAssistant(object): self.config = Config() def start(self): - """ Start home assistant. """ + """Start home assistant.""" _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) @@ -71,12 +71,11 @@ class HomeAssistant(object): self.bus.fire(EVENT_HOMEASSISTANT_START) def block_till_stopped(self): - """ Will register service homeassistant/stop and - will block until called. """ + """Register service homeassistant/stop and will block until called.""" request_shutdown = threading.Event() def stop_homeassistant(*args): - """ Stops Home Assistant. """ + """Stop Home Assistant.""" request_shutdown.set() self.services.register( @@ -98,7 +97,7 @@ class HomeAssistant(object): self.stop() def stop(self): - """ Stops Home Assistant and shuts down all threads. """ + """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") self.bus.fire(EVENT_HOMEASSISTANT_STOP) @@ -150,8 +149,7 @@ class HomeAssistant(object): class JobPriority(util.OrderedEnum): - """ Provides priorities for bus events. """ - # pylint: disable=no-init,too-few-public-methods + """Provides job priorities for event bus jobs.""" EVENT_CALLBACK = 0 EVENT_SERVICE = 1 @@ -161,7 +159,7 @@ class JobPriority(util.OrderedEnum): @staticmethod def from_event_type(event_type): - """ Returns a priority based on event type. """ + """Return a priority based on event type.""" if event_type == EVENT_TIME_CHANGED: return JobPriority.EVENT_TIME elif event_type == EVENT_STATE_CHANGED: @@ -175,8 +173,7 @@ class JobPriority(util.OrderedEnum): class EventOrigin(enum.Enum): - """ Distinguish between origin of event. """ - # pylint: disable=no-init,too-few-public-methods + """Represents origin of an event.""" local = "LOCAL" remote = "REMOTE" @@ -185,14 +182,15 @@ class EventOrigin(enum.Enum): return self.value -# pylint: disable=too-few-public-methods class Event(object): - """ Represents an event within the Bus. """ + # pylint: disable=too-few-public-methods + """Represents an event within the Bus.""" __slots__ = ['event_type', 'data', 'origin', 'time_fired'] def __init__(self, event_type, data=None, origin=EventOrigin.local, time_fired=None): + """Initialize a new event.""" self.event_type = event_type self.data = data or {} self.origin = origin @@ -200,7 +198,7 @@ class Event(object): time_fired or dt_util.utcnow()) def as_dict(self): - """ Returns a dict representation of this Event. """ + """Create a dict representation of this Event.""" return { 'event_type': self.event_type, 'data': dict(self.data), @@ -227,26 +225,23 @@ class Event(object): class EventBus(object): - """ Class that allows different components to communicate via services - and events. - """ + """Allows firing of and listening for events.""" def __init__(self, pool=None): + """Initialize a new event bus.""" self._listeners = {} self._lock = threading.Lock() self._pool = pool or create_worker_pool() @property def listeners(self): - """ Dict with events that is being listened for and the number - of listeners. - """ + """Dict with events and the number of listeners.""" with self._lock: return {key: len(self._listeners[key]) for key in self._listeners} def fire(self, event_type, event_data=None, origin=EventOrigin.local): - """ Fire an event. """ + """Fire an event.""" if not self._pool.running: raise HomeAssistantError('Home Assistant has shut down.') @@ -271,7 +266,7 @@ class EventBus(object): self._pool.add_job(job_priority, (func, event)) def listen(self, event_type, listener): - """ Listen for all events or events of a specific type. + """Listen for all events or events of a specific type. To listen to all events specify the constant ``MATCH_ALL`` as event_type. @@ -283,7 +278,7 @@ class EventBus(object): self._listeners[event_type] = [listener] def listen_once(self, event_type, listener): - """ Listen once for event of a specific type. + """Listen once for event of a specific type. To listen to all events specify the constant ``MATCH_ALL`` as event_type. @@ -292,7 +287,7 @@ class EventBus(object): """ @ft.wraps(listener) def onetime_listener(event): - """ Removes listener from eventbus and then fires listener. """ + """Remove listener from eventbus and then fires listener.""" if hasattr(onetime_listener, 'run'): return # Set variable so that we will never run twice. @@ -311,7 +306,7 @@ class EventBus(object): return onetime_listener def remove_listener(self, event_type, listener): - """ Removes a listener of a specific event_type. """ + """Remove a listener of a specific event_type.""" with self._lock: try: self._listeners[event_type].remove(listener) @@ -343,6 +338,7 @@ class State(object): # pylint: disable=too-many-arguments def __init__(self, entity_id, state, attributes=None, last_changed=None, last_updated=None): + """Initialize a new state.""" if not ENTITY_ID_PATTERN.match(entity_id): raise InvalidEntityFormatError(( "Invalid entity id encountered: {}. " @@ -363,31 +359,33 @@ class State(object): @property def domain(self): - """ Returns domain of this state. """ + """Domain of this state.""" return util.split_entity_id(self.entity_id)[0] @property def object_id(self): - """ Returns object_id of this state. """ + """Object id of this state.""" return util.split_entity_id(self.entity_id)[1] @property def name(self): - """ Name to represent this state. """ + """Name of this state.""" return ( self.attributes.get(ATTR_FRIENDLY_NAME) or self.object_id.replace('_', ' ')) def copy(self): - """ Creates a copy of itself. """ + """Return a copy of the state.""" return State(self.entity_id, self.state, dict(self.attributes), self.last_changed, self.last_updated) def as_dict(self): - """ Converts State to a dict to be used within JSON. - Ensures: state == State.from_dict(state.as_dict()) """ + """Return a dict representation of the State. + To be used for JSON serialization. + Ensures: state == State.from_dict(state.as_dict()) + """ return {'entity_id': self.entity_id, 'state': self.state, 'attributes': self.attributes, @@ -396,11 +394,11 @@ class State(object): @classmethod def from_dict(cls, json_dict): - """ Static method to create a state from a dict. - Ensures: state == State.from_json_dict(state.to_json_dict()) """ + """Initialize a state from a dict. - if not (json_dict and - 'entity_id' in json_dict and + Ensures: state == State.from_json_dict(state.to_json_dict()) + """ + if not (json_dict and 'entity_id' in json_dict and 'state' in json_dict): return None @@ -433,15 +431,16 @@ class State(object): class StateMachine(object): - """ Helper class that tracks the state of different entities. """ + """Helper class that tracks the state of different entities.""" def __init__(self, bus): + """Initialize state machine.""" self._states = {} self._bus = bus self._lock = threading.Lock() def entity_ids(self, domain_filter=None): - """ List of entity ids that are being tracked. """ + """List of entity ids that are being tracked.""" if domain_filter is None: return list(self._states.keys()) @@ -451,35 +450,43 @@ class StateMachine(object): if state.domain == domain_filter] def all(self): - """ Returns a list of all states. """ + """Create a list of all states.""" with self._lock: return [state.copy() for state in self._states.values()] def get(self, entity_id): - """ Returns the state of the specified entity. """ + """Retrieve state of entity_id or None if not found.""" state = self._states.get(entity_id.lower()) # Make a copy so people won't mutate the state return state.copy() if state else None def is_state(self, entity_id, state): - """ Returns True if entity exists and is specified state. """ + """Test if entity exists and is specified state.""" entity_id = entity_id.lower() return (entity_id in self._states and self._states[entity_id].state == state) - def remove(self, entity_id): - """ Removes an entity from the state machine. + def is_state_attr(self, entity_id, name, value): + """Test if entity exists and has a state attribute set to value.""" + entity_id = entity_id.lower() - Returns boolean to indicate if an entity was removed. """ + return (entity_id in self._states and + self._states[entity_id].attributes.get(name, None) == value) + + def remove(self, entity_id): + """Remove the state of an entity. + + Returns boolean to indicate if an entity was removed. + """ entity_id = entity_id.lower() with self._lock: return self._states.pop(entity_id, None) is not None def set(self, entity_id, new_state, attributes=None): - """ Set the state of an entity, add entity if it does not exist. + """Set the state of an entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. @@ -514,9 +521,7 @@ class StateMachine(object): self._bus.fire(EVENT_STATE_CHANGED, event_data) def track_change(self, entity_ids, action, from_state=None, to_state=None): - """ - DEPRECATED AS OF 8/4/2015 - """ + """DEPRECATED AS OF 8/4/2015.""" _LOGGER.warning( 'hass.states.track_change is deprecated. ' 'Use homeassistant.helpers.event.track_state_change instead.') @@ -527,33 +532,36 @@ class StateMachine(object): # pylint: disable=too-few-public-methods class Service(object): - """ Represents a service. """ + """Represents a callable service.""" __slots__ = ['func', 'description', 'fields'] def __init__(self, func, description, fields): + """Initialize a service.""" self.func = func self.description = description or '' self.fields = fields or {} def as_dict(self): - """ Return dictionary representation of this service. """ + """Return dictionary representation of this service.""" return { 'description': self.description, 'fields': self.fields, } def __call__(self, call): + """Execute the service.""" self.func(call) # pylint: disable=too-few-public-methods class ServiceCall(object): - """ Represents a call to a service. """ + """Represents a call to a service.""" __slots__ = ['domain', 'service', 'data'] def __init__(self, domain, service, data=None): + """Initialize a service call.""" self.domain = domain self.service = service self.data = data or {} @@ -567,9 +575,10 @@ class ServiceCall(object): class ServiceRegistry(object): - """ Offers services over the eventbus. """ + """Offers services over the eventbus.""" def __init__(self, bus, pool=None): + """Initialize a service registry.""" self._services = {} self._lock = threading.Lock() self._pool = pool or create_worker_pool() @@ -579,14 +588,14 @@ class ServiceRegistry(object): @property def services(self): - """ Dict with per domain a list of available services. """ + """Dict with per domain a list of available services.""" with self._lock: return {domain: {key: value.as_dict() for key, value in self._services[domain].items()} for domain in self._services} def has_service(self, domain, service): - """ Returns True if specified service exists. """ + """Test if specified service exists.""" return service in self._services.get(domain, []) def register(self, domain, service, service_func, description=None): @@ -611,7 +620,8 @@ class ServiceRegistry(object): def call(self, domain, service, service_data=None, blocking=False): """ - Calls specified service. + Call a service. + Specify blocking=True to wait till service is executed. Waits a maximum of SERVICE_CALL_LIMIT. @@ -635,10 +645,7 @@ class ServiceRegistry(object): executed_event = threading.Event() def service_executed(call): - """ - Called when a service is executed. - Will set the event if matches our service call. - """ + """Callback method that is called when service is executed.""" if call.data[ATTR_SERVICE_CALL_ID] == call_id: executed_event.set() @@ -653,7 +660,7 @@ class ServiceRegistry(object): return success def _event_to_service_call(self, event): - """ Calls a service from an event. """ + """Callback for SERVICE_CALLED events from the event bus.""" service_data = dict(event.data) domain = service_data.pop(ATTR_DOMAIN, None) service = service_data.pop(ATTR_SERVICE, None) @@ -670,7 +677,7 @@ class ServiceRegistry(object): (service_handler, service_call))) def _execute_service(self, service_and_call): - """ Executes a service and fires a SERVICE_EXECUTED event. """ + """Execute a service and fires a SERVICE_EXECUTED event.""" service, call = service_and_call service(call) @@ -680,16 +687,17 @@ class ServiceRegistry(object): {ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]}) def _generate_unique_id(self): - """ Generates a unique service call id. """ + """Generate a unique service call id.""" self._cur_id += 1 return "{}-{}".format(id(self), self._cur_id) class Config(object): - """ Configuration settings for Home Assistant. """ + """Configuration settings for Home Assistant.""" # pylint: disable=too-many-instance-attributes def __init__(self): + """Initialize a new config object.""" self.latitude = None self.longitude = None self.temperature_unit = None @@ -709,15 +717,15 @@ class Config(object): self.config_dir = get_default_config_dir() def distance(self, lat, lon): - """ Calculate distance from Home Assistant in meters. """ + """Calculate distance from Home Assistant in meters.""" return location.distance(self.latitude, self.longitude, lat, lon) def path(self, *path): - """ Returns path to the file within the config dir. """ + """Generate path to the file within the config dir.""" return os.path.join(self.config_dir, *path) def temperature(self, value, unit): - """ Converts temperature to user preferred unit if set. """ + """Convert temperature to user preferred unit if set.""" if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and self.temperature_unit and unit != self.temperature_unit): return value, unit @@ -732,7 +740,7 @@ class Config(object): self.temperature_unit) def as_dict(self): - """ Converts config to a dictionary. """ + """Create a dict representation of this dict.""" time_zone = self.time_zone or dt_util.UTC return { @@ -747,7 +755,7 @@ class Config(object): def create_timer(hass, interval=TIMER_INTERVAL): - """ Creates a timer. Timer will start on HOMEASSISTANT_START. """ + """Create a timer that will start on HOMEASSISTANT_START.""" # We want to be able to fire every time a minute starts (seconds=0). # We want this so other modules can use that to make sure they fire # every minute. @@ -810,12 +818,12 @@ def create_timer(hass, interval=TIMER_INTERVAL): def create_worker_pool(worker_count=None): - """ Creates a worker pool to be used. """ + """Create a worker pool.""" if worker_count is None: worker_count = MIN_WORKER_THREAD def job_handler(job): - """ Called whenever a job is available to do. """ + """Called whenever a job is available to do.""" try: func, arg = job func(arg) @@ -825,8 +833,7 @@ def create_worker_pool(worker_count=None): _LOGGER.exception("BusHandler:Exception doing job") def busy_callback(worker_count, current_jobs, pending_jobs_count): - """ Callback to be called when the pool queue gets too big. """ - + """Callback to be called when the pool queue gets too big.""" _LOGGER.warning( "WorkerPool:All %d threads are busy and %d jobs pending", worker_count, pending_jobs_count) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ec22181bf5a..4cf44737f90 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -113,12 +113,16 @@ class EntityComponent(object): def _update_entity_states(self, now): """ Update the states of all the entities. """ + with self.lock: + # We copy the entities because new entities might be detected + # during state update causing deadlocks. + entities = list(entity for entity in self.entities.values() + if entity.should_poll) + self.logger.info("Updating %s entities", self.domain) - with self.lock: - for entity in self.entities.values(): - if entity.should_poll: - entity.update_ha_state(True) + for entity in entities: + entity.update_ha_state(True) def _entity_discovered(self, service, info): """ Called when a entity is discovered. """ diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 26dca7ab0c6..06c9b4c6862 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -53,8 +53,12 @@ def color_xy_brightness_to_RGB(vX, vY, brightness): return (0, 0, 0) Y = brightness - X = (Y / vY) * vX - Z = (Y / vY) * (1 - vX - vY) + if vY != 0: + X = (Y / vY) * vX + Z = (Y / vY) * (1 - vX - vY) + else: + X = 0 + Z = 0 # Convert to RGB using Wide RGB D65 conversion. r = X * 1.612 - Y * 0.203 - Z * 0.302 diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 35795a7ae7f..a2c796c20eb 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -108,14 +108,14 @@ def datetime_to_date_str(dattim): return dattim.strftime(DATE_STR_FORMAT) -def str_to_datetime(dt_str): +def str_to_datetime(dt_str, dt_format=DATETIME_STR_FORMAT): """ Converts a string to a UTC datetime object. @rtype: datetime """ try: return dt.datetime.strptime( - dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc) + dt_str, dt_format).replace(tzinfo=pytz.utc) except ValueError: # If dt_str did not match our format return None diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 398a0a0c56c..185745d9207 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -4,6 +4,8 @@ import collections import requests from vincenty import vincenty +ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' + LocationInfo = collections.namedtuple( "LocationInfo", @@ -34,3 +36,20 @@ def detect_location_info(): def distance(lat1, lon1, lat2, lon2): """ Calculate the distance in meters between two points. """ return vincenty((lat1, lon1), (lat2, lon2)) * 1000 + + +def elevation(latitude, longitude): + """ Return elevation for given latitude and longitude. """ + + req = requests.get(ELEVATION_URL, params={ + 'locations': '{},{}'.format(latitude, longitude), + 'sensor': 'false', + }) + + if req.status_code != 200: + return 0 + + try: + return int(float(req.json()['results'][0]['elevation'])) + except (ValueError, KeyError): + return 0 diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index d0a07507bdf..bc7431ebf6d 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -43,7 +43,8 @@ def render(hass, template, variables=None, **kwargs): try: return ENV.from_string(template, { 'states': AllStates(hass), - 'is_state': hass.states.is_state + 'is_state': hass.states.is_state, + 'is_state_attr': hass.states.is_state_attr }).render(kwargs).strip() except jinja2.TemplateError as err: raise TemplateError(err) diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 5ee64771657..00000000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -testpaths = tests diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 14dfca13f23..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -requests>=2,<3 -pyyaml>=3.11,<4 -pytz>=2015.4 -pip>=7.0.0 -vincenty==0.1.3 \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index bb41f38c6f0..18d308f6d5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1 +pyvera==0.2.1 # homeassistant.components.wink # homeassistant.components.light.wink @@ -157,6 +157,9 @@ transmissionrpc==0.11 # homeassistant.components.sensor.twitch python-twitch==1.2.0 +# homeassistant.components.sensor.yr +xmltodict + # homeassistant.components.sun astral==0.8.1 @@ -170,7 +173,10 @@ hikvision==0.4 orvibo==1.1.0 # homeassistant.components.switch.wemo -pywemo==0.3.3 +pywemo==0.3.7 + +# homeassistant.components.tellduslive +tellive-py==0.5.2 # homeassistant.components.thermostat.heatmiser heatmiserV3==0.9.1 @@ -185,7 +191,7 @@ python-nest==2.6.0 radiotherm==1.2 # homeassistant.components.verisure -https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6 +vsure==0.4.3 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 00000000000..679c0e99ce5 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,6 @@ +flake8>=2.5.0 +pylint>=1.5.1 +coveralls>=1.1 +pytest>=2.6.4 +pytest-cov>=2.2.0 +betamax>=0.5.1 \ No newline at end of file diff --git a/script/bootstrap_server b/script/bootstrap_server index a5533b0596d..f71abda0e65 100755 --- a/script/bootstrap_server +++ b/script/bootstrap_server @@ -6,7 +6,7 @@ python3 -m pip install -r requirements_all.txt REQ_STATUS=$? echo "Installing development dependencies.." -python3 -m pip install flake8 pylint coveralls pytest pytest-cov +python3 -m pip install -r requirements_test.txt REQ_DEV_STATUS=$? diff --git a/script/cibuild b/script/cibuild index 778cbe0db52..beb7b22693d 100755 --- a/script/cibuild +++ b/script/cibuild @@ -5,7 +5,7 @@ cd "$(dirname "$0")/.." -if [ "$TRAVIS_PYTHON_VERSION" != "3.4" ]; then +if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then NO_LINT=1 fi diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d2626a2701a..a134afaa359 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -115,7 +115,6 @@ def main(): if sys.argv[-1] == 'validate': if validate_file(data): - print("requirements_all.txt is up to date.") sys.exit(0) print("******* ERROR") print("requirements_all.txt is not up to date") diff --git a/script/lint b/script/lint index 75667ef88a4..d99d030c86d 100755 --- a/script/lint +++ b/script/lint @@ -3,13 +3,16 @@ cd "$(dirname "$0")/.." echo "Checking style with flake8..." +tput setaf 1 flake8 --exclude www_static homeassistant - FLAKE8_STATUS=$? +tput sgr0 echo "Checking style with pylint..." +tput setaf 1 pylint homeassistant PYLINT_STATUS=$? +tput sgr0 if [ $FLAKE8_STATUS -eq 0 ] then diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000000..aab4b18bc12 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[wheel] +universal = 1 + +[pytest] +testpaths = tests + +[pep257] +ignore = D203,D105 diff --git a/setup.py b/setup.py index 5bdc07700d9..9cc79615c71 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ REQUIRES = [ 'pytz>=2015.4', 'pip>=7.0.0', 'vincenty==0.1.3', - 'jinja2>=2.8' + 'jinja2>=2.8', ] setup( @@ -33,6 +33,7 @@ setup( zip_safe=False, platforms='any', install_requires=REQUIRES, + test_suite='tests', keywords=['home', 'automation'], entry_points={ 'console_scripts': [ @@ -46,5 +47,5 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python :: 3.4', 'Topic :: Home Automation' - ] + ], ) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb2d..37d3307a4ae 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +import betamax + +with betamax.Betamax.configure() as config: + config.cassette_library_dir = 'tests/cassettes' diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json new file mode 100644 index 00000000000..6bd1601260d --- /dev/null +++ b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json @@ -0,0 +1 @@ +{"http_interactions": [{"recorded_at": "2015-12-28T01:34:34", "request": {"method": "GET", "headers": {"Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["python-requests/2.9.1"], "Connection": ["keep-alive"]}, "body": {"string": "", "encoding": "utf-8"}, "uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"}, "response": {"headers": {"Content-Length": ["3598"], "X-forecast-models": ["proff,ecdet"], "Via": ["1.1 varnish"], "Content-Encoding": ["gzip"], "Date": ["Mon, 28 Dec 2015 01:34:34 GMT"], "X-Varnish": ["2670913442 2670013167"], "Expires": ["Mon, 28 Dec 2015 02:01:51 GMT"], "Server": ["Apache"], "Age": ["1574"], "Content-Type": ["text/xml; charset=utf-8"], "X-Backend-Host": ["snipe_loc"], "X-slicenumber": ["30"], "Accept-Ranges": ["bytes"], "Last-Modified": ["Mon, 28 Dec 2015 01:08:20 GMT"], "Vary": ["Accept-Encoding"], "Connection": ["keep-alive"]}, "url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "body": {"base64_string": "H4sIAAAAAAAAA+2dW4/bNhOG7/srBN+vLFKiDotNb5o0LZBD8WU/tOidYytZoT7BlrNJf31lrw+yNKRNiocRdoFcJJFFS5T18OU7w+HdYz4qH/LVZFSOvO+z6Xx9+31dvBo8lOXydjh8fHz0H0N/sfo6pEFAhn+9f/dp/JDPRjfFfF2O5uN84FWfv50vPoxm+Xo5GudPx98txqOyWMyPLY2WhT/LS3++GO6/svqf4XT/sS+LVT4ercsh8bPhetfCwBuvqg/mk1cDGhB2Q+gNTe8DchuktzT4e/DzT57n3VVNjnZ/2/1jMcmn3ry6kleDN7/4b9989ANC2cAr89WsmNcaSu4JvQ2C6s/fA2+1mefzydkXVcez25DdBqQ6Ps+/l9Vnzq8jPZ7+ZbWYnR8Lj8fKxe5IfBOQmyCuf+nweNXD4z3cLVeLyWZceuPpaL1+NVguinn5unoyg+OHy2KWe9uHVf5YVnd56LerrqJ95NDstuXDo/BG07IoN5Oq9WDgTUeHf4TUT5Mwqv5rMf+6/z9CEp/SJKo3tLvKfLasHnC5WeVeUfXr/f39wNvMi/LVYJxP18VmPfC+jaabqonITwbDxumPxXzyuqjubHc92wYmk4E3yb9W3xj4dLB/xB/AEz8t83yyO+nLl4E3W1b9GPjhwPucjzZVd5W723pq4FNZTKd5q5WHzayYFOWPwyXGic8Ol1/d1jifl61zlqt8vT7c7nJ1+PjDH6PjnZIgTIGbHU8Xm0kxr07fnfvhw8Dbf0n1zakftE74svi6++SvH9/WPhoAn5wuHn/Ztv7U8ruPf15qepZPis2sds77N69////7s69J2/1VfH2onfPb729/u3Bhk/xx99u+b/5OXvN+Jjek0czdkR2nF2n7cqi8KYG7N6X63YyLZVE+Nfh077PZ8bb3nXe3/jH7vJju+uiP0aqc/tj1d/U73sw+56vqK830TXJPyUvfcH83MbdvYkSEZX4mQ9isYpQ8YJM6YMmhgV+no433rTrnImNZsuW6DsbGAG9EjM1SPzLEWLDpaxjb/horjKU+Mfau8NWI8XdFniMNgkQORh4svVLp5tYviM8a4odPZ42+S5wVP/3uHJI84z6HDBXJQxmSx7Smld9cz/K0I8tjsh0OdLCcSerlNAZO0MNysGnELCeNy7Wje4y/LYhZzh/hnnGv1ByPRq+cjiAgawhMa0UaeSvsDkbTtWAlftwRrEmwbUKPSG6PJCKwJmlDGnrawAo2jRqs1L4MMf+y9HeyjaZv5KTyHjl9k8qEcZ8DQwR0Csy8RbYyibYe8d4WlkB6V98jSaofjxOks8SYtww2jRrpLvSP8dcFPdL5w92z7xuScvsmRYRZIqebSbgTwfKYzbpiNtNlLzNJe7liYbuL9GCWQtdiArNR5DM9nI0dyBLj7wt6lvDHICx9I+syJwrSOXyKg7l7DvxY6ukIAqYHkhkZUXwyQ2xK55TqYjrk/gjTMijAQz1MJylwLSaYzqDEG6WYoTE7hK+BzL8v6JnOHe+ee99k3Khq/QgKzkrlZRC2zdg6cPZazNJzzB7jgZ++jf65WjprMp0hFSnCbARxWRNmQ8AtuYzZMAO8akupGcy2LLHxuuBGCX8IQtQ3ctI52PvHctJ5P813iHROcDXDlcwcNya43gWkJ7TmOl+L9HA7DHRCOgurX4Em5dwew4RIj4ETNCVoAH1/mehR6C7ZzhDRhQLo2SftCka7Z943GTfGmuFKaIZWUYgom4U10/l64Rx2pGyU6lo2EgDBTFfCWY2yTZfAsxfaSx1oEizJuxin4Fj6RlY3Zwq6eU8qh8+BE2LNcCU2Q2s2hEQnPlEgOulI9DjZOt0aiE4zSSsE9CtcEp0084s9S0QPfBfyB2EG77uqs8sGRwxZ8cKx7rl3DS+6muHKcJZNiEuSWlTvesTSjohNt4JNE2Il1440E3s9bYiFPm8ipgfegoJoDkzlaYgECZo0XoTTbzR9IyeaqdJqwNS12cyLrWa4UpwhBSYiepqcVnZbFM1prCvDGQKio0UrtogO3oIS0Y2ZzQL988yzeIWj3bPvG35oFVmGs6Q1kTqxJtJMU0hP2pqAoNYvyGqympulkewoEiwZvDWOfNrMT/wwVFFEOO5g6RPZrOZAQS3vw8juOM4PqCLLapbKtqMkqPkff14P8qgjyDNda7wplKt2odicqaihLZKDt4DLAOHLnpfEXf4wh7Fv7LnMYcALqJ4dQUBZIlehiFaQClQo29VmznbpHS5yM0jQf8EM34NKLM+YOuSoEivvC3rMwkMQpr6RTc6IlJKaQ4fPYdvbcFD17AgCpidySc00qlUQtSic42S7SlwP0uWWA4aRsQqitogO3oI80EljkmVHACHM27WagSAY6xB2jWXGwjHVsyMIGEuaaU3eBcimRAmyrOvKkUhTTjPNgFQFEWRpky1e7yAL3oIKZM1UKxIrEixpuwgn4Fi6xkap5gOo3D0HOKR6dgQB0GPAfBXwPCRhrey+BNDjrqu7NaY0S+bbhcC8ol9AB29BHujUUEqzWP4gzNtFM/1+9n3DCa6eHUFAWWhZmYiyu4VxspubhN2TmmNNkA2b2s+7FNNLep/UDN6CitdsZv8ooSRBk7eLcP6Npm9kc5qpJtlsmehc0x9Vut1NIId0kp3SNK4menfZnEW6iE4lK2j0P3qobWW3meChWP30M7Xs8FZJ55ZFDnL7QsI1WwiubAdZWMVs657KbsjUPTk4Y7qyHZik/nyhlQVacUZ2Gy+LGVoRpa0xjpCzjyuOyCX9DjKlNVxJ0KqrtmJEV3UyqMStMJBvLMQEtYyXVqZWi4lH9n6GURSlFTQhtAMrnrgl2Kw9qZ0l0kRhR7buxXFTomtx64uy8tSUlamCMMJxvbfmlYqwcsgqrrRFZVpBCYMiz4rS2kp8i8pKW7C3UlZy2TuZubqvtnAF3oJS3VdDOZLCob2fvlWgxCvqqoBIfBOQ6g8gcttHUGBLroBIyFT2SOxerjpN9ZWrlsRWiiyzezdySHIr0bPRi6l9b8Fh3t77YmpSqFZm3/o+3ceObknd9hEEyIKyhoV7UyWnhA8ZpdV1MQoL9JXjkFvFnZmr/ayGrLTpJnkXkZVAuYQ4KnJcMcT308cKlPbUy9qWuy1itcVu+wgCYkmWNqZBViPW1YnA5Hxlh9pG1JqIBZUs69eKaPiKLqyg01KozcBuepdH+J6aWbFSvZysXS/HGrG4MheVm8Xkli5QGtQqnkkQq/P+n4GuUsExEBcVrkVLjblZJFUjViS9MR1YwR3Fkt8rRvh+2llMaVYYO1px9dTZlCt1KS43C9ohQVjgK1PBloYK57q2oq+wJVenIE2MbVtsbcUVdAsq0NJe3+vSIG/jbTG0TlTFgz+gzpEHT7lyl+IytAi0/6AIW4ycKmZZFFuM6SpLGEkmvCf9X/gP3oI8tZpBVztjfD/trFBtdXvoar3Uobt5mpficrUk54gkztzsPZBqSs+iTDKVFJul5So9qxlxtTLC99TQUpsekrAttcxmZx27mSt1UflZ0jtfxUo7X3UNGWZUn8J6YZXarNAYqwTDej+tLLWtnYBZoQ1UhVx9G/bawyJpVCuffD2quu5snYW6ZBVUdvIFVQ7jhPxR3carYsjAihVQFdrOeT/2MkfWhsiMK9kwIUlUNtTYOicdnStdpd4ptFOyMBUr6r3fDt6CPK6ai5vsjOzPyrlybLiHXIkb4nKuoGxoYdJ7eloEfX0G6W4X427ZDURTOXMKRRgubJvZe8MdugUVwz2xP8r31LxKlPJHEWCLK3dRmVhMbhMGEmSntTpXUyvonpO18/n1UEuydgOUKO+WWu3sjAsppNAtqFBLe6XDK8b4ftpYe3krSa0D6xxBK+KK3gibndWePAiglTCfSjOLnCdkKTArS3QZ70xyeWEElXPVxaw2fS4zK5FWWlFzq0RPMUxozE/hDfA23hVD1numZL07XRQdcRVvhMzXgjKpRTX9YrWafmnX7a5CXUpLNiErhnpID7WY4qroGICQDRs+0V975oohvq++lorUOryO7rjF07wRLl8rlfO14kAFW9F5RtZxhvkuL0vv86pYX54gBrpsrUQydGhwYTTY9GVsRZAzZwNbTTPByijfU18rVfO1AGzZWWYYCQQvMkdLqmhWnJwMreuBFXaumZVmzoBlLnyYQk7TZWDFmXQpBz3AMpb5Lhze+2lpMaX1Omnb0rLDK8ZVugyXmSXJq0TFgA/Pp4Uq+krbfm7Qts+uyjhk0HZ6Zuo40Galdk/R0DIUOuSP7zbeF0PEUpkZsr0N5oJYHJHLcFlZUCEWUQllcqo8IzEjDDsSqwKlNvsdT+EZ1ToOCsSCz0GU7SAc4t2bWYlFkdWqlvV6Vfz77zSvQcvQ1JxxlS7DZWRBVQ1E1IpPKfAS1GJodBaTTM8ySi2w7cvYAmsjW9k7NzJV4k80yju3stghR1SyYJYmaP1vVNQz4Q2lmzCB0kXmZEnlZqXbAK78zDDuSiyNqVlyRhZGYqkILQYtAECUnyUc4l2bWUxxaqiydiduTw3tICvmKt2412ZWnJxEloz5HnVdaKgtWghF6J4Ds3RNDQ2F1/ljvI0XBpWb1UrPsuNmxVydG2Nzs6SSSdNtTpD8hmBdK84kma4yyrKrDTESC6wFbSfDwcUI797LshkwdFRHOeaq3BiXkQWVqhQAi4WnaeH1GivovtAw1lWPVHYre4zEcqWwmCmFJRrfnftYqksNVRSWVmDdDZerxWQzLqsGh4/5qHzIV9uH8bP3038Aw70aUvUAAA==", "encoding": "utf-8"}, "status": {"code": 200, "message": "OK"}}}], "recorded_with": "betamax/0.5.1"} \ No newline at end of file diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json new file mode 100644 index 00000000000..4ff2ff18df5 --- /dev/null +++ b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json @@ -0,0 +1 @@ +{"http_interactions": [{"recorded_at": "2015-12-28T01:34:34", "request": {"method": "GET", "headers": {"Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["python-requests/2.9.1"], "Connection": ["keep-alive"]}, "body": {"string": "", "encoding": "utf-8"}, "uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"}, "response": {"headers": {"Content-Length": ["3598"], "X-forecast-models": ["proff,ecdet"], "Via": ["1.1 varnish"], "Content-Encoding": ["gzip"], "Date": ["Mon, 28 Dec 2015 01:34:33 GMT"], "X-Varnish": ["2670913258 2670013167"], "Expires": ["Mon, 28 Dec 2015 02:01:51 GMT"], "Server": ["Apache"], "Age": ["1573"], "Content-Type": ["text/xml; charset=utf-8"], "X-Backend-Host": ["snipe_loc"], "X-slicenumber": ["30"], "Accept-Ranges": ["bytes"], "Last-Modified": ["Mon, 28 Dec 2015 01:08:20 GMT"], "Vary": ["Accept-Encoding"], "Connection": ["keep-alive"]}, "url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "body": {"base64_string": "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", "encoding": "utf-8"}, "status": {"code": 200, "message": "OK"}}}], "recorded_with": "betamax/0.5.1"} \ No newline at end of file diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index de8b2f8121b..26ecc26c72a 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -139,3 +139,189 @@ class TestAutomationSun(unittest.TestCase): fire_time_changed(self.hass, trigger_time) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + + def test_if_action_before(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'before': 'sunrise', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_before_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'before': 'sunrise', + 'before_offset': '+1:00:00' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'after_offset': '+1:00:00' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_before_and_after_during(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '10:00:00 16-09-2015', + sun.STATE_ATTR_NEXT_SETTING: '15:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'before': 'sunset' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py new file mode 100644 index 00000000000..b86f24455de --- /dev/null +++ b/tests/components/device_tracker/test_locative.py @@ -0,0 +1,205 @@ +""" +tests.components.device_tracker.locative +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests the locative device tracker component. +""" + +import unittest +from unittest.mock import patch + +import requests + +from homeassistant import bootstrap, const +import homeassistant.core as ha +import homeassistant.components.device_tracker as device_tracker +import homeassistant.components.http as http +import homeassistant.components.zone as zone + +SERVER_PORT = 8126 +HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) + +hass = None + + +def _url(data={}): + """ Helper method to generate urls. """ + data = "&".join(["{}={}".format(name, value) for name, value in data.items()]) + return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data) + + +@patch('homeassistant.components.http.util.get_local_ip', + return_value='127.0.0.1') +def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name + """ Initalizes a Home Assistant server. """ + global hass + + hass = ha.HomeAssistant() + + # Set up server + bootstrap.setup_component(hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_SERVER_PORT: SERVER_PORT + } + }) + + # Set up API + bootstrap.setup_component(hass, 'api') + + # Set up device tracker + bootstrap.setup_component(hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + 'platform': 'locative' + } + }) + + hass.start() + + +def tearDownModule(): # pylint: disable=invalid-name + """ Stops the Home Assistant server. """ + hass.stop() + +# Stub out update_config or else Travis CI raises an exception +@patch('homeassistant.components.device_tracker.update_config') +class TestLocative(unittest.TestCase): + """ Test Locative """ + + def test_missing_data(self, update_config): + data = { + 'latitude': 1.0, + 'longitude': 1.1, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # No data + req = requests.get(_url({})) + self.assertEqual(422, req.status_code) + + # No latitude + copy = data.copy() + del copy['latitude'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No device + copy = data.copy() + del copy['device'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No location + copy = data.copy() + del copy['id'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No trigger + copy = data.copy() + del copy['trigger'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # Test message + copy = data.copy() + copy['trigger'] = 'test' + req = requests.get(_url(copy)) + self.assertEqual(200, req.status_code) + + # Unknown trigger + copy = data.copy() + copy['trigger'] = 'foobar' + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + + def test_enter_and_exit(self, update_config): + """ Test when there is a known zone """ + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # Enter the Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'home') + + data['id'] = 'HOME' + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'not_home') + + data['id'] = 'hOmE' + data['trigger'] = 'enter' + + # Enter Home again + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'home') + + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'not_home') + + data['id'] = 'work' + data['trigger'] = 'enter' + + # Enter Work + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'work') + + + def test_exit_after_enter(self, update_config): + """ Test when an exit message comes after an enter message """ + + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # Enter Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'home') + + data['id'] = 'Work' + + # Enter Work + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'work') + + data['id'] = 'Home' + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'work') diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py new file mode 100644 index 00000000000..f58aefbce43 --- /dev/null +++ b/tests/components/sensor/test_yr.py @@ -0,0 +1,73 @@ +""" +tests.components.sensor.test_yr +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests Yr sensor. +""" +from unittest.mock import patch + +import pytest + +import homeassistant.core as ha +import homeassistant.components.sensor as sensor + + +@pytest.mark.usefixtures('betamax_session') +class TestSensorYr: + """ Test the Yr sensor. """ + + def setup_method(self, method): + self.hass = ha.HomeAssistant() + self.hass.config.latitude = 32.87336 + self.hass.config.longitude = 117.22743 + + def teardown_method(self, method): + """ Stop down stuff we started. """ + self.hass.stop() + + def test_default_setup(self, betamax_session): + with patch('homeassistant.components.sensor.yr.requests.Session', + return_value=betamax_session): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + 'elevation': 0, + } + }) + + state = self.hass.states.get('sensor.yr_symbol') + + assert state.state.isnumeric() + assert state.attributes.get('unit_of_measurement') is None + + def test_custom_setup(self, betamax_session): + with patch('homeassistant.components.sensor.yr.requests.Session', + return_value=betamax_session): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + 'elevation': 0, + 'monitored_conditions': { + 'pressure', + 'windDirection', + 'humidity', + 'fog', + 'windSpeed' + } + } + }) + + state = self.hass.states.get('sensor.yr_pressure') + assert 'hPa', state.attributes.get('unit_of_measurement') + + state = self.hass.states.get('sensor.yr_wind_direction') + assert '°', state.attributes.get('unit_of_measurement') + + state = self.hass.states.get('sensor.yr_humidity') + assert '%', state.attributes.get('unit_of_measurement') + + state = self.hass.states.get('sensor.yr_fog') + assert '%', state.attributes.get('unit_of_measurement') + + state = self.hass.states.get('sensor.yr_wind_speed') + assert 'm/s', state.attributes.get('unit_of_measurement') diff --git a/tests/components/switch/test_command_switch.py b/tests/components/switch/test_command_switch.py new file mode 100644 index 00000000000..3684f78fff4 --- /dev/null +++ b/tests/components/switch/test_command_switch.py @@ -0,0 +1,158 @@ +""" +tests.components.switch.test_command_switch +~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests command switch. +""" +import json +import os +import tempfile +import unittest + +from homeassistant import core +from homeassistant.const import STATE_ON, STATE_OFF +import homeassistant.components.switch as switch + + +class TestCommandSwitch(unittest.TestCase): + """ Test the command switch. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = core.HomeAssistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_state_none(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + + def test_state_value(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + 'value_template': '{{ value=="1" }}' + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + + def test_state_json_value(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + oncmd = json.dumps({'status': 'ok'}) + offcmd = json.dumps({'status': 'nope'}) + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo \'{}\' > {}'.format(oncmd, path), + 'offcmd': 'echo \'{}\' > {}'.format(offcmd, path), + 'value_template': '{{ value_json.status=="ok" }}' + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + def test_state_code(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index 75aec2b087c..741cfff4bb8 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -149,6 +149,45 @@ class TestAlexa(unittest.TestCase): text = req.json().get('response', {}).get('outputSpeech', {}).get('text') self.assertEqual('You told us your sign is virgo.', text) + def test_intent_request_with_slots_but_no_value(self): + data = { + 'version': '1.0', + 'session': { + 'new': False, + 'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000', + 'application': { + 'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe' + }, + 'attributes': { + 'supportedHoroscopePeriods': { + 'daily': True, + 'weekly': False, + 'monthly': False + } + }, + 'user': { + 'userId': 'amzn1.account.AM3B00000000000000000000000' + } + }, + 'request': { + 'type': 'IntentRequest', + 'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000', + 'timestamp': '2015-05-13T12:34:56Z', + 'intent': { + 'name': 'GetZodiacHoroscopeIntent', + 'slots': { + 'ZodiacSign': { + 'name': 'ZodiacSign', + } + } + } + } + } + req = _req(data) + self.assertEqual(200, req.status_code) + text = req.json().get('response', {}).get('outputSpeech', {}).get('text') + self.assertEqual('You told us your sign is .', text) + def test_intent_request_without_slots(self): data = { 'version': '1.0', diff --git a/tests/components/test_history.py b/tests/components/test_history.py index fdd8270a661..f9e773c499a 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -5,11 +5,10 @@ tests.test_component_history Tests the history component. """ # pylint: disable=protected-access,too-many-public-methods -import time +from datetime import timedelta import os import unittest from unittest.mock import patch -from datetime import timedelta import homeassistant.core as ha import homeassistant.util.dt as dt_util @@ -25,21 +24,24 @@ class TestComponentHistory(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ Init needed objects. """ self.hass = get_test_home_assistant(1) - self.init_rec = False def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ self.hass.stop() - if self.init_rec: - recorder._INSTANCE.block_till_done() - os.remove(self.hass.config.path(recorder.DB_FILE)) + db_path = self.hass.config.path(recorder.DB_FILE) + if os.path.isfile(db_path): + os.remove(db_path) def init_recorder(self): recorder.setup(self.hass, {}) self.hass.start() + self.wait_recording_done() + + def wait_recording_done(self): + """ Block till recording is done. """ + self.hass.pool.block_till_done() recorder._INSTANCE.block_till_done() - self.init_rec = True def test_setup(self): """ Test setup method of history. """ @@ -56,12 +58,11 @@ class TestComponentHistory(unittest.TestCase): for i in range(7): self.hass.states.set(entity_id, "State {}".format(i)) + self.wait_recording_done() + if i > 1: states.append(self.hass.states.get(entity_id)) - self.hass.pool.block_till_done() - recorder._INSTANCE.block_till_done() - self.assertEqual( list(reversed(states)), history.last_5_states(entity_id)) @@ -70,22 +71,9 @@ class TestComponentHistory(unittest.TestCase): self.init_recorder() states = [] - for i in range(5): - state = ha.State( - 'test.point_in_time_{}'.format(i % 5), - "State {}".format(i), - {'attribute_test': i}) - - mock_state_change_event(self.hass, state) - self.hass.pool.block_till_done() - - states.append(state) - - recorder._INSTANCE.block_till_done() - - point = dt_util.utcnow() + timedelta(seconds=1) - - with patch('homeassistant.util.dt.utcnow', return_value=point): + now = dt_util.utcnow() + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=now): for i in range(5): state = ha.State( 'test.point_in_time_{}'.format(i % 5), @@ -93,16 +81,32 @@ class TestComponentHistory(unittest.TestCase): {'attribute_test': i}) mock_state_change_event(self.hass, state) - self.hass.pool.block_till_done() + + states.append(state) + + self.wait_recording_done() + + future = now + timedelta(seconds=1) + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=future): + for i in range(5): + state = ha.State( + 'test.point_in_time_{}'.format(i % 5), + "State {}".format(i), + {'attribute_test': i}) + + mock_state_change_event(self.hass, state) + + self.wait_recording_done() # Get states returns everything before POINT self.assertEqual(states, - sorted(history.get_states(point), + sorted(history.get_states(future), key=lambda state: state.entity_id)) # Test get_state here because we have a DB setup self.assertEqual( - states[0], history.get_state(point, states[0].entity_id)) + states[0], history.get_state(future, states[0].entity_id)) def test_state_changes_during_period(self): self.init_recorder() @@ -110,19 +114,20 @@ class TestComponentHistory(unittest.TestCase): def set_state(state): self.hass.states.set(entity_id, state) - self.hass.pool.block_till_done() - recorder._INSTANCE.block_till_done() - + self.wait_recording_done() return self.hass.states.get(entity_id) - set_state('idle') - set_state('YouTube') - start = dt_util.utcnow() point = start + timedelta(seconds=1) end = point + timedelta(seconds=1) - with patch('homeassistant.util.dt.utcnow', return_value=point): + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=start): + set_state('idle') + set_state('YouTube') + + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=point): states = [ set_state('idle'), set_state('Netflix'), @@ -130,10 +135,11 @@ class TestComponentHistory(unittest.TestCase): set_state('YouTube'), ] - with patch('homeassistant.util.dt.utcnow', return_value=end): + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=end): set_state('Netflix') set_state('Plex') - self.assertEqual( - {entity_id: states}, - history.state_changes_during_period(start, end, entity_id)) + hist = history.state_changes_during_period(start, end, entity_id) + + self.assertEqual(states, hist[entity_id]) diff --git a/tests/components/test_scene.py b/tests/components/test_scene.py index 2fc8fe085c2..0f6663354dd 100644 --- a/tests/components/test_scene.py +++ b/tests/components/test_scene.py @@ -32,6 +32,60 @@ class TestScene(unittest.TestCase): 'scene': [[]] })) + def test_config_yaml_alias_anchor(self): + """ + Tests the usage of YAML aliases and anchors. The following test scene + configuration is equivalent to: + + scene: + - name: test + entities: + light_1: &light_1_state + state: 'on' + brightness: 100 + light_2: *light_1_state + + When encountering a YAML alias/anchor, the PyYAML parser will use a + reference to the original dictionary, instead of creating a copy, so + care needs to be taken to not modify the original. + """ + test_light = loader.get_component('light.test') + test_light.init() + + self.assertTrue(light.setup(self.hass, { + light.DOMAIN: {'platform': 'test'} + })) + + light_1, light_2 = test_light.DEVICES[0:2] + + light.turn_off(self.hass, [light_1.entity_id, light_2.entity_id]) + + self.hass.pool.block_till_done() + + entity_state = { + 'state': 'on', + 'brightness': 100, + } + self.assertTrue(scene.setup(self.hass, { + 'scene': [{ + 'name': 'test', + 'entities': { + light_1.entity_id: entity_state, + light_2.entity_id: entity_state, + } + }] + })) + + scene.activate(self.hass, 'scene.test') + self.hass.pool.block_till_done() + + self.assertTrue(light_1.is_on) + self.assertTrue(light_2.is_on) + self.assertEqual(100, + light_1.last_call('turn_on')[1].get('brightness')) + self.assertEqual(100, + light_2.last_call('turn_on')[1].get('brightness')) + def test_activate_scene(self): test_light = loader.get_component('light.test') test_light.init() diff --git a/tests/test_core.py b/tests/test_core.py index fee46fe2dd4..ca935e2d106 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -321,6 +321,18 @@ class TestStateMachine(unittest.TestCase): self.assertFalse(self.states.is_state('light.Bowl', 'off')) self.assertFalse(self.states.is_state('light.Non_existing', 'on')) + def test_is_state_attr(self): + """ Test is_state_attr method. """ + self.states.set("light.Bowl", "on", {"brightness": 100}) + self.assertTrue( + self.states.is_state_attr('light.Bowl', 'brightness', 100)) + self.assertFalse( + self.states.is_state_attr('light.Bowl', 'friendly_name', 200)) + self.assertFalse( + self.states.is_state_attr('light.Bowl', 'friendly_name', 'Bowl')) + self.assertFalse( + self.states.is_state_attr('light.Non_existing', 'brightness', 100)) + def test_entity_ids(self): """ Test get_entity_ids method. """ ent_ids = self.states.entity_ids() diff --git a/tests/util/test_template.py b/tests/util/test_template.py index 1ecd7d5b894..844826f80d5 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -117,6 +117,14 @@ class TestUtilTemplate(unittest.TestCase): self.hass, '{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}')) + def test_is_state_attr(self): + self.hass.states.set('test.object', 'available', {'mode': 'on'}) + self.assertEqual( + 'yes', + template.render( + self.hass, + '{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}')) + def test_states_function(self): self.hass.states.set('test.object', 'available') self.assertEqual(