This commit is contained in:
xifle 2016-01-02 18:56:14 +01:00
commit cbd3860585
70 changed files with 2236 additions and 440 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
@ -41,7 +45,6 @@ omit =
homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/geofancy.py
homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/netgear.py
@ -98,6 +101,7 @@ omit =
homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/worldclock.py homeassistant/components/sensor/worldclock.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

@ -275,7 +275,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
datefmt='%y-%m-%d %H:%M:%S')) datefmt='%y-%m-%d %H:%M:%S'))
logger = logging.getLogger('') logger = logging.getLogger('')
logger.addHandler(err_handler) logger.addHandler(err_handler)
logger.setLevel(logging.NOTSET) # this sets the minimum log level logger.setLevel(logging.INFO)
else: else:
_LOGGER.error( _LOGGER.error(

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

@ -116,7 +116,7 @@ class AlexaResponse(object):
self.should_end_session = True self.should_end_session = True
if intent is not None and 'slots' in intent: if intent is not None and 'slots' in intent:
self.variables = {key: value['value'] for key, value self.variables = {key: value['value'] for key, value
in intent['slots'].items()} in intent['slots'].items() if 'value' in value}
else: else:
self.variables = {} self.variables = {}

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,27 +41,10 @@ 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:
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 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:
trigger_sunrise(hass, action, offset) trigger_sunrise(hass, action, offset)
@ -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

@ -47,15 +47,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
response = requests.post(resource, data=payload, timeout=10, response = requests.post(resource, data=payload, timeout=10,
verify=verify_ssl) verify=verify_ssl)
if not response.ok: if not response.ok:
_LOGGER.error('Response status is "%s"', response.status_code) _LOGGER.error("Response status is '%s'", response.status_code)
return False return False
except requests.exceptions.MissingSchema: except requests.exceptions.MissingSchema:
_LOGGER.error('Missing resource or schema in configuration. ' _LOGGER.error("Missing resource or schema in configuration. "
'Add http:// to your URL.') "Add http:// or https:// to your URL")
return False return False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error('No route to resource/endpoint: %s', _LOGGER.error('No route to resource/endpoint: %s', resource)
resource)
return False return False
if use_get: if use_get:

View File

@ -1,70 +0,0 @@
"""
homeassistant.components.device_tracker.geofancy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Geofancy platform for the device tracker.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.geofancy/
"""
from homeassistant.const import (
HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR)
DEPENDENCIES = ['http']
_SEE = 0
URL_API_GEOFANCY_ENDPOINT = "/api/geofancy"
def setup_scanner(hass, config, see):
""" Set up an endpoint for the Geofancy app. """
# Use a global variable to keep setup_scanner compact when using a callback
global _SEE
_SEE = see
# POST would be semantically better, but that currently does not work
# since Geofancy sends the data as key1=value1&key2=value2
# in the request body, while Home Assistant expects json there.
hass.http.register_path(
'GET', URL_API_GEOFANCY_ENDPOINT, _handle_get_api_geofancy)
return True
def _handle_get_api_geofancy(handler, path_match, data):
""" Geofancy message received. """
if not isinstance(data, dict):
handler.write_json_message(
"Error while parsing Geofancy message.",
HTTP_INTERNAL_SERVER_ERROR)
return
if 'latitude' not in data or 'longitude' not in data:
handler.write_json_message(
"Location not specified.",
HTTP_UNPROCESSABLE_ENTITY)
return
if 'device' not in data or 'id' not in data:
handler.write_json_message(
"Device id or location id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
return
try:
gps_coords = (float(data['latitude']), float(data['longitude']))
except ValueError:
# If invalid latitude / longitude format
handler.write_json_message(
"Invalid latitude / longitude format.",
HTTP_UNPROCESSABLE_ENTITY)
return
# entity id's in Home Assistant must be alphanumerical
device_uuid = data['device']
device_entity_id = device_uuid.replace('-', '')
_SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id'])
handler.write_json_message("Geofancy message processed")

View File

@ -0,0 +1,105 @@
"""
homeassistant.components.device_tracker.locative
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Locative platform for the device tracker.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.locative/
"""
import logging
from functools import partial
from homeassistant.const import (
HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
from homeassistant.components.device_tracker import DOMAIN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
def setup_scanner(hass, config, see):
""" Set up an endpoint for the Locative app. """
# POST would be semantically better, but that currently does not work
# since Locative sends the data as key1=value1&key2=value2
# in the request body, while Home Assistant expects json there.
hass.http.register_path(
'GET', URL_API_LOCATIVE_ENDPOINT,
partial(_handle_get_api_locative, hass, see))
return True
def _handle_get_api_locative(hass, see, handler, path_match, data):
""" Locative message received. """
if not _check_data(handler, data):
return
device = data['device'].replace('-', '')
location_name = data['id'].lower()
direction = data['trigger']
if direction == 'enter':
see(dev_id=device, location_name=location_name)
handler.write_text("Setting location to {}".format(location_name))
elif direction == 'exit':
current_state = hass.states.get(
"{}.{}".format(DOMAIN, device)).state
if current_state == location_name:
see(dev_id=device, location_name=STATE_NOT_HOME)
handler.write_text("Setting location to not home")
else:
# Ignore the message if it is telling us to exit a zone that we
# aren't currently in. This occurs when a zone is entered before
# the previous zone was exited. The enter message will be sent
# first, then the exit message will be sent second.
handler.write_text(
'Ignoring exit from {} (already in {})'.format(
location_name, current_state))
elif direction == 'test':
# In the app, a test message can be sent. Just return something to
# the user to let them know that it works.
handler.write_text("Received test message.")
else:
handler.write_text(
"Received unidentified message: {}".format(direction),
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Received unidentified message from Locative: %s",
direction)
def _check_data(handler, data):
if 'latitude' not in data or 'longitude' not in data:
handler.write_text("Latitude and longitude not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Latitude and longitude not specified.")
return False
if 'device' not in data:
handler.write_text("Device id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Device id not specified.")
return False
if 'id' not in data:
handler.write_text("Location id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Location id not specified.")
return False
if 'trigger' not in data:
handler.write_text("Trigger is not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Trigger is not specified.")
return False
return True

View File

@ -21,7 +21,7 @@ from urllib.parse import urlparse, parse_qs
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.const import ( from homeassistant.const import (
SERVER_PORT, CONTENT_TYPE_JSON, SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN,
HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING,
HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH,
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED,
@ -293,6 +293,17 @@ class RequestHandler(SimpleHTTPRequestHandler):
json.dumps(data, indent=4, sort_keys=True, json.dumps(data, indent=4, sort_keys=True,
cls=rem.JSONEncoder).encode("UTF-8")) cls=rem.JSONEncoder).encode("UTF-8"))
def write_text(self, message, status_code=HTTP_OK):
""" Helper method to return a text message to the caller. """
self.send_response(status_code)
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
self.set_session_cookie_header()
self.end_headers()
self.wfile.write(message.encode("UTF-8"))
def write_file(self, path, cache_headers=True): def write_file(self, path, cache_headers=True):
""" Returns a file to the user. """ """ Returns a file to the user. """
try: try:

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)
@ -120,10 +121,17 @@ def setup_bridge(host, hass, add_devices_callback):
new_lights = [] new_lights = []
api_name = api.get('config').get('name')
if api_name == 'RaspBee-GW':
bridge_type = 'deconz'
else:
bridge_type = 'hue'
for light_id, info in api_states.items(): for light_id, info in api_states.items():
if light_id not in lights: if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info, lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights) bridge, update_lights,
bridge_type=bridge_type)
new_lights.append(lights[light_id]) new_lights.append(lights[light_id])
else: else:
lights[light_id].info = info lights[light_id].info = info
@ -162,11 +170,14 @@ def request_configuration(host, hass, add_devices_callback):
class HueLight(Light): class HueLight(Light):
""" Represents a Hue light """ """ Represents a Hue light """
def __init__(self, light_id, info, bridge, update_lights): # pylint: disable=too-many-arguments
def __init__(self, light_id, info, bridge, update_lights,
bridge_type='hue'):
self.light_id = light_id self.light_id = light_id
self.info = info self.info = info
self.bridge = bridge self.bridge = bridge
self.update_lights = update_lights self.update_lights = update_lights
self.bridge_type = bridge_type
@property @property
def unique_id(self): def unique_id(self):
@ -226,14 +237,17 @@ class HueLight(Light):
command['alert'] = 'lselect' command['alert'] = 'lselect'
elif flash == FLASH_SHORT: elif flash == FLASH_SHORT:
command['alert'] = 'select' command['alert'] = 'select'
else: elif self.bridge_type == 'hue':
command['alert'] = 'none' command['alert'] = 'none'
effect = kwargs.get(ATTR_EFFECT) effect = kwargs.get(ATTR_EFFECT)
if effect == EFFECT_COLORLOOP: if effect == EFFECT_COLORLOOP:
command['effect'] = 'colorloop' command['effect'] = 'colorloop'
else: elif effect == EFFECT_RANDOM:
command['hue'] = random.randrange(0, 65535)
command['sat'] = random.randrange(150, 254)
elif self.bridge_type == 'hue':
command['effect'] = 'none' command['effect'] = 'none'
self.bridge.set_light(self.light_id, command) self.bridge.set_light(self.light_id, command)

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

@ -14,9 +14,9 @@ from homeassistant.components.switch.vera import VeraSwitch
from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.light import ATTR_BRIGHTNESS
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' from homeassistant.const import EVENT_HOMEASSISTANT_STOP
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
'#python-vera==0.1.1'] REQUIREMENTS = ['pyvera==0.2.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,10 +36,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
device_data = config.get('device_data', {}) device_data = config.get('device_data', {})
controller = veraApi.VeraController(base_url) vera_controller, created = veraApi.init_controller(base_url)
if created:
def stop_subscription(event):
""" Shutdown Vera subscriptions and subscription thread on exit"""
_LOGGER.info("Shutting down subscriptions.")
vera_controller.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
devices = [] devices = []
try: try:
devices = controller.get_devices([ devices = vera_controller.get_devices([
'Switch', 'Switch',
'On/Off Switch', 'On/Off Switch',
'Dimmable Switch']) 'Dimmable Switch'])
@ -54,7 +63,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
exclude = extra_data.get('exclude', False) exclude = extra_data.get('exclude', False)
if exclude is not True: if exclude is not True:
lights.append(VeraLight(device, extra_data)) lights.append(VeraLight(device, vera_controller, extra_data))
add_devices_callback(lights) add_devices_callback(lights)

View File

@ -28,7 +28,7 @@ QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ? SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
""" """
EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY' EVENT_LOGBOOK_ENTRY = 'logbook_entry'
GROUP_BY_MINUTES = 15 GROUP_BY_MINUTES = 15
@ -204,7 +204,7 @@ def humanify(events):
event.time_fired, "Home Assistant", action, event.time_fired, "Home Assistant", action,
domain=HA_DOMAIN) domain=HA_DOMAIN)
elif event.event_type == EVENT_LOGBOOK_ENTRY: elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY:
domain = event.data.get(ATTR_DOMAIN) domain = event.data.get(ATTR_DOMAIN)
entity_id = event.data.get(ATTR_ENTITY_ID) entity_id = event.data.get(ATTR_ENTITY_ID)
if domain is None and entity_id is not None: if domain is None and entity_id is not None:

