Merge branch 'dev' into mysensors-component-switch

Conflicts:
	homeassistant/components/sensor/__init__.py
	homeassistant/components/switch/__init__.py
This commit is contained in:
MartinHjelmare 2015-12-31 06:05:06 +01:00
commit 6ff24ed047
47 changed files with 1171 additions and 427 deletions

View File

@ -15,6 +15,10 @@ omit =
homeassistant/components/*/modbus.py homeassistant/components/*/modbus.py
homeassistant/components/*/tellstick.py homeassistant/components/*/tellstick.py
homeassistant/components/tellduslive.py
homeassistant/components/*/tellduslive.py
homeassistant/components/*/vera.py homeassistant/components/*/vera.py
homeassistant/components/ecobee.py homeassistant/components/ecobee.py

1
.gitignore vendored
View File

@ -41,6 +41,7 @@ Icon
dist dist
build build
eggs eggs
.eggs
parts parts
bin bin
var var

View File

@ -8,8 +8,8 @@ python:
- 3.4 - 3.4
- 3.5 - 3.5
install: install:
# Validate requirements_all.txt on Python 3.5 # Validate requirements_all.txt on Python 3.4
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py develop; script/gen_requirements_all.py validate; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; tput sgr0; fi
- script/bootstrap_server - script/bootstrap_server
script: script:
- script/cibuild - script/cibuild

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013 Paulus Schoutsen Copyright (c) 2016 Paulus Schoutsen
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
alarms.extend([ alarms.extend([
VerisureAlarm(value) VerisureAlarm(value)
for value in verisure.get_alarm_status().values() for value in verisure.ALARM_STATUS.values()
if verisure.SHOW_ALARM if verisure.SHOW_ALARM
]) ])
@ -42,7 +42,6 @@ class VerisureAlarm(alarm.AlarmControlPanel):
def __init__(self, alarm_status): def __init__(self, alarm_status):
self._id = alarm_status.id self._id = alarm_status.id
self._device = verisure.MY_PAGES.DEVICE_ALARM
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@property @property
@ -62,36 +61,36 @@ class VerisureAlarm(alarm.AlarmControlPanel):
def update(self): def update(self):
""" Update alarm status """ """ Update alarm status """
verisure.update() verisure.update_alarm()
if verisure.STATUS[self._device][self._id].status == 'unarmed': if verisure.ALARM_STATUS[self._id].status == 'unarmed':
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
elif verisure.STATUS[self._device][self._id].status == 'armedhome': elif verisure.ALARM_STATUS[self._id].status == 'armedhome':
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
elif verisure.STATUS[self._device][self._id].status == 'armedaway': elif verisure.ALARM_STATUS[self._id].status == 'armedaway':
self._state = STATE_ALARM_ARMED_AWAY self._state = STATE_ALARM_ARMED_AWAY
elif verisure.STATUS[self._device][self._id].status != 'pending': elif verisure.ALARM_STATUS[self._id].status != 'pending':
_LOGGER.error( _LOGGER.error(
'Unknown alarm state %s', 'Unknown alarm state %s',
verisure.STATUS[self._device][self._id].status) verisure.ALARM_STATUS[self._id].status)
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
""" Send disarm command. """ """ Send disarm command. """
verisure.MY_PAGES.set_alarm_status( verisure.MY_PAGES.alarm.set(code, 'DISARMED')
code, _LOGGER.info('verisure alarm disarming')
verisure.MY_PAGES.ALARM_DISARMED) verisure.MY_PAGES.alarm.wait_while_pending()
_LOGGER.warning('disarming') verisure.update_alarm()
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
""" Send arm home command. """ """ Send arm home command. """
verisure.MY_PAGES.set_alarm_status( verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME')
code, _LOGGER.info('verisure alarm arming home')
verisure.MY_PAGES.ALARM_ARMED_HOME) verisure.MY_PAGES.alarm.wait_while_pending()
_LOGGER.warning('arming home') verisure.update_alarm()
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
""" Send arm away command. """ """ Send arm away command. """
verisure.MY_PAGES.set_alarm_status( verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY')
code, _LOGGER.info('verisure alarm arming away')
verisure.MY_PAGES.ALARM_ARMED_AWAY) verisure.MY_PAGES.alarm.wait_while_pending()
_LOGGER.warning('arming away') verisure.update_alarm()

View File

@ -17,6 +17,10 @@ DEPENDENCIES = ['sun']
CONF_OFFSET = 'offset' CONF_OFFSET = 'offset'
CONF_EVENT = 'event' CONF_EVENT = 'event'
CONF_BEFORE = "before"
CONF_BEFORE_OFFSET = "before_offset"
CONF_AFTER = "after"
CONF_AFTER_OFFSET = "after_offset"
EVENT_SUNSET = 'sunset' EVENT_SUNSET = 'sunset'
EVENT_SUNRISE = 'sunrise' EVENT_SUNRISE = 'sunrise'
@ -37,26 +41,9 @@ def trigger(hass, config, action):
_LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event) _LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event)
return False return False
if CONF_OFFSET in config: offset = _parse_offset(config.get(CONF_OFFSET))
raw_offset = config.get(CONF_OFFSET) if offset is False:
return False
negative_offset = False
if raw_offset.startswith('-'):
negative_offset = True
raw_offset = raw_offset[1:]
try:
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
except ValueError:
_LOGGER.error('Could not parse offset %s', raw_offset)
return False
offset = timedelta(hours=hour, minutes=minute, seconds=second)
if negative_offset:
offset *= -1
else:
offset = timedelta(0)
# Do something to call action # Do something to call action
if event == EVENT_SUNRISE: if event == EVENT_SUNRISE:
@ -67,6 +54,65 @@ def trigger(hass, config, action):
return True return True
def if_action(hass, config):
""" Wraps action method with sun based condition. """
before = config.get(CONF_BEFORE)
after = config.get(CONF_AFTER)
# Make sure required configuration keys are present
if before is None and after is None:
logging.getLogger(__name__).error(
"Missing if-condition configuration key %s or %s",
CONF_BEFORE, CONF_AFTER)
return None
# Make sure configuration keys have the right value
if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \
after not in (None, EVENT_SUNRISE, EVENT_SUNSET):
logging.getLogger(__name__).error(
"%s and %s can only be set to %s or %s",
CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET)
return None
before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET))
after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET))
if before_offset is False or after_offset is False:
return None
if before is None:
before_func = lambda: None
elif before == EVENT_SUNRISE:
before_func = lambda: sun.next_rising_utc(hass) + before_offset
else:
before_func = lambda: sun.next_setting_utc(hass) + before_offset
if after is None:
after_func = lambda: None
elif after == EVENT_SUNRISE:
after_func = lambda: sun.next_rising_utc(hass) + after_offset
else:
after_func = lambda: sun.next_setting_utc(hass) + after_offset
def time_if():
""" Validate time based if-condition """
now = dt_util.utcnow()
before = before_func()
after = after_func()
if before is not None and now > now.replace(hour=before.hour,
minute=before.minute):
return False
if after is not None and now < now.replace(hour=after.hour,
minute=after.minute):
return False
return True
return time_if
def trigger_sunrise(hass, action, offset): def trigger_sunrise(hass, action, offset):
""" Trigger action at next sun rise. """ """ Trigger action at next sun rise. """
def next_rise(): def next_rise():
@ -103,3 +149,26 @@ def trigger_sunset(hass, action, offset):
action() action()
track_point_in_utc_time(hass, sunset_automation_listener, next_set()) track_point_in_utc_time(hass, sunset_automation_listener, next_set())
def _parse_offset(raw_offset):
if raw_offset is None:
return timedelta(0)
negative_offset = False
if raw_offset.startswith('-'):
negative_offset = True
raw_offset = raw_offset[1:]
try:
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
except ValueError:
_LOGGER.error('Could not parse offset %s', raw_offset)
return False
offset = timedelta(hours=hour, minutes=minute, seconds=second)
if negative_offset:
offset *= -1
return offset

View File

@ -50,6 +50,7 @@ FLASH_LONG = "long"
# Apply an effect to the light, can be EFFECT_COLORLOOP # Apply an effect to the light, can be EFFECT_COLORLOOP
ATTR_EFFECT = "effect" ATTR_EFFECT = "effect"
EFFECT_COLORLOOP = "colorloop" EFFECT_COLORLOOP = "colorloop"
EFFECT_RANDOM = "random"
EFFECT_WHITE = "white" EFFECT_WHITE = "white"
LIGHT_PROFILES_FILE = "light_profiles.csv" LIGHT_PROFILES_FILE = "light_profiles.csv"
@ -228,7 +229,8 @@ def setup(hass, config):
if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG): if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG):
params[ATTR_FLASH] = dat[ATTR_FLASH] params[ATTR_FLASH] = dat[ATTR_FLASH]
if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE): if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE,
EFFECT_RANDOM):
params[ATTR_EFFECT] = dat[ATTR_EFFECT] params[ATTR_EFFECT] = dat[ATTR_EFFECT]
for light in target_lights: for light in target_lights:

View File

@ -10,6 +10,7 @@ import json
import logging import logging
import os import os
import socket import socket
import random
from datetime import timedelta from datetime import timedelta
from urllib.parse import urlparse from urllib.parse import urlparse
@ -20,7 +21,7 @@ from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.light import ( from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP, Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP,
ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT,
ATTR_EFFECT, EFFECT_COLORLOOP, ATTR_RGB_COLOR) ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, ATTR_RGB_COLOR)
REQUIREMENTS = ['phue==0.8'] REQUIREMENTS = ['phue==0.8']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@ -233,6 +234,9 @@ class HueLight(Light):
if effect == EFFECT_COLORLOOP: if effect == EFFECT_COLORLOOP:
command['effect'] = 'colorloop' command['effect'] = 'colorloop'
elif effect == EFFECT_RANDOM:
command['hue'] = random.randrange(0, 65535)
command['sat'] = random.randrange(150, 254)
else: else:
command['effect'] = 'none' command['effect'] = 'none'

View File

@ -42,6 +42,7 @@ turn_on:
description: Light effect description: Light effect
values: values:
- colorloop - colorloop
- random
turn_off: turn_off:
description: Turn a light off description: Turn a light off

View File

@ -30,7 +30,7 @@ DEFAULT_QOS = 0
DEFAULT_RETAIN = False DEFAULT_RETAIN = False
SERVICE_PUBLISH = 'publish' SERVICE_PUBLISH = 'publish'
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED' EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received'
REQUIREMENTS = ['paho-mqtt==1.1'] REQUIREMENTS = ['paho-mqtt==1.1']

View File

@ -16,7 +16,7 @@ import json
import atexit import atexit
from homeassistant.core import Event, EventOrigin, State from homeassistant.core import Event, EventOrigin, State
import homeassistant.util.dt as date_util import homeassistant.util.dt as dt_util
from homeassistant.remote import JSONEncoder from homeassistant.remote import JSONEncoder
from homeassistant.const import ( from homeassistant.const import (
MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
@ -62,8 +62,8 @@ def row_to_state(row):
try: try:
return State( return State(
row[1], row[2], json.loads(row[3]), row[1], row[2], json.loads(row[3]),
date_util.utc_from_timestamp(row[4]), dt_util.utc_from_timestamp(row[4]),
date_util.utc_from_timestamp(row[5])) dt_util.utc_from_timestamp(row[5]))
except ValueError: except ValueError:
# When json.loads fails # When json.loads fails
_LOGGER.exception("Error converting row to state: %s", row) _LOGGER.exception("Error converting row to state: %s", row)
@ -74,7 +74,7 @@ def row_to_event(row):
""" Convert a databse row to an event. """ """ Convert a databse row to an event. """
try: try:
return Event(row[1], json.loads(row[2]), EventOrigin(row[3]), return Event(row[1], json.loads(row[2]), EventOrigin(row[3]),
date_util.utc_from_timestamp(row[5])) dt_util.utc_from_timestamp(row[5]))
except ValueError: except ValueError:
# When json.loads fails # When json.loads fails
_LOGGER.exception("Error converting row to event: %s", row) _LOGGER.exception("Error converting row to event: %s", row)
@ -116,10 +116,10 @@ class RecorderRun(object):
self.start = _INSTANCE.recording_start self.start = _INSTANCE.recording_start
self.closed_incorrect = False self.closed_incorrect = False
else: else:
self.start = date_util.utc_from_timestamp(row[1]) self.start = dt_util.utc_from_timestamp(row[1])
if row[2] is not None: if row[2] is not None:
self.end = date_util.utc_from_timestamp(row[2]) self.end = dt_util.utc_from_timestamp(row[2])
self.closed_incorrect = bool(row[3]) self.closed_incorrect = bool(row[3])
@ -169,8 +169,8 @@ class Recorder(threading.Thread):
self.queue = queue.Queue() self.queue = queue.Queue()
self.quit_object = object() self.quit_object = object()
self.lock = threading.Lock() self.lock = threading.Lock()
self.recording_start = date_util.utcnow() self.recording_start = dt_util.utcnow()
self.utc_offset = date_util.now().utcoffset().total_seconds() self.utc_offset = dt_util.now().utcoffset().total_seconds()
def start_recording(event): def start_recording(event):
""" Start recording. """ """ Start recording. """
@ -217,10 +217,11 @@ class Recorder(threading.Thread):
def shutdown(self, event): def shutdown(self, event):
""" Tells the recorder to shut down. """ """ Tells the recorder to shut down. """
self.queue.put(self.quit_object) self.queue.put(self.quit_object)
self.block_till_done()
def record_state(self, entity_id, state, event_id): def record_state(self, entity_id, state, event_id):
""" Save a state to the database. """ """ Save a state to the database. """
now = date_util.utcnow() now = dt_util.utcnow()
# State got deleted # State got deleted
if state is None: if state is None:
@ -247,7 +248,7 @@ class Recorder(threading.Thread):
""" Save an event to the database. """ """ Save an event to the database. """
info = ( info = (
event.event_type, json.dumps(event.data, cls=JSONEncoder), event.event_type, json.dumps(event.data, cls=JSONEncoder),
str(event.origin), date_util.utcnow(), event.time_fired, str(event.origin), dt_util.utcnow(), event.time_fired,
self.utc_offset self.utc_offset
) )
@ -307,7 +308,7 @@ class Recorder(threading.Thread):
def save_migration(migration_id): def save_migration(migration_id):
""" Save and commit a migration to the database. """ """ Save and commit a migration to the database. """
cur.execute('INSERT INTO schema_version VALUES (?, ?)', cur.execute('INSERT INTO schema_version VALUES (?, ?)',
(migration_id, date_util.utcnow())) (migration_id, dt_util.utcnow()))
self.conn.commit() self.conn.commit()
_LOGGER.info("Database migrated to version %d", migration_id) _LOGGER.info("Database migrated to version %d", migration_id)
@ -420,18 +421,18 @@ class Recorder(threading.Thread):
self.query( self.query(
"""INSERT INTO recorder_runs (start, created, utc_offset) """INSERT INTO recorder_runs (start, created, utc_offset)
VALUES (?, ?, ?)""", VALUES (?, ?, ?)""",
(self.recording_start, date_util.utcnow(), self.utc_offset)) (self.recording_start, dt_util.utcnow(), self.utc_offset))
def _close_run(self): def _close_run(self):
""" Save end time for current run. """ """ Save end time for current run. """
self.query( self.query(
"UPDATE recorder_runs SET end=? WHERE start=?", "UPDATE recorder_runs SET end=? WHERE start=?",
(date_util.utcnow(), self.recording_start)) (dt_util.utcnow(), self.recording_start))
def _adapt_datetime(datetimestamp): def _adapt_datetime(datetimestamp):
""" Turn a datetime into an integer for in the DB. """ """ Turn a datetime into an integer for in the DB. """
return date_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp() return dt_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp()
def _verify_instance(): def _verify_instance():

View File

@ -73,8 +73,9 @@ def _process_config(scene_config):
for entity_id in c_entities: for entity_id in c_entities:
if isinstance(c_entities[entity_id], dict): if isinstance(c_entities[entity_id], dict):
state = c_entities[entity_id].pop('state', None) entity_attrs = c_entities[entity_id].copy()
attributes = c_entities[entity_id] state = entity_attrs.pop('state', None)
attributes = entity_attrs
else: else:
state = c_entities[entity_id] state = c_entities[entity_id]
attributes = {} attributes = {}

View File

@ -10,7 +10,7 @@ import logging
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import ( from homeassistant.components import (
wink, zwave, isy994, verisure, ecobee, mysensors) wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors)
DOMAIN = 'sensor' DOMAIN = 'sensor'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
@ -24,6 +24,7 @@ DISCOVERY_PLATFORMS = {
isy994.DISCOVER_SENSORS: 'isy994', isy994.DISCOVER_SENSORS: 'isy994',
verisure.DISCOVER_SENSORS: 'verisure', verisure.DISCOVER_SENSORS: 'verisure',
ecobee.DISCOVER_SENSORS: 'ecobee', ecobee.DISCOVER_SENSORS: 'ecobee',
tellduslive.DISCOVER_SENSORS: 'tellduslive',
mysensors.DISCOVER_SENSORS: 'mysensors', mysensors.DISCOVER_SENSORS: 'mysensors',
} }

View File

@ -53,6 +53,11 @@ class EliqSensor(Entity):
""" Returns the name. """ """ Returns the name. """
return self._name return self._name
@property
def icon(self):
""" Returns icon. """
return "mdi:speedometer"
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """ """ Unit of measurement of this entity, if any. """

View File

@ -102,13 +102,13 @@ class RestSensor(Entity):
self.rest.update() self.rest.update()
value = self.rest.data value = self.rest.data
if 'error' in value: if value is None:
self._state = value['error'] value = STATE_UNKNOWN
else: elif self._value_template is not None:
if self._value_template is not None: value = template.render_with_possible_json_value(
value = template.render_with_possible_json_value( self._hass, self._value_template, value, STATE_UNKNOWN)
self._hass, self._value_template, value, STATE_UNKNOWN)
self._state = value self._state = value
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -118,7 +118,7 @@ class RestDataGet(object):
def __init__(self, resource, verify_ssl): def __init__(self, resource, verify_ssl):
self._resource = resource self._resource = resource
self._verify_ssl = verify_ssl self._verify_ssl = verify_ssl
self.data = dict() self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
@ -126,12 +126,10 @@ class RestDataGet(object):
try: try:
response = requests.get(self._resource, timeout=10, response = requests.get(self._resource, timeout=10,
verify=self._verify_ssl) verify=self._verify_ssl)
if 'error' in self.data:
del self.data['error']
self.data = response.text self.data = response.text
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", self._resource) _LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data['error'] = STATE_UNKNOWN self.data = None
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -142,7 +140,7 @@ class RestDataPost(object):
self._resource = resource self._resource = resource
self._payload = payload self._payload = payload
self._verify_ssl = verify_ssl self._verify_ssl = verify_ssl
self.data = dict() self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
@ -150,9 +148,7 @@ class RestDataPost(object):
try: try:
response = requests.post(self._resource, data=self._payload, response = requests.post(self._resource, data=self._payload,
timeout=10, verify=self._verify_ssl) timeout=10, verify=self._verify_ssl)
if 'error' in self.data:
del self.data['error']
self.data = response.text self.data = response.text
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", self._resource) _LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data['error'] = STATE_UNKNOWN self.data = None

View File

@ -14,25 +14,25 @@ from homeassistant.const import STATE_ON, STATE_OFF
REQUIREMENTS = ['psutil==3.2.2'] REQUIREMENTS = ['psutil==3.2.2']
SENSOR_TYPES = { SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%'], 'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'],
'disk_use': ['Disk Use', 'GiB'], 'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'],
'disk_free': ['Disk Free', 'GiB'], 'disk_free': ['Disk Free', 'GiB', 'mdi:harddisk'],
'memory_use_percent': ['RAM Use', '%'], 'memory_use_percent': ['RAM Use', '%', 'mdi:memory'],
'memory_use': ['RAM Use', 'MiB'], 'memory_use': ['RAM Use', 'MiB', 'mdi:memory'],
'memory_free': ['RAM Free', 'MiB'], 'memory_free': ['RAM Free', 'MiB', 'mdi:memory'],
'processor_use': ['CPU Use', '%'], 'processor_use': ['CPU Use', '%', 'mdi:memory'],
'process': ['Process', ''], 'process': ['Process', '', 'mdi:memory'],
'swap_use_percent': ['Swap Use', '%'], 'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'],
'swap_use': ['Swap Use', 'GiB'], 'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'],
'swap_free': ['Swap Free', 'GiB'], 'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'],
'network_out': ['Sent', 'MiB'], 'network_out': ['Sent', 'MiB', 'mdi:server-network'],
'network_in': ['Recieved', 'MiB'], 'network_in': ['Recieved', 'MiB', 'mdi:server-network'],
'packets_out': ['Packets sent', ''], 'packets_out': ['Packets sent', '', 'mdi:server-network'],
'packets_in': ['Packets recieved', ''], 'packets_in': ['Packets recieved', '', 'mdi:server-network'],
'ipv4_address': ['IPv4 address', ''], 'ipv4_address': ['IPv4 address', '', 'mdi:server-network'],
'ipv6_address': ['IPv6 address', ''], 'ipv6_address': ['IPv6 address', '', 'mdi:server-network'],
'last_boot': ['Last Boot', ''], 'last_boot': ['Last Boot', '', 'mdi:clock'],
'since_last_boot': ['Since Last Boot', ''] 'since_last_boot': ['Since Last Boot', '', 'mdi:clock']
} }
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -69,6 +69,10 @@ class SystemMonitorSensor(Entity):
def name(self): def name(self):
return self._name.rstrip() return self._name.rstrip()
@property
def icon(self):
return SENSOR_TYPES[self.type][2]
@property @property
def state(self): def state(self):
""" Returns the state of the device. """ """ Returns the state of the device. """

View File

@ -0,0 +1,99 @@
"""
homeassistant.components.sensor.tellduslive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Shows sensor values from Tellstick Net/Telstick Live.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.tellduslive/
"""
import logging
from datetime import datetime
from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL
from homeassistant.helpers.entity import Entity
from homeassistant.components import tellduslive
ATTR_LAST_UPDATED = "time_last_updated"
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['tellduslive']
SENSOR_TYPE_TEMP = "temp"
SENSOR_TYPE_HUMIDITY = "humidity"
SENSOR_TYPES = {
SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"],
SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"],
}
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up Tellstick sensors. """
sensors = tellduslive.NETWORK.get_sensors()
devices = []
for component in sensors:
for sensor in component["data"]:
# one component can have more than one sensor
# (e.g. both humidity and temperature)
devices.append(TelldusLiveSensor(component["id"],
component["name"],
sensor["name"]))
add_devices(devices)
class TelldusLiveSensor(Entity):
""" Represents a Telldus Live sensor. """
def __init__(self, sensor_id, sensor_name, sensor_type):
self._sensor_id = sensor_id
self._sensor_type = sensor_type
self._state = None
self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0]
self._last_update = None
self._battery_level = None
self.update()
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def state_attributes(self):
attrs = dict()
if self._battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
if self._last_update is not None:
attrs[ATTR_LAST_UPDATED] = self._last_update
return attrs
@property
def unit_of_measurement(self):
return SENSOR_TYPES[self._sensor_type][1]
@property
def icon(self):
return SENSOR_TYPES[self._sensor_type][2]
def update(self):
values = tellduslive.NETWORK.get_sensor_value(self._sensor_id,
self._sensor_type)
self._state, self._battery_level, self._last_update = values
self._state = float(self._state)
if self._sensor_type == SENSOR_TYPE_TEMP:
self._state = round(self._state, 1)
elif self._sensor_type == SENSOR_TYPE_HUMIDITY:
self._state = int(round(self._state))
self._battery_level = round(self._battery_level * 100 / 255) # percent
self._last_update = str(datetime.fromtimestamp(self._last_update))

View File

@ -27,14 +27,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors.extend([ sensors.extend([
VerisureThermometer(value) VerisureThermometer(value)
for value in verisure.get_climate_status().values() for value in verisure.CLIMATE_STATUS.values()
if verisure.SHOW_THERMOMETERS and if verisure.SHOW_THERMOMETERS and
hasattr(value, 'temperature') and value.temperature hasattr(value, 'temperature') and value.temperature
]) ])
sensors.extend([ sensors.extend([
VerisureHygrometer(value) VerisureHygrometer(value)
for value in verisure.get_climate_status().values() for value in verisure.CLIMATE_STATUS.values()
if verisure.SHOW_HYGROMETERS and if verisure.SHOW_HYGROMETERS and
hasattr(value, 'humidity') and value.humidity hasattr(value, 'humidity') and value.humidity
]) ])
@ -47,20 +47,19 @@ class VerisureThermometer(Entity):
def __init__(self, climate_status): def __init__(self, climate_status):
self._id = climate_status.id self._id = climate_status.id
self._device = verisure.MY_PAGES.DEVICE_CLIMATE
@property @property
def name(self): def name(self):
""" Returns the name of the device. """ """ Returns the name of the device. """
return '{} {}'.format( return '{} {}'.format(
verisure.STATUS[self._device][self._id].location, verisure.CLIMATE_STATUS[self._id].location,
"Temperature") "Temperature")
@property @property
def state(self): def state(self):
""" Returns the state of the device. """ """ Returns the state of the device. """
# remove ° character # remove ° character
return verisure.STATUS[self._device][self._id].temperature[:-1] return verisure.CLIMATE_STATUS[self._id].temperature[:-1]
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@ -69,7 +68,7 @@ class VerisureThermometer(Entity):
def update(self): def update(self):
''' update sensor ''' ''' update sensor '''
verisure.update() verisure.update_climate()
class VerisureHygrometer(Entity): class VerisureHygrometer(Entity):
@ -77,20 +76,19 @@ class VerisureHygrometer(Entity):
def __init__(self, climate_status): def __init__(self, climate_status):
self._id = climate_status.id self._id = climate_status.id
self._device = verisure.MY_PAGES.DEVICE_CLIMATE
@property @property
def name(self): def name(self):
""" Returns the name of the device. """ """ Returns the name of the device. """
return '{} {}'.format( return '{} {}'.format(
verisure.STATUS[self._device][self._id].location, verisure.CLIMATE_STATUS[self._id].location,
"Humidity") "Humidity")
@property @property
def state(self): def state(self):
""" Returns the state of the device. """ """ Returns the state of the device. """
# remove % character # remove % character
return verisure.STATUS[self._device][self._id].humidity[:-1] return verisure.CLIMATE_STATUS[self._id].humidity[:-1]
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@ -99,4 +97,4 @@ class VerisureHygrometer(Entity):
def update(self): def update(self):
''' update sensor ''' ''' update sensor '''
verisure.update() verisure.update_climate()

View File

@ -36,17 +36,17 @@ sensor:
""" """
import logging import logging
import datetime
import urllib.request
import requests import requests
from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import location, dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['xmltodict', 'astral==0.8.1'] REQUIREMENTS = ['xmltodict']
# Sensor types are defined like so: # Sensor types are defined like so:
SENSOR_TYPES = { SENSOR_TYPES = {
@ -73,19 +73,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error("Latitude or longitude not set in Home Assistant config") _LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False return False
from astral import Location, GoogleGeocoder elevation = config.get('elevation')
location = Location(('', '', hass.config.latitude, hass.config.longitude,
hass.config.time_zone, 0))
google = GoogleGeocoder() if elevation is None:
try: elevation = location.elevation(hass.config.latitude,
google._get_elevation(location) # pylint: disable=protected-access hass.config.longitude)
_LOGGER.info(
'Retrieved elevation from Google: %s', location.elevation)
elevation = location.elevation
except urllib.error.URLError:
# If no internet connection available etc.
elevation = 0
coordinates = dict(lat=hass.config.latitude, coordinates = dict(lat=hass.config.latitude,
lon=hass.config.longitude, msl=elevation) lon=hass.config.longitude, msl=elevation)
@ -116,9 +108,8 @@ class YrSensor(Entity):
self.type = sensor_type self.type = sensor_type
self._state = None self._state = None
self._weather = weather self._weather = weather
self._info = ''
self._unit_of_measurement = SENSOR_TYPES[self.type][1] self._unit_of_measurement = SENSOR_TYPES[self.type][1]
self._update = datetime.datetime.fromtimestamp(0) self._update = None
self.update() self.update()
@ -134,14 +125,15 @@ class YrSensor(Entity):
@property @property
def state_attributes(self): def state_attributes(self):
""" Returns state attributes. """ """ Returns state attributes. """
data = {} data = {
data[''] = "Weather forecast from yr.no, delivered by the"\ 'about': "Weather forecast from yr.no, delivered by the"
" Norwegian Meteorological Institute and the NRK" " Norwegian Meteorological Institute and the NRK"
}
if self.type == 'symbol': if self.type == 'symbol':
symbol_nr = self._state symbol_nr = self._state
data[ATTR_ENTITY_PICTURE] = "http://api.met.no/weatherapi/weathericon/1.1/" \ data[ATTR_ENTITY_PICTURE] = \
"?symbol=" + str(symbol_nr) + \ "http://api.met.no/weatherapi/weathericon/1.1/" \
";content_type=image/png" "?symbol={0};content_type=image/png".format(symbol_nr)
return data return data
@ -150,76 +142,50 @@ class YrSensor(Entity):
""" Unit of measurement of this entity, if any. """ """ Unit of measurement of this entity, if any. """
return self._unit_of_measurement return self._unit_of_measurement
@property
def should_poll(self):
""" Return True if entity has to be polled for state. """
return True
# pylint: disable=too-many-branches, too-many-return-statements
def update(self): def update(self):
""" Gets the latest data from yr.no and updates the states. """ """ Gets the latest data from yr.no and updates the states. """
self._weather.update() now = dt_util.utcnow()
now = datetime.datetime.now()
# check if data should be updated # check if data should be updated
if now <= self._update: if self._update is not None and now <= self._update:
return return
time_data = self._weather.data['product']['time'] self._weather.update()
# pylint: disable=consider-using-enumerate
# find sensor # find sensor
for k in range(len(time_data)): for time_entry in self._weather.data['product']['time']:
valid_from = datetime.datetime.strptime(time_data[k]['@from'], valid_from = dt_util.str_to_datetime(
"%Y-%m-%dT%H:%M:%SZ") time_entry['@from'], "%Y-%m-%dT%H:%M:%SZ")
valid_to = datetime.datetime.strptime(time_data[k]['@to'], valid_to = dt_util.str_to_datetime(
"%Y-%m-%dT%H:%M:%SZ") time_entry['@to'], "%Y-%m-%dT%H:%M:%SZ")
self._update = valid_to
self._info = "Forecast between " + time_data[k]['@from'] \
+ " and " + time_data[k]['@to'] + ". "
temp_data = time_data[k]['location'] loc_data = time_entry['location']
if self.type not in temp_data and now >= valid_to:
if self.type not in loc_data or now >= valid_to:
continue continue
self._update = valid_to
if self.type == 'precipitation' and valid_from < now: if self.type == 'precipitation' and valid_from < now:
self._state = temp_data[self.type]['@value'] self._state = loc_data[self.type]['@value']
return break
elif self.type == 'symbol' and valid_from < now: elif self.type == 'symbol' and valid_from < now:
self._state = temp_data[self.type]['@number'] self._state = loc_data[self.type]['@number']
return break
elif self.type == 'temperature': elif self.type == ('temperature', 'pressure', 'humidity',
self._state = temp_data[self.type]['@value'] 'dewpointTemperature'):
return self._state = loc_data[self.type]['@value']
break
elif self.type == 'windSpeed': elif self.type == 'windSpeed':
self._state = temp_data[self.type]['@mps'] self._state = loc_data[self.type]['@mps']
return break
elif self.type == 'pressure':
self._state = temp_data[self.type]['@value']
return
elif self.type == 'windDirection': elif self.type == 'windDirection':
self._state = float(temp_data[self.type]['@deg']) self._state = float(loc_data[self.type]['@deg'])
return break
elif self.type == 'humidity': elif self.type in ('fog', 'cloudiness', 'lowClouds',
self._state = temp_data[self.type]['@value'] 'mediumClouds', 'highClouds'):
return self._state = loc_data[self.type]['@percent']
elif self.type == 'fog': break
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'cloudiness':
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'lowClouds':
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'mediumClouds':
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'highClouds':
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'dewpointTemperature':
self._state = temp_data[self.type]['@value']
return
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -230,17 +196,18 @@ class YrData(object):
self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \
'lat={lat};lon={lon};msl={msl}'.format(**coordinates) 'lat={lat};lon={lon};msl={msl}'.format(**coordinates)
self._nextrun = datetime.datetime.fromtimestamp(0) self._nextrun = None
self.data = {}
self.update() self.update()
def update(self): def update(self):
""" Gets the latest data from yr.no """ """ Gets the latest data from yr.no """
now = datetime.datetime.now()
# check if new will be available # check if new will be available
if now <= self._nextrun: if self._nextrun is not None and dt_util.utcnow() <= self._nextrun:
return return
try: try:
response = requests.get(self._url) with requests.Session() as sess:
response = sess.get(self._url)
except requests.RequestException: except requests.RequestException:
return return
if response.status_code != 200: if response.status_code != 200:
@ -252,5 +219,5 @@ class YrData(object):
model = self.data['meta']['model'] model = self.data['meta']['model']
if '@nextrun' not in model: if '@nextrun' not in model:
model = model[0] model = model[0]
self._nextrun = datetime.datetime.strptime(model['@nextrun'], self._nextrun = dt_util.str_to_datetime(model['@nextrun'],
"%Y-%m-%dT%H:%M:%SZ") "%Y-%m-%dT%H:%M:%SZ")

View File

@ -8,10 +8,9 @@ https://home-assistant.io/components/sun/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
import urllib
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.dt as dt_util from homeassistant.util import location as location_util, dt as dt_util
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
track_point_in_utc_time, track_utc_time_change) track_point_in_utc_time, track_utc_time_change)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -111,21 +110,13 @@ def setup(hass, config):
platform_config = config.get(DOMAIN, {}) platform_config = config.get(DOMAIN, {})
elevation = platform_config.get(CONF_ELEVATION) elevation = platform_config.get(CONF_ELEVATION)
if elevation is None:
elevation = location_util.elevation(latitude, longitude)
from astral import Location, GoogleGeocoder from astral import Location
location = Location(('', '', latitude, longitude, hass.config.time_zone, location = Location(('', '', latitude, longitude, hass.config.time_zone,
elevation or 0)) elevation))
if elevation is None:
google = GoogleGeocoder()
try:
google._get_elevation(location) # pylint: disable=protected-access
_LOGGER.info(
'Retrieved elevation from Google: %s', location.elevation)
except urllib.error.URLError:
# If no internet connection available etc.
pass
sun = Sun(hass, location) sun = Sun(hass, location)
sun.point_in_time_listener(dt_util.utcnow()) sun.point_in_time_listener(dt_util.utcnow())

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.components import ( from homeassistant.components import (
group, discovery, wink, isy994, verisure, zwave, mysensors) group, discovery, wink, isy994, verisure, zwave, tellduslive, mysensors)
DOMAIN = 'switch' DOMAIN = 'switch'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
@ -40,6 +40,7 @@ DISCOVERY_PLATFORMS = {
isy994.DISCOVER_SWITCHES: 'isy994', isy994.DISCOVER_SWITCHES: 'isy994',
verisure.DISCOVER_SWITCHES: 'verisure', verisure.DISCOVER_SWITCHES: 'verisure',
zwave.DISCOVER_SWITCHES: 'zwave', zwave.DISCOVER_SWITCHES: 'zwave',
tellduslive.DISCOVER_SWITCHES: 'tellduslive',
mysensors.DISCOVER_SWITCHES: 'mysensors', mysensors.DISCOVER_SWITCHES: 'mysensors',
} }

View File

@ -0,0 +1,73 @@
"""
homeassistant.components.switch.tellduslive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Tellstick switches using Tellstick Net and
the Telldus Live online service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.tellduslive/
"""
import logging
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN)
from homeassistant.components import tellduslive
from homeassistant.helpers.entity import ToggleEntity
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['tellduslive']
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Find and return Tellstick switches. """
switches = tellduslive.NETWORK.get_switches()
add_devices([TelldusLiveSwitch(switch["name"],
switch["id"])
for switch in switches if switch["type"] == "device"])
class TelldusLiveSwitch(ToggleEntity):
""" Represents a Tellstick switch. """
def __init__(self, name, switch_id):
self._name = name
self._id = switch_id
self._state = STATE_UNKNOWN
self.update()
@property
def should_poll(self):
""" Tells Home Assistant to poll this entity. """
return False
@property
def name(self):
""" Returns the name of the switch if any. """
return self._name
def update(self):
from tellive.live import const
state = tellduslive.NETWORK.get_switch_state(self._id)
if state == const.TELLSTICK_TURNON:
self._state = STATE_ON
elif state == const.TELLSTICK_TURNOFF:
self._state = STATE_OFF
else:
self._state = STATE_UNKNOWN
@property
def is_on(self):
""" True if switch is on. """
return self._state == STATE_ON
def turn_on(self, **kwargs):
""" Turns the switch on. """
if tellduslive.NETWORK.turn_switch_on(self._id):
self._state = STATE_ON
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turns the switch off. """
if tellduslive.NETWORK.turn_switch_off(self._id):
self._state = STATE_OFF
self.update_ha_state()

View File

@ -25,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
switches.extend([ switches.extend([
VerisureSmartplug(value) VerisureSmartplug(value)
for value in verisure.get_smartplug_status().values() for value in verisure.SMARTPLUG_STATUS.values()
if verisure.SHOW_SMARTPLUGS if verisure.SHOW_SMARTPLUGS
]) ])
@ -36,31 +36,29 @@ class VerisureSmartplug(SwitchDevice):
""" Represents a Verisure smartplug. """ """ Represents a Verisure smartplug. """
def __init__(self, smartplug_status): def __init__(self, smartplug_status):
self._id = smartplug_status.id self._id = smartplug_status.id
self.status_on = verisure.MY_PAGES.SMARTPLUG_ON
self.status_off = verisure.MY_PAGES.SMARTPLUG_OFF
@property @property
def name(self): def name(self):
""" Get the name (location) of the smartplug. """ """ Get the name (location) of the smartplug. """
return verisure.get_smartplug_status()[self._id].location return verisure.SMARTPLUG_STATUS[self._id].location
@property @property
def is_on(self): def is_on(self):
""" Returns True if on """ """ Returns True if on """
plug_status = verisure.get_smartplug_status()[self._id].status plug_status = verisure.SMARTPLUG_STATUS[self._id].status
return plug_status == self.status_on return plug_status == 'on'
def turn_on(self): def turn_on(self):
""" Set smartplug status on. """ """ Set smartplug status on. """
verisure.MY_PAGES.set_smartplug_status( verisure.MY_PAGES.smartplug.set(self._id, 'on')
self._id, verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'on')
self.status_on) verisure.update_smartplug()
def turn_off(self): def turn_off(self):
""" Set smartplug status off. """ """ Set smartplug status off. """
verisure.MY_PAGES.set_smartplug_status( verisure.MY_PAGES.smartplug.set(self._id, 'off')
self._id, verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'off')
self.status_off) verisure.update_smartplug()
def update(self): def update(self):
verisure.update() verisure.update_smartplug()

View File

@ -9,11 +9,14 @@ https://home-assistant.io/components/switch.wemo/
import logging import logging
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY from homeassistant.const import (
STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['pywemo==0.3.3'] REQUIREMENTS = ['pywemo==0.3.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_WEMO_SUBSCRIPTION_REGISTRY = None
# pylint: disable=unused-argument, too-many-function-args # pylint: disable=unused-argument, too-many-function-args
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@ -21,6 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
import pywemo import pywemo
import pywemo.discovery as discovery import pywemo.discovery as discovery
global _WEMO_SUBSCRIPTION_REGISTRY
if _WEMO_SUBSCRIPTION_REGISTRY is None:
_WEMO_SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry()
_WEMO_SUBSCRIPTION_REGISTRY.start()
def stop_wemo(event):
""" Shutdown Wemo subscriptions and subscription thread on exit"""
_LOGGER.info("Shutting down subscriptions.")
_WEMO_SUBSCRIPTION_REGISTRY.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo)
if discovery_info is not None: if discovery_info is not None:
location = discovery_info[2] location = discovery_info[2]
mac = discovery_info[3] mac = discovery_info[3]
@ -47,6 +62,23 @@ class WemoSwitch(SwitchDevice):
self.insight_params = None self.insight_params = None
self.maker_params = None self.maker_params = None
_WEMO_SUBSCRIPTION_REGISTRY.register(wemo)
_WEMO_SUBSCRIPTION_REGISTRY.on(
wemo, None, self._update_callback)
def _update_callback(self, _device, _params):
""" Called by the wemo device callback to update state. """
_LOGGER.info(
'Subscription update for %s, sevice=%s',
self.name, _device)
self.update_ha_state(True)
@property
def should_poll(self):
""" No polling should be needed with subscriptions """
# but leave in for initial version in case of issues.
return True
@property @property
def unique_id(self): def unique_id(self):
""" Returns the id of this WeMo switch """ """ Returns the id of this WeMo switch """

View File

@ -0,0 +1,209 @@
"""
homeassistant.components.tellduslive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tellduslive Component
This component adds support for the Telldus Live service.
Telldus Live is the online service used with Tellstick Net devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.tellduslive/
Developer access to the Telldus Live service is neccessary
API keys can be aquired from https://api.telldus.com/keys/index
Tellstick Net devices can be auto discovered using the method described in:
https://developer.telldus.com/doxygen/html/TellStickNet.html
It might be possible to communicate with the Tellstick Net device
directly, bypassing the Tellstick Live service.
This however is poorly documented and yet not fully supported (?) according to
http://developer.telldus.se/ticket/114 and
https://developer.telldus.com/doxygen/html/TellStickNet.html
API requests to certain methods, as described in
https://api.telldus.com/explore/sensor/info
are limited to one request every 10 minutes
"""
from datetime import timedelta
import logging
from homeassistant.loader import get_component
from homeassistant import bootstrap
from homeassistant.util import Throttle
from homeassistant.helpers import validate_config
from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED)
DOMAIN = "tellduslive"
DISCOVER_SWITCHES = "tellduslive.switches"
DISCOVER_SENSORS = "tellduslive.sensors"
CONF_PUBLIC_KEY = "public_key"
CONF_PRIVATE_KEY = "private_key"
CONF_TOKEN = "token"
CONF_TOKEN_SECRET = "token_secret"
REQUIREMENTS = ['tellive-py==0.5.2']
_LOGGER = logging.getLogger(__name__)
NETWORK = None
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600)
class TelldusLiveData(object):
""" Gets the latest data and update the states. """
def __init__(self, hass, config):
public_key = config[DOMAIN].get(CONF_PUBLIC_KEY)
private_key = config[DOMAIN].get(CONF_PRIVATE_KEY)
token = config[DOMAIN].get(CONF_TOKEN)
token_secret = config[DOMAIN].get(CONF_TOKEN_SECRET)
from tellive.client import LiveClient
from tellive.live import TelldusLive
self._sensors = []
self._switches = []
self._client = LiveClient(public_key=public_key,
private_key=private_key,
access_token=token,
access_secret=token_secret)
self._api = TelldusLive(self._client)
def update(self, hass, config):
""" Send discovery event if component not yet discovered """
self._update_sensors()
self._update_switches()
for component_name, found_devices, discovery_type in \
(('sensor', self._sensors, DISCOVER_SENSORS),
('switch', self._switches, DISCOVER_SWITCHES)):
if len(found_devices):
component = get_component(component_name)
bootstrap.setup_component(hass, component.DOMAIN, config)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery_type,
ATTR_DISCOVERED: {}})
def _request(self, what, **params):
""" Sends a request to the tellstick live API """
from tellive.live import const
supported_methods = const.TELLSTICK_TURNON \
| const.TELLSTICK_TURNOFF \
| const.TELLSTICK_TOGGLE
default_params = {'supportedMethods': supported_methods,
"includeValues": 1,
"includeScale": 1}
params.update(default_params)
# room for improvement: the telllive library doesn't seem to
# re-use sessions, instead it opens a new session for each request
# this needs to be fixed
response = self._client.request(what, params)
return response
def check_request(self, what, **params):
""" Make request, check result if successful """
response = self._request(what, **params)
return response['status'] == "success"
def validate_session(self):
""" Make a dummy request to see if the session is valid """
try:
response = self._request("user/profile")
return 'email' in response
except RuntimeError:
return False
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_sensors(self):
""" Get the latest sensor data from Telldus Live """
_LOGGER.info("Updating sensors from Telldus Live")
self._sensors = self._request("sensors/list")["sensor"]
def _update_switches(self):
""" Get the configured switches from Telldus Live"""
_LOGGER.info("Updating switches from Telldus Live")
self._switches = self._request("devices/list")["device"]
# filter out any group of switches
self._switches = [switch for switch in self._switches
if switch["type"] == "device"]
def get_sensors(self):
""" Get the configured sensors """
self._update_sensors()
return self._sensors
def get_switches(self):
""" Get the configured switches """
self._update_switches()
return self._switches
def get_sensor_value(self, sensor_id, sensor_name):
""" Get the latest (possibly cached) sensor value """
self._update_sensors()
for component in self._sensors:
if component["id"] == sensor_id:
for sensor in component["data"]:
if sensor["name"] == sensor_name:
return (sensor["value"],
component["battery"],
component["lastUpdated"])
def get_switch_state(self, switch_id):
""" returns state of switch. """
_LOGGER.info("Updating switch state from Telldus Live")
response = self._request("device/info", id=switch_id)["state"]
return int(response)
def turn_switch_on(self, switch_id):
""" turn switch off """
return self.check_request("device/turnOn", id=switch_id)
def turn_switch_off(self, switch_id):
""" turn switch on """
return self.check_request("device/turnOff", id=switch_id)
def setup(hass, config):
""" Setup the tellduslive component """
# fixme: aquire app key and provide authentication
# using username + password
if not validate_config(config,
{DOMAIN: [CONF_PUBLIC_KEY,
CONF_PRIVATE_KEY,
CONF_TOKEN,
CONF_TOKEN_SECRET]},
_LOGGER):
_LOGGER.error(
"Configuration Error: "
"Please make sure you have configured your keys "
"that can be aquired from https://api.telldus.com/keys/index")
return False
global NETWORK
NETWORK = TelldusLiveData(hass, config)
if not NETWORK.validate_session():
_LOGGER.error(
"Authentication Error: "
"Please make sure you have configured your keys "
"that can be aquired from https://api.telldus.com/keys/index")
return False
NETWORK.update(hass, config)
return True

View File

@ -7,6 +7,8 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/verisure/ https://home-assistant.io/components/verisure/
""" """
import logging import logging
import time
from datetime import timedelta from datetime import timedelta
from homeassistant import bootstrap from homeassistant import bootstrap
@ -26,15 +28,14 @@ DISCOVER_SWITCHES = 'verisure.switches'
DISCOVER_ALARMS = 'verisure.alarm_control_panel' DISCOVER_ALARMS = 'verisure.alarm_control_panel'
DEPENDENCIES = ['alarm_control_panel'] DEPENDENCIES = ['alarm_control_panel']
REQUIREMENTS = [ REQUIREMENTS = ['vsure==0.4.3']
'https://github.com/persandstrom/python-verisure/archive/'
'9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6'
]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MY_PAGES = None MY_PAGES = None
STATUS = {} ALARM_STATUS = {}
SMARTPLUG_STATUS = {}
CLIMATE_STATUS = {}
VERISURE_LOGIN_ERROR = None VERISURE_LOGIN_ERROR = None
VERISURE_ERROR = None VERISURE_ERROR = None
@ -47,7 +48,7 @@ SHOW_SMARTPLUGS = True
# if wrong password was given don't try again # if wrong password was given don't try again
WRONG_PASSWORD_GIVEN = False WRONG_PASSWORD_GIVEN = False
MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=5) MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=1)
def setup(hass, config): def setup(hass, config):
@ -60,10 +61,6 @@ def setup(hass, config):
from verisure import MyPages, LoginError, Error from verisure import MyPages, LoginError, Error
STATUS[MyPages.DEVICE_ALARM] = {}
STATUS[MyPages.DEVICE_CLIMATE] = {}
STATUS[MyPages.DEVICE_SMARTPLUG] = {}
global SHOW_THERMOMETERS, SHOW_HYGROMETERS, SHOW_ALARM, SHOW_SMARTPLUGS global SHOW_THERMOMETERS, SHOW_HYGROMETERS, SHOW_ALARM, SHOW_SMARTPLUGS
SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1')) SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1'))
SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1')) SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1'))
@ -84,7 +81,9 @@ def setup(hass, config):
_LOGGER.error('Could not log in to verisure mypages, %s', ex) _LOGGER.error('Could not log in to verisure mypages, %s', ex)
return False return False
update() update_alarm()
update_climate()
update_smartplug()
# Load components for the devices in the ISY controller that we support # Load components for the devices in the ISY controller that we support
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
@ -101,24 +100,10 @@ def setup(hass, config):
return True return True
def get_alarm_status():
""" Return a list of status overviews for alarm components. """
return STATUS[MY_PAGES.DEVICE_ALARM]
def get_climate_status():
""" Return a list of status overviews for alarm components. """
return STATUS[MY_PAGES.DEVICE_CLIMATE]
def get_smartplug_status():
""" Return a list of status overviews for alarm components. """
return STATUS[MY_PAGES.DEVICE_SMARTPLUG]
def reconnect(): def reconnect():
""" Reconnect to verisure mypages. """ """ Reconnect to verisure mypages. """
try: try:
time.sleep(1)
MY_PAGES.login() MY_PAGES.login()
except VERISURE_LOGIN_ERROR as ex: except VERISURE_LOGIN_ERROR as ex:
_LOGGER.error("Could not login to Verisure mypages, %s", ex) _LOGGER.error("Could not login to Verisure mypages, %s", ex)
@ -129,19 +114,31 @@ def reconnect():
@Throttle(MIN_TIME_BETWEEN_REQUESTS) @Throttle(MIN_TIME_BETWEEN_REQUESTS)
def update(): def update_alarm():
""" Updates the status of alarms. """
update_component(MY_PAGES.alarm.get, ALARM_STATUS)
@Throttle(MIN_TIME_BETWEEN_REQUESTS)
def update_climate():
""" Updates the status of climate sensors. """
update_component(MY_PAGES.climate.get, CLIMATE_STATUS)
@Throttle(MIN_TIME_BETWEEN_REQUESTS)
def update_smartplug():
""" Updates the status of smartplugs. """
update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS)
def update_component(get_function, status):
""" Updates the status of verisure components. """ """ Updates the status of verisure components. """
if WRONG_PASSWORD_GIVEN: if WRONG_PASSWORD_GIVEN:
_LOGGER.error('Wrong password') _LOGGER.error('Wrong password')
return return
try: try:
for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_ALARM): for overview in get_function():
STATUS[MY_PAGES.DEVICE_ALARM][overview.id] = overview status[overview.id] = overview
for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_CLIMATE): except (ConnectionError, VERISURE_ERROR) as ex:
STATUS[MY_PAGES.DEVICE_CLIMATE][overview.id] = overview
for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_SMARTPLUG):
STATUS[MY_PAGES.DEVICE_SMARTPLUG][overview.id] = overview
except ConnectionError as ex:
_LOGGER.error('Caught connection error %s, tries to reconnect', ex) _LOGGER.error('Caught connection error %s, tries to reconnect', ex)
reconnect() reconnect()

View File

@ -1,6 +1,5 @@
""" """
homeassistant Core components of Home Assistant.
~~~~~~~~~~~~~
Home Assistant is a Home Automation framework for observing the state Home Assistant is a Home Automation framework for observing the state
of entities and react to changes. of entities and react to changes.
@ -53,9 +52,10 @@ _MockHA = namedtuple("MockHomeAssistant", ['bus'])
class HomeAssistant(object): class HomeAssistant(object):
""" Core class to route all communication to right components. """ """Root object of the Home Assistant home automation."""
def __init__(self): def __init__(self):
"""Initialize new Home Assistant object."""
self.pool = pool = create_worker_pool() self.pool = pool = create_worker_pool()
self.bus = EventBus(pool) self.bus = EventBus(pool)
self.services = ServiceRegistry(self.bus, pool) self.services = ServiceRegistry(self.bus, pool)
@ -63,7 +63,7 @@ class HomeAssistant(object):
self.config = Config() self.config = Config()
def start(self): def start(self):
""" Start home assistant. """ """Start home assistant."""
_LOGGER.info( _LOGGER.info(
"Starting Home Assistant (%d threads)", self.pool.worker_count) "Starting Home Assistant (%d threads)", self.pool.worker_count)
@ -71,12 +71,11 @@ class HomeAssistant(object):
self.bus.fire(EVENT_HOMEASSISTANT_START) self.bus.fire(EVENT_HOMEASSISTANT_START)
def block_till_stopped(self): def block_till_stopped(self):
""" Will register service homeassistant/stop and """Register service homeassistant/stop and will block until called."""
will block until called. """
request_shutdown = threading.Event() request_shutdown = threading.Event()
def stop_homeassistant(*args): def stop_homeassistant(*args):
""" Stops Home Assistant. """ """Stop Home Assistant."""
request_shutdown.set() request_shutdown.set()
self.services.register( self.services.register(
@ -98,7 +97,7 @@ class HomeAssistant(object):
self.stop() self.stop()
def stop(self): def stop(self):
""" Stops Home Assistant and shuts down all threads. """ """Stop Home Assistant and shuts down all threads."""
_LOGGER.info("Stopping") _LOGGER.info("Stopping")
self.bus.fire(EVENT_HOMEASSISTANT_STOP) self.bus.fire(EVENT_HOMEASSISTANT_STOP)
@ -150,8 +149,7 @@ class HomeAssistant(object):
class JobPriority(util.OrderedEnum): class JobPriority(util.OrderedEnum):
""" Provides priorities for bus events. """ """Provides job priorities for event bus jobs."""
# pylint: disable=no-init,too-few-public-methods
EVENT_CALLBACK = 0 EVENT_CALLBACK = 0
EVENT_SERVICE = 1 EVENT_SERVICE = 1
@ -161,7 +159,7 @@ class JobPriority(util.OrderedEnum):
@staticmethod @staticmethod
def from_event_type(event_type): def from_event_type(event_type):
""" Returns a priority based on event type. """ """Return a priority based on event type."""
if event_type == EVENT_TIME_CHANGED: if event_type == EVENT_TIME_CHANGED:
return JobPriority.EVENT_TIME return JobPriority.EVENT_TIME
elif event_type == EVENT_STATE_CHANGED: elif event_type == EVENT_STATE_CHANGED:
@ -175,8 +173,7 @@ class JobPriority(util.OrderedEnum):
class EventOrigin(enum.Enum): class EventOrigin(enum.Enum):
""" Distinguish between origin of event. """ """Represents origin of an event."""
# pylint: disable=no-init,too-few-public-methods
local = "LOCAL" local = "LOCAL"
remote = "REMOTE" remote = "REMOTE"
@ -185,14 +182,15 @@ class EventOrigin(enum.Enum):
return self.value return self.value
# pylint: disable=too-few-public-methods
class Event(object): class Event(object):
""" Represents an event within the Bus. """ # pylint: disable=too-few-public-methods
"""Represents an event within the Bus."""
__slots__ = ['event_type', 'data', 'origin', 'time_fired'] __slots__ = ['event_type', 'data', 'origin', 'time_fired']
def __init__(self, event_type, data=None, origin=EventOrigin.local, def __init__(self, event_type, data=None, origin=EventOrigin.local,
time_fired=None): time_fired=None):
"""Initialize a new event."""
self.event_type = event_type self.event_type = event_type
self.data = data or {} self.data = data or {}
self.origin = origin self.origin = origin
@ -200,7 +198,7 @@ class Event(object):
time_fired or dt_util.utcnow()) time_fired or dt_util.utcnow())
def as_dict(self): def as_dict(self):
""" Returns a dict representation of this Event. """ """Create a dict representation of this Event."""
return { return {
'event_type': self.event_type, 'event_type': self.event_type,
'data': dict(self.data), 'data': dict(self.data),
@ -227,26 +225,23 @@ class Event(object):
class EventBus(object): class EventBus(object):
""" Class that allows different components to communicate via services """Allows firing of and listening for events."""
and events.
"""
def __init__(self, pool=None): def __init__(self, pool=None):
"""Initialize a new event bus."""
self._listeners = {} self._listeners = {}
self._lock = threading.Lock() self._lock = threading.Lock()
self._pool = pool or create_worker_pool() self._pool = pool or create_worker_pool()
@property @property
def listeners(self): def listeners(self):
""" Dict with events that is being listened for and the number """Dict with events and the number of listeners."""
of listeners.
"""
with self._lock: with self._lock:
return {key: len(self._listeners[key]) return {key: len(self._listeners[key])
for key in self._listeners} for key in self._listeners}
def fire(self, event_type, event_data=None, origin=EventOrigin.local): def fire(self, event_type, event_data=None, origin=EventOrigin.local):
""" Fire an event. """ """Fire an event."""
if not self._pool.running: if not self._pool.running:
raise HomeAssistantError('Home Assistant has shut down.') raise HomeAssistantError('Home Assistant has shut down.')
@ -271,7 +266,7 @@ class EventBus(object):
self._pool.add_job(job_priority, (func, event)) self._pool.add_job(job_priority, (func, event))
def listen(self, event_type, listener): def listen(self, event_type, listener):
""" Listen for all events or events of a specific type. """Listen for all events or events of a specific type.
To listen to all events specify the constant ``MATCH_ALL`` To listen to all events specify the constant ``MATCH_ALL``
as event_type. as event_type.
@ -283,7 +278,7 @@ class EventBus(object):
self._listeners[event_type] = [listener] self._listeners[event_type] = [listener]
def listen_once(self, event_type, listener): def listen_once(self, event_type, listener):
""" Listen once for event of a specific type. """Listen once for event of a specific type.
To listen to all events specify the constant ``MATCH_ALL`` To listen to all events specify the constant ``MATCH_ALL``
as event_type. as event_type.
@ -292,7 +287,7 @@ class EventBus(object):
""" """
@ft.wraps(listener) @ft.wraps(listener)
def onetime_listener(event): def onetime_listener(event):
""" Removes listener from eventbus and then fires listener. """ """Remove listener from eventbus and then fires listener."""
if hasattr(onetime_listener, 'run'): if hasattr(onetime_listener, 'run'):
return return
# Set variable so that we will never run twice. # Set variable so that we will never run twice.
@ -311,7 +306,7 @@ class EventBus(object):
return onetime_listener return onetime_listener
def remove_listener(self, event_type, listener): def remove_listener(self, event_type, listener):
""" Removes a listener of a specific event_type. """ """Remove a listener of a specific event_type."""
with self._lock: with self._lock:
try: try:
self._listeners[event_type].remove(listener) self._listeners[event_type].remove(listener)
@ -343,6 +338,7 @@ class State(object):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, entity_id, state, attributes=None, last_changed=None, def __init__(self, entity_id, state, attributes=None, last_changed=None,
last_updated=None): last_updated=None):
"""Initialize a new state."""
if not ENTITY_ID_PATTERN.match(entity_id): if not ENTITY_ID_PATTERN.match(entity_id):
raise InvalidEntityFormatError(( raise InvalidEntityFormatError((
"Invalid entity id encountered: {}. " "Invalid entity id encountered: {}. "
@ -363,31 +359,33 @@ class State(object):
@property @property
def domain(self): def domain(self):
""" Returns domain of this state. """ """Domain of this state."""
return util.split_entity_id(self.entity_id)[0] return util.split_entity_id(self.entity_id)[0]
@property @property
def object_id(self): def object_id(self):
""" Returns object_id of this state. """ """Object id of this state."""
return util.split_entity_id(self.entity_id)[1] return util.split_entity_id(self.entity_id)[1]
@property @property
def name(self): def name(self):
""" Name to represent this state. """ """Name of this state."""
return ( return (
self.attributes.get(ATTR_FRIENDLY_NAME) or self.attributes.get(ATTR_FRIENDLY_NAME) or
self.object_id.replace('_', ' ')) self.object_id.replace('_', ' '))
def copy(self): def copy(self):
""" Creates a copy of itself. """ """Return a copy of the state."""
return State(self.entity_id, self.state, return State(self.entity_id, self.state,
dict(self.attributes), self.last_changed, dict(self.attributes), self.last_changed,
self.last_updated) self.last_updated)
def as_dict(self): def as_dict(self):
""" Converts State to a dict to be used within JSON. """Return a dict representation of the State.
Ensures: state == State.from_dict(state.as_dict()) """
To be used for JSON serialization.
Ensures: state == State.from_dict(state.as_dict())
"""
return {'entity_id': self.entity_id, return {'entity_id': self.entity_id,
'state': self.state, 'state': self.state,
'attributes': self.attributes, 'attributes': self.attributes,
@ -396,11 +394,11 @@ class State(object):
@classmethod @classmethod
def from_dict(cls, json_dict): def from_dict(cls, json_dict):
""" Static method to create a state from a dict. """Initialize a state from a dict.
Ensures: state == State.from_json_dict(state.to_json_dict()) """
if not (json_dict and Ensures: state == State.from_json_dict(state.to_json_dict())
'entity_id' in json_dict and """
if not (json_dict and 'entity_id' in json_dict and
'state' in json_dict): 'state' in json_dict):
return None return None
@ -433,15 +431,16 @@ class State(object):
class StateMachine(object): class StateMachine(object):
""" Helper class that tracks the state of different entities. """ """Helper class that tracks the state of different entities."""
def __init__(self, bus): def __init__(self, bus):
"""Initialize state machine."""
self._states = {} self._states = {}
self._bus = bus self._bus = bus
self._lock = threading.Lock() self._lock = threading.Lock()
def entity_ids(self, domain_filter=None): def entity_ids(self, domain_filter=None):
""" List of entity ids that are being tracked. """ """List of entity ids that are being tracked."""
if domain_filter is None: if domain_filter is None:
return list(self._states.keys()) return list(self._states.keys())
@ -451,35 +450,36 @@ class StateMachine(object):
if state.domain == domain_filter] if state.domain == domain_filter]
def all(self): def all(self):
""" Returns a list of all states. """ """Create a list of all states."""
with self._lock: with self._lock:
return [state.copy() for state in self._states.values()] return [state.copy() for state in self._states.values()]
def get(self, entity_id): def get(self, entity_id):
""" Returns the state of the specified entity. """ """Retrieve state of entity_id or None if not found."""
state = self._states.get(entity_id.lower()) state = self._states.get(entity_id.lower())
# Make a copy so people won't mutate the state # Make a copy so people won't mutate the state
return state.copy() if state else None return state.copy() if state else None
def is_state(self, entity_id, state): def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """ """Test if entity exists and is specified state."""
entity_id = entity_id.lower() entity_id = entity_id.lower()
return (entity_id in self._states and return (entity_id in self._states and
self._states[entity_id].state == state) self._states[entity_id].state == state)
def remove(self, entity_id): def remove(self, entity_id):
""" Removes an entity from the state machine. """Remove the state of an entity.
Returns boolean to indicate if an entity was removed. """ Returns boolean to indicate if an entity was removed.
"""
entity_id = entity_id.lower() entity_id = entity_id.lower()
with self._lock: with self._lock:
return self._states.pop(entity_id, None) is not None return self._states.pop(entity_id, None) is not None
def set(self, entity_id, new_state, attributes=None): def set(self, entity_id, new_state, attributes=None):
""" Set the state of an entity, add entity if it does not exist. """Set the state of an entity, add entity if it does not exist.
Attributes is an optional dict to specify attributes of this state. Attributes is an optional dict to specify attributes of this state.
@ -514,9 +514,7 @@ class StateMachine(object):
self._bus.fire(EVENT_STATE_CHANGED, event_data) self._bus.fire(EVENT_STATE_CHANGED, event_data)
def track_change(self, entity_ids, action, from_state=None, to_state=None): def track_change(self, entity_ids, action, from_state=None, to_state=None):
""" """DEPRECATED AS OF 8/4/2015."""
DEPRECATED AS OF 8/4/2015
"""
_LOGGER.warning( _LOGGER.warning(
'hass.states.track_change is deprecated. ' 'hass.states.track_change is deprecated. '
'Use homeassistant.helpers.event.track_state_change instead.') 'Use homeassistant.helpers.event.track_state_change instead.')
@ -527,33 +525,36 @@ class StateMachine(object):
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class Service(object): class Service(object):
""" Represents a service. """ """Represents a callable service."""
__slots__ = ['func', 'description', 'fields'] __slots__ = ['func', 'description', 'fields']
def __init__(self, func, description, fields): def __init__(self, func, description, fields):
"""Initialize a service."""
self.func = func self.func = func
self.description = description or '' self.description = description or ''
self.fields = fields or {} self.fields = fields or {}
def as_dict(self): def as_dict(self):
""" Return dictionary representation of this service. """ """Return dictionary representation of this service."""
return { return {
'description': self.description, 'description': self.description,
'fields': self.fields, 'fields': self.fields,
} }
def __call__(self, call): def __call__(self, call):
"""Execute the service."""
self.func(call) self.func(call)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class ServiceCall(object): class ServiceCall(object):
""" Represents a call to a service. """ """Represents a call to a service."""
__slots__ = ['domain', 'service', 'data'] __slots__ = ['domain', 'service', 'data']
def __init__(self, domain, service, data=None): def __init__(self, domain, service, data=None):
"""Initialize a service call."""
self.domain = domain self.domain = domain
self.service = service self.service = service
self.data = data or {} self.data = data or {}
@ -567,9 +568,10 @@ class ServiceCall(object):
class ServiceRegistry(object): class ServiceRegistry(object):
""" Offers services over the eventbus. """ """Offers services over the eventbus."""
def __init__(self, bus, pool=None): def __init__(self, bus, pool=None):
"""Initialize a service registry."""
self._services = {} self._services = {}
self._lock = threading.Lock() self._lock = threading.Lock()
self._pool = pool or create_worker_pool() self._pool = pool or create_worker_pool()
@ -579,14 +581,14 @@ class ServiceRegistry(object):
@property @property
def services(self): def services(self):
""" Dict with per domain a list of available services. """ """Dict with per domain a list of available services."""
with self._lock: with self._lock:
return {domain: {key: value.as_dict() for key, value return {domain: {key: value.as_dict() for key, value
in self._services[domain].items()} in self._services[domain].items()}
for domain in self._services} for domain in self._services}
def has_service(self, domain, service): def has_service(self, domain, service):
""" Returns True if specified service exists. """ """Test if specified service exists."""
return service in self._services.get(domain, []) return service in self._services.get(domain, [])
def register(self, domain, service, service_func, description=None): def register(self, domain, service, service_func, description=None):
@ -611,7 +613,8 @@ class ServiceRegistry(object):
def call(self, domain, service, service_data=None, blocking=False): def call(self, domain, service, service_data=None, blocking=False):
""" """
Calls specified service. Call a service.
Specify blocking=True to wait till service is executed. Specify blocking=True to wait till service is executed.
Waits a maximum of SERVICE_CALL_LIMIT. Waits a maximum of SERVICE_CALL_LIMIT.
@ -635,10 +638,7 @@ class ServiceRegistry(object):
executed_event = threading.Event() executed_event = threading.Event()
def service_executed(call): def service_executed(call):
""" """Callback method that is called when service is executed."""
Called when a service is executed.
Will set the event if matches our service call.
"""
if call.data[ATTR_SERVICE_CALL_ID] == call_id: if call.data[ATTR_SERVICE_CALL_ID] == call_id:
executed_event.set() executed_event.set()
@ -653,7 +653,7 @@ class ServiceRegistry(object):
return success return success
def _event_to_service_call(self, event): def _event_to_service_call(self, event):
""" Calls a service from an event. """ """Callback for SERVICE_CALLED events from the event bus."""
service_data = dict(event.data) service_data = dict(event.data)
domain = service_data.pop(ATTR_DOMAIN, None) domain = service_data.pop(ATTR_DOMAIN, None)
service = service_data.pop(ATTR_SERVICE, None) service = service_data.pop(ATTR_SERVICE, None)
@ -670,7 +670,7 @@ class ServiceRegistry(object):
(service_handler, service_call))) (service_handler, service_call)))
def _execute_service(self, service_and_call): def _execute_service(self, service_and_call):
""" Executes a service and fires a SERVICE_EXECUTED event. """ """Execute a service and fires a SERVICE_EXECUTED event."""
service, call = service_and_call service, call = service_and_call
service(call) service(call)
@ -680,16 +680,17 @@ class ServiceRegistry(object):
{ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]}) {ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]})
def _generate_unique_id(self): def _generate_unique_id(self):
""" Generates a unique service call id. """ """Generate a unique service call id."""
self._cur_id += 1 self._cur_id += 1
return "{}-{}".format(id(self), self._cur_id) return "{}-{}".format(id(self), self._cur_id)
class Config(object): class Config(object):
""" Configuration settings for Home Assistant. """ """Configuration settings for Home Assistant."""
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__(self): def __init__(self):
"""Initialize a new config object."""
self.latitude = None self.latitude = None
self.longitude = None self.longitude = None
self.temperature_unit = None self.temperature_unit = None
@ -709,15 +710,15 @@ class Config(object):
self.config_dir = get_default_config_dir() self.config_dir = get_default_config_dir()
def distance(self, lat, lon): def distance(self, lat, lon):
""" Calculate distance from Home Assistant in meters. """ """Calculate distance from Home Assistant in meters."""
return location.distance(self.latitude, self.longitude, lat, lon) return location.distance(self.latitude, self.longitude, lat, lon)
def path(self, *path): def path(self, *path):
""" Returns path to the file within the config dir. """ """Generate path to the file within the config dir."""
return os.path.join(self.config_dir, *path) return os.path.join(self.config_dir, *path)
def temperature(self, value, unit): def temperature(self, value, unit):
""" Converts temperature to user preferred unit if set. """ """Convert temperature to user preferred unit if set."""
if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and
self.temperature_unit and unit != self.temperature_unit): self.temperature_unit and unit != self.temperature_unit):
return value, unit return value, unit
@ -732,7 +733,7 @@ class Config(object):
self.temperature_unit) self.temperature_unit)
def as_dict(self): def as_dict(self):
""" Converts config to a dictionary. """ """Create a dict representation of this dict."""
time_zone = self.time_zone or dt_util.UTC time_zone = self.time_zone or dt_util.UTC
return { return {
@ -747,7 +748,7 @@ class Config(object):
def create_timer(hass, interval=TIMER_INTERVAL): def create_timer(hass, interval=TIMER_INTERVAL):
""" Creates a timer. Timer will start on HOMEASSISTANT_START. """ """Create a timer that will start on HOMEASSISTANT_START."""
# We want to be able to fire every time a minute starts (seconds=0). # We want to be able to fire every time a minute starts (seconds=0).
# We want this so other modules can use that to make sure they fire # We want this so other modules can use that to make sure they fire
# every minute. # every minute.
@ -810,12 +811,12 @@ def create_timer(hass, interval=TIMER_INTERVAL):
def create_worker_pool(worker_count=None): def create_worker_pool(worker_count=None):
""" Creates a worker pool to be used. """ """Create a worker pool."""
if worker_count is None: if worker_count is None:
worker_count = MIN_WORKER_THREAD worker_count = MIN_WORKER_THREAD
def job_handler(job): def job_handler(job):
""" Called whenever a job is available to do. """ """Called whenever a job is available to do."""
try: try:
func, arg = job func, arg = job
func(arg) func(arg)
@ -825,8 +826,7 @@ def create_worker_pool(worker_count=None):
_LOGGER.exception("BusHandler:Exception doing job") _LOGGER.exception("BusHandler:Exception doing job")
def busy_callback(worker_count, current_jobs, pending_jobs_count): def busy_callback(worker_count, current_jobs, pending_jobs_count):
""" Callback to be called when the pool queue gets too big. """ """Callback to be called when the pool queue gets too big."""
_LOGGER.warning( _LOGGER.warning(
"WorkerPool:All %d threads are busy and %d jobs pending", "WorkerPool:All %d threads are busy and %d jobs pending",
worker_count, pending_jobs_count) worker_count, pending_jobs_count)

View File

@ -53,8 +53,12 @@ def color_xy_brightness_to_RGB(vX, vY, brightness):
return (0, 0, 0) return (0, 0, 0)
Y = brightness Y = brightness
X = (Y / vY) * vX if vY != 0:
Z = (Y / vY) * (1 - vX - vY) X = (Y / vY) * vX
Z = (Y / vY) * (1 - vX - vY)
else:
X = 0
Z = 0
# Convert to RGB using Wide RGB D65 conversion. # Convert to RGB using Wide RGB D65 conversion.
r = X * 1.612 - Y * 0.203 - Z * 0.302 r = X * 1.612 - Y * 0.203 - Z * 0.302

View File

@ -108,14 +108,14 @@ def datetime_to_date_str(dattim):
return dattim.strftime(DATE_STR_FORMAT) return dattim.strftime(DATE_STR_FORMAT)
def str_to_datetime(dt_str): def str_to_datetime(dt_str, dt_format=DATETIME_STR_FORMAT):
""" Converts a string to a UTC datetime object. """ Converts a string to a UTC datetime object.
@rtype: datetime @rtype: datetime
""" """
try: try:
return dt.datetime.strptime( return dt.datetime.strptime(
dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc) dt_str, dt_format).replace(tzinfo=pytz.utc)
except ValueError: # If dt_str did not match our format except ValueError: # If dt_str did not match our format
return None return None

View File

@ -4,6 +4,8 @@ import collections
import requests import requests
from vincenty import vincenty from vincenty import vincenty
ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json'
LocationInfo = collections.namedtuple( LocationInfo = collections.namedtuple(
"LocationInfo", "LocationInfo",
@ -34,3 +36,20 @@ def detect_location_info():
def distance(lat1, lon1, lat2, lon2): def distance(lat1, lon1, lat2, lon2):
""" Calculate the distance in meters between two points. """ """ Calculate the distance in meters between two points. """
return vincenty((lat1, lon1), (lat2, lon2)) * 1000 return vincenty((lat1, lon1), (lat2, lon2)) * 1000
def elevation(latitude, longitude):
""" Return elevation for given latitude and longitude. """
req = requests.get(ELEVATION_URL, params={
'locations': '{},{}'.format(latitude, longitude),
'sensor': 'false',
})
if req.status_code != 200:
return 0
try:
return int(float(req.json()['results'][0]['elevation']))
except (ValueError, KeyError):
return 0

View File

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

View File

@ -1,5 +0,0 @@
requests>=2,<3
pyyaml>=3.11,<4
pytz>=2015.4
pip>=7.0.0
vincenty==0.1.3

View File

@ -161,7 +161,6 @@ python-twitch==1.2.0
xmltodict xmltodict
# homeassistant.components.sun # homeassistant.components.sun
# homeassistant.components.sensor.yr
astral==0.8.1 astral==0.8.1
# homeassistant.components.switch.edimax # homeassistant.components.switch.edimax
@ -174,7 +173,10 @@ hikvision==0.4
orvibo==1.1.0 orvibo==1.1.0
# homeassistant.components.switch.wemo # homeassistant.components.switch.wemo
pywemo==0.3.3 pywemo==0.3.4
# homeassistant.components.tellduslive
tellive-py==0.5.2
# homeassistant.components.thermostat.heatmiser # homeassistant.components.thermostat.heatmiser
heatmiserV3==0.9.1 heatmiserV3==0.9.1
@ -189,7 +191,7 @@ python-nest==2.6.0
radiotherm==1.2 radiotherm==1.2
# homeassistant.components.verisure # homeassistant.components.verisure
https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6 vsure==0.4.3
# homeassistant.components.zwave # homeassistant.components.zwave
pydispatcher==2.0.5 pydispatcher==2.0.5

6
requirements_test.txt Normal file
View File

@ -0,0 +1,6 @@
flake8>=2.5.0
pylint>=1.5.1
coveralls>=1.1
pytest>=2.6.4
pytest-cov>=2.2.0
betamax>=0.5.1

View File

@ -6,7 +6,7 @@ python3 -m pip install -r requirements_all.txt
REQ_STATUS=$? REQ_STATUS=$?
echo "Installing development dependencies.." echo "Installing development dependencies.."
python3 -m pip install flake8 pylint coveralls pytest pytest-cov python3 -m pip install -r requirements_test.txt
REQ_DEV_STATUS=$? REQ_DEV_STATUS=$?

View File

@ -5,7 +5,7 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
if [ "$TRAVIS_PYTHON_VERSION" != "3.4" ]; then if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then
NO_LINT=1 NO_LINT=1
fi fi

View File

@ -115,7 +115,6 @@ def main():
if sys.argv[-1] == 'validate': if sys.argv[-1] == 'validate':
if validate_file(data): if validate_file(data):
print("requirements_all.txt is up to date.")
sys.exit(0) sys.exit(0)
print("******* ERROR") print("******* ERROR")
print("requirements_all.txt is not up to date") print("requirements_all.txt is not up to date")

View File

@ -3,13 +3,16 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
echo "Checking style with flake8..." echo "Checking style with flake8..."
tput setaf 1
flake8 --exclude www_static homeassistant flake8 --exclude www_static homeassistant
FLAKE8_STATUS=$? FLAKE8_STATUS=$?
tput sgr0
echo "Checking style with pylint..." echo "Checking style with pylint..."
tput setaf 1
pylint homeassistant pylint homeassistant
PYLINT_STATUS=$? PYLINT_STATUS=$?
tput sgr0
if [ $FLAKE8_STATUS -eq 0 ] if [ $FLAKE8_STATUS -eq 0 ]
then then

8
setup.cfg Normal file
View File

@ -0,0 +1,8 @@
[wheel]
universal = 1
[pytest]
testpaths = tests
[pep257]
ignore = D203,D105

View File

@ -16,7 +16,7 @@ REQUIRES = [
'pytz>=2015.4', 'pytz>=2015.4',
'pip>=7.0.0', 'pip>=7.0.0',
'vincenty==0.1.3', 'vincenty==0.1.3',
'jinja2>=2.8' 'jinja2>=2.8',
] ]
setup( setup(
@ -33,6 +33,7 @@ setup(
zip_safe=False, zip_safe=False,
platforms='any', platforms='any',
install_requires=REQUIRES, install_requires=REQUIRES,
test_suite='tests',
keywords=['home', 'automation'], keywords=['home', 'automation'],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
@ -46,5 +47,5 @@ setup(
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.4',
'Topic :: Home Automation' 'Topic :: Home Automation'
] ],
) )

View File

@ -0,0 +1,4 @@
import betamax
with betamax.Betamax.configure() as config:
config.cassette_library_dir = 'tests/cassettes'

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -139,3 +139,189 @@ class TestAutomationSun(unittest.TestCase):
fire_time_changed(self.hass, trigger_time) fire_time_changed(self.hass, trigger_time)
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
def test_if_action_before(self):
self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, {
sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015',
})
automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'before': 'sunrise',
},
'action': {
'service': 'test.automation'
}
}
})
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_action_after(self):
self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, {
sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015',
})
automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'after': 'sunrise',
},
'action': {
'service': 'test.automation'
}
}
})
now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_action_before_with_offset(self):
self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, {
sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015',
})
automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'before': 'sunrise',
'before_offset': '+1:00:00'
},
'action': {
'service': 'test.automation'
}
}
})
now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_action_after_with_offset(self):
self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, {
sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015',
})
automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'after': 'sunrise',
'after_offset': '+1:00:00'
},
'action': {
'service': 'test.automation'
}
}
})
now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_action_before_and_after_during(self):
self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, {
sun.STATE_ATTR_NEXT_RISING: '10:00:00 16-09-2015',
sun.STATE_ATTR_NEXT_SETTING: '15:00:00 16-09-2015',
})
automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'after': 'sunrise',
'before': 'sunset'
},
'action': {
'service': 'test.automation'
}
}
})
now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))

View File

@ -4,66 +4,70 @@ tests.components.sensor.test_yr
Tests Yr sensor. Tests Yr sensor.
""" """
import unittest from unittest.mock import patch
import pytest
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.components.sensor as sensor import homeassistant.components.sensor as sensor
class TestSensorYr(unittest.TestCase): @pytest.mark.usefixtures('betamax_session')
class TestSensorYr:
""" Test the Yr sensor. """ """ Test the Yr sensor. """
def setUp(self): # pylint: disable=invalid-name def setup_method(self, method):
self.hass = ha.HomeAssistant() self.hass = ha.HomeAssistant()
latitude = 32.87336 self.hass.config.latitude = 32.87336
longitude = 117.22743 self.hass.config.longitude = 117.22743
# Compare it with the real data def teardown_method(self, method):
self.hass.config.latitude = latitude
self.hass.config.longitude = longitude
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass.stop() self.hass.stop()
def test_default_setup(self): def test_default_setup(self, betamax_session):
self.assertTrue(sensor.setup(self.hass, { with patch('homeassistant.components.sensor.yr.requests.Session',
'sensor': { return_value=betamax_session):
'platform': 'yr', assert sensor.setup(self.hass, {
} 'sensor': {
})) 'platform': 'yr',
'elevation': 0,
}
})
state = self.hass.states.get('sensor.yr_symbol') state = self.hass.states.get('sensor.yr_symbol')
self.assertTrue(state.state.isnumeric()) assert state.state.isnumeric()
self.assertEqual(None, assert state.attributes.get('unit_of_measurement') is None
state.attributes.get('unit_of_measurement'))
def test_custom_setup(self): def test_custom_setup(self, betamax_session):
self.assertTrue(sensor.setup(self.hass, { with patch('homeassistant.components.sensor.yr.requests.Session',
'sensor': { return_value=betamax_session):
'platform': 'yr', assert sensor.setup(self.hass, {
'monitored_conditions': {'pressure', 'windDirection', 'humidity', 'fog', 'windSpeed'} 'sensor': {
} 'platform': 'yr',
})) 'elevation': 0,
state = self.hass.states.get('sensor.yr_symbol') 'monitored_conditions': {
self.assertEqual(None, state) 'pressure',
'windDirection',
'humidity',
'fog',
'windSpeed'
}
}
})
state = self.hass.states.get('sensor.yr_pressure') state = self.hass.states.get('sensor.yr_pressure')
self.assertEqual('hPa', assert 'hPa', state.attributes.get('unit_of_measurement')
state.attributes.get('unit_of_measurement'))
state = self.hass.states.get('sensor.yr_wind_direction') state = self.hass.states.get('sensor.yr_wind_direction')
self.assertEqual('°', assert '°', state.attributes.get('unit_of_measurement')
state.attributes.get('unit_of_measurement'))
state = self.hass.states.get('sensor.yr_humidity') state = self.hass.states.get('sensor.yr_humidity')
self.assertEqual('%', assert '%', state.attributes.get('unit_of_measurement')
state.attributes.get('unit_of_measurement'))
state = self.hass.states.get('sensor.yr_fog') state = self.hass.states.get('sensor.yr_fog')
self.assertEqual('%', assert '%', state.attributes.get('unit_of_measurement')
state.attributes.get('unit_of_measurement'))
state = self.hass.states.get('sensor.yr_wind_speed') state = self.hass.states.get('sensor.yr_wind_speed')
self.assertEqual('m/s', assert 'm/s', state.attributes.get('unit_of_measurement')
state.attributes.get('unit_of_measurement'))

View File

@ -5,11 +5,10 @@ tests.test_component_history
Tests the history component. Tests the history component.
""" """
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import time from datetime import timedelta
import os import os
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from datetime import timedelta
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -25,21 +24,24 @@ class TestComponentHistory(unittest.TestCase):
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name
""" Init needed objects. """ """ Init needed objects. """
self.hass = get_test_home_assistant(1) self.hass = get_test_home_assistant(1)
self.init_rec = False
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass.stop() self.hass.stop()
if self.init_rec: db_path = self.hass.config.path(recorder.DB_FILE)
recorder._INSTANCE.block_till_done() if os.path.isfile(db_path):
os.remove(self.hass.config.path(recorder.DB_FILE)) os.remove(db_path)
def init_recorder(self): def init_recorder(self):
recorder.setup(self.hass, {}) recorder.setup(self.hass, {})
self.hass.start() self.hass.start()
self.wait_recording_done()
def wait_recording_done(self):
""" Block till recording is done. """
self.hass.pool.block_till_done()
recorder._INSTANCE.block_till_done() recorder._INSTANCE.block_till_done()
self.init_rec = True
def test_setup(self): def test_setup(self):
""" Test setup method of history. """ """ Test setup method of history. """
@ -56,12 +58,11 @@ class TestComponentHistory(unittest.TestCase):
for i in range(7): for i in range(7):
self.hass.states.set(entity_id, "State {}".format(i)) self.hass.states.set(entity_id, "State {}".format(i))
self.wait_recording_done()
if i > 1: if i > 1:
states.append(self.hass.states.get(entity_id)) states.append(self.hass.states.get(entity_id))
self.hass.pool.block_till_done()
recorder._INSTANCE.block_till_done()
self.assertEqual( self.assertEqual(
list(reversed(states)), history.last_5_states(entity_id)) list(reversed(states)), history.last_5_states(entity_id))
@ -70,22 +71,9 @@ class TestComponentHistory(unittest.TestCase):
self.init_recorder() self.init_recorder()
states = [] states = []
for i in range(5): now = dt_util.utcnow()
state = ha.State( with patch('homeassistant.components.recorder.dt_util.utcnow',
'test.point_in_time_{}'.format(i % 5), return_value=now):
"State {}".format(i),
{'attribute_test': i})
mock_state_change_event(self.hass, state)
self.hass.pool.block_till_done()
states.append(state)
recorder._INSTANCE.block_till_done()
point = dt_util.utcnow() + timedelta(seconds=1)
with patch('homeassistant.util.dt.utcnow', return_value=point):
for i in range(5): for i in range(5):
state = ha.State( state = ha.State(
'test.point_in_time_{}'.format(i % 5), 'test.point_in_time_{}'.format(i % 5),
@ -93,16 +81,32 @@ class TestComponentHistory(unittest.TestCase):
{'attribute_test': i}) {'attribute_test': i})
mock_state_change_event(self.hass, state) mock_state_change_event(self.hass, state)
self.hass.pool.block_till_done()
states.append(state)
self.wait_recording_done()
future = now + timedelta(seconds=1)
with patch('homeassistant.components.recorder.dt_util.utcnow',
return_value=future):
for i in range(5):
state = ha.State(
'test.point_in_time_{}'.format(i % 5),
"State {}".format(i),
{'attribute_test': i})
mock_state_change_event(self.hass, state)
self.wait_recording_done()
# Get states returns everything before POINT # Get states returns everything before POINT
self.assertEqual(states, self.assertEqual(states,
sorted(history.get_states(point), sorted(history.get_states(future),
key=lambda state: state.entity_id)) key=lambda state: state.entity_id))
# Test get_state here because we have a DB setup # Test get_state here because we have a DB setup
self.assertEqual( self.assertEqual(
states[0], history.get_state(point, states[0].entity_id)) states[0], history.get_state(future, states[0].entity_id))
def test_state_changes_during_period(self): def test_state_changes_during_period(self):
self.init_recorder() self.init_recorder()
@ -110,19 +114,20 @@ class TestComponentHistory(unittest.TestCase):
def set_state(state): def set_state(state):
self.hass.states.set(entity_id, state) self.hass.states.set(entity_id, state)
self.hass.pool.block_till_done() self.wait_recording_done()
recorder._INSTANCE.block_till_done()
return self.hass.states.get(entity_id) return self.hass.states.get(entity_id)
set_state('idle')
set_state('YouTube')
start = dt_util.utcnow() start = dt_util.utcnow()
point = start + timedelta(seconds=1) point = start + timedelta(seconds=1)
end = point + timedelta(seconds=1) end = point + timedelta(seconds=1)
with patch('homeassistant.util.dt.utcnow', return_value=point): with patch('homeassistant.components.recorder.dt_util.utcnow',
return_value=start):
set_state('idle')
set_state('YouTube')
with patch('homeassistant.components.recorder.dt_util.utcnow',
return_value=point):
states = [ states = [
set_state('idle'), set_state('idle'),
set_state('Netflix'), set_state('Netflix'),
@ -130,10 +135,11 @@ class TestComponentHistory(unittest.TestCase):
set_state('YouTube'), set_state('YouTube'),
] ]
with patch('homeassistant.util.dt.utcnow', return_value=end): with patch('homeassistant.components.recorder.dt_util.utcnow',
return_value=end):
set_state('Netflix') set_state('Netflix')
set_state('Plex') set_state('Plex')
self.assertEqual( hist = history.state_changes_during_period(start, end, entity_id)
{entity_id: states},
history.state_changes_during_period(start, end, entity_id)) self.assertEqual(states, hist[entity_id])

View File

@ -32,6 +32,60 @@ class TestScene(unittest.TestCase):
'scene': [[]] 'scene': [[]]
})) }))
def test_config_yaml_alias_anchor(self):
"""
Tests the usage of YAML aliases and anchors. The following test scene
configuration is equivalent to:
scene:
- name: test
entities:
light_1: &light_1_state
state: 'on'
brightness: 100
light_2: *light_1_state
When encountering a YAML alias/anchor, the PyYAML parser will use a
reference to the original dictionary, instead of creating a copy, so
care needs to be taken to not modify the original.
"""
test_light = loader.get_component('light.test')
test_light.init()
self.assertTrue(light.setup(self.hass, {
light.DOMAIN: {'platform': 'test'}
}))
light_1, light_2 = test_light.DEVICES[0:2]
light.turn_off(self.hass, [light_1.entity_id, light_2.entity_id])
self.hass.pool.block_till_done()
entity_state = {
'state': 'on',
'brightness': 100,
}
self.assertTrue(scene.setup(self.hass, {
'scene': [{
'name': 'test',
'entities': {
light_1.entity_id: entity_state,
light_2.entity_id: entity_state,
}
}]
}))
scene.activate(self.hass, 'scene.test')
self.hass.pool.block_till_done()
self.assertTrue(light_1.is_on)
self.assertTrue(light_2.is_on)
self.assertEqual(100,
light_1.last_call('turn_on')[1].get('brightness'))
self.assertEqual(100,
light_2.last_call('turn_on')[1].get('brightness'))
def test_activate_scene(self): def test_activate_scene(self):
test_light = loader.get_component('light.test') test_light = loader.get_component('light.test')
test_light.init() test_light.init()