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/*/tellstick.py
homeassistant/components/tellduslive.py
homeassistant/components/*/tellduslive.py
homeassistant/components/*/vera.py
homeassistant/components/ecobee.py

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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=$?
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=$?

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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