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/*/tellstick.py
homeassistant/components/tellduslive.py
homeassistant/components/*/tellduslive.py
homeassistant/components/*/vera.py
homeassistant/components/ecobee.py
@ -41,7 +45,6 @@ omit =
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/geofancy.py
homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/netgear.py
@ -98,6 +101,7 @@ omit =
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py
homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/worldclock.py

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,15 +47,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
response = requests.post(resource, data=payload, timeout=10,
verify=verify_ssl)
if not response.ok:
_LOGGER.error('Response status is "%s"', response.status_code)
_LOGGER.error("Response status is '%s'", response.status_code)
return False
except requests.exceptions.MissingSchema:
_LOGGER.error('Missing resource or schema in configuration. '
'Add http:// to your URL.')
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// or https:// to your URL")
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('No route to resource/endpoint: %s',
resource)
_LOGGER.error('No route to resource/endpoint: %s', resource)
return False
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
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_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH,
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,
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):
""" Returns a file to the user. """
try:

View File

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

View File

@ -10,6 +10,7 @@ import json
import logging
import os
import socket
import random
from datetime import timedelta
from urllib.parse import urlparse
@ -20,7 +21,7 @@ from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP,
ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT,
ATTR_EFFECT, EFFECT_COLORLOOP, ATTR_RGB_COLOR)
ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, ATTR_RGB_COLOR)
REQUIREMENTS = ['phue==0.8']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@ -120,10 +121,17 @@ def setup_bridge(host, hass, add_devices_callback):
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():
if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights)
bridge, update_lights,
bridge_type=bridge_type)
new_lights.append(lights[light_id])
else:
lights[light_id].info = info
@ -162,11 +170,14 @@ def request_configuration(host, hass, add_devices_callback):
class HueLight(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.info = info
self.bridge = bridge
self.update_lights = update_lights
self.bridge_type = bridge_type
@property
def unique_id(self):
@ -226,14 +237,17 @@ class HueLight(Light):
command['alert'] = 'lselect'
elif flash == FLASH_SHORT:
command['alert'] = 'select'
else:
elif self.bridge_type == 'hue':
command['alert'] = 'none'
effect = kwargs.get(ATTR_EFFECT)
if effect == 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'
self.bridge.set_light(self.light_id, command)

View File

@ -42,6 +42,7 @@ turn_on:
description: Light effect
values:
- colorloop
- random
turn_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
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/'
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
'#python-vera==0.1.1']
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
REQUIREMENTS = ['pyvera==0.2.1']
_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', {})
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 = []
try:
devices = controller.get_devices([
devices = vera_controller.get_devices([
'Switch',
'On/Off Switch',
'Dimmable Switch'])
@ -54,7 +63,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
exclude = extra_data.get('exclude', False)
if exclude is not True:
lights.append(VeraLight(device, extra_data))
lights.append(VeraLight(device, vera_controller, extra_data))
add_devices_callback(lights)

View File

@ -28,7 +28,7 @@ QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
"""
EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY'
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
GROUP_BY_MINUTES = 15
@ -204,7 +204,7 @@ def humanify(events):
event.time_fired, "Home Assistant", action,
domain=HA_DOMAIN)
elif event.event_type == EVENT_LOGBOOK_ENTRY:
elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY:
domain = event.data.get(ATTR_DOMAIN)
entity_id = event.data.get(ATTR_ENTITY_ID)
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
logger = logging.getLogger('')
logger.setLevel(logging.NOTSET)
# Set log filter for all log handler
for handler in logging.root.handlers:
handler.setLevel(logging.NOTSET)
handler.addFilter(HomeAssistantLogFilter(logfilter))
return True

View File

@ -142,10 +142,14 @@ class MpdDevice(MediaPlayerDevice):
def media_title(self):
""" Title of current playing media. """
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
elif title is None:
return name
else:
return '{}: {}'.format(name, title)

View File

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

View File

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

View File

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

View File

@ -9,7 +9,8 @@ https://home-assistant.io/components/sensor/
import logging
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'
SCAN_INTERVAL = 30
@ -22,7 +23,8 @@ DISCOVERY_PLATFORMS = {
zwave.DISCOVER_SENSORS: 'zwave',
isy994.DISCOVER_SENSORS: 'isy994',
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. """
return self._name
@property
def icon(self):
""" Returns icon. """
return "mdi:speedometer"
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """

View File

@ -10,7 +10,7 @@ from datetime import timedelta
import logging
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.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,
verify=verify_ssl)
if not response.ok:
_LOGGER.error('Response status is "%s"', response.status_code)
_LOGGER.error("Response status is '%s'", response.status_code)
return False
except requests.exceptions.MissingSchema:
_LOGGER.error('Missing resource or schema in configuration. '
'Add http:// to your URL.')
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// or https:// to your URL")
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('No route to resource/endpoint. '
'Please check the URL in the configuration file.')
_LOGGER.error("No route to resource/endpoint: %s", resource)
return False
if use_get:
@ -78,7 +77,7 @@ class RestSensor(Entity):
self._hass = hass
self.rest = rest
self._name = name
self._state = 'n/a'
self._state = STATE_UNKNOWN
self._unit_of_measurement = unit_of_measurement
self._value_template = value_template
self.update()
@ -103,13 +102,13 @@ class RestSensor(Entity):
self.rest.update()
value = self.rest.data
if 'error' in value:
self._state = value['error']
else:
if self._value_template is not None:
value = template.render_with_possible_json_value(
self._hass, self._value_template, value, 'N/A')
self._state = value
if value is None:
value = STATE_UNKNOWN
elif self._value_template is not None:
value = template.render_with_possible_json_value(
self._hass, self._value_template, value, STATE_UNKNOWN)
self._state = value
# pylint: disable=too-few-public-methods
@ -119,7 +118,7 @@ class RestDataGet(object):
def __init__(self, resource, verify_ssl):
self._resource = resource
self._verify_ssl = verify_ssl
self.data = dict()
self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
@ -127,12 +126,10 @@ class RestDataGet(object):
try:
response = requests.get(self._resource, timeout=10,
verify=self._verify_ssl)
if 'error' in self.data:
del self.data['error']
self.data = response.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint.")
self.data['error'] = 'N/A'
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data = None
# pylint: disable=too-few-public-methods
@ -143,7 +140,7 @@ class RestDataPost(object):
self._resource = resource
self._payload = payload
self._verify_ssl = verify_ssl
self.data = dict()
self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
@ -151,9 +148,7 @@ class RestDataPost(object):
try:
response = requests.post(self._resource, data=self._payload,
timeout=10, verify=self._verify_ssl)
if 'error' in self.data:
del self.data['error']
self.data = response.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint.")
self.data['error'] = 'N/A'
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data = None

View File

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

View File

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

View File

@ -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.const import (
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/'
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
'#python-vera==0.1.1']
REQUIREMENTS = ['pyvera==0.2.1']
_LOGGER = logging.getLogger(__name__)
@ -37,7 +35,16 @@ def get_devices(hass, config):
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']
devices = []
try:
@ -53,7 +60,8 @@ def get_devices(hass, config):
exclude = extra_data.get('exclude', False)
if exclude is not True:
vera_sensors.append(VeraSensor(device, extra_data))
vera_sensors.append(
VeraSensor(device, vera_controller, extra_data))
return vera_sensors
@ -66,8 +74,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class VeraSensor(Entity):
""" 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.controller = controller
self.extra_data = extra_data
if self.extra_data and self.extra_data.get('name'):
self._name = self.extra_data.get('name')
@ -76,6 +85,16 @@ class VeraSensor(Entity):
self.current_value = ''
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):
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
return attr
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
def update(self):
if self.vera_device.category == "Temperature Sensor":
self.vera_device.refresh_value('CurrentTemperature')

View File

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

View File

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

View File

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

View File

@ -10,6 +10,8 @@ import logging
import subprocess
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.util import template
_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():
devices.append(
CommandSwitch(
hass,
properties.get('name', dev_name),
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)
class CommandSwitch(SwitchDevice):
""" 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._state = False
self._command_on = command_on
self._command_off = command_off
self._command_state = command_state
self._value_template = value_template
@staticmethod
def _switch(command):
@ -51,10 +63,27 @@ class CommandSwitch(SwitchDevice):
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
def should_poll(self):
""" No polling needed. """
return False
""" Only poll if we have statecmd. """
return self._command_state is not None
@property
def name(self):
@ -66,14 +95,34 @@ class CommandSwitch(SwitchDevice):
""" True if device is on. """
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):
""" 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.update_ha_state()
self.update_ha_state()
def turn_off(self, **kwargs):
""" 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.update_ha_state()
self.update_ha_state()

View File

@ -18,7 +18,7 @@ DEFAULT_BODY_ON = "ON"
DEFAULT_BODY_OFF = "OFF"
# pylint: disable=unused-argument
# pylint: disable=unused-argument,
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Get REST switch. """
@ -32,11 +32,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
requests.get(resource, timeout=10)
except requests.exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// to your URL.")
"Add http:// or https:// to your URL")
return False
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint. "
"Please check the IP address in the configuration file.")
_LOGGER.error("No route to resource/endpoint: %s", resource)
return False
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.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/'
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
'#python-vera==0.1.1']
REQUIREMENTS = ['pyvera==0.2.1']
_LOGGER = logging.getLogger(__name__)
@ -37,7 +39,16 @@ def get_devices(hass, config):
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 = []
try:
devices = vera_controller.get_devices([
@ -53,7 +64,8 @@ def get_devices(hass, config):
exclude = extra_data.get('exclude', False)
if exclude is not True:
vera_switches.append(VeraSwitch(device, extra_data))
vera_switches.append(
VeraSwitch(device, vera_controller, extra_data))
return vera_switches
@ -66,9 +78,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class VeraSwitch(ToggleEntity):
""" 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.extra_data = extra_data
self.controller = controller
if self.extra_data and self.extra_data.get('name'):
self._name = self.extra_data.get('name')
else:
@ -77,6 +90,16 @@ class VeraSwitch(ToggleEntity):
# for debouncing status check after command is sent
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
def name(self):
""" Get the mame of the switch. """
@ -118,6 +141,11 @@ class VeraSwitch(ToggleEntity):
self.vera_device.switch_off()
self.is_on_status = False
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
@property
def is_on(self):
""" True if device is on. """

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -113,12 +113,16 @@ class EntityComponent(object):
def _update_entity_states(self, now):
""" 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)
with self.lock:
for entity in self.entities.values():
if entity.should_poll:
entity.update_ha_state(True)
for entity in entities:
entity.update_ha_state(True)
def _entity_discovered(self, service, info):
""" Called when a entity is discovered. """

View File

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

View File

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

View File

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

View File

@ -43,7 +43,8 @@ def render(hass, template, variables=None, **kwargs):
try:
return ENV.from_string(template, {
'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()
except jinja2.TemplateError as 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.sensor.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.light.wink
@ -157,6 +157,9 @@ transmissionrpc==0.11
# homeassistant.components.sensor.twitch
python-twitch==1.2.0
# homeassistant.components.sensor.yr
xmltodict
# homeassistant.components.sun
astral==0.8.1
@ -170,7 +173,10 @@ hikvision==0.4
orvibo==1.1.0
# homeassistant.components.switch.wemo
pywemo==0.3.3
pywemo==0.3.7
# homeassistant.components.tellduslive
tellive-py==0.5.2
# homeassistant.components.thermostat.heatmiser
heatmiserV3==0.9.1
@ -185,7 +191,7 @@ python-nest==2.6.0
radiotherm==1.2
# homeassistant.components.verisure
https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6
vsure==0.4.3
# homeassistant.components.zwave
pydispatcher==2.0.5

6
requirements_test.txt Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

8
setup.cfg Normal file
View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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')
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):
data = {
'version': '1.0',

View File

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

View File

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

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.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):
""" Test get_entity_ids method. """
ent_ids = self.states.entity_ids()

View File

@ -117,6 +117,14 @@ class TestUtilTemplate(unittest.TestCase):
self.hass,
'{% 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):
self.hass.states.set('test.object', 'available')
self.assertEqual(