Merge pull request #121 from balloob/dev

Update master with latest changes
This commit is contained in:
Paulus Schoutsen 2015-05-14 22:50:22 -07:00
commit 00664ac601
95 changed files with 3000 additions and 587 deletions

View File

@ -2,6 +2,8 @@
source = homeassistant
omit =
homeassistant/__main__.py
homeassistant/external/*
# omit pieces of code that rely on external devices being present
@ -11,29 +13,37 @@ omit =
homeassistant/components/zwave.py
homeassistant/components/*/zwave.py
homeassistant/components/modbus.py
homeassistant/components/*/modbus.py
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/*/tellstick.py
homeassistant/components/*/vera.py
homeassistant/components/keyboard.py
homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/nest.py
homeassistant/components/light/hue.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushover.py
homeassistant/components/media_player/cast.py
homeassistant/components/browser.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/keyboard.py
homeassistant/components/light/hue.py
homeassistant/components/media_player/cast.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushover.py
homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/transmission.py
homeassistant/components/isy994.py
homeassistant/components/light/isy994.py
homeassistant/components/switch/isy994.py
homeassistant/components/sensor/isy994.py
homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/nest.py
[report]

3
.gitmodules vendored
View File

@ -19,3 +19,6 @@
[submodule "homeassistant/external/nzbclients"]
path = homeassistant/external/nzbclients
url = https://github.com/jamespcole/home-assistant-nzb-clients.git
[submodule "homeassistant/external/pymysensors"]
path = homeassistant/external/pymysensors
url = https://github.com/theolind/pymysensors

View File

@ -28,7 +28,8 @@ wink:
access_token: 'YOUR_TOKEN'
device_tracker:
# The following types are available: netgear, tomato, luci, nmap_tracker
# The following types are available: ddwrt, netgear, tomato, luci,
# and nmap_tracker
platform: netgear
host: 192.168.1.1
username: admin
@ -65,7 +66,7 @@ device_sun_light_trigger:
# Optional: disable lights being turned off when everybody leaves the house
# disable_turn_off: 1
# A comma seperated list of states that have to be tracked as a single group
# A comma separated list of states that have to be tracked as a single group
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
group:
living_room:

View File

@ -22,7 +22,7 @@ DOMAIN = "example"
# List of component names (string) your component depends upon
# We depend on group because group will be loaded after all the components that
# initalize devices have been setup.
# initialize devices have been setup.
DEPENDENCIES = ['group']
# Configuration key for the entity id we are targetting
@ -115,5 +115,5 @@ def setup(hass, config):
# Register our service with HASS.
hass.services.register(DOMAIN, SERVICE_FLASH, flash_service)
# Tells the bootstrapper that the component was succesfully initialized
# Tells the bootstrapper that the component was successfully initialized
return True

View File

@ -12,11 +12,8 @@ import logging
import threading
import enum
import re
import datetime as dt
import functools as ft
import requests
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
@ -24,6 +21,7 @@ from homeassistant.const import (
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED,
TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME)
import homeassistant.util as util
import homeassistant.util.dt as date_util
DOMAIN = "homeassistant"
@ -109,7 +107,20 @@ class HomeAssistant(object):
def track_point_in_time(self, action, point_in_time):
"""
Adds a listener that fires once at or after a spefic point in time.
Adds a listener that fires once after a spefic point in time.
"""
utc_point_in_time = date_util.as_utc(point_in_time)
@ft.wraps(action)
def utc_converter(utc_now):
""" Converts passed in UTC now to local now. """
action(date_util.as_local(utc_now))
self.track_point_in_utc_time(utc_converter, utc_point_in_time)
def track_point_in_utc_time(self, action, point_in_time):
"""
Adds a listener that fires once after a specific point in UTC time.
"""
@ft.wraps(action)
@ -135,11 +146,19 @@ class HomeAssistant(object):
self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener)
return point_in_time_listener
# pylint: disable=too-many-arguments
def track_utc_time_change(self, action,
year=None, month=None, day=None,
hour=None, minute=None, second=None):
""" Adds a listener that will fire if time matches a pattern. """
self.track_time_change(
action, year, month, day, hour, minute, second, utc=True)
# pylint: disable=too-many-arguments
def track_time_change(self, action,
year=None, month=None, day=None,
hour=None, minute=None, second=None):
""" Adds a listener that will fire if time matches a pattern. """
hour=None, minute=None, second=None, utc=False):
""" Adds a listener that will fire if UTC time matches a pattern. """
# We do not have to wrap the function with time pattern matching logic
# if no pattern given
@ -155,6 +174,9 @@ class HomeAssistant(object):
""" Listens for matching time_changed events. """
now = event.data[ATTR_NOW]
if not utc:
now = date_util.as_local(now)
mat = _matcher
if mat(now.year, year) and \
@ -305,7 +327,7 @@ def create_worker_pool():
for start, job in current_jobs:
_LOGGER.warning("WorkerPool:Current job from %s: %s",
util.datetime_to_str(start), job)
date_util.datetime_to_local_str(start), job)
return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback)
@ -333,7 +355,7 @@ class Event(object):
self.data = data or {}
self.origin = origin
self.time_fired = util.strip_microseconds(
time_fired or dt.datetime.now())
time_fired or date_util.utcnow())
def as_dict(self):
""" Returns a dict representation of this Event. """
@ -341,7 +363,7 @@ class Event(object):
'event_type': self.event_type,
'data': dict(self.data),
'origin': str(self.origin),
'time_fired': util.datetime_to_str(self.time_fired),
'time_fired': date_util.datetime_to_str(self.time_fired),
}
def __repr__(self):
@ -354,6 +376,13 @@ class Event(object):
return "<Event {}[{}]>".format(self.event_type,
str(self.origin)[0])
def __eq__(self, other):
return (self.__class__ == other.__class__ and
self.event_type == other.event_type and
self.data == other.data and
self.origin == other.origin and
self.time_fired == other.time_fired)
class EventBus(object):
""" Class that allows different components to communicate via services
@ -376,6 +405,9 @@ class EventBus(object):
def fire(self, event_type, event_data=None, origin=EventOrigin.local):
""" Fire an event. """
if not self._pool.running:
raise HomeAssistantError('Home Assistant has shut down.')
with self._lock:
# Copy the list of the current listeners because some listeners
# remove themselves as a listener while being executed which
@ -474,13 +506,14 @@ class State(object):
self.entity_id = entity_id.lower()
self.state = state
self.attributes = attributes or {}
self.last_updated = last_updated or dt.datetime.now()
self.last_updated = date_util.strip_microseconds(
last_updated or date_util.utcnow())
# Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict())
# This behavior occurs because to_dict uses datetime_to_str
# which does not preserve microseconds
self.last_changed = util.strip_microseconds(
self.last_changed = date_util.strip_microseconds(
last_changed or self.last_updated)
@property
@ -512,8 +545,8 @@ class State(object):
return {'entity_id': self.entity_id,
'state': self.state,
'attributes': self.attributes,
'last_changed': util.datetime_to_str(self.last_changed),
'last_updated': util.datetime_to_str(self.last_updated)}
'last_changed': date_util.datetime_to_str(self.last_changed),
'last_updated': date_util.datetime_to_str(self.last_updated)}
@classmethod
def from_dict(cls, json_dict):
@ -528,12 +561,12 @@ class State(object):
last_changed = json_dict.get('last_changed')
if last_changed:
last_changed = util.str_to_datetime(last_changed)
last_changed = date_util.str_to_datetime(last_changed)
last_updated = json_dict.get('last_updated')
if last_updated:
last_updated = util.str_to_datetime(last_updated)
last_updated = date_util.str_to_datetime(last_updated)
return cls(json_dict['entity_id'], json_dict['state'],
json_dict.get('attributes'), last_changed, last_updated)
@ -550,7 +583,7 @@ class State(object):
return "<state {}={}{} @ {}>".format(
self.entity_id, self.state, attr,
util.datetime_to_str(self.last_changed))
date_util.datetime_to_local_str(self.last_changed))
class StateMachine(object):
@ -587,7 +620,7 @@ class StateMachine(object):
"""
Returns all states that have been changed since point_in_time.
"""
point_in_time = util.strip_microseconds(point_in_time)
point_in_time = date_util.strip_microseconds(point_in_time)
with self._lock:
return [state for state in self._states.values()
@ -849,7 +882,7 @@ class Timer(threading.Thread):
last_fired_on_second = -1
calc_now = dt.datetime.now
calc_now = date_util.utcnow
interval = self.interval
while not self._stop_event.isSet():
@ -875,7 +908,13 @@ class Timer(threading.Thread):
last_fired_on_second = now.second
self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
# Event might have been set while sleeping
if not self._stop_event.isSet():
try:
self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
except HomeAssistantError:
# HA raises error if firing event after it has shut down
break
class Config(object):
@ -898,44 +937,9 @@ class Config(object):
# Directory that holds the configuration
self.config_dir = os.path.join(os.getcwd(), 'config')
def auto_detect(self):
""" Will attempt to detect config of Home Assistant. """
# Only detect if location or temp unit missing
if None not in (self.latitude, self.longitude, self.temperature_unit):
return
_LOGGER.info('Auto detecting location and temperature unit')
try:
info = requests.get(
'https://freegeoip.net/json/', timeout=5).json()
except requests.RequestException:
return
if self.latitude is None and self.longitude is None:
self.latitude = info['latitude']
self.longitude = info['longitude']
if self.temperature_unit is None:
# From Wikipedia:
# Fahrenheit is used in the Bahamas, Belize, the Cayman Islands,
# Palau, and the United States and associated territories of
# American Samoa and the U.S. Virgin Islands
if info['country_code'] in ('BS', 'BZ', 'KY', 'PW',
'US', 'AS', 'VI'):
self.temperature_unit = TEMP_FAHRENHEIT
else:
self.temperature_unit = TEMP_CELCIUS
if self.location_name is None:
self.location_name = info['city']
if self.time_zone is None:
self.time_zone = info['time_zone']
def path(self, path):
def path(self, *path):
""" Returns path to the file within the config dir. """
return os.path.join(self.config_dir, path)
return os.path.join(self.config_dir, *path)
def temperature(self, value, unit):
""" Converts temperature to user preferred unit if set. """
@ -955,6 +959,17 @@ class Config(object):
# Could not convert value to float
return value, unit
def as_dict(self):
""" Converts config to a dictionary. """
return {
'latitude': self.latitude,
'longitude': self.longitude,
'temperature_unit': self.temperature_unit,
'location_name': self.location_name,
'time_zone': self.time_zone.zone,
'components': self.components,
}
class HomeAssistantError(Exception):
""" General Home Assistant exception occured. """

View File

@ -7,6 +7,14 @@ import argparse
import importlib
# Home Assistant dependencies, mapped module -> package name
DEPENDENCIES = {
'requests': 'requests',
'yaml': 'pyyaml',
'pytz': 'pytz',
}
def validate_python():
""" Validate we're running the right Python version. """
major, minor = sys.version_info[:2]
@ -20,13 +28,13 @@ def validate_dependencies():
""" Validate all dependencies that HA uses. """
import_fail = False
for module in ['requests']:
for module, name in DEPENDENCIES.items():
try:
importlib.import_module(module)
except ImportError:
import_fail = True
print(
'Fatal Error: Unable to find dependency {}'.format(module))
'Fatal Error: Unable to find dependency {}'.format(name))
if import_fail:
print(("Install dependencies by running: "
@ -72,24 +80,13 @@ def ensure_config_path(config_dir):
'directory {} ').format(config_dir))
sys.exit()
# Try to use yaml configuration first
config_path = os.path.join(config_dir, 'configuration.yaml')
if not os.path.isfile(config_path):
config_path = os.path.join(config_dir, 'home-assistant.conf')
import homeassistant.config as config_util
# Ensure a config file exists to make first time usage easier
if not os.path.isfile(config_path):
config_path = os.path.join(config_dir, 'configuration.yaml')
try:
with open(config_path, 'w') as conf:
conf.write("frontend:\n\n")
conf.write("discovery:\n\n")
conf.write("history:\n\n")
conf.write("logbook:\n\n")
except IOError:
print(('Fatal Error: No configuration file found and unable '
'to write a default one to {}').format(config_path))
sys.exit()
config_path = config_util.ensure_config_exists(config_dir)
if config_path is None:
print('Error getting configuration path')
sys.exit()
return config_path

View File

@ -10,25 +10,27 @@ start by calling homeassistant.start_home_assistant(bus)
"""
import os
import configparser
import yaml
import io
import logging
from collections import defaultdict
import homeassistant
import homeassistant.util as util
import homeassistant.util.dt as date_util
import homeassistant.config as config_util
import homeassistant.loader as loader
import homeassistant.components as core_components
import homeassistant.components.group as group
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_VISIBILITY,
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_CUSTOMIZE,
TEMP_CELCIUS, TEMP_FAHRENHEIT)
_LOGGER = logging.getLogger(__name__)
ATTR_COMPONENT = "component"
ATTR_COMPONENT = 'component'
PLATFORM_FORMAT = '{}.{}'
def setup_component(hass, domain, config=None):
@ -67,7 +69,7 @@ def _setup_component(hass, domain, config):
if missing_deps:
_LOGGER.error(
"Not initializing %s because not all dependencies loaded: %s",
'Not initializing %s because not all dependencies loaded: %s',
domain, ", ".join(missing_deps))
return False
@ -87,14 +89,42 @@ def _setup_component(hass, domain, config):
return True
else:
_LOGGER.error("component %s failed to initialize", domain)
_LOGGER.error('component %s failed to initialize', domain)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error during setup of component %s", domain)
_LOGGER.exception('Error during setup of component %s', domain)
return False
def prepare_setup_platform(hass, config, domain, platform_name):
""" Loads a platform and makes sure dependencies are setup. """
_ensure_loader_prepared(hass)
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
platform = loader.get_component(platform_path)
# Not found
if platform is None:
return None
# Already loaded or no dependencies
elif (platform_path in hass.config.components or
not hasattr(platform, 'DEPENDENCIES')):
return platform
# Load dependencies
for component in platform.DEPENDENCIES:
if not setup_component(hass, component, config):
_LOGGER.error(
'Unable to prepare setup for platform %s because dependency '
'%s could not be initialized', platform_path, component)
return None
return platform
# pylint: disable=too-many-branches, too-many-statements
def from_config_dict(config, hass=None):
"""
@ -122,12 +152,12 @@ def from_config_dict(config, hass=None):
if ' ' not in key and key != homeassistant.DOMAIN)
if not core_components.setup(hass, config):
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted.")
_LOGGER.error('Home Assistant core failed to initialize. '
'Further initialization aborted.')
return hass
_LOGGER.info("Home Assistant core initialized")
_LOGGER.info('Home Assistant core initialized')
# Setup the components
for domain in loader.load_order_components(components):
@ -148,26 +178,7 @@ def from_config_file(config_path, hass=None):
# Set config dir to directory holding config file
hass.config.config_dir = os.path.abspath(os.path.dirname(config_path))
config_dict = {}
# check config file type
if os.path.splitext(config_path)[1] == '.yaml':
# Read yaml
config_dict = yaml.load(io.open(config_path, 'r'))
# If YAML file was empty
if config_dict is None:
config_dict = {}
else:
# Read config
config = configparser.ConfigParser()
config.read(config_path)
for section in config.sections():
config_dict[section] = {}
for key, val in config.items(section):
config_dict[section][key] = val
config_dict = config_util.load_config_file(config_path)
return from_config_dict(config_dict, hass)
@ -177,7 +188,7 @@ def enable_logging(hass):
logging.basicConfig(level=logging.INFO)
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path("home-assistant.log")
err_log_path = hass.config.path('home-assistant.log')
err_path_exists = os.path.isfile(err_log_path)
# Check if we can write to the error log if it exists or that
@ -196,30 +207,73 @@ def enable_logging(hass):
else:
_LOGGER.error(
"Unable to setup error log %s (access denied)", err_log_path)
'Unable to setup error log %s (access denied)', err_log_path)
def process_ha_core_config(hass, config):
""" Processes the [homeassistant] section from the config. """
hac = hass.config
def set_time_zone(time_zone_str):
""" Helper method to set time zone in HA. """
if time_zone_str is None:
return
time_zone = date_util.get_time_zone(time_zone_str)
if time_zone:
hac.time_zone = time_zone
date_util.set_default_time_zone(time_zone)
else:
_LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name'),
(CONF_TIME_ZONE, 'time_zone')):
(CONF_NAME, 'location_name')):
if key in config:
setattr(hass.config, attr, config[key])
setattr(hac, attr, config[key])
for entity_id, hidden in config.get(CONF_VISIBILITY, {}).items():
Entity.overwrite_hidden(entity_id, hidden == 'hide')
set_time_zone(config.get(CONF_TIME_ZONE))
for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items():
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
if CONF_TEMPERATURE_UNIT in config:
unit = config[CONF_TEMPERATURE_UNIT]
if unit == 'C':
hass.config.temperature_unit = TEMP_CELCIUS
hac.temperature_unit = TEMP_CELCIUS
elif unit == 'F':
hass.config.temperature_unit = TEMP_FAHRENHEIT
hac.temperature_unit = TEMP_FAHRENHEIT
hass.config.auto_detect()
# If we miss some of the needed values, auto detect them
if None not in (
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
return
_LOGGER.info('Auto detecting location and temperature unit')
info = util.detect_location_info()
if info is None:
_LOGGER.error('Could not detect location information')
return
if hac.latitude is None and hac.longitude is None:
hac.latitude = info.latitude
hac.longitude = info.longitude
if hac.temperature_unit is None:
if info.use_fahrenheit:
hac.temperature_unit = TEMP_FAHRENHEIT
else:
hac.temperature_unit = TEMP_CELCIUS
if hac.location_name is None:
hac.location_name = info.city
if hac.time_zone is None:
set_time_zone(info.time_zone)
def _ensure_loader_prepared(hass):

View File

@ -15,6 +15,7 @@ import homeassistant.remote as rem
from homeassistant.const import (
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_BOOTSTRAP,
EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
HTTP_UNPROCESSABLE_ENTITY)
@ -42,6 +43,13 @@ def setup(hass, config):
# /api/stream
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
# /api/config
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
# /api/bootstrap
hass.http.register_path(
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
# /states
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
hass.http.register_path(
@ -140,6 +148,23 @@ def _handle_get_api_stream(handler, path_match, data):
hass.bus.remove_listener(MATCH_ALL, forward_events)
def _handle_get_api_config(handler, path_match, data):
""" Returns the Home Assistant config. """
handler.write_json(handler.server.hass.config.as_dict())
def _handle_get_api_bootstrap(handler, path_match, data):
""" Returns all data needed to bootstrap Home Assistant. """
hass = handler.server.hass
handler.write_json({
'config': hass.config.as_dict(),
'states': hass.states.all(),
'events': _events_json(hass),
'services': _services_json(hass),
})
def _handle_get_api_states(handler, path_match, data):
""" Returns a dict containing all entity ids and their state. """
handler.write_json(handler.server.hass.states.all())
@ -190,9 +215,7 @@ def _handle_post_state_entity(handler, path_match, data):
def _handle_get_api_events(handler, path_match, data):
""" Handles getting overview of event listeners. """
handler.write_json([{"event": key, "listener_count": value}
for key, value
in handler.server.hass.bus.listeners.items()])
handler.write_json(_events_json(handler.server.hass))
def _handle_api_post_events_event(handler, path_match, event_data):
@ -227,10 +250,7 @@ def _handle_api_post_events_event(handler, path_match, event_data):
def _handle_get_api_services(handler, path_match, data):
""" Handles getting overview of services. """
handler.write_json(
[{"domain": key, "services": value}
for key, value
in handler.server.hass.services.services.items()])
handler.write_json(_services_json(handler.server.hass))
# pylint: disable=invalid-name
@ -312,3 +332,15 @@ def _handle_get_api_components(handler, path_match, data):
""" Returns all the loaded components. """
handler.write_json(handler.server.hass.config.components)
def _services_json(hass):
""" Generate services data to JSONify. """
return [{"domain": key, "services": value}
for key, value in hass.services.services.items()]
def _events_json(hass):
""" Generate event data to JSONify. """
return [{"event": key, "listener_count": value}
for key, value in hass.bus.listeners.items()]

View File

@ -1,8 +1,8 @@
"""
homeassistant.components.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sets up a demo environment that mimics interaction with devices
Sets up a demo environment that mimics interaction with devices.
"""
import time
@ -32,7 +32,13 @@ def setup(hass, config):
hass.states.set('a.Demo_Mode', 'Enabled')
# Setup sun
loader.get_component('sun').setup(hass, config)
if not hass.config.latitude:
hass.config.latitude = '32.87336'
if not hass.config.longitude:
hass.config.longitude = '117.22743'
bootstrap.setup_component(hass, 'sun')
# Setup demo platforms
for component in COMPONENTS_WITH_DEMO_PLATFORM:

View File

@ -6,8 +6,9 @@ Provides functionality to turn on lights based on
the state of the sun and devices.
"""
import logging
from datetime import datetime, timedelta
from datetime import timedelta
import homeassistant.util.dt as dt_util
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from . import light, sun, device_tracker, group
@ -115,7 +116,7 @@ def setup(hass, config):
new_state.state == STATE_HOME:
# These variables are needed for the elif check
now = datetime.now()
now = dt_util.now()
start_point = calc_time_for_light_when_sunset()
# Do we need lights?

View File

@ -8,11 +8,12 @@ import logging
import threading
import os
import csv
from datetime import datetime, timedelta
from datetime import timedelta
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config
import homeassistant.util as util
import homeassistant.util.dt as dt_util
from homeassistant.const import (
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
@ -113,7 +114,7 @@ class DeviceTracker(object):
""" Reload known devices file. """
self._read_known_devices_file()
self.update_devices(datetime.now())
self.update_devices(dt_util.utcnow())
dev_group.update_tracked_entity_ids(self.device_entity_ids)
@ -125,7 +126,7 @@ class DeviceTracker(object):
seconds = range(0, 60, seconds)
_LOGGER.info("Device tracker interval second=%s", seconds)
hass.track_time_change(update_device_state, second=seconds)
hass.track_utc_time_change(update_device_state, second=seconds)
hass.services.register(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD,
@ -226,7 +227,7 @@ class DeviceTracker(object):
self.untracked_devices.clear()
with open(known_dev_path) as inp:
default_last_seen = datetime(1990, 1, 1)
default_last_seen = dt_util.utcnow().replace(year=1990)
# To track which devices need an entity_id assigned
need_entity_id = []

View File

@ -1,4 +1,35 @@
""" Supports scanning a DD-WRT router. """
"""
homeassistant.components.device_tracker.ddwrt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a DD-WRT router for device
presence.
Configuration:
To use the DD-WRT tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: ddwrt
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
@ -20,7 +51,7 @@ _DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a DdWrt scanner. """
""" Validates config and returns a DD-WRT scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
@ -93,7 +124,7 @@ class DdWrtDeviceScanner(object):
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the DdWrt router is up to date.
""" Ensures the information from the DD-WRT router is up to date.
Returns boolean if scanning successful. """
if not self.success_init:
return False
@ -111,8 +142,8 @@ class DdWrtDeviceScanner(object):
self.last_results = []
active_clients = data.get('active_wireless', None)
if active_clients:
# This is really lame, instead of using JSON the ddwrt UI
# uses it's own data format for some reason and then
# This is really lame, instead of using JSON the DD-WRT UI
# uses its own data format for some reason and then
# regex's out values so I guess I have to do the same,
# LAME!!!
@ -132,7 +163,7 @@ class DdWrtDeviceScanner(object):
return False
def get_ddwrt_data(self, url):
""" Retrieve data from DD-WRT and return parsed result """
""" Retrieve data from DD-WRT and return parsed result. """
try:
response = requests.get(
url,
@ -154,8 +185,7 @@ class DdWrtDeviceScanner(object):
def _parse_ddwrt_response(data_str):
""" Parse the awful DD-WRT data format, why didn't they use JSON????.
This code is a python version of how they are parsing in the JS """
""" Parse the DD-WRT data format. """
return {
key: val for key, val in _DDWRT_DATA_REGEX
.findall(data_str)}

View File

@ -1,4 +1,39 @@
""" Supports scanning a OpenWRT router. """
"""
homeassistant.components.device_tracker.luci
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a OpenWRT router for device
presence.
It's required that the luci RPC package is installed on the OpenWRT router:
# opkg install luci-mod-rpc
Configuration:
To use the Luci tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: luci
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
import json
from datetime import timedelta

View File

@ -1,4 +1,35 @@
""" Supports scanning a Netgear router. """
"""
homeassistant.components.device_tracker.netgear
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a Netgear router for device
presence.
Configuration:
To use the Netgear tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: netgear
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import threading
@ -30,7 +61,7 @@ def get_scanner(hass, config):
class NetgearDeviceScanner(object):
""" This class queries a Netgear wireless router using the SOAP-api. """
""" This class queries a Netgear wireless router using the SOAP-API. """
def __init__(self, host, username, password):
self.last_results = []

View File

@ -1,6 +1,27 @@
""" Supports scanning using nmap. """
"""
homeassistant.components.device_tracker.nmap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a network with nmap.
Configuration:
To use the nmap tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: nmap_tracker
hosts: 192.168.1.1/24
Variables:
hosts
*Required
The IP addresses to scan in the network-prefix notation (192.168.1.1/24) or
the range notation (192.168.1.1-255).
"""
import logging
from datetime import timedelta, datetime
from datetime import timedelta
from collections import namedtuple
import subprocess
import re
@ -8,6 +29,7 @@ import re
from libnmap.process import NmapProcess
from libnmap.parser import NmapParser, NmapParserException
import homeassistant.util.dt as dt_util
from homeassistant.const import CONF_HOSTS
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle, convert
@ -36,7 +58,7 @@ Device = namedtuple("Device", ["mac", "name", "ip", "last_update"])
def _arp(ip_address):
""" Get the MAC address for a given IP """
""" Get the MAC address for a given IP. """
cmd = ['arp', '-n', ip_address]
arp = subprocess.Popen(cmd, stdout=subprocess.PIPE)
out, _ = arp.communicate()
@ -85,7 +107,7 @@ class NmapDeviceScanner(object):
Returns True if successful, False otherwise. """
try:
results = NmapParser.parse(stdout)
now = datetime.now()
now = dt_util.now()
self.last_results = []
for host in results.hosts:
if host.is_up():
@ -119,7 +141,7 @@ class NmapDeviceScanner(object):
options = "-F --host-timeout 5"
exclude_targets = set()
if self.home_interval:
now = datetime.now()
now = dt_util.now()
for host in self.last_results:
if host.last_update + self.home_interval > now:
exclude_targets.add(host)

View File

@ -1,4 +1,41 @@
""" Supports scanning a Tomato router. """
"""
homeassistant.components.device_tracker.tomato
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a Tomato router for device
presence.
Configuration:
To use the Tomato tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: tomato
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
http_id: ABCDEFG
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
http_id
*Required
The value can be obtained by logging in to the Tomato admin interface and
search for http_id in the page source code.
"""
import logging
import json
from datetime import timedelta

View File

@ -1,11 +1,13 @@
"""
homeassistant.components.discovery
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Starts a service to scan in intervals for new devices.
Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
Knows which components handle certain types, will make sure they are
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
"""
import logging
import threading

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "93774c7a1643c7e3f9cbbb1554b36683"
VERSION = "28c0680cf6ebd969dc5710c22d9c4075"

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@
"bower_components"
],
"dependencies": {
"webcomponentsjs": "Polymer/webcomponentsjs#~0.5.5",
"webcomponentsjs": "Polymer/webcomponentsjs#~0.6",
"font-roboto": "Polymer/font-roboto#~0.5.5",
"core-header-panel": "polymer/core-header-panel#~0.5.5",
"core-toolbar": "polymer/core-toolbar#~0.5.5",
@ -36,9 +36,12 @@
"paper-slider": "polymer/paper-slider#~0.5.5",
"paper-checkbox": "polymer/paper-checkbox#~0.5.5",
"color-picker-element": "~0.0.2",
"google-apis": "GoogleWebComponents/google-apis#~0.4.2",
"google-apis": "GoogleWebComponents/google-apis#~0.4.4",
"core-drawer-panel": "polymer/core-drawer-panel#~0.5.5",
"core-scroll-header-panel": "polymer/core-scroll-header-panel#~0.5.5",
"moment": "~2.9.0"
"moment": "~2.10.2"
},
"resolutions": {
"webcomponentsjs": "~0.6"
}
}

View File

@ -8,16 +8,17 @@
</template>
<script>
(function() {
var timeFormatOptions = {hour: 'numeric', minute: '2-digit'};
var uiUtil = window.hass.uiUtil;
Polymer({
time: "",
dateObjChanged: function(oldVal, newVal) {
if (!newVal) {
if (newVal) {
this.time = uiUtil.formatTime(newVal);
} else {
this.time = "";
}
this.time = newVal.toLocaleTimeString([], timeFormatOptions);
},
});
})();

View File

@ -2,7 +2,7 @@
<link rel="import" href="../resources/moment-js.html">
<polymer-element name="relative-ha-datetime" attributes="datetime">
<polymer-element name="relative-ha-datetime" attributes="datetime datetimeObj">
<template>
{{ relativeTime }}
</template>
@ -34,8 +34,15 @@
this.updateRelative();
},
datetimeObjChanged: function(oldVal, newVal) {
this.parsedDateTime = newVal;
this.updateRelative();
},
updateRelative: function() {
this.relativeTime = this.parsedDateTime ? moment(this.parsedDateTime).fromNow() : "";
this.relativeTime = this.parsedDateTime ?
moment(this.parsedDateTime).fromNow() : "";
},
});
})();

View File

@ -37,8 +37,8 @@
</div>
<div class="time-ago">
<core-tooltip label="{{stateObj.lastChanged}}" position="bottom">
<relative-ha-datetime datetime="{{stateObj.lastChanged}}"></relative-ha-datetime>
<core-tooltip label="{{stateObj.lastChangedAsDate | formatDateTime}}" position="bottom">
<relative-ha-datetime datetimeObj="{{stateObj.lastChangedAsDate}}"></relative-ha-datetime>
</core-tooltip>
</div>

View File

@ -11,7 +11,9 @@
<div>
<state-card-content stateObj="{{stateObj}}" style='margin-bottom: 24px;'>
</state-card-content>
<state-timeline stateHistory="{{stateHistory}}" isLoadingData="{{isLoadingHistoryData}}"></state-timeline>
<template if="{{hasHistoryComponent}}">
<state-timeline stateHistory="{{stateHistory}}" isLoadingData="{{isLoadingHistoryData}}"></state-timeline>
</template>
<more-info-content
stateObj="{{stateObj}}"
dialogOpen="{{dialogOpen}}"></more-info-content>

@ -1 +1 @@
Subproject commit 56f896efa573aaa9554812a3c41b78278bce2064
Subproject commit 69ee1c49af12caf00655c66d56474b5c1bcac1c1

View File

@ -11,38 +11,43 @@
<div layout justified horizontal class='data-entry' id='rising'>
<div class='key'>
Rising <relative-ha-datetime datetime="{{stateObj.attributes.next_rising}}"></relative-ha-datetime>
Rising <relative-ha-datetime datetimeObj="{{rising}}"></relative-ha-datetime>
</div>
<div class='value'>
{{stateObj.attributes.next_rising | HATimeStripDate}}
{{rising | formatTime}}
</div>
</div>
<div layout justified horizontal class='data-entry' id='setting'>
<div class='key'>
Setting <relative-ha-datetime datetime="{{stateObj.attributes.next_setting}}"></relative-ha-datetime>
Setting <relative-ha-datetime datetimeObj="{{setting}}"></relative-ha-datetime>
</div>
<div class='value'>
{{stateObj.attributes.next_setting | HATimeStripDate}}
{{setting | formatTime}}
</div>
</div>
</div>
</template>
<script>
(function() {
var parseDateTime = window.hass.util.parseDateTime;
Polymer({
stateObjChanged: function() {
var rising = parseDateTime(this.stateObj.attributes.next_rising);
var setting = parseDateTime(this.stateObj.attributes.next_setting);
rising: null,
setting: null,
if(rising > setting) {
stateObjChanged: function() {
this.rising = parseDateTime(this.stateObj.attributes.next_rising);
this.setting = parseDateTime(this.stateObj.attributes.next_setting);
if(self.rising > self.setting) {
this.$.sunData.appendChild(this.$.rising);
} else {
this.$.sunData.appendChild(this.$.setting);
}
}
});
})();
</script>
</polymer-element>

View File

@ -7,18 +7,6 @@
'light', 'group', 'sun', 'configurator', 'thermostat', 'script'
];
// Register some polymer filters
PolymerExpressions.prototype.HATimeToDate = function(timeString) {
if (!timeString) return;
return window.hass.util.parseDateTime(timeString);
};
PolymerExpressions.prototype.HATimeStripDate = function(timeString) {
return (timeString || "").split(' ')[0];
};
// Add some frontend specific helpers to the models
Object.defineProperties(window.hass.stateModel.prototype, {
// how to render the card for this state
@ -81,3 +69,5 @@
window.hass.uiUtil = {};
})();
</script>
<link rel="import" href="./moment-js.html">

View File

@ -3,3 +3,22 @@
-->
<script src="../bower_components/moment/moment.js"></script>
<script>
window.hass.uiUtil.formatTime = function(dateObj) {
return moment(dateObj).format('LT');
};
window.hass.uiUtil.formatDateTime = function(dateObj) {
return moment(dateObj).format('lll');
};
window.hass.uiUtil.formatDate = function(dateObj) {
return moment(dateObj).format('ll');
};
PolymerExpressions.prototype.formatTime = window.hass.uiUtil.formatTime;
PolymerExpressions.prototype.formatDateTime = window.hass.uiUtil.formatDateTime;
PolymerExpressions.prototype.formatDate = window.hass.uiUtil.formatDate;
</script>

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
"""
homeassistant.components.groups
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
homeassistant.components.group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to group devices that can be turned on or off.
"""

View File

@ -5,10 +5,11 @@ homeassistant.components.history
Provide pre-made queries on top of the recorder component.
"""
import re
from datetime import datetime, timedelta
from datetime import timedelta
from itertools import groupby
from collections import defaultdict
import homeassistant.util.dt as date_util
import homeassistant.components.recorder as recorder
DOMAIN = 'history'
@ -22,7 +23,7 @@ def last_5_states(entity_id):
query = """
SELECT * FROM states WHERE entity_id=? AND
last_changed=last_updated
ORDER BY last_changed DESC LIMIT 0, 5
ORDER BY state_id DESC LIMIT 0, 5
"""
return recorder.query_states(query, (entity_id, ))
@ -30,7 +31,7 @@ def last_5_states(entity_id):
def state_changes_during_period(start_time, end_time=None, entity_id=None):
"""
Return states changes during period start_time - end_time.
Return states changes during UTC period start_time - end_time.
"""
where = "last_changed=last_updated AND last_changed > ? "
data = [start_time]
@ -64,17 +65,17 @@ def state_changes_during_period(start_time, end_time=None, entity_id=None):
return result
def get_states(point_in_time, entity_ids=None, run=None):
def get_states(utc_point_in_time, entity_ids=None, run=None):
""" Returns the states at a specific point in time. """
if run is None:
run = recorder.run_information(point_in_time)
run = recorder.run_information(utc_point_in_time)
# History did not run before point_in_time
# History did not run before utc_point_in_time
if run is None:
return []
where = run.where_after_start_run + "AND created < ? "
where_data = [point_in_time]
where_data = [utc_point_in_time]
if entity_ids is not None:
where += "AND entity_id IN ({}) ".format(
@ -93,9 +94,9 @@ def get_states(point_in_time, entity_ids=None, run=None):
return recorder.query_states(query, where_data)
def get_state(point_in_time, entity_id, run=None):
def get_state(utc_point_in_time, entity_id, run=None):
""" Return a state at a specific point in time. """
states = get_states(point_in_time, (entity_id,), run)
states = get_states(utc_point_in_time, (entity_id,), run)
return states[0] if states else None
@ -128,7 +129,7 @@ def _api_last_5_states(handler, path_match, data):
def _api_history_period(handler, path_match, data):
""" Return history over a period of time. """
# 1 day for now..
start_time = datetime.now() - timedelta(seconds=86400)
start_time = date_util.utcnow() - timedelta(seconds=86400)
entity_id = data.get('filter_entity_id')

View File

@ -1,6 +1,6 @@
"""
homeassistant.components.httpinterface
~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module provides an API and a HTTP interface for debug purposes.

View File

@ -1,4 +1,7 @@
"""
homeassistant.components.isy994
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Connects to an ISY-994 controller and loads relevant components to control its
devices. Also contains the base classes for ISY Sensors, Lights, and Switches.
"""
@ -13,7 +16,8 @@ from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, EVENT_PLATFORM_DISCOVERED,
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
EVENT_HOMEASSISTANT_STOP, ATTR_SERVICE, ATTR_DISCOVERED,
ATTR_FRIENDLY_NAME)
# homeassistant constants
DOMAIN = "isy994"
@ -31,7 +35,7 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""
Setup isy994 component.
Setup ISY994 component.
This will automatically import associated lights, switches, and sensors.
"""
try:
@ -77,6 +81,9 @@ def setup(hass, config):
if not ISY.connected:
return False
# listen for HA stop to disconnect
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
# Load components for the devices in the ISY controller that we support
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
('light', DISCOVER_LIGHTS),
@ -91,8 +98,13 @@ def setup(hass, config):
return True
def stop(event):
""" Cleanup the ISY subscription. """
ISY.auto_update = False
class ISYDeviceABC(ToggleEntity):
""" Abstract Class for an ISY device within home assistant. """
""" Abstract Class for an ISY device. """
_attrs = {}
_onattrs = []
@ -133,7 +145,7 @@ class ISYDeviceABC(ToggleEntity):
@property
def value(self):
""" returns the unclean value from the controller """
""" Returns the unclean value from the controller. """
# pylint: disable=protected-access
return self.node.status._val
@ -147,7 +159,7 @@ class ISYDeviceABC(ToggleEntity):
@property
def unique_id(self):
""" Returns the id of this isy sensor """
""" Returns the id of this ISY sensor. """
# pylint: disable=protected-access
return self.node._id
@ -190,7 +202,7 @@ class ISYDeviceABC(ToggleEntity):
return self.value
def turn_on(self, **kwargs):
""" turns the device on """
""" Turns the device on. """
if self.domain is not 'sensor':
attrs = [kwargs.get(name) for name in self._onattrs]
self.node.on(*attrs)
@ -198,7 +210,7 @@ class ISYDeviceABC(ToggleEntity):
_LOGGER.error('ISY cannot turn on sensors.')
def turn_off(self, **kwargs):
""" turns the device off """
""" Turns the device off. """
if self.domain is not 'sensor':
self.node.off()
else:

View File

@ -1,5 +1,5 @@
"""
homeassistant.keyboard
homeassistant.components.keyboard
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to emulate keyboard presses on host machine.

View File

@ -1,4 +1,10 @@
""" Provides demo lights. """
"""
homeassistant.components.light.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform that implements lights.
"""
import random
from homeassistant.helpers.entity import ToggleEntity

View File

@ -1,17 +1,16 @@
"""
homeassistant.components.logbook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Parses events and generates a human log
Parses events and generates a human log.
"""
from datetime import datetime
from itertools import groupby
from homeassistant import State, DOMAIN as HA_DOMAIN
from homeassistant.const import (
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.util as util
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
import homeassistant.components.sun as sun
@ -38,10 +37,11 @@ def setup(hass, config):
def _handle_get_logbook(handler, path_match, data):
""" Return logbook entries. """
start_today = datetime.now().date()
start_today = dt_util.now().replace(hour=0, minute=0, second=0)
handler.write_json(humanify(
recorder.query_events(QUERY_EVENTS_AFTER, (start_today,))))
recorder.query_events(
QUERY_EVENTS_AFTER, (dt_util.as_utc(start_today),))))
class Entry(object):
@ -60,7 +60,7 @@ class Entry(object):
def as_dict(self):
""" Convert Entry to a dict to be used within JSON. """
return {
'when': util.datetime_to_str(self.when),
'when': dt_util.datetime_to_str(self.when),
'name': self.name,
'message': self.message,
'domain': self.domain,

View File

@ -1,10 +1,10 @@
"""
homeassistant.components.media_player.chromecast
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
homeassistant.components.media_player.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo implementation of the media player.
"""
"""
from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_DURATION,

View File

@ -1,9 +1,12 @@
"""
components.modbus
~~~~~~~~~~~~~~~~~~~~~~~~~
homeassistant.components.modbus
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Modbus component, using pymodbus (python3 branch)
typical declaration in configuration.yaml
Configuration:
To use the Modbus component you will need to add something like the following
to your config/configuration.yaml
#Modbus TCP
modbus:
@ -27,10 +30,8 @@ import logging
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
# The domain of your component. Should be equal to the name of your component
DOMAIN = "modbus"
# List of component names (string) your component depends upon
DEPENDENCIES = []
# Type of network
@ -86,11 +87,11 @@ def setup(hass, config):
return False
def stop_modbus(event):
""" Stop Modbus service"""
""" Stop Modbus service. """
NETWORK.close()
def start_modbus(event):
""" Start Modbus service"""
""" Start Modbus service. """
NETWORK.connect()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)

View File

@ -1,6 +1,6 @@
"""
homeassistant.components.notify
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to notify people.
"""
@ -73,7 +73,7 @@ def setup(hass, config):
# pylint: disable=too-few-public-methods
class BaseNotificationService(object):
""" Provides an ABC for notifcation services. """
""" Provides an ABC for notification services. """
def send_message(self, message, **kwargs):
"""

View File

@ -0,0 +1,161 @@
"""
homeassistant.components.notify.instapush
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Instapush notification service.
Configuration:
To use the Instapush notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: instapush
api_key: YOUR_APP_KEY
app_secret: YOUR_APP_SECRET
event: YOUR_EVENT
tracker: YOUR_TRACKER
VARIABLES:
api_key
*Required
To retrieve this value log into your account at https://instapush.im and go
to 'APPS', choose an app, and check 'Basic Info'.
app_secret
*Required
To get this value log into your account at https://instapush.im and go to
'APPS'. The 'Application ID' can be found under 'Basic Info'.
event
*Required
To retrieve a valid event log into your account at https://instapush.im and go
to 'APPS'. If you have no events to use with Home Assistant, create one event
for your app.
tracker
*Required
To retrieve the tracker value log into your account at https://instapush.im and
go to 'APPS', choose the app, and check the event entries.
Example usage of Instapush if you have an event 'notification' and a tracker
'home-assistant'.
curl -X POST \
-H "x-instapush-appid: YOUR_APP_KEY" \
-H "x-instapush-appsecret: YOUR_APP_SECRET" \
-H "Content-Type: application/json" \
-d '{"event":"notification","trackers":{"home-assistant":"Switch 1"}}' \
https://api.instapush.im/v1/post
Details for the API : https://instapush.im/developer/rest
"""
import logging
import json
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'https://api.instapush.im/v1/'
def get_service(hass, config):
""" Get the instapush notification service. """
if not validate_config(config,
{DOMAIN: [CONF_API_KEY,
'app_secret',
'event',
'tracker']},
_LOGGER):
return None
try:
import requests
except ImportError:
_LOGGER.exception(
"Unable to import requests. "
"Did you maybe not install the 'Requests' package?")
return None
# pylint: disable=unused-variable
try:
response = requests.get(_RESOURCE)
except requests.ConnectionError:
_LOGGER.error(
"Connection error "
"Please check if https://instapush.im is available.")
return None
instapush = requests.Session()
headers = {'x-instapush-appid': config[DOMAIN][CONF_API_KEY],
'x-instapush-appsecret': config[DOMAIN]['app_secret']}
response = instapush.get(_RESOURCE + 'events/list',
headers=headers)
try:
if response.json()['error']:
_LOGGER.error(response.json()['msg'])
# pylint: disable=bare-except
except:
try:
next(events for events in response.json()
if events['title'] == config[DOMAIN]['event'])
except StopIteration:
_LOGGER.error(
"No event match your given value. "
"Please create an event at https://instapush.im")
else:
return InstapushNotificationService(
config[DOMAIN].get(CONF_API_KEY),
config[DOMAIN]['app_secret'],
config[DOMAIN]['event'],
config[DOMAIN]['tracker']
)
# pylint: disable=too-few-public-methods
class InstapushNotificationService(BaseNotificationService):
""" Implements notification service for Instapush. """
def __init__(self, api_key, app_secret, event, tracker):
# pylint: disable=no-name-in-module, unused-variable
from requests import Session
self._api_key = api_key
self._app_secret = app_secret
self._event = event
self._tracker = tracker
self._headers = {
'x-instapush-appid': self._api_key,
'x-instapush-appsecret': self._app_secret,
'Content-Type': 'application/json'}
self.instapush = Session()
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
title = kwargs.get(ATTR_TITLE)
data = {"event": self._event,
"trackers": {self._tracker: title + " : " + message}}
response = self.instapush.post(
_RESOURCE + 'post',
data=json.dumps(data),
headers=self._headers)
if response.json()['status'] == 401:
_LOGGER.error(
response.json()['msg'],
"Please check your details at https://instapush.im/")

View File

@ -0,0 +1,97 @@
"""
homeassistant.components.notify.nma
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
NMA (Notify My Android) notification service.
Configuration:
To use the NMA notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: nma
api_key: YOUR_API_KEY
VARIABLES:
api_key
*Required
Enter the API key for NMA. Go to https://www.notifymyandroid.com and create a
new API key to use with Home Assistant.
Details for the API : https://www.notifymyandroid.com/api.jsp
"""
import logging
import xml.etree.ElementTree as ET
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'https://www.notifymyandroid.com/publicapi/'
def get_service(hass, config):
""" Get the NMA notification service. """
if not validate_config(config,
{DOMAIN: [CONF_API_KEY]},
_LOGGER):
return None
try:
# pylint: disable=unused-variable
from requests import Session
except ImportError:
_LOGGER.exception(
"Unable to import requests. "
"Did you maybe not install the 'Requests' package?")
return None
nma = Session()
response = nma.get(_RESOURCE + 'verify',
params={"apikey": config[DOMAIN][CONF_API_KEY]})
tree = ET.fromstring(response.content)
if tree[0].tag == 'error':
_LOGGER.error("Wrong API key supplied. %s", tree[0].text)
else:
return NmaNotificationService(config[DOMAIN][CONF_API_KEY])
# pylint: disable=too-few-public-methods
class NmaNotificationService(BaseNotificationService):
""" Implements notification service for NMA. """
def __init__(self, api_key):
# pylint: disable=no-name-in-module, unused-variable
from requests import Session
self._api_key = api_key
self._data = {"apikey": self._api_key}
self.nma = Session()
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
title = kwargs.get(ATTR_TITLE)
self._data['application'] = 'home-assistant'
self._data['event'] = title
self._data['description'] = message
self._data['priority'] = 0
response = self.nma.get(_RESOURCE + 'notify',
params=self._data)
tree = ET.fromstring(response.content)
if tree[0].tag == 'error':
_LOGGER.exception(
"Unable to perform request. Error: %s", tree[0].text)

View File

@ -1,5 +1,24 @@
"""
homeassistant.components.notify.pushbullet
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PushBullet platform for notify component.
Configuration:
To use the PushBullet notifier you will need to add something like the
following to your config/configuration.yaml
notify:
platform: pushbullet
api_key: YOUR_API_KEY
Variables:
api_key
*Required
Enter the API key for PushBullet. Go to https://www.pushbullet.com/ to retrieve
your API key.
"""
import logging

View File

@ -1,4 +1,7 @@
"""
homeassistant.components.notify.pushover
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Pushover platform for notify component.
Configuration:
@ -11,7 +14,7 @@ notify:
api_key: ABCDEFGHJKLMNOPQRSTUVXYZ
user_key: ABCDEFGHJKLMNOPQRSTUVXYZ
VARIABLES:
Variables:
api_key
*Required

View File

@ -0,0 +1,127 @@
"""
homeassistant.components.notify.xmpp
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Jabber (XMPP) notification service.
Configuration:
To use the Jabber notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: xmpp
sender: YOUR_JID
password: YOUR_JABBER_ACCOUNT_PASSWORD
recipient: YOUR_RECIPIENT
Variables:
sender
*Required
The Jabber ID (JID) that will act as origin of the messages. Add your JID
including the domain, e.g. your_name@jabber.org.
password
*Required
The password for your given Jabber account.
recipient
*Required
The Jabber ID (JID) that will receive the messages.
"""
import logging
_LOGGER = logging.getLogger(__name__)
try:
import sleekxmpp
except ImportError:
_LOGGER.exception(
"Unable to import sleekxmpp. "
"Did you maybe not install the 'SleekXMPP' package?")
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
def get_service(hass, config):
""" Get the Jabber (XMPP) notification service. """
if not validate_config(config,
{DOMAIN: ['sender',
'password',
'recipient']},
_LOGGER):
return None
try:
SendNotificationBot(config[DOMAIN]['sender'] + '/home-assistant',
config[DOMAIN]['password'],
config[DOMAIN]['recipient'],
'')
except ImportError:
_LOGGER.exception(
"Unable to contact jabber server."
"Please check your credentials.")
return None
return XmppNotificationService(config[DOMAIN]['sender'],
config[DOMAIN]['password'],
config[DOMAIN]['recipient'])
# pylint: disable=too-few-public-methods
class XmppNotificationService(BaseNotificationService):
""" Implements notification service for Jabber (XMPP). """
def __init__(self, sender, password, recipient):
self._sender = sender
self._password = password
self._recipient = recipient
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
title = kwargs.get(ATTR_TITLE)
data = title + ": " + message
SendNotificationBot(self._sender + '/home-assistant',
self._password,
self._recipient,
data)
class SendNotificationBot(sleekxmpp.ClientXMPP):
""" Service for sending Jabber (XMPP) messages. """
def __init__(self, jid, password, recipient, msg):
super(SendNotificationBot, self).__init__(jid, password)
logging.basicConfig(level=logging.ERROR)
self.recipient = recipient
self.msg = msg
self.use_tls = True
self.use_ipv6 = False
self.add_event_handler('failed_auth', self.check_credentials)
self.add_event_handler('session_start', self.start)
self.connect()
self.process(block=False)
def start(self, event):
""" Starts the communication and sends the message. """
self.send_presence()
self.get_roster()
self.send_message(mto=self.recipient, mbody=self.msg, mtype='chat')
self.disconnect(wait=True)
def check_credentials(self, event):
"""" Disconnect from the server if credentials are invalid. """
self.disconnect()

View File

@ -10,11 +10,11 @@ import threading
import queue
import sqlite3
from datetime import datetime, date
import time
import json
import atexit
from homeassistant import Event, EventOrigin, State
import homeassistant.util.dt as date_util
from homeassistant.remote import JSONEncoder
from homeassistant.const import (
MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
@ -60,8 +60,9 @@ def row_to_state(row):
""" Convert a databsae row to a state. """
try:
return State(
row[1], row[2], json.loads(row[3]), datetime.fromtimestamp(row[4]),
datetime.fromtimestamp(row[5]))
row[1], row[2], json.loads(row[3]),
date_util.utc_from_timestamp(row[4]),
date_util.utc_from_timestamp(row[5]))
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to state: %s", row)
@ -72,7 +73,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].lower()],
datetime.fromtimestamp(row[5]))
date_util.utc_from_timestamp(row[5]))
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to event: %s", row)
@ -113,10 +114,10 @@ class RecorderRun(object):
self.start = _INSTANCE.recording_start
self.closed_incorrect = False
else:
self.start = datetime.fromtimestamp(row[1])
self.start = date_util.utc_from_timestamp(row[1])
if row[2] is not None:
self.end = datetime.fromtimestamp(row[2])
self.end = date_util.utc_from_timestamp(row[2])
self.closed_incorrect = bool(row[3])
@ -166,7 +167,8 @@ class Recorder(threading.Thread):
self.queue = queue.Queue()
self.quit_object = object()
self.lock = threading.Lock()
self.recording_start = datetime.now()
self.recording_start = date_util.utcnow()
self.utc_offset = date_util.now().utcoffset().total_seconds()
def start_recording(event):
""" Start recording. """
@ -187,16 +189,21 @@ class Recorder(threading.Thread):
if event == self.quit_object:
self._close_run()
self._close_connection()
self.queue.task_done()
return
elif event.event_type == EVENT_TIME_CHANGED:
self.queue.task_done()
continue
elif event.event_type == EVENT_STATE_CHANGED:
self.record_state(
event.data['entity_id'], event.data.get('new_state'))
event_id = self.record_event(event)
self.record_event(event)
if event.event_type == EVENT_STATE_CHANGED:
self.record_state(
event.data['entity_id'], event.data.get('new_state'),
event_id)
self.queue.task_done()
def event_listener(self, event):
""" Listens for new events on the EventBus and puts them
@ -207,33 +214,43 @@ class Recorder(threading.Thread):
""" Tells the recorder to shut down. """
self.queue.put(self.quit_object)
def record_state(self, entity_id, state):
def record_state(self, entity_id, state, event_id):
""" Save a state to the database. """
now = datetime.now()
now = date_util.utcnow()
# State got deleted
if state is None:
info = (entity_id, '', "{}", now, now, now)
state_state = ''
state_attr = '{}'
last_changed = last_updated = now
else:
info = (
entity_id.lower(), state.state, json.dumps(state.attributes),
state.last_changed, state.last_updated, now)
state_state = state.state
state_attr = json.dumps(state.attributes)
last_changed = state.last_changed
last_updated = state.last_updated
info = (
entity_id, state_state, state_attr, last_changed, last_updated,
now, self.utc_offset, event_id)
self.query(
"INSERT INTO states ("
"entity_id, state, attributes, last_changed, last_updated,"
"created) VALUES (?, ?, ?, ?, ?, ?)", info)
"created, utc_offset, event_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
info)
def record_event(self, event):
""" Save an event to the database. """
info = (
event.event_type, json.dumps(event.data, cls=JSONEncoder),
str(event.origin), datetime.now(), event.time_fired,
str(event.origin), date_util.utcnow(), event.time_fired,
self.utc_offset
)
self.query(
return self.query(
"INSERT INTO events ("
"event_type, event_data, origin, created, time_fired"
") VALUES (?, ?, ?, ?, ?)", info)
"event_type, event_data, origin, created, time_fired, utc_offset"
") VALUES (?, ?, ?, ?, ?, ?)", info, RETURN_LASTROWID)
def query(self, sql_query, data=None, return_value=None):
""" Query the database. """
@ -262,6 +279,10 @@ class Recorder(threading.Thread):
"Error querying the database using: %s", sql_query)
return []
def block_till_done(self):
""" Blocks till all events processed. """
self.queue.join()
def _setup_connection(self):
""" Ensure database is ready to fly. """
db_path = self.hass.config.path(DB_FILE)
@ -282,7 +303,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, datetime.now()))
(migration_id, date_util.utcnow()))
self.conn.commit()
_LOGGER.info("Database migrated to version %d", migration_id)
@ -297,7 +318,7 @@ class Recorder(threading.Thread):
migration_id = 0
if migration_id < 1:
cur.execute("""
self.query("""
CREATE TABLE recorder_runs (
run_id integer primary key,
start integer,
@ -306,7 +327,7 @@ class Recorder(threading.Thread):
created integer)
""")
cur.execute("""
self.query("""
CREATE TABLE events (
event_id integer primary key,
event_type text,
@ -314,10 +335,10 @@ class Recorder(threading.Thread):
origin text,
created integer)
""")
cur.execute(
self.query(
'CREATE INDEX events__event_type ON events(event_type)')
cur.execute("""
self.query("""
CREATE TABLE states (
state_id integer primary key,
entity_id text,
@ -327,20 +348,57 @@ class Recorder(threading.Thread):
last_updated integer,
created integer)
""")
cur.execute('CREATE INDEX states__entity_id ON states(entity_id)')
self.query('CREATE INDEX states__entity_id ON states(entity_id)')
save_migration(1)
if migration_id < 2:
cur.execute("""
self.query("""
ALTER TABLE events
ADD COLUMN time_fired integer
""")
cur.execute('UPDATE events SET time_fired=created')
self.query('UPDATE events SET time_fired=created')
save_migration(2)
if migration_id < 3:
utc_offset = self.utc_offset
self.query("""
ALTER TABLE recorder_runs
ADD COLUMN utc_offset integer
""")
self.query("""
ALTER TABLE events
ADD COLUMN utc_offset integer
""")
self.query("""
ALTER TABLE states
ADD COLUMN utc_offset integer
""")
self.query("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
self.query("UPDATE events SET utc_offset=?", [utc_offset])
self.query("UPDATE states SET utc_offset=?", [utc_offset])
save_migration(3)
if migration_id < 4:
# We had a bug where we did not save utc offset for recorder runs
self.query(
"""UPDATE recorder_runs SET utc_offset=?
WHERE utc_offset IS NULL""", [self.utc_offset])
self.query("""
ALTER TABLE states
ADD COLUMN event_id integer
""")
save_migration(4)
def _close_connection(self):
""" Close connection to the database. """
_LOGGER.info("Closing database")
@ -356,19 +414,20 @@ class Recorder(threading.Thread):
_LOGGER.warning("Found unfinished sessions")
self.query(
"INSERT INTO recorder_runs (start, created) VALUES (?, ?)",
(self.recording_start, datetime.now()))
"""INSERT INTO recorder_runs (start, created, utc_offset)
VALUES (?, ?, ?)""",
(self.recording_start, date_util.utcnow(), self.utc_offset))
def _close_run(self):
""" Save end time for current run. """
self.query(
"UPDATE recorder_runs SET end=? WHERE start=?",
(datetime.now(), self.recording_start))
(date_util.utcnow(), self.recording_start))
def _adapt_datetime(datetimestamp):
""" Turn a datetime into an integer for in the DB. """
return time.mktime(datetimestamp.timetuple())
return date_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp()
def _verify_instance():

View File

@ -1,19 +1,19 @@
"""
homeassistant.components.scheduler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A component that will act as a scheduler and performe actions based
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A component that will act as a scheduler and perform actions based
on the events in the schedule.
It will read a json object from schedule.json in the config dir
and create a schedule based on it.
Each schedule is a JSON with the keys id, name, description,
entity_ids, and events.
- days is an array with the weekday number (monday=0) that the schdule
- days is an array with the weekday number (monday=0) that the schedule
is active
- entity_ids an array with entity ids that the events in the schedule should
effect (can also be groups)
- events is an array of objects that describe the different events that is
supported. Read in the events descriptions for more information
supported. Read in the events descriptions for more information.
"""
import logging
import json
@ -22,7 +22,6 @@ from homeassistant import bootstrap
from homeassistant.loader import get_component
from homeassistant.const import ATTR_ENTITY_ID
# The domain of your component. Should be equal to the name of your component
DOMAIN = 'scheduler'
DEPENDENCIES = []
@ -33,10 +32,10 @@ _SCHEDULE_FILE = 'schedule.json'
def setup(hass, config):
""" Create the schedules """
""" Create the schedules. """
def setup_listener(schedule, event_data):
""" Creates the event listener based on event_data """
""" Creates the event listener based on event_data. """
event_type = event_data['type']
component = event_type
@ -52,7 +51,7 @@ def setup(hass, config):
event_data)
def setup_schedule(schedule_data):
""" setup a schedule based on the description """
""" Setup a schedule based on the description. """
schedule = Schedule(schedule_data['id'],
name=schedule_data['name'],
@ -97,17 +96,17 @@ class Schedule(object):
self.__event_listeners = []
def add_event_listener(self, event_listener):
""" Add a event to the schedule """
""" Add a event to the schedule. """
self.__event_listeners.append(event_listener)
def schedule(self, hass):
""" Schedule all the events in the schdule """
""" Schedule all the events in the schedule. """
for event in self.__event_listeners:
event.schedule(hass)
class EventListener(object):
""" The base EventListner class that the schedule uses """
""" The base EventListener class that the schedule uses. """
def __init__(self, schedule):
self.my_schedule = schedule
@ -122,7 +121,7 @@ class EventListener(object):
# pylint: disable=too-few-public-methods
class ServiceEventListener(EventListener):
""" A EventListner that calls a service when executed """
""" A EventListener that calls a service when executed. """
def __init__(self, schdule, service):
EventListener.__init__(self, schdule)
@ -130,7 +129,7 @@ class ServiceEventListener(EventListener):
(self.domain, self.service) = service.split('.')
def execute(self, hass):
""" Call the service """
""" Call the service. """
data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids}
hass.call_service(self.domain, self.service, data)

View File

@ -1,4 +1,6 @@
"""
homeassistant.components.scheduler.time
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An event in the scheduler component that will call the service
every specified day at the time specified.
A time event need to have the type 'time', which service to call and at
@ -11,17 +13,17 @@ which time.
}
"""
from datetime import datetime, timedelta
from datetime import timedelta
import logging
import homeassistant.util.dt as dt_util
from homeassistant.components.scheduler import ServiceEventListener
_LOGGER = logging.getLogger(__name__)
def create_event_listener(schedule, event_listener_data):
""" Create a TimeEvent based on the description """
""" Create a TimeEvent based on the description. """
service = event_listener_data['service']
(hour, minute, second) = [int(x) for x in
@ -32,7 +34,7 @@ def create_event_listener(schedule, event_listener_data):
# pylint: disable=too-few-public-methods
class TimeEventListener(ServiceEventListener):
""" The time event that the scheduler uses """
""" The time event that the scheduler uses. """
# pylint: disable=too-many-arguments
def __init__(self, schedule, service, hour, minute, second):
@ -43,16 +45,14 @@ class TimeEventListener(ServiceEventListener):
self.second = second
def schedule(self, hass):
""" Schedule this event so that it will be called """
""" Schedule this event so that it will be called. """
next_time = datetime.now().replace(hour=self.hour,
minute=self.minute,
second=self.second,
microsecond=0)
next_time = dt_util.now().replace(
hour=self.hour, minute=self.minute, second=self.second)
# Calculate the next time the event should be executed.
# That is the next day that the schedule is configured to run
while next_time < datetime.now() or \
while next_time < dt_util.now() or \
next_time.weekday() not in self.my_schedule.days:
next_time = next_time + timedelta(days=1)

View File

@ -6,7 +6,8 @@ Scripts are a sequence of actions that can be triggered manually
by the user or automatically based upon automation events, etc.
"""
import logging
from datetime import datetime, timedelta
from datetime import timedelta
import homeassistant.util.dt as date_util
import threading
from homeassistant.util import split_entity_id
@ -109,7 +110,7 @@ class Script(object):
self._call_service(action)
elif CONF_DELAY in action:
delay = timedelta(**action[CONF_DELAY])
point_in_time = datetime.now() + delay
point_in_time = date_util.now() + delay
self.listener = self.hass.track_point_in_time(
self, point_in_time)
return False

View File

@ -1,4 +1,10 @@
""" Support for Wink sensors. """
"""
homeassistant.components.sensor.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform that has two fake sensors.
"""
from homeassistant.helpers.entity import Entity
from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL

View File

@ -1,8 +1,11 @@
""" Support for ISY994 sensors. """
# system imports
"""
homeassistant.components.sensor.isy994
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for ISY994 sensors.
"""
import logging
# homeassistant imports
from homeassistant.components.isy994 import (ISY, ISYDeviceABC, SENSOR_STRING,
HIDDEN_STRING)
from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_HOME,
@ -24,7 +27,7 @@ DEFAULT_HIDDEN_WEATHER = ['Temperature_High', 'Temperature_Low', 'Feels_Like',
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the isy994 platform. """
""" Sets up the ISY994 platform. """
# pylint: disable=protected-access
logger = logging.getLogger(__name__)
devs = []
@ -81,7 +84,7 @@ class WeatherPseudoNode(object):
class ISYSensorDevice(ISYDeviceABC):
""" represents a isy sensor within home assistant. """
""" Represents a ISY sensor. """
_domain = 'sensor'

View File

@ -0,0 +1,158 @@
"""
homeassistant.components.sensor.mysensors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for MySensors sensors.
Configuration:
To use the MySensors sensor you will need to add something like the
following to your config/configuration.yaml
sensor:
platform: mysensors
port: '/dev/ttyACM0'
Variables:
port
*Required
Port of your connection to your MySensors device.
"""
import logging
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.pymysensors.mysensors.mysensors as mysensors
import homeassistant.external.pymysensors.mysensors.const as const
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP,
TEMP_CELCIUS, TEMP_FAHRENHEIT,
STATE_ON, STATE_OFF)
CONF_PORT = "port"
CONF_DEBUG = "debug"
CONF_PERSISTENCE = "persistence"
ATTR_NODE_ID = "node_id"
ATTR_CHILD_ID = "child_id"
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Setup the mysensors platform. """
devices = {} # keep track of devices added to HA
# Just assume celcius means that the user wants metric for now.
# It may make more sense to make this a global config option in the future.
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
def sensor_update(update_type, nid):
""" Callback for sensor updates from the MySensors gateway. """
_LOGGER.info("sensor_update %s: node %s", update_type, nid)
sensor = gateway.sensors[nid]
if sensor.sketch_name is None:
return
if nid not in devices:
devices[nid] = {}
node = devices[nid]
new_devices = []
for child_id, child in sensor.children.items():
if child_id not in node:
node[child_id] = {}
for value_type, value in child.values.items():
if value_type not in node[child_id]:
name = '{} {}.{}'.format(sensor.sketch_name, nid, child.id)
node[child_id][value_type] = \
MySensorsNodeValue(
nid, child_id, name, value_type, is_metric)
new_devices.append(node[child_id][value_type])
else:
node[child_id][value_type].update_sensor(
value, sensor.battery_level)
if new_devices:
_LOGGER.info("adding new devices: %s", new_devices)
add_devices(new_devices)
port = config.get(CONF_PORT)
if port is None:
_LOGGER.error("Missing required key 'port'")
return False
persistence = config.get(CONF_PERSISTENCE, True)
gateway = mysensors.SerialGateway(port, sensor_update,
persistence=persistence)
gateway.metric = is_metric
gateway.debug = config.get(CONF_DEBUG, False)
gateway.start()
if persistence:
for nid in gateway.sensors:
sensor_update('sensor_update', nid)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: gateway.stop())
class MySensorsNodeValue(Entity):
""" Represents the value of a MySensors child node. """
# pylint: disable=too-many-arguments
def __init__(self, node_id, child_id, name, value_type, metric):
self._name = name
self.node_id = node_id
self.child_id = child_id
self.battery_level = 0
self.value_type = value_type
self.metric = metric
self._value = ''
@property
def should_poll(self):
""" MySensor gateway pushes its state to HA. """
return False
@property
def name(self):
""" The name of this sensor. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
return self._value
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity. """
if self.value_type == const.SetReq.V_TEMP:
return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT
elif self.value_type == const.SetReq.V_HUM or \
self.value_type == const.SetReq.V_DIMMER or \
self.value_type == const.SetReq.V_LIGHT_LEVEL:
return '%'
return None
@property
def state_attributes(self):
""" Returns the state attributes. """
return {
ATTR_NODE_ID: self.node_id,
ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
def update_sensor(self, value, battery_level):
""" Update a sensor with the latest value from the controller. """
_LOGGER.info("%s value = %s", self._name, value)
if self.value_type == const.SetReq.V_TRIPPED or \
self.value_type == const.SetReq.V_ARMED:
self._value = STATE_ON if int(value) == 1 else STATE_OFF
else:
self._value = value
self.battery_level = battery_level
self.update_ha_state()

View File

@ -0,0 +1,174 @@
"""
homeassistant.components.sensor.openweathermap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OpenWeatherMap (OWM) service.
Configuration:
To use the OpenWeatherMap sensor you will need to add something like the
following to your config/configuration.yaml
sensor:
platform: openweathermap
api_key: YOUR_APP_KEY
monitored_variables:
- type: 'weather'
- type: 'temperature'
- type: 'wind_speed'
- type: 'humidity'
- type: 'pressure'
- type: 'clouds'
- type: 'rain'
- type: 'snow'
VARIABLES:
api_key
*Required
To retrieve this value log into your account at http://openweathermap.org/
monitored_variables
*Required
An array specifying the variables to monitor.
These are the variables for the monitored_variables array:
type
*Required
The variable you wish to monitor, see the configuration example above for a
list of all available variables
Details for the API : http://bugs.openweathermap.org/projects/api/wiki
Only metric measurements are supported at the moment.
"""
import logging
from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT)
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_THROTTLED_REFRESH = None
SENSOR_TYPES = {
'weather': ['Condition', ''],
'temperature': ['Temperature', ''],
'wind_speed': ['Wind speed', 'm/s'],
'humidity': ['Humidity', '%'],
'pressure': ['Pressure', 'hPa'],
'clouds': ['Cloud coverage', '%'],
'rain': ['Rain', 'mm'],
'snow': ['Snow', 'mm']
}
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the OpenWeatherMap sensor. """
if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False
try:
from pyowm import OWM
except ImportError:
_LOGGER.exception(
"Unable to import pyowm. "
"Did you maybe not install the 'PyOWM' package?")
return None
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
unit = hass.config.temperature_unit
owm = OWM(config.get(CONF_API_KEY, None))
obs = owm.weather_at_coords(hass.config.latitude, hass.config.longitude)
if not owm:
_LOGGER.error(
"Connection error "
"Please check your settings for OpenWeatherMap.")
return None
dev = []
for variable in config['monitored_variables']:
if variable['type'] not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable['type'])
else:
dev.append(OpenWeatherMapSensor(variable['type'], obs, unit))
add_devices(dev)
# pylint: disable=too-few-public-methods
class OpenWeatherMapSensor(Entity):
""" Implements an OpenWeatherMap sensor. """
def __init__(self, sensor_type, weather_data, unit):
self.client_name = 'Weather - '
self._name = SENSOR_TYPES[sensor_type][0]
self.owa_client = weather_data
self._unit = unit
self.type = sensor_type
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self.update()
@property
def name(self):
return self.client_name + ' ' + self._name
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._unit_of_measurement
# pylint: disable=too-many-branches
def update(self):
""" Gets the latest data from OWM and updates the states. """
data = self.owa_client.get_weather()
if self.type == 'weather':
self._state = data.get_detailed_status()
if self.type == 'temperature':
if self._unit == TEMP_CELCIUS:
self._state = round(data.get_temperature('celsius')['temp'],
1)
elif self._unit == TEMP_FAHRENHEIT:
self._state = round(data.get_temperature('fahrenheit')['temp'],
1)
else:
self._state = round(data.get_temperature()['temp'], 1)
elif self.type == 'wind_speed':
self._state = data.get_wind()['speed']
elif self.type == 'humidity':
self._state = data.get_humidity()
elif self.type == 'pressure':
self._state = round(data.get_pressure()['press'], 0)
elif self.type == 'clouds':
self._state = data.get_clouds()
elif self.type == 'rain':
if data.get_rain():
self._state = round(data.get_rain()['3h'], 0)
else:
self._state = 'not raining'
self._unit_of_measurement = ''
elif self.type == 'snow':
if data.get_snow():
self._state = round(data.get_snow(), 0)
else:
self._state = 'not snowing'
self._unit_of_measurement = ''

View File

@ -1,6 +1,6 @@
"""
homeassistant.components.sensor.sabnzbd
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Monitors SABnzbd NZB client API
@ -22,14 +22,12 @@ sensor:
- type: 'disk_size'
- type: 'disk_free'
VARIABLES:
Variables:
base_url
*Required
This is the base URL of your SABnzbd instance including the port number if not
running on 80
Example: http://192.168.1.32:8124/
running on 80. Example: http://192.168.1.32:8124/
name
*Optional
@ -44,9 +42,7 @@ These are the variables for the monitored_variables array:
type
*Required
The variable you wish to monitor, see the configuration example above for a
list of all available variables
list of all available variables.
"""
from homeassistant.util import Throttle
@ -75,7 +71,7 @@ _THROTTLED_REFRESH = None
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the sensors """
""" Sets up the sensors. """
api_key = config.get("api_key")
base_url = config.get("base_url")
name = config.get("name", "SABnzbd")

View File

@ -1,9 +1,46 @@
"""
homeassistant.components.sensor.systemmonitor
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Shows system monitor values such as: disk, memory and processor use
Configuration:
To use the System monitor sensor you will need to add something like the
following to your config/configuration.yaml
sensor:
platform: systemmonitor
resources:
- type: 'disk_use_percent'
arg: '/'
- type: 'disk_use'
arg: '/home'
- type: 'disk_free'
arg: '/'
- type: 'memory_use_percent'
- type: 'memory_use'
- type: 'memory_free'
- type: 'processor_use'
- type: 'process'
arg: 'octave-cli'
Variables:
resources
*Required
An array specifying the variables to monitor.
These are the variables for the resources array:
type
*Required
The variable you wish to monitor, see the configuration example above for a
sample list of variables.
arg
*Optional
Additional details for the type, eg. path, binary name, etc.
"""
from homeassistant.helpers.entity import Entity
@ -28,7 +65,7 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the sensors """
""" Sets up the sensors. """
dev = []
for resource in config['resources']:
@ -43,7 +80,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class SystemMonitorSensor(Entity):
""" A system monitor sensor """
""" A system monitor sensor. """
def __init__(self, sensor_type, argument=''):
self._name = SENSOR_TYPES[sensor_type][0] + ' ' + argument

View File

@ -0,0 +1,112 @@
"""
homeassistant.components.sensor.time_date
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Date and Time service.
Configuration:
To use the Date and Time sensor you will need to add something like the
following to your config/configuration.yaml
sensor:
platform: time_date
display_options:
- type: 'time'
- type: 'date'
- type: 'date_time'
- type: 'time_date'
- type: 'time_utc'
- type: 'beat'
Variables:
display_options
*Required
An array specifying the variables to display.
These are the variables for the display_options array.:
type
*Required
The variable you wish to display, see the configuration example above for a
list of all available variables.
"""
import logging
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
OPTION_TYPES = {
'time': 'Time',
'date': 'Date',
'date_time': 'Date & Time',
'time_date': 'Time & Date',
'beat': 'Time (beat)',
'time_utc': 'Time (UTC)',
}
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the Time and Date sensor. """
if hass.config.time_zone is None:
_LOGGER.error("Timezone is not set in Home Assistant config")
return False
dev = []
for variable in config['display_options']:
if variable['type'] not in OPTION_TYPES:
_LOGGER.error('Option type: "%s" does not exist', variable['type'])
else:
dev.append(TimeDateSensor(variable['type']))
add_devices(dev)
# pylint: disable=too-few-public-methods
class TimeDateSensor(Entity):
""" Implements a Time and Date sensor. """
def __init__(self, option_type):
self._name = OPTION_TYPES[option_type]
self.type = option_type
self._state = 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
def update(self):
""" Gets the latest data and updates the states. """
time_date = dt_util.utcnow()
time = dt_util.datetime_to_short_time_str(dt_util.as_local(time_date))
time_utc = dt_util.datetime_to_short_time_str(time_date)
date = dt_util.datetime_to_short_date_str(dt_util.as_local(time_date))
# Calculate the beat (Swatch Internet Time) time without date.
hours, minutes, seconds = time_date.strftime('%H:%M:%S').split(':')
beat = ((int(seconds) + (int(minutes) * 60) + ((int(hours) + 1) *
3600)) / 86.4)
if self.type == 'time':
self._state = time
elif self.type == 'date':
self._state = date
elif self.type == 'date_time':
self._state = date + ', ' + time
elif self.type == 'time_date':
self._state = time + ', ' + date
elif self.type == 'time_utc':
self._state = time_utc
elif self.type == 'beat':
self._state = '{0:.2f}'.format(beat)

View File

@ -1,6 +1,6 @@
"""
homeassistant.components.sensor.transmission
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Monitors Transmission BitTorrent client API
@ -21,17 +21,15 @@ sensor:
- type: 'download_speed'
- type: 'upload_speed'
VARIABLES:
Variables:
host
*Required
This is the IP address of your Transmission Daemon
Example: 192.168.1.32
This is the IP address of your Transmission daemon. Example: 192.168.1.32
port
*Optional
The port your Transmission daemon uses, defaults to 9091
Example: 8080
The port your Transmission daemon uses, defaults to 9091. Example: 8080
username
*Required
@ -43,7 +41,7 @@ Your Transmission password
name
*Optional
The name to use when displaying this Transmission instance
The name to use when displaying this Transmission instance.
monitored_variables
*Required
@ -54,9 +52,7 @@ These are the variables for the monitored_variables array:
type
*Required
The variable you wish to monitor, see the configuration example above for a
list of all available variables
list of all available variables.
"""
from homeassistant.util import Throttle
@ -84,7 +80,7 @@ _THROTTLED_REFRESH = None
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the sensors """
""" Sets up the sensors. """
host = config.get(CONF_HOST)
username = config.get(CONF_USERNAME, None)
password = config.get(CONF_PASSWORD, None)
@ -123,7 +119,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class TransmissionSensor(Entity):
""" A Transmission sensor """
""" A Transmission sensor. """
def __init__(self, sensor_type, transmission_client, client_name):
self._name = SENSOR_TYPES[sensor_type][0]
@ -158,7 +154,7 @@ class TransmissionSensor(Entity):
)
def update(self):
""" Gets the latest from Transmission and updates the state. """
""" Gets the latest data from Transmission and updates the state. """
self.refresh_transmission_data()
if self.type == 'current_status':
if self.transmission_client.session:

View File

@ -1,7 +1,11 @@
"""
homeassistant.components.sensor.vera
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Vera sensors.
Configuration:
To use the Vera sensors you will need to add something like the following to
your config/configuration.yaml
@ -15,7 +19,7 @@ sensor:
13:
name: Another sensor
VARIABLES:
Variables:
vera_controller_url
*Required
@ -96,12 +100,12 @@ def get_devices(hass, config):
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Performs setup for Vera controller devices """
""" Performs setup for Vera controller devices. """
add_devices(get_devices(hass, config))
class VeraSensor(Entity):
""" Represents a Vera Sensor """
""" Represents a Vera Sensor. """
def __init__(self, vera_device, extra_data=None):
self.vera_device = vera_device

View File

@ -20,7 +20,7 @@ DEPENDENCIES = ['group', 'device_tracker', 'light']
CONF_KNOWN_LIGHT = "known_light"
# Attribute to tell which light has to flash whem an unknown person comes home
# If ommitted will flash all.
# If omitted will flash all.
CONF_UNKNOWN_LIGHT = "unknown_light"
# Services to test the alarms

View File

@ -22,10 +22,16 @@ which event (sunset or sunrise) and the offset.
"""
import logging
from datetime import datetime, timedelta
from datetime import timedelta
from homeassistant.util import str_to_datetime, datetime_to_str
try:
import ephem
except ImportError:
# Error will be raised during setup
ephem = None
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
from homeassistant.components.scheduler import ServiceEventListener
DEPENDENCIES = []
@ -49,13 +55,21 @@ def is_on(hass, entity_id=None):
def next_setting(hass, entity_id=None):
""" Returns the datetime object representing the next sun setting. """
""" Returns the local datetime object of the next sun setting. """
utc_next = next_setting_utc(hass, entity_id)
return dt_util.as_local(utc_next) if utc_next else None
def next_setting_utc(hass, entity_id=None):
""" Returns the UTC datetime object of the next sun setting. """
entity_id = entity_id or ENTITY_ID
state = hass.states.get(ENTITY_ID)
try:
return str_to_datetime(state.attributes[STATE_ATTR_NEXT_SETTING])
return dt_util.str_to_datetime(
state.attributes[STATE_ATTR_NEXT_SETTING])
except (AttributeError, KeyError):
# AttributeError if state is None
# KeyError if STATE_ATTR_NEXT_SETTING does not exist
@ -63,13 +77,21 @@ def next_setting(hass, entity_id=None):
def next_rising(hass, entity_id=None):
""" Returns the datetime object representing the next sun rising. """
""" Returns the local datetime object of the next sun rising. """
utc_next = next_rising_utc(hass, entity_id)
return dt_util.as_local(utc_next) if utc_next else None
def next_rising_utc(hass, entity_id=None):
""" Returns the UTC datetime object of the next sun rising. """
entity_id = entity_id or ENTITY_ID
state = hass.states.get(ENTITY_ID)
try:
return str_to_datetime(state.attributes[STATE_ATTR_NEXT_RISING])
return dt_util.str_to_datetime(
state.attributes[STATE_ATTR_NEXT_RISING])
except (AttributeError, KeyError):
# AttributeError if state is None
# KeyError if STATE_ATTR_NEXT_RISING does not exist
@ -80,78 +102,94 @@ def setup(hass, config):
""" Tracks the state of the sun. """
logger = logging.getLogger(__name__)
try:
import ephem
except ImportError:
if ephem is None:
logger.exception("Error while importing dependency ephem.")
return False
sun = ephem.Sun() # pylint: disable=no-member
latitude = str(hass.config.latitude)
longitude = str(hass.config.longitude)
# Validate latitude and longitude
observer = ephem.Observer()
errors = []
try:
observer.lat = latitude # pylint: disable=assigning-non-slot
except ValueError:
errors.append("invalid value for latitude given: {}".format(latitude))
try:
observer.long = longitude # pylint: disable=assigning-non-slot
except ValueError:
errors.append("invalid value for latitude given: {}".format(latitude))
if errors:
logger.error("Error setting up: %s", ", ".join(errors))
if None in (hass.config.latitude, hass.config.longitude):
logger.error("Latitude or longitude not set in Home Assistant config")
return False
def update_sun_state(now):
""" Method to update the current state of the sun and
set time of next setting and rising. """
utc_offset = datetime.utcnow() - datetime.now()
utc_now = now + utc_offset
try:
sun = Sun(hass, str(hass.config.latitude), str(hass.config.longitude))
except ValueError:
# Raised when invalid latitude or longitude is given to Observer
logger.exception("Invalid value for latitude or longitude")
return False
observer = ephem.Observer()
observer.lat = latitude # pylint: disable=assigning-non-slot
observer.long = longitude # pylint: disable=assigning-non-slot
next_rising_dt = ephem.localtime(
observer.next_rising(sun, start=utc_now))
next_setting_dt = ephem.localtime(
observer.next_setting(sun, start=utc_now))
if next_rising_dt > next_setting_dt:
new_state = STATE_ABOVE_HORIZON
next_change = next_setting_dt
else:
new_state = STATE_BELOW_HORIZON
next_change = next_rising_dt
logger.info("%s. Next change: %s",
new_state, next_change.strftime("%H:%M"))
state_attributes = {
STATE_ATTR_NEXT_RISING: datetime_to_str(next_rising_dt),
STATE_ATTR_NEXT_SETTING: datetime_to_str(next_setting_dt)
}
hass.states.set(ENTITY_ID, new_state, state_attributes)
# +1 second so Ephem will report it has set
hass.track_point_in_time(update_sun_state,
next_change + timedelta(seconds=1))
update_sun_state(datetime.now())
sun.point_in_time_listener(dt_util.utcnow())
return True
class Sun(Entity):
""" Represents the Sun. """
entity_id = ENTITY_ID
def __init__(self, hass, latitude, longitude):
self.hass = hass
self.observer = ephem.Observer()
# pylint: disable=assigning-non-slot
self.observer.lat = latitude
# pylint: disable=assigning-non-slot
self.observer.long = longitude
self._state = self.next_rising = self.next_setting = None
@property
def should_poll(self):
""" We trigger updates ourselves after sunset/sunrise """
return False
@property
def name(self):
return "Sun"
@property
def state(self):
if self.next_rising > self.next_setting:
return STATE_ABOVE_HORIZON
return STATE_BELOW_HORIZON
@property
def state_attributes(self):
return {
STATE_ATTR_NEXT_RISING: dt_util.datetime_to_str(self.next_rising),
STATE_ATTR_NEXT_SETTING: dt_util.datetime_to_str(self.next_setting)
}
@property
def next_change(self):
""" Returns the datetime when the next change to the state is. """
return min(self.next_rising, self.next_setting)
def update_as_of(self, utc_point_in_time):
""" Calculate sun state at a point in UTC time. """
sun = ephem.Sun() # pylint: disable=no-member
# pylint: disable=assigning-non-slot
self.observer.date = ephem.date(utc_point_in_time)
self.next_rising = self.observer.next_rising(
sun,
start=utc_point_in_time).datetime().replace(tzinfo=dt_util.UTC)
self.next_setting = self.observer.next_setting(
sun,
start=utc_point_in_time).datetime().replace(tzinfo=dt_util.UTC)
def point_in_time_listener(self, now):
""" Called when the state of the sun has changed. """
self.update_as_of(now)
self.update_ha_state()
# Schedule next update at next_change+1 second so sun state has changed
self.hass.track_point_in_utc_time(
self.point_in_time_listener,
self.next_change + timedelta(seconds=1))
def create_event_listener(schedule, event_listener_data):
""" Create a sun event listener based on the description. """
@ -195,7 +233,7 @@ class SunEventListener(ServiceEventListener):
else:
next_time = next_event + self.offset
while next_time < datetime.now() or \
while next_time < dt_util.now() or \
next_time.weekday() not in self.my_schedule.days:
next_time = next_time + timedelta(days=1)

View File

@ -1,4 +1,10 @@
""" Demo platform that has two fake switches. """
"""
homeassistant.components.switch.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform that has two fake switches.
"""
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME

View File

@ -1,8 +1,11 @@
""" Support for ISY994 switch. """
# system imports
"""
homeassistant.components.switch.isy994
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for ISY994 switches.
"""
import logging
# homeassistant imports
from homeassistant.components.isy994 import (ISY, ISYDeviceABC, SENSOR_STRING,
HIDDEN_STRING)
from homeassistant.const import STATE_ON, STATE_OFF # STATE_OPEN, STATE_CLOSED
@ -12,7 +15,7 @@ from homeassistant.const import STATE_ON, STATE_OFF # STATE_OPEN, STATE_CLOSED
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the isy994 platform. """
""" Sets up the ISY994 platform. """
# pylint: disable=too-many-locals
logger = logging.getLogger(__name__)
devs = []
@ -54,7 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class ISYSwitchDevice(ISYDeviceABC):
""" represents as isy light within home assistant. """
""" Represents as ISY light. """
_domain = 'switch'
_dtype = 'binary'
@ -62,7 +65,7 @@ class ISYSwitchDevice(ISYDeviceABC):
class ISYProgramDevice(ISYSwitchDevice):
""" represents a door that can be manipulated within home assistant. """
""" Represents a door that can be manipulated. """
_domain = 'switch'
_dtype = 'binary'
@ -74,9 +77,9 @@ class ISYProgramDevice(ISYSwitchDevice):
self.action_node = actions
def turn_on(self, **kwargs):
""" turns the device on/closes the device """
""" Turns the device on/closes the device. """
self.action_node.runThen()
def turn_off(self, **kwargs):
""" turns the device off/opens the device """
""" Turns the device off/opens the device. """
self.action_node.runElse()

View File

@ -1,4 +1,7 @@
"""
homeassistant.components.switch.modbus
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Modbus switches.
Configuration:
@ -53,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class ModbusSwitch(ToggleEntity):
""" Represents a Modbus Switch """
""" Represents a Modbus switch. """
def __init__(self, name, slave, register, bit):
self._name = name

View File

@ -1,4 +1,9 @@
""" Support for Tellstick switches. """
"""
homeassistant.components.switch.tellstick
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Tellstick switches.
"""
import logging
@ -9,7 +14,7 @@ import tellcore.constants as tellcore_constants
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return tellstick switches. """
""" Find and return Tellstick switches. """
try:
import tellcore.telldus as telldus
except ImportError:
@ -30,7 +35,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class TellstickSwitchDevice(ToggleEntity):
""" represents a Tellstick switch within home assistant. """
""" Represents a Tellstick switch within Home Assistant. """
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF)

View File

@ -1,4 +1,9 @@
""" Support for WeMo switchces. """
"""
homeassistant.components.switch.wemo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for WeMo switches.
"""
import logging
from homeassistant.helpers.entity import ToggleEntity
@ -8,7 +13,7 @@ from homeassistant.components.switch import (
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return wemo switches. """
""" Find and return WeMo switches. """
try:
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.pywemo.pywemo as pywemo
@ -39,7 +44,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class WemoSwitch(ToggleEntity):
""" represents a WeMo switch within home assistant. """
""" Represents a WeMo switch within Home Assistant. """
def __init__(self, wemo):
self.wemo = wemo
@ -78,5 +83,5 @@ class WemoSwitch(ToggleEntity):
self.wemo.off()
def update(self):
""" Update Wemo state. """
""" Update WeMo state. """
self.wemo.get_state(True)

View File

@ -1,4 +1,9 @@
""" Support for WeMo switchces. """
"""
homeassistant.components.switch.wink
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Wink switches.
"""
import logging
# pylint: disable=no-name-in-module, import-error

View File

@ -1,7 +1,10 @@
"""
Demo platform that offers a fake thermostat.
"""
homeassistant.components.thermostat.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform that offers a fake thermostat.
"""
from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import TEMP_CELCIUS, TEMP_FAHRENHEIT

View File

@ -1,4 +1,7 @@
"""
homeassistant.components.wink
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Connects to a Wink hub and loads relevant components to control its devices.
"""
import logging
@ -53,14 +56,14 @@ def setup(hass, config):
class WinkToggleDevice(ToggleEntity):
""" represents a Wink switch within home assistant. """
""" Represents a Wink switch within Home Assistant. """
def __init__(self, wink):
self.wink = wink
@property
def unique_id(self):
""" Returns the id of this WeMo switch """
""" Returns the id of this Wink switch. """
return "{}.{}".format(self.__class__, self.wink.deviceId())
@property

140
homeassistant/config.py Normal file
View File

@ -0,0 +1,140 @@
"""
homeassistant.config
~~~~~~~~~~~~~~~~~~~~
Module to help with parsing and generating configuration files.
"""
import logging
import os
from homeassistant import HomeAssistantError
from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME,
CONF_TIME_ZONE)
import homeassistant.util as util
_LOGGER = logging.getLogger(__name__)
YAML_CONFIG_FILE = 'configuration.yaml'
CONF_CONFIG_FILE = 'home-assistant.conf'
DEFAULT_COMPONENTS = [
'discovery', 'frontend', 'conversation', 'history', 'logbook']
def ensure_config_exists(config_dir, detect_location=True):
""" Ensures a config file exists in given config dir.
Creating a default one if needed.
Returns path to the config file. """
config_path = find_config_file(config_dir)
if config_path is None:
_LOGGER.info("Unable to find configuration. Creating default one")
config_path = create_default_config(config_dir, detect_location)
return config_path
def create_default_config(config_dir, detect_location=True):
""" Creates a default configuration file in given config dir.
Returns path to new config file if success, None if failed. """
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
# Writing files with YAML does not create the most human readable results
# So we're hard coding a YAML template.
try:
with open(config_path, 'w') as config_file:
location_info = detect_location and util.detect_location_info()
if location_info:
temp_unit = 'F' if location_info.use_fahrenheit else 'C'
auto_config = {
CONF_NAME: 'Home',
CONF_LATITUDE: location_info.latitude,
CONF_LONGITUDE: location_info.longitude,
CONF_TEMPERATURE_UNIT: temp_unit,
CONF_TIME_ZONE: location_info.time_zone,
}
config_file.write("homeassistant:\n")
for key, value in auto_config.items():
config_file.write(" {}: {}\n".format(key, value))
config_file.write("\n")
for component in DEFAULT_COMPONENTS:
config_file.write("{}:\n\n".format(component))
return config_path
except IOError:
_LOGGER.exception(
'Unable to write default configuration file %s', config_path)
return None
def find_config_file(config_dir):
""" Looks in given directory for supported config files. """
for filename in (YAML_CONFIG_FILE, CONF_CONFIG_FILE):
config_path = os.path.join(config_dir, filename)
if os.path.isfile(config_path):
return config_path
return None
def load_config_file(config_path):
""" Loads given config file. """
config_ext = os.path.splitext(config_path)[1]
if config_ext == '.yaml':
return load_yaml_config_file(config_path)
elif config_ext == '.conf':
return load_conf_config_file(config_path)
def load_yaml_config_file(config_path):
""" Parse a YAML configuration file. """
import yaml
try:
with open(config_path) as conf_file:
# If configuration file is empty YAML returns None
# We convert that to an empty dict
conf_dict = yaml.load(conf_file) or {}
except yaml.YAMLError:
_LOGGER.exception('Error reading YAML configuration file')
raise HomeAssistantError()
if not isinstance(conf_dict, dict):
_LOGGER.error(
'The configuration file %s does not contain a dictionary',
os.path.basename(config_path))
raise HomeAssistantError()
return conf_dict
def load_conf_config_file(config_path):
""" Parse the old style conf configuration. """
import configparser
config_dict = {}
config = configparser.ConfigParser()
config.read(config_path)
for section in config.sections():
config_dict[section] = {}
for key, val in config.items(section):
config_dict[section][key] = val
return config_dict

View File

@ -11,7 +11,7 @@ CONF_LONGITUDE = "longitude"
CONF_TEMPERATURE_UNIT = "temperature_unit"
CONF_NAME = "name"
CONF_TIME_ZONE = "time_zone"
CONF_VISIBILITY = "visibility"
CONF_CUSTOMIZE = "customize"
CONF_PLATFORM = "platform"
CONF_HOST = "host"
@ -111,6 +111,7 @@ SERVER_PORT = 8123
URL_ROOT = "/"
URL_API = "/api/"
URL_API_STREAM = "/api/stream"
URL_API_CONFIG = "/api/config"
URL_API_STATES = "/api/states"
URL_API_STATES_ENTITY = "/api/states/{}"
URL_API_EVENTS = "/api/events"
@ -119,6 +120,7 @@ URL_API_SERVICES = "/api/services"
URL_API_SERVICES_SERVICE = "/api/services/{}/{}"
URL_API_EVENT_FORWARD = "/api/event_forwarding"
URL_API_COMPONENTS = "/api/components"
URL_API_BOOTSTRAP = "/api/bootstrap"
HTTP_OK = 200
HTTP_CREATED = 201

@ -1 +1 @@
Subproject commit 6e712dd65e474bf623b35c54f5290dbac192c7e4
Subproject commit b2cad7c2b959efa8eee9b5ac62d87232bf0b5176

@ -1 +1 @@
Subproject commit 45fae73c1f44342010fa07f3ed8909bf2819a508
Subproject commit 4eaeb3367f9ada05dae3319cf24ab1da5de1aa89

@ -1 +1 @@
Subproject commit f9f9ba36934f087b9c4241303b900794a7eb6c08
Subproject commit f01997498fe190d6ac2a2c375a739024843bd44d

1
homeassistant/external/pymysensors vendored Submodule

@ -0,0 +1 @@
Subproject commit cd5ef892eeec0ad027727f7e8f757e7f2927da97

@ -1 +1 @@
Subproject commit 7f6c383ded75f1273cbca28e858b8a8c96da66d4
Subproject commit ca94e41faa48c783f600a2efd550c6b7dae01b0d

View File

@ -5,14 +5,17 @@ homeassistant.helpers.entity
Provides ABC for entities in HA.
"""
from collections import defaultdict
from homeassistant import NoEntitySpecifiedError
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, ATTR_HIDDEN, STATE_ON,
STATE_OFF, DEVICE_DEFAULT_NAME, TEMP_CELCIUS, TEMP_FAHRENHEIT)
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, ATTR_HIDDEN,
STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, TEMP_CELCIUS,
TEMP_FAHRENHEIT)
# Dict mapping entity_id to a boolean that overwrites the hidden property
_OVERWRITE_HIDDEN = {}
_OVERWRITE = defaultdict(dict)
class Entity(object):
@ -121,8 +124,15 @@ class Entity(object):
if ATTR_UNIT_OF_MEASUREMENT not in attr and self.unit_of_measurement:
attr[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement
if _OVERWRITE_HIDDEN.get(self.entity_id, self.hidden):
attr[ATTR_HIDDEN] = True
if self.hidden:
attr[ATTR_HIDDEN] = self.hidden
# overwrite properties that have been set in the config file
attr.update(_OVERWRITE.get(self.entity_id, {}))
# remove hidden property if false so it won't show up
if not attr.get(ATTR_HIDDEN, True):
attr.pop(ATTR_HIDDEN)
# Convert temperature if we detect one
if attr.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_CELCIUS,
@ -143,15 +153,18 @@ class Entity(object):
return "<Entity {}: {}>".format(self.name, self.state)
@staticmethod
def overwrite_hidden(entity_id, hidden):
def overwrite_attribute(entity_id, attrs, vals):
"""
Overwrite the hidden property of an entity.
Set hidden to None to remove any overwritten value in place.
Overwrite any attribute of an entity.
This function should receive a list of attributes and a
list of values. Set attribute to None to remove any overwritten
value in place.
"""
if hidden is None:
_OVERWRITE_HIDDEN.pop(entity_id, None)
else:
_OVERWRITE_HIDDEN[entity_id.lower()] = hidden
for attr, val in zip(attrs, vals):
if val is None:
_OVERWRITE[entity_id.lower()].pop(attr, None)
else:
_OVERWRITE[entity_id.lower()][attr] = val
class ToggleEntity(Entity):

View File

@ -4,7 +4,7 @@ homeassistant.helpers.entity_component
Provides helpers for components that manage entities.
"""
from homeassistant.loader import get_component
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.helpers import (
generate_entity_id, config_per_platform, extract_entity_ids)
from homeassistant.components import group, discovery
@ -35,12 +35,16 @@ class EntityComponent(object):
self.group = None
self.is_polling = False
self.config = None
def setup(self, config):
"""
Sets up a full entity component:
- Loads the platforms from the config
- Will listen for supported discovered platforms
"""
self.config = config
# Look in config for Domain, Domain 2, Domain 3 etc and load them
for p_type, p_config in \
config_per_platform(config, self.domain, self.logger):
@ -115,18 +119,20 @@ class EntityComponent(object):
self._update_entity_states,
second=range(0, 60, self.scan_interval))
def _setup_platform(self, platform_type, config, discovery_info=None):
def _setup_platform(self, platform_type, platform_config,
discovery_info=None):
""" Tries to setup a platform for this component. """
platform_name = '{}.{}'.format(self.domain, platform_type)
platform = get_component(platform_name)
platform = prepare_setup_platform(
self.hass, self.config, self.domain, platform_type)
if platform is None:
self.logger.error('Unable to find platform %s', platform_type)
return
platform_name = '{}.{}'.format(self.domain, platform_type)
try:
platform.setup_platform(
self.hass, config, self.add_entities, discovery_info)
self.hass, platform_config, self.add_entities, discovery_info)
self.hass.config.components.append(platform_name)
@ -135,15 +141,16 @@ class EntityComponent(object):
# Support old deprecated method for now - 3/1/2015
if hasattr(platform, 'get_devices'):
self.logger.warning(
"Please upgrade %s to return new entities using "
"setup_platform. See %s/demo.py for an example.",
'Please upgrade %s to return new entities using '
'setup_platform. See %s/demo.py for an example.',
platform_name, self.domain)
self.add_entities(platform.get_devices(self.hass, config))
self.add_entities(
platform.get_devices(self.hass, platform_config))
else:
self.logger.exception(
"Error while setting up platform %s", platform_type)
'Error while setting up platform %s', platform_type)
except Exception: # pylint: disable=broad-except
self.logger.exception(
"Error while setting up platform %s", platform_type)
'Error while setting up platform %s', platform_type)

View File

@ -5,9 +5,9 @@ homeassistant.helpers.state
Helpers that help with state related things.
"""
import logging
from datetime import datetime
from homeassistant import State
import homeassistant.util.dt as dt_util
from homeassistant.const import (
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
@ -26,7 +26,7 @@ class TrackStates(object):
self.states = []
def __enter__(self):
self.now = datetime.now()
self.now = dt_util.utcnow()
return self.states
def __exit__(self, exc_type, exc_value, traceback):

View File

@ -8,7 +8,7 @@ import collections
from itertools import chain
import threading
import queue
from datetime import datetime, timedelta
from datetime import datetime
import re
import enum
import socket
@ -16,12 +16,19 @@ import random
import string
from functools import wraps
import requests
# DEPRECATED AS OF 4/27/2015 - moved to homeassistant.util.dt package
# pylint: disable=unused-import
from .dt import ( # noqa
datetime_to_str, str_to_datetime, strip_microseconds,
datetime_to_local_str, utcnow)
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)')
RE_SLUGIFY = re.compile(r'[^A-Za-z0-9_]+')
DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
def sanitize_filename(filename):
""" Sanitizes a filename by removing .. / and \\. """
@ -40,33 +47,6 @@ def slugify(text):
return RE_SLUGIFY.sub("", text)
def datetime_to_str(dattim):
""" Converts datetime to a string format.
@rtype : str
"""
return dattim.strftime(DATE_STR_FORMAT)
def str_to_datetime(dt_str):
""" Converts a string to a datetime object.
@rtype: datetime
"""
try:
return datetime.strptime(dt_str, DATE_STR_FORMAT)
except ValueError: # If dt_str did not match our format
return None
def strip_microseconds(dattim):
""" Returns a copy of dattime object but with microsecond set to 0. """
if dattim.microsecond:
return dattim - timedelta(microseconds=dattim.microsecond)
else:
return dattim
def split_entity_id(entity_id):
""" Splits a state entity_id into domain, object_id. """
return entity_id.split(".", 1)
@ -79,7 +59,7 @@ def repr_helper(inp):
repr_helper(key)+"="+repr_helper(item) for key, item
in inp.items())
elif isinstance(inp, datetime):
return datetime_to_str(inp)
return datetime_to_local_str(inp)
else:
return str(inp)
@ -174,6 +154,32 @@ def get_random_string(length=10):
return ''.join(generator.choice(source_chars) for _ in range(length))
LocationInfo = collections.namedtuple(
"LocationInfo",
['ip', 'country_code', 'country_name', 'region_code', 'region_name',
'city', 'zip_code', 'time_zone', 'latitude', 'longitude',
'use_fahrenheit'])
def detect_location_info():
""" Detect location information. """
try:
raw_info = requests.get(
'https://freegeoip.net/json/', timeout=5).json()
except requests.RequestException:
return
data = {key: raw_info.get(key) for key in LocationInfo._fields}
# From Wikipedia: Fahrenheit is used in the Bahamas, Belize,
# the Cayman Islands, Palau, and the United States and associated
# territories of American Samoa and the U.S. Virgin Islands
data['use_fahrenheit'] = data['country_code'] in (
'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI')
return LocationInfo(**data)
class OrderedEnum(enum.Enum):
""" Taken from Python 3.4.0 docs. """
# pylint: disable=no-init, too-few-public-methods
@ -436,7 +442,7 @@ class ThreadPool(object):
return
# Add to current running jobs
job_log = (datetime.now(), job)
job_log = (utcnow(), job)
self.current_jobs.append(job_log)
# Do the job

114
homeassistant/util/dt.py Normal file
View File

@ -0,0 +1,114 @@
"""
homeassistant.util.dt
~~~~~~~~~~~~~~~~~~~~~
Provides helper methods to handle the time in HA.
"""
import datetime as dt
import pytz
DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
DATE_SHORT_STR_FORMAT = "%Y-%m-%d"
TIME_SHORT_STR_FORMAT = "%H:%M"
UTC = DEFAULT_TIME_ZONE = pytz.utc
def set_default_time_zone(time_zone):
""" Sets a default time zone to be used when none is specified. """
global DEFAULT_TIME_ZONE # pylint: disable=global-statement
assert isinstance(time_zone, dt.tzinfo)
DEFAULT_TIME_ZONE = time_zone
def get_time_zone(time_zone_str):
""" Get time zone from string. Return None if unable to determine. """
try:
return pytz.timezone(time_zone_str)
except pytz.exceptions.UnknownTimeZoneError:
return None
def utcnow():
""" Get now in UTC time. """
return dt.datetime.now(pytz.utc)
def now(time_zone=None):
""" Get now in specified time zone. """
return dt.datetime.now(time_zone or DEFAULT_TIME_ZONE)
def as_utc(dattim):
""" Return a datetime as UTC time.
Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE. """
if dattim.tzinfo == pytz.utc:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE)
return dattim.astimezone(pytz.utc)
def as_local(dattim):
""" Converts a UTC datetime object to local time_zone. """
if dattim.tzinfo == DEFAULT_TIME_ZONE:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=pytz.utc)
return dattim.astimezone(DEFAULT_TIME_ZONE)
def utc_from_timestamp(timestamp):
""" Returns a UTC time from a timestamp. """
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=pytz.utc)
def datetime_to_local_str(dattim, time_zone=None):
""" Converts datetime to specified time_zone and returns a string. """
return datetime_to_str(as_local(dattim))
def datetime_to_str(dattim):
""" Converts datetime to a string format.
@rtype : str
"""
return dattim.strftime(DATE_STR_FORMAT)
def datetime_to_short_time_str(dattim):
""" Converts datetime to a string format as short time.
@rtype : str
"""
return dattim.strftime(TIME_SHORT_STR_FORMAT)
def datetime_to_short_date_str(dattim):
""" Converts datetime to a string format as short date.
@rtype : str
"""
return dattim.strftime(DATE_SHORT_STR_FORMAT)
def str_to_datetime(dt_str):
""" Converts a string to a UTC datetime object.
@rtype: datetime
"""
try:
return dt.datetime.strptime(
dt_str, DATE_STR_FORMAT).replace(tzinfo=pytz.utc)
except ValueError: # If dt_str did not match our format
return None
def strip_microseconds(dattim):
""" Returns a copy of dattime object but with microsecond set to 0. """
return dattim.replace(microsecond=0)

View File

@ -1,6 +1,7 @@
# required for Home Assistant core
requests>=2.0
pyyaml>=3.11
pytz>=2015.2
# optional, needed for specific components
@ -40,8 +41,15 @@ PyISY>=1.0.2
# sensor.systemmonitor
psutil>=2.2.1
#pushover notifications
# pushover notifications
python-pushover>=0.2
# Transmission Torrent Client
transmissionrpc>=0.11
# OpenWeatherMap Web API
pyowm>=2.2.0
# XMPP Bindings (notify.xmpp)
sleekxmpp>=1.3.1

View File

@ -3,4 +3,8 @@ if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
python3 -m unittest discover tests
if [ "$1" = "coverage" ]; then
coverage run -m unittest discover tests
else
python3 -m unittest discover tests
fi

View File

@ -26,6 +26,10 @@ class MockScanner(object):
""" Make a device leave the house. """
self.devices_home.remove(device)
def reset(self):
""" Resets which devices are home. """
self.devices_home = []
def scan_devices(self):
""" Returns a list of fake devices. """

View File

@ -5,16 +5,36 @@ tests.helper
Helper method for writing tests.
"""
import os
from datetime import timedelta
import homeassistant as ha
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.const import (
STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED,
EVENT_STATE_CHANGED)
from homeassistant.components import sun
def get_test_home_assistant():
def get_test_config_dir():
""" Returns a path to a test config dir. """
return os.path.join(os.path.dirname(__file__), "config")
def get_test_home_assistant(num_threads=None):
""" Returns a Home Assistant object pointing at test config dir. """
if num_threads:
orig_num_threads = ha.MIN_WORKER_THREAD
ha.MIN_WORKER_THREAD = num_threads
hass = ha.HomeAssistant()
hass.config.config_dir = os.path.join(os.path.dirname(__file__), "config")
if num_threads:
ha.MIN_WORKER_THREAD = orig_num_threads
hass.config.config_dir = get_test_config_dir()
hass.config.latitude = 32.87336
hass.config.longitude = -117.22743
return hass
@ -32,6 +52,44 @@ def mock_service(hass, domain, service):
return calls
def trigger_device_tracker_scan(hass):
""" Triggers the device tracker to scan. """
hass.bus.fire(
EVENT_TIME_CHANGED,
{'now':
dt_util.utcnow().replace(second=0) + timedelta(hours=1)})
def ensure_sun_risen(hass):
""" Trigger sun to rise if below horizon. """
if not sun.is_on(hass):
hass.bus.fire(
EVENT_TIME_CHANGED,
{'now':
sun.next_rising_utc(hass) + timedelta(seconds=10)})
def ensure_sun_set(hass):
""" Trigger sun to set if above horizon. """
if sun.is_on(hass):
hass.bus.fire(
EVENT_TIME_CHANGED,
{'now':
sun.next_setting_utc(hass) + timedelta(seconds=10)})
def mock_state_change_event(hass, new_state, old_state=None):
event_data = {
'entity_id': new_state.entity_id,
'new_state': new_state,
}
if old_state:
event_data['old_state'] = old_state
hass.bus.fire(EVENT_STATE_CHANGED, event_data)
class MockModule(object):
""" Provides a fake module. """

View File

@ -4,13 +4,10 @@ tests.test_component_demo
Tests demo component.
"""
# pylint: disable=too-many-public-methods,protected-access
import unittest
import homeassistant as ha
import homeassistant.components.demo as demo
from homeassistant.const import (
SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, ATTR_ENTITY_ID)
class TestDemo(unittest.TestCase):
@ -23,46 +20,6 @@ class TestDemo(unittest.TestCase):
""" Stop down stuff we started. """
self.hass.stop()
def test_services(self):
""" Test the demo services. """
# Test turning on and off different types
demo.setup(self.hass, {})
for domain in ('light', 'switch'):
# Focus on 1 entity
entity_id = self.hass.states.entity_ids(domain)[0]
self.hass.services.call(
domain, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id})
self.hass.pool.block_till_done()
self.assertEqual(STATE_ON, self.hass.states.get(entity_id).state)
self.hass.services.call(
domain, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id})
self.hass.pool.block_till_done()
self.assertEqual(STATE_OFF, self.hass.states.get(entity_id).state)
# Act on all
self.hass.services.call(domain, SERVICE_TURN_ON)
self.hass.pool.block_till_done()
for entity_id in self.hass.states.entity_ids(domain):
self.assertEqual(
STATE_ON, self.hass.states.get(entity_id).state)
self.hass.services.call(domain, SERVICE_TURN_OFF)
self.hass.pool.block_till_done()
for entity_id in self.hass.states.entity_ids(domain):
self.assertEqual(
STATE_OFF, self.hass.states.get(entity_id).state)
def test_if_demo_state_shows_by_default(self):
""" Test if demo state shows if we give no configuration. """
demo.setup(self.hass, {demo.DOMAIN: {}})

View File

@ -0,0 +1,127 @@
"""
tests.test_component_device_sun_light_trigger
~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests device sun light trigger component.
"""
# pylint: disable=too-many-public-methods,protected-access
import os
import unittest
import homeassistant.loader as loader
from homeassistant.const import CONF_PLATFORM
from homeassistant.components import (
device_tracker, light, sun, device_sun_light_trigger)
from helpers import (
get_test_home_assistant, ensure_sun_risen, ensure_sun_set,
trigger_device_tracker_scan)
KNOWN_DEV_PATH = None
def setUpModule(): # pylint: disable=invalid-name
""" Initalizes a Home Assistant server. """
global KNOWN_DEV_PATH
hass = get_test_home_assistant()
loader.prepare(hass)
KNOWN_DEV_PATH = hass.config.path(
device_tracker.KNOWN_DEVICES_FILE)
hass.stop()
with open(KNOWN_DEV_PATH, 'w') as fil:
fil.write('device,name,track,picture\n')
fil.write('DEV1,device 1,1,http://example.com/dev1.jpg\n')
fil.write('DEV2,device 2,1,http://example.com/dev2.jpg\n')
def tearDownModule(): # pylint: disable=invalid-name
""" Stops the Home Assistant server. """
os.remove(KNOWN_DEV_PATH)
class TestDeviceSunLightTrigger(unittest.TestCase):
""" Test the device sun light trigger module. """
def setUp(self): # pylint: disable=invalid-name
self.hass = get_test_home_assistant()
self.scanner = loader.get_component(
'device_tracker.test').get_scanner(None, None)
self.scanner.reset()
self.scanner.come_home('DEV1')
loader.get_component('light.test').init()
device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}
})
light.setup(self.hass, {
light.DOMAIN: {CONF_PLATFORM: 'test'}
})
sun.setup(self.hass, {})
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_lights_on_when_sun_sets(self):
""" Test lights go on when there is someone home and the sun sets. """
device_sun_light_trigger.setup(
self.hass, {device_sun_light_trigger.DOMAIN: {}})
ensure_sun_risen(self.hass)
light.turn_off(self.hass)
self.hass.pool.block_till_done()
ensure_sun_set(self.hass)
self.hass.pool.block_till_done()
self.assertTrue(light.is_on(self.hass))
def test_lights_turn_off_when_everyone_leaves(self):
""" Test lights turn off when everyone leaves the house. """
light.turn_on(self.hass)
self.hass.pool.block_till_done()
device_sun_light_trigger.setup(
self.hass, {device_sun_light_trigger.DOMAIN: {}})
self.scanner.leave_home('DEV1')
trigger_device_tracker_scan(self.hass)
self.hass.pool.block_till_done()
self.assertFalse(light.is_on(self.hass))
def test_lights_turn_on_when_coming_home_after_sun_set(self):
""" Test lights turn on when coming home after sun set. """
light.turn_off(self.hass)
ensure_sun_set(self.hass)
self.hass.pool.block_till_done()
device_sun_light_trigger.setup(
self.hass, {device_sun_light_trigger.DOMAIN: {}})
self.scanner.come_home('DEV2')
trigger_device_tracker_scan(self.hass)
self.hass.pool.block_till_done()
self.assertTrue(light.is_on(self.hass))

View File

@ -1,17 +1,18 @@
"""
tests.test_component_group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tests.test_component_device_tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the group compoments.
Tests the device tracker compoments.
"""
# pylint: disable=protected-access,too-many-public-methods
import unittest
from datetime import datetime, timedelta
from datetime import timedelta
import logging
import os
import homeassistant as ha
import homeassistant.loader as loader
import homeassistant.util.dt as dt_util
from homeassistant.const import (
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM)
import homeassistant.components.device_tracker as device_tracker
@ -80,6 +81,8 @@ class TestComponentsDeviceTracker(unittest.TestCase):
scanner = loader.get_component(
'device_tracker.test').get_scanner(None, None)
scanner.reset()
scanner.come_home('DEV1')
scanner.come_home('DEV2')
@ -116,7 +119,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev2 = device_tracker.ENTITY_ID_FORMAT.format('device_2')
dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3')
now = datetime.now()
now = dt_util.utcnow()
# Device scanner scans every 12 seconds. We need to sync our times to
# be every 12 seconds or else the time_changed event will be ignored.

View File

@ -0,0 +1,137 @@
"""
tests.test_component_history
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the history component.
"""
# pylint: disable=protected-access,too-many-public-methods
import time
import os
import unittest
import homeassistant as ha
import homeassistant.util.dt as dt_util
from homeassistant.components import history, recorder, http
from helpers import get_test_home_assistant, mock_state_change_event
SERVER_PORT = 8126
class TestComponentHistory(unittest.TestCase):
""" Tests homeassistant.components.history module. """
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))
def init_recorder(self):
recorder.setup(self.hass, {})
self.hass.start()
recorder._INSTANCE.block_till_done()
self.init_rec = True
def test_setup(self):
""" Test setup method of history. """
http.setup(self.hass, {
http.DOMAIN: {http.CONF_SERVER_PORT: SERVER_PORT}})
self.assertTrue(history.setup(self.hass, {}))
def test_last_5_states(self):
""" Test retrieving the last 5 states. """
self.init_recorder()
states = []
entity_id = 'test.last_5_states'
for i in range(7):
self.hass.states.set(entity_id, "State {}".format(i))
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))
def test_get_states(self):
""" Test getting states at a specific point in time. """
self.init_recorder()
states = []
# Create 10 states for 5 different entities
# After the first 5, sleep a second and save the time
# history.get_states takes the latest states BEFORE point X
for i in range(10):
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()
recorder._INSTANCE.block_till_done()
if i < 5:
states.append(state)
if i == 4:
time.sleep(1)
point = dt_util.utcnow()
self.assertEqual(
states,
sorted(
history.get_states(point), 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))
def test_state_changes_during_period(self):
self.init_recorder()
entity_id = 'media_player.test'
def set_state(state):
self.hass.states.set(entity_id, state)
self.hass.pool.block_till_done()
recorder._INSTANCE.block_till_done()
return self.hass.states.get(entity_id)
set_state('idle')
set_state('YouTube')
start = dt_util.utcnow()
time.sleep(1)
states = [
set_state('idle'),
set_state('Netflix'),
set_state('Plex'),
set_state('YouTube'),
]
time.sleep(1)
end = dt_util.utcnow()
set_state('Netflix')
set_state('Plex')
self.assertEqual(
{entity_id: states},
history.state_changes_during_period(start, end, entity_id))

View File

@ -0,0 +1,98 @@
"""
tests.test_component_logbook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the logbook component.
"""
# pylint: disable=protected-access,too-many-public-methods
import unittest
from datetime import timedelta
import homeassistant as ha
from homeassistant.const import (
EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.util.dt as dt_util
from homeassistant.components import logbook, http
from helpers import get_test_home_assistant
SERVER_PORT = 8127
class TestComponentHistory(unittest.TestCase):
""" Tests homeassistant.components.history module. """
def test_setup(self):
""" Test setup method. """
try:
hass = get_test_home_assistant()
http.setup(hass, {
http.DOMAIN: {http.CONF_SERVER_PORT: SERVER_PORT}})
self.assertTrue(logbook.setup(hass, {}))
finally:
hass.stop()
def test_humanify_filter_sensor(self):
""" Test humanify filter too frequent sensor values. """
entity_id = 'sensor.bla'
pointA = dt_util.strip_microseconds(dt_util.utcnow().replace(minute=2))
pointB = pointA.replace(minute=5)
pointC = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
eventA = self.create_state_changed_event(pointA, entity_id, 10)
eventB = self.create_state_changed_event(pointB, entity_id, 20)
eventC = self.create_state_changed_event(pointC, entity_id, 30)
entries = list(logbook.humanify((eventA, eventB, eventC)))
self.assertEqual(2, len(entries))
self.assert_entry(
entries[0], pointB, 'bla', domain='sensor', entity_id=entity_id)
self.assert_entry(
entries[1], pointC, 'bla', domain='sensor', entity_id=entity_id)
def test_home_assistant_start_stop_grouped(self):
""" Tests if home assistant start and stop events are grouped if
occuring in the same minute. """
entries = list(logbook.humanify((
ha.Event(EVENT_HOMEASSISTANT_STOP),
ha.Event(EVENT_HOMEASSISTANT_START),
)))
self.assertEqual(1, len(entries))
self.assert_entry(
entries[0], name='Home Assistant', message='restarted',
domain=ha.DOMAIN)
def assert_entry(self, entry, when=None, name=None, message=None,
domain=None, entity_id=None):
""" Asserts an entry is what is expected """
if when:
self.assertEqual(when, entry.when)
if name:
self.assertEqual(name, entry.name)
if message:
self.assertEqual(message, entry.message)
if domain:
self.assertEqual(domain, entry.domain)
if entity_id:
self.assertEqual(entity_id, entry.entity_id)
def create_state_changed_event(self, event_time_fired, entity_id, state):
""" Create state changed event. """
# Logbook only cares about state change events that
# contain an old state but will not actually act on it.
state = ha.State(entity_id, state).as_dict()
return ha.Event(EVENT_STATE_CHANGED, {
'entity_id': entity_id,
'old_state': state,
'new_state': state,
}, time_fired=event_time_fired)

View File

@ -0,0 +1,70 @@
"""
tests.test_component_recorder
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Recorder component.
"""
# pylint: disable=too-many-public-methods,protected-access
import unittest
import os
from homeassistant.const import MATCH_ALL
from homeassistant.components import recorder
from helpers import get_test_home_assistant
class TestRecorder(unittest.TestCase):
""" Test the chromecast module. """
def setUp(self): # pylint: disable=invalid-name
self.hass = get_test_home_assistant()
recorder.setup(self.hass, {})
self.hass.start()
recorder._INSTANCE.block_till_done()
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
recorder._INSTANCE.block_till_done()
os.remove(self.hass.config.path(recorder.DB_FILE))
def test_saving_state(self):
""" Tests saving and restoring a state. """
entity_id = 'test.recorder'
state = 'restoring_from_db'
attributes = {'test_attr': 5, 'test_attr_10': 'nice'}
self.hass.states.set(entity_id, state, attributes)
self.hass.pool.block_till_done()
recorder._INSTANCE.block_till_done()
states = recorder.query_states('SELECT * FROM states')
self.assertEqual(1, len(states))
self.assertEqual(self.hass.states.get(entity_id), states[0])
def test_saving_event(self):
""" Tests saving and restoring an event. """
event_type = 'EVENT_TEST'
event_data = {'test_attr': 5, 'test_attr_10': 'nice'}
events = []
def event_listener(event):
""" Records events from eventbus. """
if event.event_type == event_type:
events.append(event)
self.hass.bus.listen(MATCH_ALL, event_listener)
self.hass.bus.fire(event_type, event_data)
self.hass.pool.block_till_done()
recorder._INSTANCE.block_till_done()
db_events = recorder.query_events(
'SELECT * FROM events WHERE event_type = ?', (event_type, ))
self.assertEqual(events, db_events)

View File

@ -6,11 +6,12 @@ Tests Sun component.
"""
# pylint: disable=too-many-public-methods,protected-access
import unittest
import datetime as dt
from datetime import timedelta
import ephem
import homeassistant as ha
import homeassistant.util.dt as dt_util
import homeassistant.components.sun as sun
@ -42,22 +43,20 @@ class TestSun(unittest.TestCase):
observer.lat = '32.87336' # pylint: disable=assigning-non-slot
observer.long = '117.22743' # pylint: disable=assigning-non-slot
utc_now = dt.datetime.utcnow()
utc_now = dt_util.utcnow()
body_sun = ephem.Sun() # pylint: disable=no-member
next_rising_dt = ephem.localtime(
observer.next_rising(body_sun, start=utc_now))
next_setting_dt = ephem.localtime(
observer.next_setting(body_sun, start=utc_now))
next_rising_dt = observer.next_rising(
body_sun, start=utc_now).datetime().replace(tzinfo=dt_util.UTC)
next_setting_dt = observer.next_setting(
body_sun, start=utc_now).datetime().replace(tzinfo=dt_util.UTC)
# Home Assistant strips out microseconds
# strip it out of the datetime objects
next_rising_dt = next_rising_dt - dt.timedelta(
microseconds=next_rising_dt.microsecond)
next_setting_dt = next_setting_dt - dt.timedelta(
microseconds=next_setting_dt.microsecond)
next_rising_dt = dt_util.strip_microseconds(next_rising_dt)
next_setting_dt = dt_util.strip_microseconds(next_setting_dt)
self.assertEqual(next_rising_dt, sun.next_rising(self.hass))
self.assertEqual(next_setting_dt, sun.next_setting(self.hass))
self.assertEqual(next_rising_dt, sun.next_rising_utc(self.hass))
self.assertEqual(next_setting_dt, sun.next_setting_utc(self.hass))
# Point it at a state without the proper attributes
self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON)
@ -84,7 +83,7 @@ class TestSun(unittest.TestCase):
self.assertIsNotNone(test_time)
self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
{ha.ATTR_NOW: test_time + dt.timedelta(seconds=5)})
{ha.ATTR_NOW: test_time + timedelta(seconds=5)})
self.hass.pool.block_till_done()

180
tests/test_config.py Normal file
View File

@ -0,0 +1,180 @@
"""
tests.test_config
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests config utils.
"""
# pylint: disable=too-many-public-methods,protected-access
import unittest
import unittest.mock as mock
import os
from homeassistant import DOMAIN, HomeAssistantError
import homeassistant.util as util
import homeassistant.config as config_util
from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME,
CONF_TIME_ZONE)
from helpers import get_test_config_dir
CONFIG_DIR = get_test_config_dir()
YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE)
CONF_PATH = os.path.join(CONFIG_DIR, config_util.CONF_CONFIG_FILE)
def create_file(path):
""" Creates an empty file. """
with open(path, 'w'):
pass
def mock_detect_location_info():
""" Mock implementation of util.detect_location_info. """
return util.LocationInfo(
ip='1.1.1.1',
country_code='US',
country_name='United States',
region_code='CA',
region_name='California',
city='San Diego',
zip_code='92122',
time_zone='America/Los_Angeles',
latitude='2.0',
longitude='1.0',
use_fahrenheit=True,
)
class TestConfig(unittest.TestCase):
""" Test the config utils. """
def tearDown(self): # pylint: disable=invalid-name
""" Clean up. """
for path in (YAML_PATH, CONF_PATH):
if os.path.isfile(path):
os.remove(path)
def test_create_default_config(self):
""" Test creationg of default config. """
config_util.create_default_config(CONFIG_DIR, False)
self.assertTrue(os.path.isfile(YAML_PATH))
def test_find_config_file_yaml(self):
""" Test if it finds a YAML config file. """
create_file(YAML_PATH)
self.assertEqual(YAML_PATH, config_util.find_config_file(CONFIG_DIR))
def test_find_config_file_conf(self):
""" Test if it finds the old CONF config file. """
create_file(CONF_PATH)
self.assertEqual(CONF_PATH, config_util.find_config_file(CONFIG_DIR))
def test_find_config_file_prefers_yaml_over_conf(self):
""" Test if find config prefers YAML over CONF if both exist. """
create_file(YAML_PATH)
create_file(CONF_PATH)
self.assertEqual(YAML_PATH, config_util.find_config_file(CONFIG_DIR))
def test_ensure_config_exists_creates_config(self):
""" Test that calling ensure_config_exists creates a new config file if
none exists. """
config_util.ensure_config_exists(CONFIG_DIR, False)
self.assertTrue(os.path.isfile(YAML_PATH))
def test_ensure_config_exists_uses_existing_config(self):
""" Test that calling ensure_config_exists uses existing config. """
create_file(YAML_PATH)
config_util.ensure_config_exists(CONFIG_DIR, False)
with open(YAML_PATH) as f:
content = f.read()
# File created with create_file are empty
self.assertEqual('', content)
def test_load_yaml_config_converts_empty_files_to_dict(self):
""" Test that loading an empty file returns an empty dict. """
create_file(YAML_PATH)
self.assertIsInstance(
config_util.load_yaml_config_file(YAML_PATH), dict)
def test_load_yaml_config_raises_error_if_not_dict(self):
""" Test error raised when YAML file is not a dict. """
with open(YAML_PATH, 'w') as f:
f.write('5')
with self.assertRaises(HomeAssistantError):
config_util.load_yaml_config_file(YAML_PATH)
def test_load_yaml_config_raises_error_if_malformed_yaml(self):
""" Test error raised if invalid YAML. """
with open(YAML_PATH, 'w') as f:
f.write(':')
with self.assertRaises(HomeAssistantError):
config_util.load_yaml_config_file(YAML_PATH)
def test_load_config_loads_yaml_config(self):
""" Test correct YAML config loading. """
with open(YAML_PATH, 'w') as f:
f.write('hello: world')
self.assertEqual({'hello': 'world'},
config_util.load_config_file(YAML_PATH))
def test_load_config_loads_conf_config(self):
""" Test correct YAML config loading. """
create_file(CONF_PATH)
self.assertEqual({}, config_util.load_config_file(CONF_PATH))
def test_conf_config_file(self):
""" Test correct CONF config loading. """
with open(CONF_PATH, 'w') as f:
f.write('[ha]\ntime_zone=America/Los_Angeles')
self.assertEqual({'ha': {'time_zone': 'America/Los_Angeles'}},
config_util.load_conf_config_file(CONF_PATH))
def test_create_default_config_detect_location(self):
""" Test that detect location sets the correct config keys. """
with mock.patch('homeassistant.util.detect_location_info',
mock_detect_location_info):
config_util.ensure_config_exists(CONFIG_DIR)
config = config_util.load_config_file(YAML_PATH)
self.assertIn(DOMAIN, config)
ha_conf = config[DOMAIN]
expected_values = {
CONF_LATITUDE: 2.0,
CONF_LONGITUDE: 1.0,
CONF_TEMPERATURE_UNIT: 'F',
CONF_NAME: 'Home',
CONF_TIME_ZONE: 'America/Los_Angeles'
}
self.assertEqual(expected_values, ha_conf)
def test_create_default_config_returns_none_if_write_error(self):
"""
Test that writing default config to non existing folder returns None.
"""
self.assertIsNone(
config_util.create_default_config(
os.path.join(CONFIG_DIR, 'non_existing_dir/'), False))

View File

@ -30,7 +30,11 @@ class TestHomeAssistant(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
try:
self.hass.stop()
except ha.HomeAssistantError:
# Already stopped after the block till stopped test
pass
def test_get_config_path(self):
""" Test get_config_path method. """
@ -72,7 +76,7 @@ class TestHomeAssistant(unittest.TestCase):
runs = []
self.hass.track_point_in_time(
self.hass.track_point_in_utc_time(
lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(before_birthday)
@ -88,7 +92,7 @@ class TestHomeAssistant(unittest.TestCase):
self.hass.pool.block_till_done()
self.assertEqual(1, len(runs))
self.hass.track_point_in_time(
self.hass.track_point_in_utc_time(
lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(after_birthday)

View File

@ -25,7 +25,8 @@ class TestHelpersEntity(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
entity.Entity.overwrite_hidden(self.entity.entity_id, None)
entity.Entity.overwrite_attribute(self.entity.entity_id,
[ATTR_HIDDEN], [None])
def test_default_hidden_not_in_attributes(self):
""" Test that the default hidden property is set to False. """
@ -43,7 +44,8 @@ class TestHelpersEntity(unittest.TestCase):
def test_overwriting_hidden_property_to_true(self):
""" Test we can overwrite hidden property to True. """
entity.Entity.overwrite_hidden(self.entity.entity_id, True)
entity.Entity.overwrite_attribute(self.entity.entity_id,
[ATTR_HIDDEN], [True])
self.entity.update_ha_state()
state = self.hass.states.get(self.entity.entity_id)
@ -51,7 +53,8 @@ class TestHelpersEntity(unittest.TestCase):
def test_overwriting_hidden_property_to_false(self):
""" Test we can overwrite hidden property to True. """
entity.Entity.overwrite_hidden(self.entity.entity_id, False)
entity.Entity.overwrite_attribute(self.entity.entity_id,
[ATTR_HIDDEN], [False])
self.entity.hidden = True
self.entity.update_ha_state()

View File

@ -35,17 +35,6 @@ class TestUtil(unittest.TestCase):
self.assertEqual("Test_More", util.slugify("Test More"))
self.assertEqual("Test_More", util.slugify("Test_(More)"))
def test_datetime_to_str(self):
""" Test datetime_to_str. """
self.assertEqual("12:00:00 09-07-1986",
util.datetime_to_str(datetime(1986, 7, 9, 12, 0, 0)))
def test_str_to_datetime(self):
""" Test str_to_datetime. """
self.assertEqual(datetime(1986, 7, 9, 12, 0, 0),
util.str_to_datetime("12:00:00 09-07-1986"))
self.assertIsNone(util.str_to_datetime("not a datetime string"))
def test_split_entity_id(self):
""" Test split_entity_id. """
self.assertEqual(['domain', 'object_id'],

137
tests/test_util_dt.py Normal file
View File

@ -0,0 +1,137 @@
"""
tests.test_util
~~~~~~~~~~~~~~~~~
Tests Home Assistant date util methods.
"""
# pylint: disable=too-many-public-methods
import unittest
from datetime import datetime, timedelta
import homeassistant.util.dt as dt_util
TEST_TIME_ZONE = 'America/Los_Angeles'
class TestDateUtil(unittest.TestCase):
""" Tests util date methods. """
def setUp(self):
self.orig_default_time_zone = dt_util.DEFAULT_TIME_ZONE
def tearDown(self):
dt_util.set_default_time_zone(self.orig_default_time_zone)
def test_get_time_zone_retrieves_valid_time_zone(self):
""" Test getting a time zone. """
time_zone = dt_util.get_time_zone(TEST_TIME_ZONE)
self.assertIsNotNone(time_zone)
self.assertEqual(TEST_TIME_ZONE, time_zone.zone)
def test_get_time_zone_returns_none_for_garbage_time_zone(self):
""" Test getting a non existing time zone. """
time_zone = dt_util.get_time_zone("Non existing time zone")
self.assertIsNone(time_zone)
def test_set_default_time_zone(self):
""" Test setting default time zone. """
time_zone = dt_util.get_time_zone(TEST_TIME_ZONE)
dt_util.set_default_time_zone(time_zone)
# We cannot compare the timezones directly because of DST
self.assertEqual(time_zone.zone, dt_util.now().tzinfo.zone)
def test_utcnow(self):
""" Test the UTC now method. """
self.assertAlmostEqual(
dt_util.utcnow().replace(tzinfo=None),
datetime.utcnow(),
delta=timedelta(seconds=1))
def test_now(self):
""" Test the now method. """
dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE))
self.assertAlmostEqual(
dt_util.as_utc(dt_util.now()).replace(tzinfo=None),
datetime.utcnow(),
delta=timedelta(seconds=1))
def test_as_utc_with_naive_object(self):
utcnow = datetime.utcnow()
self.assertEqual(utcnow,
dt_util.as_utc(utcnow).replace(tzinfo=None))
def test_as_utc_with_utc_object(self):
utcnow = dt_util.utcnow()
self.assertEqual(utcnow, dt_util.as_utc(utcnow))
def test_as_utc_with_local_object(self):
dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE))
localnow = dt_util.now()
utcnow = dt_util.as_utc(localnow)
self.assertEqual(localnow, utcnow)
self.assertNotEqual(localnow.tzinfo, utcnow.tzinfo)
def test_as_local_with_naive_object(self):
now = dt_util.now()
self.assertAlmostEqual(
now, dt_util.as_local(datetime.utcnow()),
delta=timedelta(seconds=1))
def test_as_local_with_local_object(self):
now = dt_util.now()
self.assertEqual(now, now)
def test_as_local_with_utc_object(self):
dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE))
utcnow = dt_util.utcnow()
localnow = dt_util.as_local(utcnow)
self.assertEqual(localnow, utcnow)
self.assertNotEqual(localnow.tzinfo, utcnow.tzinfo)
def test_utc_from_timestamp(self):
""" Test utc_from_timestamp method. """
self.assertEqual(
datetime(1986, 7, 9, tzinfo=dt_util.UTC),
dt_util.utc_from_timestamp(521251200))
def test_datetime_to_str(self):
""" Test datetime_to_str. """
self.assertEqual(
"12:00:00 09-07-1986",
dt_util.datetime_to_str(datetime(1986, 7, 9, 12, 0, 0)))
def test_datetime_to_local_str(self):
""" Test datetime_to_local_str. """
self.assertEqual(
dt_util.datetime_to_str(dt_util.now()),
dt_util.datetime_to_local_str(dt_util.utcnow()))
def test_str_to_datetime_converts_correctly(self):
""" Test str_to_datetime converts strings. """
self.assertEqual(
datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC),
dt_util.str_to_datetime("12:00:00 09-07-1986"))
def test_str_to_datetime_returns_none_for_incorrect_format(self):
""" Test str_to_datetime returns None if incorrect format. """
self.assertIsNone(dt_util.str_to_datetime("not a datetime string"))
def test_strip_microseconds(self):
test_time = datetime(2015, 1, 1, microsecond=5000)
self.assertNotEqual(0, test_time.microsecond)
self.assertEqual(0, dt_util.strip_microseconds(test_time).microsecond)