diff --git a/.coveragerc b/.coveragerc index bf0db4bd986..54d26f3f57c 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 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/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/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/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..77672c9aaf5 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) @@ -233,6 +234,9 @@ class HueLight(Light): if effect == EFFECT_COLORLOOP: command['effect'] = 'colorloop' + elif effect == EFFECT_RANDOM: + command['hue'] = random.randrange(0, 65535) + command['sat'] = random.randrange(150, 254) else: command['effect'] = 'none' 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/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 1689f7a8889..88071c0b5fb 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -10,7 +10,7 @@ import logging from homeassistant.helpers.entity_component import EntityComponent from homeassistant.components import ( - wink, zwave, isy994, verisure, ecobee, mysensors) + wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors) DOMAIN = 'sensor' SCAN_INTERVAL = 30 @@ -24,6 +24,7 @@ DISCOVERY_PLATFORMS = { isy994.DISCOVER_SENSORS: 'isy994', verisure.DISCOVER_SENSORS: 'verisure', ecobee.DISCOVER_SENSORS: 'ecobee', + tellduslive.DISCOVER_SENSORS: 'tellduslive', mysensors.DISCOVER_SENSORS: 'mysensors', } 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 a6b5c518eee..f6a56d3a99e 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -102,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, STATE_UNKNOWN) - 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 @@ -118,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): @@ -126,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: %s", self._resource) - self.data['error'] = STATE_UNKNOWN + self.data = None # pylint: disable=too-few-public-methods @@ -142,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): @@ -150,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: %s", self._resource) - self.data['error'] = STATE_UNKNOWN + 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/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 index 36dd193da97..cda9ba1b78f 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -36,17 +36,17 @@ sensor: """ import logging -import datetime -import urllib.request + 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', 'astral==0.8.1'] +REQUIREMENTS = ['xmltodict'] # Sensor types are defined like so: SENSOR_TYPES = { @@ -73,19 +73,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - from astral import Location, GoogleGeocoder - location = Location(('', '', hass.config.latitude, hass.config.longitude, - hass.config.time_zone, 0)) + elevation = config.get('elevation') - google = GoogleGeocoder() - try: - google._get_elevation(location) # pylint: disable=protected-access - _LOGGER.info( - 'Retrieved elevation from Google: %s', location.elevation) - elevation = location.elevation - except urllib.error.URLError: - # If no internet connection available etc. - elevation = 0 + 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) @@ -116,9 +108,8 @@ class YrSensor(Entity): self.type = sensor_type self._state = None self._weather = weather - self._info = '' self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._update = datetime.datetime.fromtimestamp(0) + self._update = None self.update() @@ -134,14 +125,15 @@ class YrSensor(Entity): @property def state_attributes(self): """ Returns state attributes. """ - data = {} - data[''] = "Weather forecast from yr.no, delivered by the"\ - " Norwegian Meteorological Institute and the NRK" + 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=" + str(symbol_nr) + \ - ";content_type=image/png" + data[ATTR_ENTITY_PICTURE] = \ + "http://api.met.no/weatherapi/weathericon/1.1/" \ + "?symbol={0};content_type=image/png".format(symbol_nr) return data @@ -150,76 +142,50 @@ class YrSensor(Entity): """ Unit of measurement of this entity, if any. """ return self._unit_of_measurement - @property - def should_poll(self): - """ Return True if entity has to be polled for state. """ - return True - - # pylint: disable=too-many-branches, too-many-return-statements def update(self): """ Gets the latest data from yr.no and updates the states. """ - self._weather.update() - now = datetime.datetime.now() + now = dt_util.utcnow() # check if data should be updated - if now <= self._update: + if self._update is not None and now <= self._update: return - time_data = self._weather.data['product']['time'] + self._weather.update() - # pylint: disable=consider-using-enumerate # find sensor - for k in range(len(time_data)): - valid_from = datetime.datetime.strptime(time_data[k]['@from'], - "%Y-%m-%dT%H:%M:%SZ") - valid_to = datetime.datetime.strptime(time_data[k]['@to'], - "%Y-%m-%dT%H:%M:%SZ") - self._update = valid_to - self._info = "Forecast between " + time_data[k]['@from'] \ - + " and " + time_data[k]['@to'] + ". " + 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") - temp_data = time_data[k]['location'] - if self.type not in temp_data and now >= valid_to: + 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 = temp_data[self.type]['@value'] - return + self._state = loc_data[self.type]['@value'] + break elif self.type == 'symbol' and valid_from < now: - self._state = temp_data[self.type]['@number'] - return - elif self.type == 'temperature': - self._state = temp_data[self.type]['@value'] - return + 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 = temp_data[self.type]['@mps'] - return - elif self.type == 'pressure': - self._state = temp_data[self.type]['@value'] - return + self._state = loc_data[self.type]['@mps'] + break elif self.type == 'windDirection': - self._state = float(temp_data[self.type]['@deg']) - return - elif self.type == 'humidity': - self._state = temp_data[self.type]['@value'] - return - elif self.type == 'fog': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'cloudiness': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'lowClouds': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'mediumClouds': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'highClouds': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'dewpointTemperature': - self._state = temp_data[self.type]['@value'] - return + 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 @@ -230,17 +196,18 @@ class YrData(object): self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) - self._nextrun = datetime.datetime.fromtimestamp(0) + self._nextrun = None + self.data = {} self.update() def update(self): """ Gets the latest data from yr.no """ - now = datetime.datetime.now() # check if new will be available - if now <= self._nextrun: + if self._nextrun is not None and dt_util.utcnow() <= self._nextrun: return try: - response = requests.get(self._url) + with requests.Session() as sess: + response = sess.get(self._url) except requests.RequestException: return if response.status_code != 200: @@ -252,5 +219,5 @@ class YrData(object): model = self.data['meta']['model'] if '@nextrun' not in model: model = model[0] - self._nextrun = datetime.datetime.strptime(model['@nextrun'], - "%Y-%m-%dT%H:%M:%SZ") + 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 9f9bcc18604..a05a673c3dd 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, mysensors) + group, discovery, wink, isy994, verisure, zwave, tellduslive, mysensors) 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', mysensors.DISCOVER_SWITCHES: 'mysensors', } 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/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..a343711ccc3 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.4'] _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/core.py b/homeassistant/core.py index 8ea55c653e3..e2650969eb0 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,36 @@ 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. + """Remove the state of an entity. - Returns boolean to indicate if an entity was removed. """ + 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 +514,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 +525,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 +568,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 +581,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 +613,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 +638,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 +653,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 +670,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 +680,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 +710,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 +733,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 +748,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 +811,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 +826,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/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/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 718aad7000d..e8709c789e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,6 @@ python-twitch==1.2.0 xmltodict # homeassistant.components.sun -# homeassistant.components.sensor.yr astral==0.8.1 # homeassistant.components.switch.edimax @@ -174,7 +173,10 @@ hikvision==0.4 orvibo==1.1.0 # homeassistant.components.switch.wemo -pywemo==0.3.3 +pywemo==0.3.4 + +# homeassistant.components.tellduslive +tellive-py==0.5.2 # homeassistant.components.thermostat.heatmiser heatmiserV3==0.9.1 @@ -189,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/sensor/test_yr.py b/tests/components/sensor/test_yr.py index aa9a5a59944..f58aefbce43 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -4,66 +4,70 @@ tests.components.sensor.test_yr Tests Yr sensor. """ -import unittest +from unittest.mock import patch + +import pytest import homeassistant.core as ha import homeassistant.components.sensor as sensor -class TestSensorYr(unittest.TestCase): +@pytest.mark.usefixtures('betamax_session') +class TestSensorYr: """ Test the Yr sensor. """ - def setUp(self): # pylint: disable=invalid-name + def setup_method(self, method): self.hass = ha.HomeAssistant() - latitude = 32.87336 - longitude = 117.22743 + self.hass.config.latitude = 32.87336 + self.hass.config.longitude = 117.22743 - # Compare it with the real data - self.hass.config.latitude = latitude - self.hass.config.longitude = longitude - - def tearDown(self): # pylint: disable=invalid-name + def teardown_method(self, method): """ Stop down stuff we started. """ self.hass.stop() - def test_default_setup(self): - self.assertTrue(sensor.setup(self.hass, { - 'sensor': { - 'platform': 'yr', - } - })) + 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') - self.assertTrue(state.state.isnumeric()) - self.assertEqual(None, - state.attributes.get('unit_of_measurement')) + assert state.state.isnumeric() + assert state.attributes.get('unit_of_measurement') is None - def test_custom_setup(self): - self.assertTrue(sensor.setup(self.hass, { - 'sensor': { - 'platform': 'yr', - 'monitored_conditions': {'pressure', 'windDirection', 'humidity', 'fog', 'windSpeed'} - } - })) - state = self.hass.states.get('sensor.yr_symbol') - self.assertEqual(None, state) + 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') - self.assertEqual('hPa', - state.attributes.get('unit_of_measurement')) + assert 'hPa', state.attributes.get('unit_of_measurement') state = self.hass.states.get('sensor.yr_wind_direction') - self.assertEqual('°', - state.attributes.get('unit_of_measurement')) + assert '°', state.attributes.get('unit_of_measurement') state = self.hass.states.get('sensor.yr_humidity') - self.assertEqual('%', - state.attributes.get('unit_of_measurement')) + assert '%', state.attributes.get('unit_of_measurement') state = self.hass.states.get('sensor.yr_fog') - self.assertEqual('%', - state.attributes.get('unit_of_measurement')) + assert '%', state.attributes.get('unit_of_measurement') state = self.hass.states.get('sensor.yr_wind_speed') - self.assertEqual('m/s', - state.attributes.get('unit_of_measurement')) + assert 'm/s', state.attributes.get('unit_of_measurement') 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()