View File

@ -76,8 +76,12 @@ def setup(hass, config=None):
logfilter[LOGGER_LOGS] = logs logfilter[LOGGER_LOGS] = logs
logger = logging.getLogger('')
logger.setLevel(logging.NOTSET)
# Set log filter for all log handler # Set log filter for all log handler
for handler in logging.root.handlers: for handler in logging.root.handlers:
handler.setLevel(logging.NOTSET)
handler.addFilter(HomeAssistantLogFilter(logfilter)) handler.addFilter(HomeAssistantLogFilter(logfilter))
return True return True

View File

@ -142,10 +142,14 @@ class MpdDevice(MediaPlayerDevice):
def media_title(self): def media_title(self):
""" Title of current playing media. """ """ Title of current playing media. """
name = self.currentsong.get('name', None) name = self.currentsong.get('name', None)
title = self.currentsong['title'] title = self.currentsong.get('title', None)
if name is None: if name is None and title is None:
return "None"
elif name is None:
return title return title
elif title is None:
return name
else: else:
return '{}: {}'.format(name, title) return '{}: {}'.format(name, title)

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

@ -9,7 +9,8 @@ https://home-assistant.io/components/sensor/
import logging import logging
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import wink, zwave, isy994, verisure, ecobee from homeassistant.components import (wink, zwave, isy994,
verisure, ecobee, tellduslive)
DOMAIN = 'sensor' DOMAIN = 'sensor'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
@ -22,7 +23,8 @@ DISCOVERY_PLATFORMS = {
zwave.DISCOVER_SENSORS: 'zwave', zwave.DISCOVER_SENSORS: 'zwave',
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',
} }

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

