diff --git a/.coveragerc b/.coveragerc index 7ebab01d399..95816fa55a9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -49,6 +49,7 @@ omit = homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py + homeassistant/components/media_player/plex.py homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/sonos.py homeassistant/components/notify/file.py @@ -69,6 +70,7 @@ omit = homeassistant/components/sensor/glances.py homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/rest.py homeassistant/components/sensor/rfxtrx.py homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/sabnzbd.py diff --git a/.gitignore b/.gitignore index 881411c54ea..8935ffedc17 100644 --- a/.gitignore +++ b/.gitignore @@ -15,10 +15,6 @@ tests/config/home-assistant.log *.sublime-project *.sublime-workspace -# Hide code validator output -pep8.txt -pylint.txt - # Hide some OS X stuff .DS_Store .AppleDouble @@ -30,6 +26,9 @@ Icon .idea +# pytest +.cache + # GITHUB Proposed Python stuff: *.py[cod] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f2fd110a1d..f646766a231 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ For help on building your component, please see the [developer documentation](ht After you finish adding support for your device: - Update the supported devices in the `README.md` file. - - Add any new dependencies to `requirements.txt`. + - Add any new dependencies to `requirements_all.txt`. There is no ordering right now, so just add it to the end. - Update the `.coveragerc` file. - Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io). - Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`. diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index 5acca361a30..44153c7876d 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -1,7 +1,9 @@ homeassistant: # Omitted values in this section will be auto detected using freegeoip.net - # Location required to calculate the time the sun rises and sets + # Location required to calculate the time the sun rises and sets. + # Cooridinates are also used for location for weather related components. + # Google Maps can be used to determine more precise GPS cooridinates. latitude: 32.87336 longitude: 117.22743 diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a7e4dbfdc14..b2e5fa51540 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -297,11 +297,15 @@ def process_ha_core_config(hass, config): else: _LOGGER.error('Received invalid time zone %s', time_zone_str) - for key, attr in ((CONF_LATITUDE, 'latitude'), - (CONF_LONGITUDE, 'longitude'), - (CONF_NAME, 'location_name')): + for key, attr, typ in ((CONF_LATITUDE, 'latitude', float), + (CONF_LONGITUDE, 'longitude', float), + (CONF_NAME, 'location_name', str)): if key in config: - setattr(hac, attr, config[key]) + try: + setattr(hac, attr, typ(config[key])) + except ValueError: + _LOGGER.error('Received invalid %s value for %s: %s', + typ.__name__, key, attr) set_time_zone(config.get(CONF_TIME_ZONE)) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 108cc88741b..e4c794df424 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -103,6 +103,10 @@ def _handle_get_api_stream(handler, path_match, data): write_lock = threading.Lock() block = threading.Event() + restrict = data.get('restrict') + if restrict: + restrict = restrict.split(',') + def write_message(payload): """ Writes a message to the output. """ with write_lock: @@ -118,7 +122,8 @@ def _handle_get_api_stream(handler, path_match, data): """ Forwards events to the open request. """ nonlocal gracefully_closed - if block.is_set() or event.event_type == EVENT_TIME_CHANGED: + if block.is_set() or event.event_type == EVENT_TIME_CHANGED or \ + restrict and event.event_type not in restrict: return elif event.event_type == EVENT_HOMEASSISTANT_STOP: gracefully_closed = True diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 45859617624..b734728e59b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -16,9 +16,9 @@ DOMAIN = 'automation' DEPENDENCIES = ['group'] CONF_ALIAS = 'alias' -CONF_SERVICE = 'execute_service' -CONF_SERVICE_ENTITY_ID = 'service_entity_id' -CONF_SERVICE_DATA = 'service_data' +CONF_SERVICE = 'service' +CONF_SERVICE_ENTITY_ID = 'entity_id' +CONF_SERVICE_DATA = 'data' CONF_CONDITION = 'condition' CONF_ACTION = 'action' @@ -40,25 +40,45 @@ def setup(hass, config): found = 1 while config_key in config: - p_config = _migrate_old_config(config[config_key]) + # check for one block syntax + if isinstance(config[config_key], dict): + config_block = _migrate_old_config(config[config_key]) + name = config_block.get(CONF_ALIAS, config_key) + _setup_automation(hass, config_block, name, config) + + # check for multiple block syntax + elif isinstance(config[config_key], list): + for list_no, config_block in enumerate(config[config_key]): + name = config_block.get(CONF_ALIAS, + "{}, {}".format(config_key, list_no)) + _setup_automation(hass, config_block, name, config) + + # any scalar value is incorrect + else: + _LOGGER.error('Error in config in section %s.', config_key) + found += 1 config_key = "{} {}".format(DOMAIN, found) - name = p_config.get(CONF_ALIAS, config_key) - action = _get_action(hass, p_config.get(CONF_ACTION, {}), name) + return True + + +def _setup_automation(hass, config_block, name, config): + """ Setup one instance of automation """ + + action = _get_action(hass, config_block.get(CONF_ACTION, {}), name) + + if action is None: + return False + + if CONF_CONDITION in config_block or CONF_CONDITION_TYPE in config_block: + action = _process_if(hass, config, config_block, action) if action is None: - continue - - if CONF_CONDITION in p_config or CONF_CONDITION_TYPE in p_config: - action = _process_if(hass, config, p_config, action) - - if action is None: - continue - - _process_trigger(hass, config, p_config.get(CONF_TRIGGER, []), name, - action) + return False + _process_trigger(hass, config, config_block.get(CONF_TRIGGER, []), name, + action) return True @@ -118,7 +138,10 @@ def _migrate_old_config(config): ('trigger', 'state_from', 'from'), ('trigger', 'state_hours', 'hours'), ('trigger', 'state_minutes', 'minutes'), - ('trigger', 'state_seconds', 'seconds')): + ('trigger', 'state_seconds', 'seconds'), + ('action', 'execute_service', 'service'), + ('action', 'service_entity_id', 'entity_id'), + ('action', 'service_data', 'data')): if key in new_conf[cat]: new_conf[cat][new_key] = new_conf[cat].pop(key) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 22be921f66a..c5b0ee47923 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -20,11 +20,12 @@ def trigger(hass, config, action): _LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE) return False - event_data = config.get(CONF_EVENT_DATA, {}) + event_data = config.get(CONF_EVENT_DATA) def handle_event(event): """ Listens for events and calls the action when data matches. """ - if event_data == event.data: + if not event_data or all(val == event.data.get(key) for key, val + in event_data.items()): action() hass.bus.listen(event_type, handle_event) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 559832eee80..1d97ccc135d 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -79,11 +79,11 @@ def if_action(hass, config): now = dt_util.now() if before is not None and now > now.replace(hour=before.hour, minute=before.minute): - return False + return False if after is not None and now < now.replace(hour=after.hour, minute=after.minute): - return False + return False if weekday is not None: now_weekday = WEEKDAYS[now.weekday()] diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index beb7a63b47c..f0ce2259dc4 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -33,10 +33,10 @@ def setup(hass, config): # Setup sun if not hass.config.latitude: - hass.config.latitude = '32.87336' + hass.config.latitude = 32.87336 if not hass.config.longitude: - hass.config.longitude = '117.22743' + hass.config.longitude = 117.22743 bootstrap.setup_component(hass, 'sun') @@ -108,7 +108,9 @@ def setup(hass, config): "http://graph.facebook.com/297400035/picture", ATTR_FRIENDLY_NAME: 'Paulus'}) hass.states.set("device_tracker.anne_therese", "not_home", - {ATTR_FRIENDLY_NAME: 'Anne Therese'}) + {ATTR_FRIENDLY_NAME: 'Anne Therese', + 'latitude': hass.config.latitude + 0.002, + 'longitude': hass.config.longitude + 0.002}) hass.states.set("group.all_devices", "home", { diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index d33d182dd2c..27e9417ab5b 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -17,7 +17,12 @@ device_tracker: # New found devices auto found track_new_devices: yes + + # Maximum distance from home we consider people home + range_home: 100 """ +# pylint: disable=too-many-instance-attributes, too-many-arguments +# pylint: disable=too-many-locals import csv from datetime import timedelta import logging @@ -52,7 +57,7 @@ CONF_TRACK_NEW = "track_new_devices" DEFAULT_CONF_TRACK_NEW = True CONF_CONSIDER_HOME = 'consider_home' -DEFAULT_CONF_CONSIDER_HOME = 180 # seconds +DEFAULT_CONSIDER_HOME = 180 # seconds CONF_SCAN_INTERVAL = "interval_seconds" DEFAULT_SCAN_INTERVAL = 12 @@ -60,6 +65,9 @@ DEFAULT_SCAN_INTERVAL = 12 CONF_AWAY_HIDE = 'hide_if_away' DEFAULT_AWAY_HIDE = False +CONF_HOME_RANGE = 'home_range' +DEFAULT_HOME_RANGE = 100 + SERVICE_SEE = 'see' ATTR_LATITUDE = 'latitude' @@ -69,6 +77,8 @@ ATTR_DEV_ID = 'dev_id' ATTR_HOST_NAME = 'host_name' ATTR_LOCATION_NAME = 'location_name' ATTR_GPS = 'gps' +ATTR_GPS_ACCURACY = 'gps_accuracy' +ATTR_BATTERY = 'battery' DISCOVERY_PLATFORMS = { discovery.SERVICE_NETGEAR: 'netgear', @@ -86,7 +96,7 @@ def is_on(hass, entity_id=None): def see(hass, mac=None, dev_id=None, host_name=None, location_name=None, - gps=None): + gps=None, gps_accuracy=None, battery=None): """ Call service to notify you see device. """ data = {key: value for key, value in ((ATTR_MAC, mac), @@ -106,13 +116,17 @@ def setup(hass, config): os.remove(csv_path) conf = config.get(DOMAIN, {}) - consider_home = util.convert(conf.get(CONF_CONSIDER_HOME), int, - DEFAULT_CONF_CONSIDER_HOME) + consider_home = timedelta( + seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int, + DEFAULT_CONSIDER_HOME)) track_new = util.convert(conf.get(CONF_TRACK_NEW), bool, DEFAULT_CONF_TRACK_NEW) + home_range = util.convert(conf.get(CONF_HOME_RANGE), int, + DEFAULT_HOME_RANGE) - devices = load_config(yaml_path, hass, timedelta(seconds=consider_home)) - tracker = DeviceTracker(hass, consider_home, track_new, devices) + devices = load_config(yaml_path, hass, consider_home, home_range) + tracker = DeviceTracker(hass, consider_home, track_new, home_range, + devices) def setup_platform(p_type, p_config, disc_info=None): """ Setup a device tracker platform. """ @@ -158,7 +172,7 @@ def setup(hass, config): """ Service to see a device. """ args = {key: value for key, value in call.data.items() if key in (ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME, - ATTR_GPS)} + ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)} tracker.see(**args) hass.services.register(DOMAIN, SERVICE_SEE, see_service) @@ -168,12 +182,13 @@ def setup(hass, config): class DeviceTracker(object): """ Track devices """ - def __init__(self, hass, consider_home, track_new, devices): + def __init__(self, hass, consider_home, track_new, home_range, devices): self.hass = hass self.devices = {dev.dev_id: dev for dev in devices} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} - self.consider_home = timedelta(seconds=consider_home) + self.consider_home = consider_home self.track_new = track_new + self.home_range = home_range self.lock = threading.Lock() for device in devices: @@ -183,7 +198,7 @@ class DeviceTracker(object): self.group = None def see(self, mac=None, dev_id=None, host_name=None, location_name=None, - gps=None): + gps=None, gps_accuracy=None, battery=None): """ Notify device tracker that you see a device. """ with self.lock: if mac is None and dev_id is None: @@ -198,20 +213,21 @@ class DeviceTracker(object): device = self.devices.get(dev_id) if device: - device.seen(host_name, location_name, gps) + device.seen(host_name, location_name, gps, gps_accuracy, + battery) if device.track: device.update_ha_state() return # If no device can be found, create it device = Device( - self.hass, self.consider_home, self.track_new, dev_id, mac, - (host_name or dev_id).replace('_', ' ')) + self.hass, self.consider_home, self.home_range, self.track_new, + dev_id, mac, (host_name or dev_id).replace('_', ' ')) self.devices[dev_id] = device if mac is not None: self.mac_to_dev[mac] = device - device.seen(host_name, location_name, gps) + device.seen(host_name, location_name, gps, gps_accuracy, battery) if device.track: device.update_ha_state() @@ -239,19 +255,20 @@ class DeviceTracker(object): class Device(Entity): """ Tracked device. """ - # pylint: disable=too-many-instance-attributes, too-many-arguments host_name = None location_name = None gps = None + gps_accuracy = 0 last_seen = None + battery = None # Track if the last update of this device was HOME last_update_home = False _state = STATE_NOT_HOME - def __init__(self, hass, consider_home, track, dev_id, mac, name=None, - picture=None, away_hide=False): + def __init__(self, hass, consider_home, home_range, track, dev_id, mac, + name=None, picture=None, away_hide=False): self.hass = hass self.entity_id = ENTITY_ID_FORMAT.format(dev_id) @@ -259,6 +276,8 @@ class Device(Entity): # detected anymore. self.consider_home = consider_home + # Distance in meters + self.home_range = home_range # Device ID self.dev_id = dev_id self.mac = mac @@ -273,6 +292,13 @@ class Device(Entity): self.config_picture = picture self.away_hide = away_hide + @property + def gps_home(self): + """ Return if device is within range of home. """ + distance = max( + 0, self.hass.config.distance(*self.gps) - self.gps_accuracy) + return self.gps is not None and distance <= self.home_range + @property def name(self): """ Returns the name of the entity. """ @@ -292,8 +318,12 @@ class Device(Entity): attr[ATTR_ENTITY_PICTURE] = self.config_picture if self.gps: - attr[ATTR_LATITUDE] = self.gps[0], - attr[ATTR_LONGITUDE] = self.gps[1], + attr[ATTR_LATITUDE] = self.gps[0] + attr[ATTR_LONGITUDE] = self.gps[1] + attr[ATTR_GPS_ACCURACY] = self.gps_accuracy + + if self.battery: + attr[ATTR_BATTERY] = self.battery return attr @@ -302,12 +332,23 @@ class Device(Entity): """ If device should be hidden. """ return self.away_hide and self.state != STATE_HOME - def seen(self, host_name=None, location_name=None, gps=None): + def seen(self, host_name=None, location_name=None, gps=None, + gps_accuracy=0, battery=None): """ Mark the device as seen. """ self.last_seen = dt_util.utcnow() self.host_name = host_name self.location_name = location_name - self.gps = gps + self.gps_accuracy = gps_accuracy + self.battery = battery + if gps is None: + self.gps = None + else: + try: + self.gps = tuple(float(val) for val in gps) + except ValueError: + _LOGGER.warning('Could not parse gps value for %s: %s', + self.dev_id, gps) + self.gps = None self.update() def stale(self, now=None): @@ -321,6 +362,8 @@ class Device(Entity): return elif self.location_name: self._state = self.location_name + elif self.gps is not None: + self._state = STATE_HOME if self.gps_home else STATE_NOT_HOME elif self.stale(): self._state = STATE_NOT_HOME self.last_update_home = False @@ -338,21 +381,21 @@ def convert_csv_config(csv_path, yaml_path): (util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(), used_ids) used_ids.add(dev_id) - device = Device(None, None, row['track'] == '1', dev_id, + device = Device(None, None, None, row['track'] == '1', dev_id, row['device'], row['name'], row['picture']) update_config(yaml_path, dev_id, device) return True -def load_config(path, hass, consider_home): +def load_config(path, hass, consider_home, home_range): """ Load devices from YAML config file. """ if not os.path.isfile(path): return [] return [ - Device(hass, consider_home, device.get('track', False), + Device(hass, consider_home, home_range, device.get('track', False), str(dev_id).lower(), str(device.get('mac')).upper(), device.get('name'), device.get('picture'), - device.get(CONF_AWAY_HIDE, False)) + device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) for dev_id, device in load_yaml_config_file(path).items()] diff --git a/homeassistant/components/device_tracker/demo.py b/homeassistant/components/device_tracker/demo.py new file mode 100644 index 00000000000..e8cf906be9e --- /dev/null +++ b/homeassistant/components/device_tracker/demo.py @@ -0,0 +1,50 @@ +""" +homeassistant.components.device_tracker.demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Demo platform for the device tracker. + +device_tracker: + platform: demo +""" +import random + +from homeassistant.components.device_tracker import DOMAIN + + +def setup_scanner(hass, config, see): + """ Set up a demo tracker. """ + + def offset(): + """ Return random offset. """ + return (random.randrange(500, 2000)) / 2e5 * random.choice((-1, 1)) + + def random_see(dev_id, name): + """ Randomize a sighting. """ + see( + dev_id=dev_id, + host_name=name, + gps=(hass.config.latitude + offset(), + hass.config.longitude + offset()), + gps_accuracy=random.randrange(50, 150), + battery=random.randrange(10, 90) + ) + + def observe(call=None): + """ Observe three entities. """ + random_see('demo_paulus', 'Paulus') + random_see('demo_anne_therese', 'Anne Therese') + + observe() + + see( + dev_id='demo_home_boy', + host_name='Home Boy', + gps=[hass.config.latitude - 0.00002, hass.config.longitude + 0.00002], + gps_accuracy=20, + battery=53 + ) + + hass.services.register(DOMAIN, 'demo', observe) + + return True diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py new file mode 100644 index 00000000000..9ef227909e1 --- /dev/null +++ b/homeassistant/components/device_tracker/owntracks.py @@ -0,0 +1,54 @@ +""" +homeassistant.components.device_tracker.owntracks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +OwnTracks platform for the device tracker. + +device_tracker: + platform: owntracks +""" +import json +import logging + +import homeassistant.components.mqtt as mqtt + +DEPENDENCIES = ['mqtt'] + +LOCATION_TOPIC = 'owntracks/+/+' + + +def setup_scanner(hass, config, see): + """ Set up a OwnTracksks tracker. """ + + def owntracks_location_update(topic, payload, qos): + """ MQTT message received. """ + + # Docs on available data: + # http://owntracks.org/booklet/tech/json/#_typelocation + try: + data = json.loads(payload) + except ValueError: + # If invalid JSON + logging.getLogger(__name__).error( + 'Unable to parse payload as JSON: %s', payload) + return + + if data.get('_type') != 'location': + return + + parts = topic.split('/') + kwargs = { + 'dev_id': '{}_{}'.format(parts[1], parts[2]), + 'host_name': parts[1], + 'gps': (data['lat'], data['lon']), + } + if 'acc' in data: + kwargs['gps_accuracy'] = data['acc'] + if 'batt' in data: + kwargs['battery'] = data['batt'] + + see(**kwargs) + + mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) + + return True diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 6a780693f25..450019022e1 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -19,7 +19,7 @@ from homeassistant.const import ( DOMAIN = "discovery" DEPENDENCIES = [] -REQUIREMENTS = ['netdisco==0.4'] +REQUIREMENTS = ['netdisco==0.4.1'] SCAN_INTERVAL = 300 # seconds diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 902b14e38b3..419e48d55b5 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,8 @@ _LOGGER = logging.getLogger(__name__) FRONTEND_URLS = [ - URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent'] + URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState', + '/devEvent'] STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)') diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 7b434017191..2d3bbed8e5b 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "0ab148ece11ddde26b95460c2c91da3d" +VERSION = "7301f590f66ffc5b34d3991fc9eb0247" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 0434c21893b..ccc003f604d 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,6 +1,6 @@ -- \ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 63e039a221a..be2312de033 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 63e039a221ae6771e0d7c6990d9a93b7cc22fc64 +Subproject commit be2312de03386b05c6d0ff91abe894fd36338d96 diff --git a/homeassistant/components/frontend/www_static/images/leaflet/layers-2x.png b/homeassistant/components/frontend/www_static/images/leaflet/layers-2x.png new file mode 100644 index 00000000000..a2cf7f9efef Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/leaflet/layers-2x.png differ diff --git a/homeassistant/components/frontend/www_static/images/leaflet/layers.png b/homeassistant/components/frontend/www_static/images/leaflet/layers.png new file mode 100644 index 00000000000..bca0a0e4296 Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/leaflet/layers.png differ diff --git a/homeassistant/components/frontend/www_static/images/leaflet/marker-icon-2x.png b/homeassistant/components/frontend/www_static/images/leaflet/marker-icon-2x.png new file mode 100644 index 00000000000..0015b6495fa Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/leaflet/marker-icon-2x.png differ diff --git a/homeassistant/components/frontend/www_static/images/leaflet/marker-icon.png b/homeassistant/components/frontend/www_static/images/leaflet/marker-icon.png new file mode 100644 index 00000000000..e2e9f757f51 Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/leaflet/marker-icon.png differ diff --git a/homeassistant/components/frontend/www_static/images/leaflet/marker-shadow.png b/homeassistant/components/frontend/www_static/images/leaflet/marker-shadow.png new file mode 100644 index 00000000000..d1e773c715a Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/leaflet/marker-shadow.png differ diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 01f75eabb5a..a723f9cbd71 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -147,8 +147,6 @@ def _api_history_period(handler, path_match, data): end_time = start_time + one_day - print("Fetching", start_time, end_time) - entity_id = data.get('filter_entity_id') handler.write_json( diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 8b2e2a6252c..bae720db8dc 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -232,7 +232,12 @@ class RequestHandler(SimpleHTTPRequestHandler): def log_message(self, fmt, *arguments): """ Redirect built-in log to HA logging """ - _LOGGER.info(fmt, *arguments) + if self.server.no_password_set: + _LOGGER.info(fmt, *arguments) + else: + _LOGGER.info( + fmt, *(arg.replace(self.server.api_password, '*******') + if isinstance(arg, str) else arg for arg in arguments)) def _handle_request(self, method): # pylint: disable=too-many-branches """ Does some common checks and calls appropriate method. """ diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 8068d20bb74..819dce499e9 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -6,12 +6,14 @@ Support for Tellstick lights. import logging # pylint: disable=no-name-in-module, import-error from homeassistant.components.light import Light, ATTR_BRIGHTNESS -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, + ATTR_FRIENDLY_NAME) import tellcore.constants as tellcore_constants - -REQUIREMENTS = ['tellcore-py==1.0.4'] +from tellcore.library import DirectCallbackDispatcher +REQUIREMENTS = ['tellcore-py==1.1.2'] +# pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Tellstick lights. """ @@ -22,13 +24,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): "Failed to import tellcore") return [] - core = telldus.TelldusCore() + core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) + switches_and_lights = core.devices() lights = [] for switch in switches_and_lights: if switch.methods(tellcore_constants.TELLSTICK_DIM): lights.append(TellstickLight(switch)) + + def _device_event_callback(id_, method, data, cid): + """ Called from the TelldusCore library to update one device """ + for light_device in lights: + if light_device.tellstick_device.id == id_: + # Execute the update in another thread + light_device.update_ha_state(True) + break + + callback_id = core.register_device_event(_device_event_callback) + + def unload_telldus_lib(event): + """ Un-register the callback bindings """ + if callback_id is not None: + core.unregister_callback(callback_id) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib) + add_devices_callback(lights) @@ -40,15 +61,15 @@ class TellstickLight(Light): tellcore_constants.TELLSTICK_UP | tellcore_constants.TELLSTICK_DOWN) - def __init__(self, tellstick): - self.tellstick = tellstick - self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name} + def __init__(self, tellstick_device): + self.tellstick_device = tellstick_device + self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name} self._brightness = 0 @property def name(self): """ Returns the name of the switch if any. """ - return self.tellstick.name + return self.tellstick_device.name @property def is_on(self): @@ -62,8 +83,9 @@ class TellstickLight(Light): def turn_off(self, **kwargs): """ Turns the switch off. """ - self.tellstick.turn_off() + self.tellstick_device.turn_off() self._brightness = 0 + self.update_ha_state() def turn_on(self, **kwargs): """ Turns the switch on. """ @@ -74,11 +96,12 @@ class TellstickLight(Light): else: self._brightness = brightness - self.tellstick.dim(self._brightness) + self.tellstick_device.dim(self._brightness) + self.update_ha_state() def update(self): """ Update state of the light. """ - last_command = self.tellstick.last_sent_command( + last_command = self.tellstick_device.last_sent_command( self.last_sent_command_mask) if last_command == tellcore_constants.TELLSTICK_TURNON: @@ -88,6 +111,11 @@ class TellstickLight(Light): elif (last_command == tellcore_constants.TELLSTICK_DIM or last_command == tellcore_constants.TELLSTICK_UP or last_command == tellcore_constants.TELLSTICK_DOWN): - last_sent_value = self.tellstick.last_sent_value() + last_sent_value = self.tellstick_device.last_sent_value() if last_sent_value is not None: self._brightness = last_sent_value + + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index b653d0f76f2..0d5ef7f3867 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -132,7 +132,10 @@ def humanify(events): # Process events for event in events_batch: if event.event_type == EVENT_STATE_CHANGED: - entity_id = event.data['entity_id'] + entity_id = event.data.get('entity_id') + + if entity_id is None: + continue if entity_id.startswith('sensor.'): last_sensor_event[entity_id] = event @@ -159,10 +162,12 @@ def humanify(events): to_state = State.from_dict(event.data.get('new_state')) - # if last_changed == last_updated only attributes have changed - # we do not report on that yet. + # if last_changed != last_updated only attributes have changed + # we do not report on that yet. Also filter auto groups. if not to_state or \ - to_state.last_changed != to_state.last_updated: + to_state.last_changed != to_state.last_updated or \ + to_state.domain == 'group' and \ + to_state.attributes.get('auto', False): continue domain = to_state.domain diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py new file mode 100644 index 00000000000..a43916f10e3 --- /dev/null +++ b/homeassistant/components/media_player/plex.py @@ -0,0 +1,188 @@ +""" +homeassistant.components.media_player.plex +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides an interface to the Plex API + +Configuration: + +To use Plex add something like this to your configuration: + +media_player: + platform: plex + name: plex_server + user: plex + password: my_secure_password + +Variables: + +name +*Required +The name of the backend device (Under Plex Media Server > settings > server). + +user +*Required +The Plex username + +password +*Required +The Plex password +""" + +import logging + +from homeassistant.components.media_player import ( + MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) +from homeassistant.const import ( + STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) + +REQUIREMENTS = ['https://github.com/miniconfig/python-plexapi/archive/' + '437e36dca3b7780dc0cb73941d662302c0cd2fa9.zip' + '#python-plexapi==1.0.2'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + +# pylint: disable=abstract-method +# pylint: disable=unused-argument + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the plex platform. """ + from plexapi.myplex import MyPlexUser + name = config.get('name', '') + user = config.get('user', '') + password = config.get('password', '') + plexuser = MyPlexUser.signin(user, password) + plexserver = plexuser.getResource(name).connect() + dev = plexserver.clients() + for device in dev: + if "PlayStation" not in device.name: + add_devices([PlexClient(device.name, plexserver)]) + + +class PlexClient(MediaPlayerDevice): + """ Represents a Plex device. """ + + # pylint: disable=too-many-public-methods + + def __init__(self, name, plexserver): + self.client = plexserver.client(name) + self._name = name + self._media = None + self.update() + self.server = plexserver + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + if self._media is None: + return STATE_IDLE + else: + state = self._media.get('state') + if state == 'playing': + return STATE_PLAYING + elif state == 'paused': + return STATE_PAUSED + return STATE_UNKNOWN + + def update(self): + timeline = self.client.timeline() + for timeline_item in timeline: + if timeline_item.get('state') in ('playing', 'paused'): + self._media = timeline_item + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + if self._media is not None: + return self._media.get('ratingKey') + + @property + def media_content_type(self): + """ Content type of current playing media. """ + if self._media is None: + return None + media_type = self.server.library.getByKey( + self.media_content_id).type + if media_type == 'episode': + return MEDIA_TYPE_TVSHOW + elif media_type == 'movie': + return MEDIA_TYPE_VIDEO + return None + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + if self._media is not None: + total_time = self._media.get('duration') + return total_time + + @property + def media_image_url(self): + """ Image url of current playing media. """ + if self._media is not None: + return self.server.library.getByKey(self.media_content_id).thumbUrl + return None + + @property + def media_title(self): + """ Title of current playing media. """ + # find a string we can use as a title + if self._media is not None: + return self.server.library.getByKey(self.media_content_id).title + + @property + def media_season(self): + """ Season of curent playing media. (TV Show only) """ + if self._media is not None: + show_season = self.server.library.getByKey( + self.media_content_id).season().index + return show_season + return None + + @property + def media_series_title(self): + """ Series title of current playing media. (TV Show only)""" + if self._media is not None: + series_title = self.server.library.getByKey( + self.media_content_id).show().title + return series_title + return None + + @property + def media_episode(self): + """ Episode of current playing media. (TV Show only) """ + if self._media is not None: + show_episode = self.server.library.getByKey( + self.media_content_id).index + return show_episode + return None + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_PLEX + + def media_play(self): + """ media_play media player. """ + self.client.play() + + def media_pause(self): + """ media_pause media player. """ + self.client.pause() + + def media_next_track(self): + """ Send next track command. """ + self.client.skipNext() + + def media_previous_track(self): + """ Send previous track command. """ + self.client.skipPrevious() diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 0195ed18d8a..c4f70b6d6d3 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -1,7 +1,7 @@ """ homeassistant.components.script ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - +entity_id Scripts are a sequence of actions that can be triggered manually by the user or automatically based upon automation events, etc. """ @@ -25,6 +25,7 @@ CONF_SEQUENCE = "sequence" CONF_EVENT = "event" CONF_EVENT_DATA = "event_data" CONF_DELAY = "delay" +ATTR_ENTITY_ID = "entity_id" _LOGGER = logging.getLogger(__name__) @@ -43,15 +44,22 @@ def setup(hass, config): hass.services.register(DOMAIN, name, script) scripts.append(script) + def _get_entities(service): + """ Make sure that we always get a list of entities """ + if isinstance(service.data[ATTR_ENTITY_ID], list): + return service.data[ATTR_ENTITY_ID] + else: + return [service.data[ATTR_ENTITY_ID]] + def turn_on(service): """ Calls a script. """ - for entity_id in service.data['entity_id']: + for entity_id in _get_entities(service): domain, service = split_entity_id(entity_id) hass.services.call(domain, service, {}) def turn_off(service): """ Cancels a script. """ - for entity_id in service.data['entity_id']: + for entity_id in _get_entities(service): for script in scripts: if script.entity_id == entity_id: script.cancel() diff --git a/homeassistant/components/sensor/command_sensor.py b/homeassistant/components/sensor/command_sensor.py index d20b7c8a42a..a6e6c19fdb8 100644 --- a/homeassistant/components/sensor/command_sensor.py +++ b/homeassistant/components/sensor/command_sensor.py @@ -108,12 +108,15 @@ class CommandSensor(Entity): self.data.update() value = self.data.value - if value is not None: - if self._corr_factor is not None: - self._state = round((int(value) * self._corr_factor), - self._decimal_places) - else: - self._state = value + try: + if value is not None: + if self._corr_factor is not None: + self._state = round((float(value) * self._corr_factor), + self._decimal_places) + else: + self._state = value + except ValueError: + self._state = value # pylint: disable=too-few-public-methods diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py new file mode 100644 index 00000000000..bc76d309c0f --- /dev/null +++ b/homeassistant/components/sensor/rest.py @@ -0,0 +1,182 @@ +""" +homeassistant.components.sensor.rest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The rest sensor will consume JSON responses sent by an exposed REST API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rest.html +""" +import logging +import requests +from json import loads +from datetime import timedelta + +from homeassistant.util import Throttle +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'REST Sensor' +DEFAULT_METHOD = 'GET' + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + + +# pylint: disable=unused-variable +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the REST sensor. """ + + use_get = False + use_post = False + + resource = config.get('resource', None) + method = config.get('method', DEFAULT_METHOD) + payload = config.get('payload', None) + + if method == 'GET': + use_get = True + elif method == 'POST': + use_post = True + + try: + if use_get: + response = requests.get(resource, timeout=10) + elif use_post: + response = requests.post(resource, data=payload, timeout=10) + if not response.ok: + _LOGGER.error('Response status is "%s"', response.status_code) + return False + except requests.exceptions.MissingSchema: + _LOGGER.error('Missing resource or schema in configuration. ' + 'Add http:// to your URL.') + return False + except requests.exceptions.ConnectionError: + _LOGGER.error('No route to resource/endpoint. ' + 'Please check the URL in the configuration file.') + return False + + try: + data = loads(response.text) + except ValueError: + _LOGGER.error('No valid JSON in the response in: %s', data) + return False + + try: + data[config.get('variable')] + except KeyError: + _LOGGER.error('Variable "%s" not found in response: "%s"', + config.get('variable'), data) + return False + + if use_get: + rest = RestDataGet(resource) + elif use_post: + rest = RestDataPost(resource, payload) + + add_devices([RestSensor(rest, + config.get('name', DEFAULT_NAME), + config.get('variable'), + config.get('unit_of_measurement'), + config.get('correction_factor', None), + config.get('decimal_places', None))]) + + +# pylint: disable=too-many-arguments +class RestSensor(Entity): + """ Implements a REST sensor. """ + + def __init__(self, rest, name, variable, unit_of_measurement, corr_factor, + decimal_places): + self.rest = rest + self._name = name + self._variable = variable + self._state = 'n/a' + self._unit_of_measurement = unit_of_measurement + self._corr_factor = corr_factor + self._decimal_places = decimal_places + self.update() + + @property + def name(self): + """ The name of the sensor. """ + return self._name + + @property + def unit_of_measurement(self): + """ Unit the value is expressed in. """ + return self._unit_of_measurement + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + def update(self): + """ Gets the latest data from REST API and updates the state. """ + self.rest.update() + value = self.rest.data + + if 'error' in value: + self._state = value['error'] + else: + try: + if value is not None: + if self._corr_factor is not None \ + and self._decimal_places is not None: + self._state = round( + (float(value[self._variable]) * + float(self._corr_factor)), + self._decimal_places) + elif self._corr_factor is not None \ + and self._decimal_places is None: + self._state = round(float(value[self._variable]) * + float(self._corr_factor)) + else: + self._state = value[self._variable] + except ValueError: + self._state = value[self._variable] + + +# pylint: disable=too-few-public-methods +class RestDataGet(object): + """ Class for handling the data retrieval with GET method. """ + + def __init__(self, resource): + self._resource = resource + self.data = dict() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data from REST service with GET method. """ + try: + response = requests.get(self._resource, timeout=10) + if 'error' in self.data: + del self.data['error'] + self.data = response.json() + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to resource/endpoint.") + self.data['error'] = 'N/A' + + +# pylint: disable=too-few-public-methods +class RestDataPost(object): + """ Class for handling the data retrieval with POST method. """ + + def __init__(self, resource, payload): + self._resource = resource + self._payload = payload + self.data = dict() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data from REST service with POST method. """ + try: + response = requests.post(self._resource, data=self._payload, + timeout=10) + if 'error' in self.data: + del self.data['error'] + self.data = response.json() + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to resource/endpoint.") + self.data['error'] = 'N/A' diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 7ee0fc19a99..6ec24d18ef1 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -34,7 +34,7 @@ import homeassistant.util as util DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit']) -REQUIREMENTS = ['tellcore-py==1.0.4'] +REQUIREMENTS = ['tellcore-py==1.1.2'] # pylint: disable=unused-argument diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index ae064d4fdb8..1a0f7097b52 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -11,14 +11,14 @@ signal_repetitions: 3 """ import logging - -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, + ATTR_FRIENDLY_NAME) from homeassistant.helpers.entity import ToggleEntity import tellcore.constants as tellcore_constants - +from tellcore.library import DirectCallbackDispatcher SINGAL_REPETITIONS = 1 -REQUIREMENTS = ['tellcore-py==1.0.4'] +REQUIREMENTS = ['tellcore-py==1.1.2'] # pylint: disable=unused-argument @@ -31,16 +31,34 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): "Failed to import tellcore") return + core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) + signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS) - core = telldus.TelldusCore() switches_and_lights = core.devices() switches = [] for switch in switches_and_lights: if not switch.methods(tellcore_constants.TELLSTICK_DIM): - switches.append(TellstickSwitchDevice(switch, signal_repetitions)) + switches.append( + TellstickSwitchDevice(switch, signal_repetitions)) + + def _device_event_callback(id_, method, data, cid): + """ Called from the TelldusCore library to update one device """ + for switch_device in switches: + if switch_device.tellstick_device.id == id_: + switch_device.update_ha_state() + break + + callback_id = core.register_device_event(_device_event_callback) + + def unload_telldus_lib(event): + """ Un-register the callback bindings """ + if callback_id is not None: + core.unregister_callback(callback_id) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib) add_devices_callback(switches) @@ -50,15 +68,20 @@ class TellstickSwitchDevice(ToggleEntity): last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | tellcore_constants.TELLSTICK_TURNOFF) - def __init__(self, tellstick, signal_repetitions): - self.tellstick = tellstick - self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name} + def __init__(self, tellstick_device, signal_repetitions): + self.tellstick_device = tellstick_device + self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name} self.signal_repetitions = signal_repetitions + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + @property def name(self): """ Returns the name of the switch if any. """ - return self.tellstick.name + return self.tellstick_device.name @property def state_attributes(self): @@ -68,7 +91,7 @@ class TellstickSwitchDevice(ToggleEntity): @property def is_on(self): """ True if switch is on. """ - last_command = self.tellstick.last_sent_command( + last_command = self.tellstick_device.last_sent_command( self.last_sent_command_mask) return last_command == tellcore_constants.TELLSTICK_TURNON @@ -76,9 +99,11 @@ class TellstickSwitchDevice(ToggleEntity): def turn_on(self, **kwargs): """ Turns the switch on. """ for _ in range(self.signal_repetitions): - self.tellstick.turn_on() + self.tellstick_device.turn_on() + self.update_ha_state() def turn_off(self, **kwargs): """ Turns the switch off. """ for _ in range(self.signal_repetitions): - self.tellstick.turn_off() + self.tellstick_device.turn_off() + self.update_ha_state() diff --git a/homeassistant/const.py b/homeassistant/const.py index 21a2e6c41e3..c644f6883d1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """ Constants used by Home Assistant components. """ -__version__ = "0.7.3dev0" +__version__ = "0.7.4dev0" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' diff --git a/homeassistant/core.py b/homeassistant/core.py index df18d7e7902..d0494e070f6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -26,6 +26,7 @@ from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError) import homeassistant.util as util import homeassistant.util.dt as date_util +import homeassistant.util.location as location import homeassistant.helpers.temperature as temp_helper from homeassistant.config import get_default_config_dir @@ -676,6 +677,10 @@ class Config(object): # Directory that holds the configuration self.config_dir = get_default_config_dir() + def distance(self, lat, lon): + """ 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. """ return os.path.join(self.config_dir, *path) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index d4a18806a17..909b86a67ed 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -9,7 +9,9 @@ import logging from homeassistant.core import State import homeassistant.util.dt as dt_util from homeassistant.const import ( - STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) + STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, + STATE_PLAYING, STATE_PAUSED, ATTR_ENTITY_ID) _LOGGER = logging.getLogger(__name__) @@ -55,7 +57,11 @@ def reproduce_state(hass, states, blocking=False): state.entity_id) continue - if state.state == STATE_ON: + if state.domain == 'media_player' and state.state == STATE_PAUSED: + service = SERVICE_MEDIA_PAUSE + elif state.domain == 'media_player' and state.state == STATE_PLAYING: + service = SERVICE_MEDIA_PLAY + elif state.state == STATE_ON: service = SERVICE_TURN_ON elif state.state == STATE_OFF: service = SERVICE_TURN_OFF diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 8cc008613cb..ade15131a8f 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -1,5 +1,6 @@ """Module with location helpers.""" import collections +from math import radians, cos, sin, asin, sqrt import requests @@ -28,3 +29,20 @@ def detect_location_info(): 'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI') return LocationInfo(**data) + + +# From: http://stackoverflow.com/a/4913653/646416 +def distance(lon1, lat1, lon2, lat2): + """ + Calculate the great circle distance in meters between two points specified + in decimal degrees on the earth using the Haversine algorithm. + """ + # convert decimal degrees to radians + lon1, lat1, lon2, lat2 = (radians(val) for val in (lon1, lat1, lon2, lat2)) + + dlon = lon2 - lon1 + dlat = lat2 - lat1 + angle = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 + # Radius of earth in meters. + radius = 6371000 + return 2 * radius * asin(sqrt(angle)) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000000..5ee64771657 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests diff --git a/requirements_all.txt b/requirements_all.txt index 19cc04016a8..04c944e4447 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ pychromecast==0.6.12 pyuserinput==0.1.9 # Tellstick bindings (*.tellstick) -tellcore-py==1.0.4 +tellcore-py==1.1.2 # Nmap bindings (device_tracker.nmap) python-nmap==0.4.3 @@ -86,7 +86,7 @@ https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75 pynetgear==0.3 # Netdisco (discovery) -netdisco==0.4 +netdisco==0.4.1 # Wemo (switch.wemo) pywemo==0.3 @@ -133,3 +133,6 @@ https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5 # Sonos bindings (media_player.sonos) SoCo==0.11.1 + +# PlexAPI (media_player.plex) +https://github.com/miniconfig/python-plexapi/archive/437e36dca3b7780dc0cb73941d662302c0cd2fa9.zip#python-plexapi==1.0.2 diff --git a/script/cibuild b/script/cibuild index ade1b1d91c5..bd8ac963429 100755 --- a/script/cibuild +++ b/script/cibuild @@ -3,5 +3,12 @@ # script/cibuild: Setup environment for CI to run tests. This is primarily # designed to run on the continuous integration server. +cd "$(dirname "$0")/.." + script/test coverage + +STATUS=$? + coveralls + +exit $STATUS diff --git a/script/hass-daemon b/script/hass-daemon index d11c2669e87..bb14ce7f0a6 100755 --- a/script/hass-daemon +++ b/script/hass-daemon @@ -34,25 +34,27 @@ RUN_AS="USER" PID_FILE="/var/run/hass.pid" CONFIG_DIR="/var/opt/homeassistant" FLAGS="-v --config $CONFIG_DIR --pid-file $PID_FILE --daemon" +REDIRECT="> $CONFIG_DIR/home-assistant.log 2>&1" start() { - if [ -f $PID_FILE ] && kill -0 $(cat $PID_FILE); then + if [ -f $PID_FILE ] && kill -0 $(cat $PID_FILE) 2> /dev/null; then echo 'Service already running' >&2 return 1 fi echo 'Starting service…' >&2 - local CMD="$PRE_EXEC hass $FLAGS;" + local CMD="$PRE_EXEC hass $FLAGS $REDIRECT;" su -c "$CMD" $RUN_AS echo 'Service started' >&2 } stop() { - if [ ! -f "$PID_FILE" ] || ! kill -0 $(cat "$PID_FILE"); then + if [ ! -f "$PID_FILE" ] || ! kill -0 $(cat "$PID_FILE") 2> /dev/null; then echo 'Service not running' >&2 return 1 fi echo 'Stopping service…' >&2 kill -3 $(cat "$PID_FILE") + while ps -p $(cat "$PID_FILE") > /dev/null 2>&1; do sleep 1;done; echo 'Service stopped' >&2 } diff --git a/script/home-assistant@.service b/script/home-assistant@.service new file mode 100644 index 00000000000..983844a95a3 --- /dev/null +++ b/script/home-assistant@.service @@ -0,0 +1,14 @@ +# This is a simple service file for systems with systemd to tun HA as user. +# +[Unit] +Description=Home Assistant for %i +After=network.target + +[Service] +Type=simple +User=%i +WorkingDirectory=%h +ExecStart=/usr/bin/hass --config %h/.homeassistant/ + +[Install] +WantedBy=multi-user.target diff --git a/script/lint b/script/lint index 120f364120f..75667ef88a4 100755 --- a/script/lint +++ b/script/lint @@ -3,7 +3,17 @@ cd "$(dirname "$0")/.." echo "Checking style with flake8..." -flake8 homeassistant +flake8 --exclude www_static homeassistant + +FLAKE8_STATUS=$? echo "Checking style with pylint..." pylint homeassistant +PYLINT_STATUS=$? + +if [ $FLAKE8_STATUS -eq 0 ] +then + exit $PYLINT_STATUS +else + exit $FLAKE8_STATUS +fi diff --git a/script/release b/script/release new file mode 100755 index 00000000000..40d906b17bf --- /dev/null +++ b/script/release @@ -0,0 +1,21 @@ +# Pushes a new version to PyPi + +cd "$(dirname "$0")/.." + +head -n 3 homeassistant/const.py | tail -n 1 | grep dev + +if [ $? -eq 0 ] +then + echo "Release version should not contain dev tag" + exit 1 +fi + +CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD` + +if [ "$CURRENT_BRANCH" != "master" ] +then + echo "You have to be on the master branch to release." + exit 1 +fi + +python3 setup.py sdist bdist_wheel upload diff --git a/script/test b/script/test index 56fe4dcec89..d407f57a338 100755 --- a/script/test +++ b/script/test @@ -7,10 +7,21 @@ cd "$(dirname "$0")/.." script/lint +LINT_STATUS=$? + echo "Running tests..." if [ "$1" = "coverage" ]; then - py.test --cov homeassistant tests + py.test --cov --cov-report= + TEST_STATUS=$? else - py.test tests + py.test + TEST_STATUS=$? +fi + +if [ $LINT_STATUS -eq 0 ] +then + exit $TEST_STATUS +else + exit $LINT_STATUS fi diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index 01867f3850e..465faf4ec8f 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -75,7 +75,7 @@ class TestAutomationEvent(unittest.TestCase): 'event_type': 'test_event', }, 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } })) @@ -93,12 +93,13 @@ class TestAutomationEvent(unittest.TestCase): 'event_data': {'some_attr': 'some_value'} }, 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } })) - self.hass.bus.fire('test_event', {'some_attr': 'some_value'}) + self.hass.bus.fire('test_event', {'some_attr': 'some_value', + 'another': 'value'}) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) @@ -111,7 +112,7 @@ class TestAutomationEvent(unittest.TestCase): 'event_data': {'some_attr': 'some_value'} }, 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } })) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 6a011a072a5..3f6b0dab6f1 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -10,7 +10,7 @@ import homeassistant.components.automation as automation from homeassistant.const import ATTR_ENTITY_ID -class TestAutomationEvent(unittest.TestCase): +class TestAutomation(unittest.TestCase): """ Test the event automation. """ def setUp(self): # pylint: disable=invalid-name @@ -26,7 +26,7 @@ class TestAutomationEvent(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_service_data_not_a_dict(self): + def test_old_config_service_data_not_a_dict(self): automation.setup(self.hass, { automation.DOMAIN: { 'platform': 'event', @@ -87,6 +87,24 @@ class TestAutomationEvent(unittest.TestCase): self.assertEqual(['hello.world', 'hello.world2'], self.calls[0].data.get(ATTR_ENTITY_ID)) + def test_service_data_not_a_dict(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'service': 'test.automation', + 'data': 100, + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_service_specify_data(self): automation.setup(self.hass, { automation.DOMAIN: { @@ -95,8 +113,8 @@ class TestAutomationEvent(unittest.TestCase): 'event_type': 'test_event', }, 'action': { - 'execute_service': 'test.automation', - 'service_data': {'some': 'data'} + 'service': 'test.automation', + 'data': {'some': 'data'} } } }) @@ -114,8 +132,8 @@ class TestAutomationEvent(unittest.TestCase): 'event_type': 'test_event', }, 'action': { - 'execute_service': 'test.automation', - 'service_entity_id': 'hello.world' + 'service': 'test.automation', + 'entity_id': 'hello.world' } } }) @@ -134,8 +152,8 @@ class TestAutomationEvent(unittest.TestCase): 'event_type': 'test_event', }, 'action': { - 'execute_service': 'test.automation', - 'service_entity_id': ['hello.world', 'hello.world2'] + 'service': 'test.automation', + 'entity_id': ['hello.world', 'hello.world2'] } } }) @@ -160,7 +178,7 @@ class TestAutomationEvent(unittest.TestCase): } ], 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } }) @@ -195,7 +213,7 @@ class TestAutomationEvent(unittest.TestCase): } ], 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } }) @@ -239,7 +257,7 @@ class TestAutomationEvent(unittest.TestCase): } ], 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } }) @@ -278,7 +296,7 @@ class TestAutomationEvent(unittest.TestCase): ], 'condition': 'use_trigger_values', 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } }) @@ -314,7 +332,7 @@ class TestAutomationEvent(unittest.TestCase): ], 'condition': 'use_trigger_values', 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } }) @@ -322,3 +340,34 @@ class TestAutomationEvent(unittest.TestCase): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + + def test_automation_list_setting(self): + """ Event is not a valid condition. Will it still work? """ + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: [{ + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + + 'action': { + 'service': 'test.automation', + } + }, { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event_2', + }, + 'action': { + 'service': 'test.automation', + } + }] + })) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.bus.fire('test_event_2') + self.hass.pool.block_till_done() + self.assertEqual(2, len(self.calls)) diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index d5e969abe5d..174ef91f1c4 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -77,7 +77,7 @@ class TestAutomationState(unittest.TestCase): 'topic': 'test-topic' }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -95,7 +95,7 @@ class TestAutomationState(unittest.TestCase): 'payload': 'hello' }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -113,7 +113,7 @@ class TestAutomationState(unittest.TestCase): 'payload': 'hello' }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index e946c138a95..a04b8d01f4e 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -35,7 +35,7 @@ class TestAutomationNumericState(unittest.TestCase): 'below': 10, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -56,7 +56,7 @@ class TestAutomationNumericState(unittest.TestCase): 'below': 10, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -78,7 +78,7 @@ class TestAutomationNumericState(unittest.TestCase): 'below': 10, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -97,7 +97,7 @@ class TestAutomationNumericState(unittest.TestCase): 'above': 10, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -119,7 +119,7 @@ class TestAutomationNumericState(unittest.TestCase): 'above': 10, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -142,7 +142,7 @@ class TestAutomationNumericState(unittest.TestCase): 'above': 10, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -162,7 +162,7 @@ class TestAutomationNumericState(unittest.TestCase): 'above': 5, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -181,7 +181,7 @@ class TestAutomationNumericState(unittest.TestCase): 'above': 5, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -203,7 +203,7 @@ class TestAutomationNumericState(unittest.TestCase): 'above': 5, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -226,7 +226,7 @@ class TestAutomationNumericState(unittest.TestCase): 'above': 5, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -244,7 +244,7 @@ class TestAutomationNumericState(unittest.TestCase): 'entity_id': 'test.another_entity', }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -269,7 +269,7 @@ class TestAutomationNumericState(unittest.TestCase): 'below': test_state + 2 }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } }) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index b0410c75014..a7c13e866c6 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -164,7 +164,7 @@ class TestAutomationState(unittest.TestCase): 'entity_id': 'test.entity', }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -182,7 +182,7 @@ class TestAutomationState(unittest.TestCase): 'from': 'hello' }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -200,7 +200,7 @@ class TestAutomationState(unittest.TestCase): 'to': 'world' }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -218,7 +218,7 @@ class TestAutomationState(unittest.TestCase): 'state': 'world' }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -237,7 +237,7 @@ class TestAutomationState(unittest.TestCase): 'to': 'world' }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -256,7 +256,7 @@ class TestAutomationState(unittest.TestCase): 'to': 'world' }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -277,7 +277,7 @@ class TestAutomationState(unittest.TestCase): 'to': 'world' }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -294,7 +294,7 @@ class TestAutomationState(unittest.TestCase): 'entity_id': 'test.anoter_entity', }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -318,7 +318,7 @@ class TestAutomationState(unittest.TestCase): 'state': test_state }], 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } }) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 4781c5be79b..de8b2f8121b 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -51,7 +51,7 @@ class TestAutomationSun(unittest.TestCase): 'event': 'sunset', }, 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } })) @@ -77,7 +77,7 @@ class TestAutomationSun(unittest.TestCase): 'event': 'sunrise', }, 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } })) @@ -104,7 +104,7 @@ class TestAutomationSun(unittest.TestCase): 'offset': '0:30:00' }, 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } })) @@ -131,7 +131,7 @@ class TestAutomationSun(unittest.TestCase): 'offset': '-0:30:00' }, 'action': { - 'execute_service': 'test.automation', + 'service': 'test.automation', } } })) diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 95997bfec42..e233c93988d 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -36,9 +36,9 @@ class TestAutomationTime(unittest.TestCase): def test_old_config_if_fires_when_hour_matches(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'time', + 'platform': 'time', time.CONF_HOURS: 0, - automation.CONF_SERVICE: 'test.automation' + 'execute_service': 'test.automation' } })) @@ -51,9 +51,9 @@ class TestAutomationTime(unittest.TestCase): def test_old_config_if_fires_when_minute_matches(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'time', + 'platform': 'time', time.CONF_MINUTES: 0, - automation.CONF_SERVICE: 'test.automation' + 'execute_service': 'test.automation' } })) @@ -66,9 +66,9 @@ class TestAutomationTime(unittest.TestCase): def test_old_config_if_fires_when_second_matches(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'time', + 'platform': 'time', time.CONF_SECONDS: 0, - automation.CONF_SERVICE: 'test.automation' + 'execute_service': 'test.automation' } })) @@ -85,7 +85,7 @@ class TestAutomationTime(unittest.TestCase): time.CONF_HOURS: 0, time.CONF_MINUTES: 0, time.CONF_SECONDS: 0, - automation.CONF_SERVICE: 'test.automation' + 'execute_service': 'test.automation' } })) @@ -101,7 +101,7 @@ class TestAutomationTime(unittest.TestCase): automation.DOMAIN: { CONF_PLATFORM: 'event', event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', + 'execute_service': 'test.automation', 'if': { CONF_PLATFORM: 'time', time.CONF_BEFORE: '10:00' @@ -131,7 +131,7 @@ class TestAutomationTime(unittest.TestCase): automation.DOMAIN: { CONF_PLATFORM: 'event', event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', + 'execute_service': 'test.automation', 'if': { CONF_PLATFORM: 'time', time.CONF_AFTER: '10:00' @@ -161,7 +161,7 @@ class TestAutomationTime(unittest.TestCase): automation.DOMAIN: { CONF_PLATFORM: 'event', event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', + 'execute_service': 'test.automation', 'if': { CONF_PLATFORM: 'time', time.CONF_WEEKDAY: 'mon', @@ -192,7 +192,7 @@ class TestAutomationTime(unittest.TestCase): automation.DOMAIN: { CONF_PLATFORM: 'event', event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', + 'execute_service': 'test.automation', 'if': { CONF_PLATFORM: 'time', time.CONF_WEEKDAY: ['mon', 'tue'], @@ -234,7 +234,7 @@ class TestAutomationTime(unittest.TestCase): 'hours': 0, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -252,7 +252,7 @@ class TestAutomationTime(unittest.TestCase): 'minutes': 0, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -270,7 +270,7 @@ class TestAutomationTime(unittest.TestCase): 'seconds': 0, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -290,7 +290,7 @@ class TestAutomationTime(unittest.TestCase): 'seconds': 3, }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -309,7 +309,7 @@ class TestAutomationTime(unittest.TestCase): 'after': '5:00:00', }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -332,7 +332,7 @@ class TestAutomationTime(unittest.TestCase): # Total seconds. Hour = 3600 second }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } })) @@ -356,7 +356,7 @@ class TestAutomationTime(unittest.TestCase): 'before': '10:00', }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } }) @@ -390,7 +390,7 @@ class TestAutomationTime(unittest.TestCase): 'after': '10:00', }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } }) @@ -424,7 +424,7 @@ class TestAutomationTime(unittest.TestCase): 'weekday': 'mon', }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } }) @@ -459,7 +459,7 @@ class TestAutomationTime(unittest.TestCase): 'weekday': ['mon', 'tue'], }, 'action': { - 'execute_service': 'test.automation' + 'service': 'test.automation' } } }) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 8b086e97c88..fb368bf863a 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -103,12 +103,12 @@ class TestComponentsDeviceTracker(unittest.TestCase): def test_reading_yaml_config(self): dev_id = 'test' device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ', - 'Test name', 'http://test.picture', True) + self.hass, timedelta(seconds=180), 0, True, dev_id, + 'AB:CD:EF:GH:IJ', 'Test name', 'http://test.picture', True) device_tracker.update_config(self.yaml_devices, dev_id, device) self.assertTrue(device_tracker.setup(self.hass, {})) config = device_tracker.load_config(self.yaml_devices, self.hass, - device.consider_home)[0] + device.consider_home, 0)[0] self.assertEqual(device.dev_id, config.dev_id) self.assertEqual(device.track, config.track) self.assertEqual(device.mac, config.mac) @@ -126,7 +126,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertTrue(device_tracker.setup(self.hass, { device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) config = device_tracker.load_config(self.yaml_devices, self.hass, - timedelta(seconds=0))[0] + timedelta(seconds=0), 0)[0] self.assertEqual('dev1', config.dev_id) self.assertEqual(True, config.track) @@ -176,7 +176,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): picture = 'http://placehold.it/200x200' device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, dev_id, None, + self.hass, timedelta(seconds=180), 0, True, dev_id, None, friendly_name, picture, away_hide=True) device_tracker.update_config(self.yaml_devices, dev_id, device) @@ -191,7 +191,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): dev_id = 'test_entity' entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, dev_id, None, + self.hass, timedelta(seconds=180), 0, True, dev_id, None, away_hide=True) device_tracker.update_config(self.yaml_devices, dev_id, device) @@ -208,7 +208,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): dev_id = 'test_entity' entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, dev_id, None, + self.hass, timedelta(seconds=180), 0, True, dev_id, None, away_hide=True) device_tracker.update_config(self.yaml_devices, dev_id, device)