This commit is contained in:
sfam 2015-09-24 21:19:21 +00:00
commit 94eb54ff00
53 changed files with 2552 additions and 298 deletions

View File

@ -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

7
.gitignore vendored
View File

@ -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]

View File

@ -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`.

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()]

View File

@ -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",
{

View File

@ -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()]

View File

@ -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

View File

@ -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

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
DOMAIN = "discovery"
DEPENDENCIES = []
REQUIREMENTS = ['netdisco==0.4']
REQUIREMENTS = ['netdisco==0.4.1']
SCAN_INTERVAL = 300 # seconds

View File

@ -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/]+)|)')

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "0ab148ece11ddde26b95460c2c91da3d"
VERSION = "7301f590f66ffc5b34d3991fc9eb0247"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 63e039a221ae6771e0d7c6990d9a93b7cc22fc64
Subproject commit be2312de03386b05c6d0ff91abe894fd36338d96

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

View File

@ -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(

View File

@ -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. """

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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()

View File

@ -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 = '*'

View File

@ -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)

View File

@ -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

View File

@ -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))

2
pytest.ini Normal file
View File

@ -0,0 +1,2 @@
[pytest]
testpaths = tests

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

21
script/release Executable file
View File

@ -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

View File

@ -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

View File

@ -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',
}
}
}))

View File

@ -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))

View File

@ -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'
}
}
}))

View File

@ -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'
}
}
})

View File

@ -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'
}
}
})

View File

@ -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',
}
}
}))

View File

@ -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'
}
}
})

View File

@ -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)