@ -10,7 +10,7 @@ from datetime import timedelta
import logging import logging
import requests import requests
from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.const import (CONF_VALUE_TEMPLATE, STATE_UNKNOWN)
from homeassistant.util import template, Throttle from homeassistant.util import template, Throttle
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -47,15 +47,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
response = requests.post(resource, data=payload, timeout=10, response = requests.post(resource, data=payload, timeout=10,
verify=verify_ssl) verify=verify_ssl)
if not response.ok: if not response.ok:
_LOGGER.error('Response status is "%s"', response.status_code) _LOGGER.error("Response status is '%s'", response.status_code)
return False return False
except requests.exceptions.MissingSchema: except requests.exceptions.MissingSchema:
_LOGGER.error('Missing resource or schema in configuration. ' _LOGGER.error("Missing resource or schema in configuration. "
'Add http:// to your URL.') "Add http:// or https:// to your URL")
return False return False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error('No route to resource/endpoint. ' _LOGGER.error("No route to resource/endpoint: %s", resource)
'Please check the URL in the configuration file.')
return False return False
if use_get: if use_get:
@ -78,7 +77,7 @@ class RestSensor(Entity):
self._hass = hass self._hass = hass
self.rest = rest self.rest = rest
self._name = name self._name = name
self._state = 'n/a' self._state = STATE_UNKNOWN
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._value_template = value_template self._value_template = value_template
self.update() self.update()
@ -103,12 +102,12 @@ 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, 'N/A') self._hass, self._value_template, value, STATE_UNKNOWN)
self._state = value self._state = value
@ -119,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):
@ -127,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.") _LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data['error'] = 'N/A' self.data = None
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -143,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):
@ -151,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.") _LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data['error'] = 'N/A' 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

@ -0,0 +1,117 @@
"""
homeassistant.components.sensor.torque
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Get data from the Torque OBD application.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.torque/
"""
import re
from homeassistant.const import HTTP_OK
from homeassistant.helpers.entity import Entity
DOMAIN = 'torque'
DEPENDENCIES = ['http']
SENSOR_EMAIL_FIELD = 'eml'
DEFAULT_NAME = 'vehicle'
ENTITY_NAME_FORMAT = '{0} {1}'
API_PATH = '/api/torque'
SENSOR_NAME_KEY = r'userFullName(\w+)'
SENSOR_UNIT_KEY = r'userUnit(\w+)'
SENSOR_VALUE_KEY = r'k(\w+)'
NAME_KEY = re.compile(SENSOR_NAME_KEY)
UNIT_KEY = re.compile(SENSOR_UNIT_KEY)
VALUE_KEY = re.compile(SENSOR_VALUE_KEY)
def decode(value):
""" Double-decode required. """
return value.encode('raw_unicode_escape').decode('utf-8')
def convert_pid(value):
""" Convert pid from hex string to integer. """
return int(value, 16)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Set up Torque platform. """
vehicle = config.get('name', DEFAULT_NAME)
email = config.get('email', None)
sensors = {}
def _receive_data(handler, path_match, data):
""" Received data from Torque. """
handler.send_response(HTTP_OK)
handler.end_headers()
if email is not None and email != data[SENSOR_EMAIL_FIELD]:
return
names = {}
units = {}
for key in data:
is_name = NAME_KEY.match(key)
is_unit = UNIT_KEY.match(key)
is_value = VALUE_KEY.match(key)
if is_name:
pid = convert_pid(is_name.group(1))
names[pid] = decode(data[key])
elif is_unit:
pid = convert_pid(is_unit.group(1))
units[pid] = decode(data[key])
elif is_value:
pid = convert_pid(is_value.group(1))
if pid in sensors:
sensors[pid].on_update(data[key])
for pid in names:
if pid not in sensors:
sensors[pid] = TorqueSensor(
ENTITY_NAME_FORMAT.format(vehicle, names[pid]),
units.get(pid, None))
add_devices([sensors[pid]])
hass.http.register_path('GET', API_PATH, _receive_data)
return True
class TorqueSensor(Entity):
""" Represents a Torque sensor. """
def __init__(self, name, unit):
self._name = name
self._unit = unit
self._state = None
@property
def name(self):
""" Returns the name of the sensor. """
return self._name
@property
def unit_of_measurement(self):
""" Returns the unit of measurement. """
return self._unit
@property
def state(self):
""" State of the sensor. """
return self._state
@property
def icon(self):
""" Sensor default icon. """
return 'mdi:car'
def on_update(self, value):
""" Receive an update. """
self._state = value
self.update_ha_state()

View File

@ -13,11 +13,9 @@ import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME,
TEMP_CELCIUS, TEMP_FAHRENHEIT) TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' REQUIREMENTS = ['pyvera==0.2.1']
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
'#python-vera==0.1.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,7 +35,16 @@ def get_devices(hass, config):
device_data = config.get('device_data', {}) device_data = config.get('device_data', {})
vera_controller = veraApi.VeraController(base_url) vera_controller, created = veraApi.init_controller(base_url)
if created:
def stop_subscription(event):
""" Shutdown Vera subscriptions and subscription thread on exit"""
_LOGGER.info("Shutting down subscriptions.")
vera_controller.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
categories = ['Temperature Sensor', 'Light Sensor', 'Sensor'] categories = ['Temperature Sensor', 'Light Sensor', 'Sensor']
devices = [] devices = []
try: try:
@ -53,7 +60,8 @@ def get_devices(hass, config):
exclude = extra_data.get('exclude', False) exclude = extra_data.get('exclude', False)
if exclude is not True: if exclude is not True:
vera_sensors.append(VeraSensor(device, extra_data)) vera_sensors.append(
VeraSensor(device, vera_controller, extra_data))
return vera_sensors return vera_sensors
@ -66,8 +74,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class VeraSensor(Entity): class VeraSensor(Entity):
""" Represents a Vera Sensor. """ """ Represents a Vera Sensor. """
def __init__(self, vera_device, extra_data=None): def __init__(self, vera_device, controller, extra_data=None):
self.vera_device = vera_device self.vera_device = vera_device
self.controller = controller
self.extra_data = extra_data self.extra_data = extra_data
if self.extra_data and self.extra_data.get('name'): if self.extra_data and self.extra_data.get('name'):
self._name = self.extra_data.get('name') self._name = self.extra_data.get('name')
@ -76,6 +85,16 @@ class VeraSensor(Entity):
self.current_value = '' self.current_value = ''
self._temperature_units = None self._temperature_units = None
self.controller.register(vera_device)
self.controller.on(
vera_device, self._update_callback)
def _update_callback(self, _device):
""" Called by the vera device callback to update state. """
_LOGGER.info(
'Subscription update for %s', self.name)
self.update_ha_state(True)
def __str__(self): def __str__(self):
return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state) return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state)
@ -117,6 +136,11 @@ class VeraSensor(Entity):
attr['Vera Device Id'] = self.vera_device.vera_device_id attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr return attr
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
def update(self): def update(self):
if self.vera_device.category == "Temperature Sensor": if self.vera_device.category == "Temperature Sensor":
self.vera_device.refresh_value('CurrentTemperature') self.vera_device.refresh_value('CurrentTemperature')

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

@ -0,0 +1,223 @@
"""
homeassistant.components.sensor.yr
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Yr.no weather service.
Configuration:
Will show a symbol for the current weather as default:
sensor:
platform: yr
Will show temperatue and wind direction:
sensor:
platform: yr
monitored_conditions:
- temperature
- windDirection
Will show all available sensors:
sensor:
platform: yr
monitored_conditions:
- temperature
- symbol
- precipitation
- windSpeed
- pressure
- windDirection
- humidity
- fog
- cloudiness
- lowClouds
- mediumClouds
- highClouds
- dewpointTemperature
"""
import logging
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']
# Sensor types are defined like so:
SENSOR_TYPES = {
'symbol': ['Symbol', ''],
'precipitation': ['Condition', 'mm'],
'temperature': ['Temperature', '°C'],
'windSpeed': ['Wind speed', 'm/s'],
'pressure': ['Pressure', 'hPa'],
'windDirection': ['Wind direction', '°'],
'humidity': ['Humidity', '%'],
'fog': ['Fog', '%'],
'cloudiness': ['Cloudiness', '%'],
'lowClouds': ['Low clouds', '%'],
'mediumClouds': ['Medium clouds', '%'],
'highClouds': ['High clouds', '%'],
'dewpointTemperature': ['Dewpoint temperature', '°C'],
}
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the yr.no sensor. """
if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False
elevation = config.get('elevation')
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)
weather = YrData(coordinates)
dev = []
if 'monitored_conditions' in config:
for variable in config['monitored_conditions']:
if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(YrSensor(variable, weather))
# add symbol as default sensor
if len(dev) == 0:
dev.append(YrSensor("symbol", weather))
add_devices(dev)
# pylint: disable=too-many-instance-attributes
class YrSensor(Entity):
""" Implements an Yr.no sensor. """
def __init__(self, sensor_type, weather):
self.client_name = 'yr'
self._name = SENSOR_TYPES[sensor_type][0]
self.type = sensor_type
self._state = None
self._weather = weather
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
self._update = None
self.update()
@property
def name(self):
return '{} {}'.format(self.client_name, self._name)
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def state_attributes(self):
""" Returns state attributes. """
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={0};content_type=image/png".format(symbol_nr)
return data
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._unit_of_measurement
def update(self):
""" Gets the latest data from yr.no and updates the states. """
now = dt_util.utcnow()
# check if data should be updated
if self._update is not None and now <= self._update:
return
self._weather.update()
# find sensor
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")
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 = loc_data[self.type]['@value']
break
elif self.type == 'symbol' and valid_from < now:
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 = loc_data[self.type]['@mps']
break
elif self.type == 'windDirection':
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
class YrData(object):
""" Gets the latest data and updates the states. """
def __init__(self, coordinates):
self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \
'lat={lat};lon={lon};msl={msl}'.format(**coordinates)
self._nextrun = None
self.data = {}
self.update()
def update(self):
""" Gets the latest data from yr.no """
# check if new will be available
if self._nextrun is not None and dt_util.utcnow() <= self._nextrun:
return
try:
with requests.Session() as sess:
response = sess.get(self._url)
except requests.RequestException:
return
if response.status_code != 200:
return
data = response.text
import xmltodict
self.data = xmltodict.parse(data)['weatherdata']
model = self.data['meta']['model']
if '@nextrun' not in model:
model = model[0]
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 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) group, discovery, wink, isy994, verisure, zwave, tellduslive)
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',
} }
PROP_TO_ATTR = { PROP_TO_ATTR = {

View File

@ -10,6 +10,8 @@ import logging
import subprocess import subprocess
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.util import template
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,20 +26,30 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
for dev_name, properties in switches.items(): for dev_name, properties in switches.items():
devices.append( devices.append(
CommandSwitch( CommandSwitch(
hass,
properties.get('name', dev_name), properties.get('name', dev_name),
properties.get('oncmd', 'true'), properties.get('oncmd', 'true'),
properties.get('offcmd', 'true'))) properties.get('offcmd', 'true'),
properties.get('statecmd', False),
properties.get(CONF_VALUE_TEMPLATE, False)))
add_devices_callback(devices) add_devices_callback(devices)
class CommandSwitch(SwitchDevice): class CommandSwitch(SwitchDevice):
""" Represents a switch that can be togggled using shell commands. """ """ Represents a switch that can be togggled using shell commands. """
def __init__(self, name, command_on, command_off):
# pylint: disable=too-many-arguments
def __init__(self, hass, name, command_on, command_off,
command_state, value_template):
self._hass = hass
self._name = name self._name = name
self._state = False self._state = False
self._command_on = command_on self._command_on = command_on
self._command_off = command_off self._command_off = command_off
self._command_state = command_state
self._value_template = value_template
@staticmethod @staticmethod
def _switch(command): def _switch(command):
@ -51,10 +63,27 @@ class CommandSwitch(SwitchDevice):
return success return success
@staticmethod
def _query_state_value(command):
""" Execute state command for return value. """
_LOGGER.info('Running state command: %s', command)
try:
return_value = subprocess.check_output(command, shell=True)
return return_value.strip().decode('utf-8')
except subprocess.CalledProcessError:
_LOGGER.error('Command failed: %s', command)
@staticmethod
def _query_state_code(command):
""" Execute state command for return code. """
_LOGGER.info('Running state command: %s', command)
return subprocess.call(command, shell=True) == 0
@property @property
def should_poll(self): def should_poll(self):
""" No polling needed. """ """ Only poll if we have statecmd. """
return False return self._command_state is not None
@property @property
def name(self): def name(self):
@ -66,14 +95,34 @@ class CommandSwitch(SwitchDevice):
""" True if device is on. """ """ True if device is on. """
return self._state return self._state
def _query_state(self):
""" Query for state. """
if not self._command_state:
_LOGGER.error('No state command specified')
return
if self._value_template:
return CommandSwitch._query_state_value(self._command_state)
return CommandSwitch._query_state_code(self._command_state)
def update(self):
""" Update device state. """
if self._command_state:
payload = str(self._query_state())
if self._value_template:
payload = template.render_with_possible_json_value(
self._hass, self._value_template, payload)
self._state = (payload.lower() == "true")
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turn the device on. """ """ Turn the device on. """
if CommandSwitch._switch(self._command_on): if (CommandSwitch._switch(self._command_on) and
not self._command_state):
self._state = True self._state = True
self.update_ha_state() self.update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turn the device off. """ """ Turn the device off. """
if CommandSwitch._switch(self._command_off): if (CommandSwitch._switch(self._command_off) and
not self._command_state):
self._state = False self._state = False
self.update_ha_state() self.update_ha_state()

View File

@ -18,7 +18,7 @@ DEFAULT_BODY_ON = "ON"
DEFAULT_BODY_OFF = "OFF" DEFAULT_BODY_OFF = "OFF"
# pylint: disable=unused-argument # pylint: disable=unused-argument,
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Get REST switch. """ """ Get REST switch. """
@ -32,11 +32,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
requests.get(resource, timeout=10) requests.get(resource, timeout=10)
except requests.exceptions.MissingSchema: except requests.exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. " _LOGGER.error("Missing resource or schema in configuration. "
"Add http:// to your URL.") "Add http:// or https:// to your URL")
return False return False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint. " _LOGGER.error("No route to resource/endpoint: %s", resource)
"Please check the IP address in the configuration file.")
return False return False
add_devices_callback([RestSwitch( add_devices_callback([RestSwitch(

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

@ -13,11 +13,13 @@ import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) ATTR_BATTERY_LEVEL,
ATTR_TRIPPED,
ATTR_ARMED,
ATTR_LAST_TRIP_TIME,
EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' REQUIREMENTS = ['pyvera==0.2.1']
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
'#python-vera==0.1.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,7 +39,16 @@ def get_devices(hass, config):
device_data = config.get('device_data', {}) device_data = config.get('device_data', {})
vera_controller = veraApi.VeraController(base_url) vera_controller, created = veraApi.init_controller(base_url)
if created:
def stop_subscription(event):
""" Shutdown Vera subscriptions and subscription thread on exit"""
_LOGGER.info("Shutting down subscriptions.")
vera_controller.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
devices = [] devices = []
try: try:
devices = vera_controller.get_devices([ devices = vera_controller.get_devices([
@ -53,7 +64,8 @@ def get_devices(hass, config):
exclude = extra_data.get('exclude', False) exclude = extra_data.get('exclude', False)
if exclude is not True: if exclude is not True:
vera_switches.append(VeraSwitch(device, extra_data)) vera_switches.append(
VeraSwitch(device, vera_controller, extra_data))
return vera_switches return vera_switches
@ -66,9 +78,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class VeraSwitch(ToggleEntity): class VeraSwitch(ToggleEntity):
""" Represents a Vera Switch. """ """ Represents a Vera Switch. """
def __init__(self, vera_device, extra_data=None): def __init__(self, vera_device, controller, extra_data=None):
self.vera_device = vera_device self.vera_device = vera_device
self.extra_data = extra_data self.extra_data = extra_data
self.controller = controller
if self.extra_data and self.extra_data.get('name'): if self.extra_data and self.extra_data.get('name'):
self._name = self.extra_data.get('name') self._name = self.extra_data.get('name')
else: else:
@ -77,6 +90,16 @@ class VeraSwitch(ToggleEntity):
# for debouncing status check after command is sent # for debouncing status check after command is sent
self.last_command_send = 0 self.last_command_send = 0
self.controller.register(vera_device)
self.controller.on(
vera_device, self._update_callback)
def _update_callback(self, _device):
""" Called by the vera device callback to update state. """
_LOGGER.info(
'Subscription update for %s', self.name)
self.update_ha_state(True)
@property @property
def name(self): def name(self):
""" Get the mame of the switch. """ """ Get the mame of the switch. """
@ -118,6 +141,11 @@ class VeraSwitch(ToggleEntity):
self.vera_device.switch_off() self.vera_device.switch_off()
self.is_on_status = False self.is_on_status = False
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
@property @property
def is_on(self): def is_on(self):
""" True if device is on. """ """ True if device is on. """

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.7']
_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,7 +1,7 @@
# coding: utf-8 # coding: utf-8
""" Constants used by Home Assistant components. """ """ Constants used by Home Assistant components. """
__version__ = "0.10.0.dev0" __version__ = "0.11.0.dev0"
# Can be used to specify a catch all when registering state or event listeners. # Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = '*' MATCH_ALL = '*'

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)
@ -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):
# pylint: disable=too-few-public-methods
"""Represents an event within the Bus.""" """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,20 +225,17 @@ 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}
@ -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
@ -436,6 +434,7 @@ 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()
@ -451,28 +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 is_state_attr(self, entity_id, name, value):
""" Removes an entity from the state machine. """Test if entity exists and has a state attribute set to value."""
entity_id = entity_id.lower()
Returns boolean to indicate if an entity was removed. """ return (entity_id in self._states and
self._states[entity_id].attributes.get(name, None) == value)
def remove(self, entity_id):
"""Remove the state of an entity.
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:
@ -514,9 +521,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,11 +532,12 @@ 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 {}
@ -544,6 +550,7 @@ class Service(object):
} }
def __call__(self, call): def __call__(self, call):
"""Execute the service."""
self.func(call) self.func(call)
@ -554,6 +561,7 @@ class ServiceCall(object):
__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 {}
@ -570,6 +578,7 @@ 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()
@ -586,7 +595,7 @@ class ServiceRegistry(object):
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 +620,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 +645,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 +660,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 +677,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,7 +687,7 @@ 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)
@ -690,6 +697,7 @@ class Config(object):
# 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
@ -713,11 +721,11 @@ class Config(object):
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 +740,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 +755,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,7 +818,7 @@ 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
@ -826,7 +834,6 @@ def create_worker_pool(worker_count=None):
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

@ -113,11 +113,15 @@ class EntityComponent(object):
def _update_entity_states(self, now): def _update_entity_states(self, now):
""" Update the states of all the entities. """ """ Update the states of all the entities. """
with self.lock:
# We copy the entities because new entities might be detected
# during state update causing deadlocks.
entities = list(entity for entity in self.entities.values()
if entity.should_poll)
self.logger.info("Updating %s entities", self.domain) self.logger.info("Updating %s entities", self.domain)
with self.lock: for entity in entities:
for entity in self.entities.values():
if entity.should_poll:
entity.update_ha_state(True) entity.update_ha_state(True)
def _entity_discovered(self, service, info): def _entity_discovered(self, service, info):

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
if vY != 0:
X = (Y / vY) * vX X = (Y / vY) * vX
Z = (Y / vY) * (1 - vX - vY) 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

@ -43,7 +43,8 @@ def render(hass, template, variables=None, **kwargs):
try: try:
return ENV.from_string(template, { return ENV.from_string(template, {
'states': AllStates(hass), 'states': AllStates(hass),
'is_state': hass.states.is_state 'is_state': hass.states.is_state,
'is_state_attr': hass.states.is_state_attr
}).render(kwargs).strip() }).render(kwargs).strip()
except jinja2.TemplateError as err: except jinja2.TemplateError as err:
raise TemplateError(err) raise TemplateError(err)

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

@ -59,7 +59,7 @@ tellcore-py==1.1.2
# homeassistant.components.light.vera # homeassistant.components.light.vera
# homeassistant.components.sensor.vera # homeassistant.components.sensor.vera
# homeassistant.components.switch.vera # homeassistant.components.switch.vera
https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1 pyvera==0.2.1
# homeassistant.components.wink # homeassistant.components.wink
# homeassistant.components.light.wink # homeassistant.components.light.wink
@ -157,6 +157,9 @@ transmissionrpc==0.11
# homeassistant.components.sensor.twitch # homeassistant.components.sensor.twitch
python-twitch==1.2.0 python-twitch==1.2.0
# homeassistant.components.sensor.yr
xmltodict
# homeassistant.components.sun # homeassistant.components.sun
astral==0.8.1 astral==0.8.1
@ -170,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.7
# homeassistant.components.tellduslive
tellive-py==0.5.2
# homeassistant.components.thermostat.heatmiser # homeassistant.components.thermostat.heatmiser
heatmiserV3==0.9.1 heatmiserV3==0.9.1
@ -185,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

@ -0,0 +1,205 @@
"""
tests.components.device_tracker.locative
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the locative device tracker component.
"""
import unittest
from unittest.mock import patch
import requests
from homeassistant import bootstrap, const
import homeassistant.core as ha
import homeassistant.components.device_tracker as device_tracker
import homeassistant.components.http as http
import homeassistant.components.zone as zone
SERVER_PORT = 8126
HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT)
hass = None
def _url(data={}):
""" Helper method to generate urls. """
data = "&".join(["{}={}".format(name, value) for name, value in data.items()])
return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data)
@patch('homeassistant.components.http.util.get_local_ip',
return_value='127.0.0.1')
def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
""" Initalizes a Home Assistant server. """
global hass
hass = ha.HomeAssistant()
# Set up server
bootstrap.setup_component(hass, http.DOMAIN, {
http.DOMAIN: {
http.CONF_SERVER_PORT: SERVER_PORT
}
})
# Set up API
bootstrap.setup_component(hass, 'api')
# Set up device tracker
bootstrap.setup_component(hass, device_tracker.DOMAIN, {
device_tracker.DOMAIN: {
'platform': 'locative'
}
})
hass.start()
def tearDownModule(): # pylint: disable=invalid-name
""" Stops the Home Assistant server. """
hass.stop()
# Stub out update_config or else Travis CI raises an exception
@patch('homeassistant.components.device_tracker.update_config')
class TestLocative(unittest.TestCase):
""" Test Locative """
def test_missing_data(self, update_config):
data = {
'latitude': 1.0,
'longitude': 1.1,
'device': '123',
'id': 'Home',
'trigger': 'enter'
}
# No data
req = requests.get(_url({}))
self.assertEqual(422, req.status_code)
# No latitude
copy = data.copy()
del copy['latitude']
req = requests.get(_url(copy))
self.assertEqual(422, req.status_code)
# No device
copy = data.copy()
del copy['device']
req = requests.get(_url(copy))
self.assertEqual(422, req.status_code)
# No location
copy = data.copy()
del copy['id']
req = requests.get(_url(copy))
self.assertEqual(422, req.status_code)
# No trigger
copy = data.copy()
del copy['trigger']
req = requests.get(_url(copy))
self.assertEqual(422, req.status_code)
# Test message
copy = data.copy()
copy['trigger'] = 'test'
req = requests.get(_url(copy))
self.assertEqual(200, req.status_code)
# Unknown trigger
copy = data.copy()
copy['trigger'] = 'foobar'
req = requests.get(_url(copy))
self.assertEqual(422, req.status_code)
def test_enter_and_exit(self, update_config):
""" Test when there is a known zone """
data = {
'latitude': 40.7855,
'longitude': -111.7367,
'device': '123',
'id': 'Home',
'trigger': 'enter'
}
# Enter the Home
req = requests.get(_url(data))
self.assertEqual(200, req.status_code)
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
self.assertEqual(state_name, 'home')
data['id'] = 'HOME'
data['trigger'] = 'exit'
# Exit Home
req = requests.get(_url(data))
self.assertEqual(200, req.status_code)
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
self.assertEqual(state_name, 'not_home')
data['id'] = 'hOmE'
data['trigger'] = 'enter'
# Enter Home again
req = requests.get(_url(data))
self.assertEqual(200, req.status_code)
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
self.assertEqual(state_name, 'home')
data['trigger'] = 'exit'
# Exit Home
req = requests.get(_url(data))
self.assertEqual(200, req.status_code)
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
self.assertEqual(state_name, 'not_home')
data['id'] = 'work'
data['trigger'] = 'enter'
# Enter Work
req = requests.get(_url(data))
self.assertEqual(200, req.status_code)
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
self.assertEqual(state_name, 'work')
def test_exit_after_enter(self, update_config):
""" Test when an exit message comes after an enter message """
data = {
'latitude': 40.7855,
'longitude': -111.7367,
'device': '123',
'id': 'Home',
'trigger': 'enter'
}
# Enter Home
req = requests.get(_url(data))
self.assertEqual(200, req.status_code)
state = hass.states.get('{}.{}'.format('device_tracker', data['device']))
self.assertEqual(state.state, 'home')
data['id'] = 'Work'
# Enter Work
req = requests.get(_url(data))
self.assertEqual(200, req.status_code)
state = hass.states.get('{}.{}'.format('device_tracker', data['device']))
self.assertEqual(state.state, 'work')
data['id'] = 'Home'
data['trigger'] = 'exit'
# Exit Home
req = requests.get(_url(data))
self.assertEqual(200, req.status_code)
state = hass.states.get('{}.{}'.format('device_tracker', data['device']))
self.assertEqual(state.state, 'work')

View File

@ -0,0 +1,73 @@
"""
tests.components.sensor.test_yr
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Yr sensor.
"""
from unittest.mock import patch
import pytest
import homeassistant.core as ha
import homeassistant.components.sensor as sensor
@pytest.mark.usefixtures('betamax_session')
class TestSensorYr:
""" Test the Yr sensor. """
def setup_method(self, method):
self.hass = ha.HomeAssistant()
self.hass.config.latitude = 32.87336
self.hass.config.longitude = 117.22743
def teardown_method(self, method):
""" Stop down stuff we started. """
self.hass.stop()
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')
assert state.state.isnumeric()
assert state.attributes.get('unit_of_measurement') is None
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')
assert 'hPa', state.attributes.get('unit_of_measurement')
state = self.hass.states.get('sensor.yr_wind_direction')
assert '°', state.attributes.get('unit_of_measurement')
state = self.hass.states.get('sensor.yr_humidity')
assert '%', state.attributes.get('unit_of_measurement')
state = self.hass.states.get('sensor.yr_fog')
assert '%', state.attributes.get('unit_of_measurement')
state = self.hass.states.get('sensor.yr_wind_speed')
assert 'm/s', state.attributes.get('unit_of_measurement')

View File

@ -0,0 +1,158 @@
"""
tests.components.switch.test_command_switch
~~~~~~~~~~~~~~~~~~~~~~~~
Tests command switch.
"""
import json
import os
import tempfile
import unittest
from homeassistant import core
from homeassistant.const import STATE_ON, STATE_OFF
import homeassistant.components.switch as switch
class TestCommandSwitch(unittest.TestCase):
""" Test the command switch. """
def setUp(self): # pylint: disable=invalid-name
self.hass = core.HomeAssistant()
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_state_none(self):
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, 'switch_status')
test_switch = {
'oncmd': 'echo 1 > {}'.format(path),
'offcmd': 'echo 0 > {}'.format(path),
}
self.assertTrue(switch.setup(self.hass, {
'switch': {
'platform': 'command_switch',
'switches': {
'test': test_switch
}
}
}))
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
switch.turn_on(self.hass, 'switch.test')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_ON, state.state)
switch.turn_off(self.hass, 'switch.test')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
def test_state_value(self):
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, 'switch_status')
test_switch = {
'statecmd': 'cat {}'.format(path),
'oncmd': 'echo 1 > {}'.format(path),
'offcmd': 'echo 0 > {}'.format(path),
'value_template': '{{ value=="1" }}'
}
self.assertTrue(switch.setup(self.hass, {
'switch': {
'platform': 'command_switch',
'switches': {
'test': test_switch
}
}
}))
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
switch.turn_on(self.hass, 'switch.test')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_ON, state.state)
switch.turn_off(self.hass, 'switch.test')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
def test_state_json_value(self):
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, 'switch_status')
oncmd = json.dumps({'status': 'ok'})
offcmd = json.dumps({'status': 'nope'})
test_switch = {
'statecmd': 'cat {}'.format(path),
'oncmd': 'echo \'{}\' > {}'.format(oncmd, path),
'offcmd': 'echo \'{}\' > {}'.format(offcmd, path),
'value_template': '{{ value_json.status=="ok" }}'
}
self.assertTrue(switch.setup(self.hass, {
'switch': {
'platform': 'command_switch',
'switches': {
'test': test_switch
}
}
}))
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
switch.turn_on(self.hass, 'switch.test')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_ON, state.state)
switch.turn_off(self.hass, 'switch.test')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
def test_state_code(self):
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, 'switch_status')
test_switch = {
'statecmd': 'cat {}'.format(path),
'oncmd': 'echo 1 > {}'.format(path),
'offcmd': 'echo 0 > {}'.format(path),
}
self.assertTrue(switch.setup(self.hass, {
'switch': {
'platform': 'command_switch',
'switches': {
'test': test_switch
}
}
}))
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
switch.turn_on(self.hass, 'switch.test')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_ON, state.state)
switch.turn_off(self.hass, 'switch.test')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_ON, state.state)

View File

@ -149,6 +149,45 @@ class TestAlexa(unittest.TestCase):
text = req.json().get('response', {}).get('outputSpeech', {}).get('text') text = req.json().get('response', {}).get('outputSpeech', {}).get('text')
self.assertEqual('You told us your sign is virgo.', text) self.assertEqual('You told us your sign is virgo.', text)
def test_intent_request_with_slots_but_no_value(self):
data = {
'version': '1.0',
'session': {
'new': False,
'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000',
'application': {
'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'
},
'attributes': {
'supportedHoroscopePeriods': {
'daily': True,
'weekly': False,
'monthly': False
}
},
'user': {
'userId': 'amzn1.account.AM3B00000000000000000000000'
}
},
'request': {
'type': 'IntentRequest',
'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000',
'timestamp': '2015-05-13T12:34:56Z',
'intent': {
'name': 'GetZodiacHoroscopeIntent',
'slots': {
'ZodiacSign': {
'name': 'ZodiacSign',
}
}
}
}
}
req = _req(data)
self.assertEqual(200, req.status_code)
text = req.json().get('response', {}).get('outputSpeech', {}).get('text')
self.assertEqual('You told us your sign is .', text)
def test_intent_request_without_slots(self): def test_intent_request_without_slots(self):
data = { data = {
'version': '1.0', 'version': '1.0',

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,6 +71,9 @@ class TestComponentHistory(unittest.TestCase):
self.init_recorder() self.init_recorder()
states = [] states = []
now = dt_util.utcnow()
with patch('homeassistant.components.recorder.dt_util.utcnow',
return_value=now):
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),
@ -77,15 +81,14 @@ 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) states.append(state)
recorder._INSTANCE.block_till_done() self.wait_recording_done()
point = dt_util.utcnow() + timedelta(seconds=1) future = now + timedelta(seconds=1)
with patch('homeassistant.components.recorder.dt_util.utcnow',
with patch('homeassistant.util.dt.utcnow', return_value=point): return_value=future):
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 +96,17 @@ 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()
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()

View File

@ -321,6 +321,18 @@ class TestStateMachine(unittest.TestCase):
self.assertFalse(self.states.is_state('light.Bowl', 'off')) self.assertFalse(self.states.is_state('light.Bowl', 'off'))
self.assertFalse(self.states.is_state('light.Non_existing', 'on')) self.assertFalse(self.states.is_state('light.Non_existing', 'on'))
def test_is_state_attr(self):
""" Test is_state_attr method. """
self.states.set("light.Bowl", "on", {"brightness": 100})
self.assertTrue(
self.states.is_state_attr('light.Bowl', 'brightness', 100))
self.assertFalse(
self.states.is_state_attr('light.Bowl', 'friendly_name', 200))
self.assertFalse(
self.states.is_state_attr('light.Bowl', 'friendly_name', 'Bowl'))
self.assertFalse(
self.states.is_state_attr('light.Non_existing', 'brightness', 100))
def test_entity_ids(self): def test_entity_ids(self):
""" Test get_entity_ids method. """ """ Test get_entity_ids method. """
ent_ids = self.states.entity_ids() ent_ids = self.states.entity_ids()

View File

@ -117,6 +117,14 @@ class TestUtilTemplate(unittest.TestCase):
self.hass, self.hass,
'{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}')) '{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}'))
def test_is_state_attr(self):
self.hass.states.set('test.object', 'available', {'mode': 'on'})
self.assertEqual(
'yes',
template.render(
self.hass,
'{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}'))
def test_states_function(self): def test_states_function(self):
self.hass.states.set('test.object', 'available') self.hass.states.set('test.object', 'available')
self.assertEqual( self.assertEqual(