Merge branch 'dev' into line-chart-dev

merging in the latest changes
This commit is contained in:
jamespcole 2015-04-04 11:07:28 +11:00
commit 9499a8297a
68 changed files with 1473 additions and 585 deletions

View File

@ -12,21 +12,22 @@ omit =
homeassistant/components/*/zwave.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/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/light/vera.py
homeassistant/components/sensor/vera.py
homeassistant/components/switch/vera.py
homeassistant/components/device_tracker/ddwrt.py
[report]

View File

@ -6,7 +6,7 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho
It offers the following functionality through built-in components:
* Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com))
* Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index))
* Track and control [Philips Hue](http://meethue.com) lights
* Track and control [WeMo switches](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)
* Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast)

View File

@ -1,8 +1,20 @@
homeassistant:
# Omitted values in this section will be auto detected using freegeoip.net
# Location required to calculate the time the sun rises and sets
latitude: 32.87336
longitude: 117.22743
# C for Celcius, F for Fahrenheit
temperature_unit: C
# Pick yours from here:
# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
time_zone: America/Los_Angeles
# Name of the location where Home Assistant is running
name: Home
http:
api_password: mypass
# Set to 1 to enable development mode

View File

@ -15,11 +15,14 @@ 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,
EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL,
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED)
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED,
TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME)
import homeassistant.util as util
DOMAIN = "homeassistant"
@ -49,19 +52,34 @@ class HomeAssistant(object):
self.bus = EventBus(pool)
self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus)
self.config = Config()
# List of loaded components
self.components = []
@property
def components(self):
""" DEPRECATED 3/21/2015. Use hass.config.components """
_LOGGER.warning(
'hass.components is deprecated. Use hass.config.components')
return self.config.components
# Remote.API object pointing at local API
self.local_api = None
@property
def local_api(self):
""" DEPRECATED 3/21/2015. Use hass.config.api """
_LOGGER.warning(
'hass.local_api is deprecated. Use hass.config.api')
return self.config.api
# Directory that holds the configuration
self.config_dir = os.path.join(os.getcwd(), 'config')
@property
def config_dir(self):
""" DEPRECATED 3/18/2015. Use hass.config.config_dir """
_LOGGER.warning(
'hass.config_dir is deprecated. Use hass.config.config_dir')
return self.config.config_dir
def get_config_path(self, path):
""" Returns path to the file within the config dir. """
return os.path.join(self.config_dir, path)
""" DEPRECATED 3/18/2015. Use hass.config.path """
_LOGGER.warning(
'hass.get_config_path is deprecated. Use hass.config.path')
return self.config.path(path)
def start(self):
""" Start home assistant. """
@ -307,19 +325,23 @@ class EventOrigin(enum.Enum):
class Event(object):
""" Represents an event within the Bus. """
__slots__ = ['event_type', 'data', 'origin']
__slots__ = ['event_type', 'data', 'origin', 'time_fired']
def __init__(self, event_type, data=None, origin=EventOrigin.local):
def __init__(self, event_type, data=None, origin=EventOrigin.local,
time_fired=None):
self.event_type = event_type
self.data = data or {}
self.origin = origin
self.time_fired = util.strip_microseconds(
time_fired or dt.datetime.now())
def as_dict(self):
""" Returns a dict representation of this Event. """
return {
'event_type': self.event_type,
'data': dict(self.data),
'origin': str(self.origin)
'origin': str(self.origin),
'time_fired': util.datetime_to_str(self.time_fired),
}
def __repr__(self):
@ -441,7 +463,9 @@ class State(object):
__slots__ = ['entity_id', 'state', 'attributes',
'last_changed', 'last_updated']
def __init__(self, entity_id, state, attributes=None, last_changed=None):
# pylint: disable=too-many-arguments
def __init__(self, entity_id, state, attributes=None, last_changed=None,
last_updated=None):
if not ENTITY_ID_PATTERN.match(entity_id):
raise InvalidEntityFormatError((
"Invalid entity id encountered: {}. "
@ -450,7 +474,7 @@ class State(object):
self.entity_id = entity_id.lower()
self.state = state
self.attributes = attributes or {}
self.last_updated = dt.datetime.now()
self.last_updated = last_updated or dt.datetime.now()
# Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict())
@ -464,6 +488,18 @@ class State(object):
""" Returns domain of this state. """
return util.split_entity_id(self.entity_id)[0]
@property
def object_id(self):
""" Returns object_id of this state. """
return util.split_entity_id(self.entity_id)[1]
@property
def name(self):
""" Name to represent this state. """
return (
self.attributes.get(ATTR_FRIENDLY_NAME) or
self.object_id.replace('_', ' '))
def copy(self):
""" Creates a copy of itself. """
return State(self.entity_id, self.state,
@ -476,7 +512,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_changed': util.datetime_to_str(self.last_changed),
'last_updated': util.datetime_to_str(self.last_updated)}
@classmethod
def from_dict(cls, json_dict):
@ -493,8 +530,13 @@ class State(object):
if last_changed:
last_changed = util.str_to_datetime(last_changed)
last_updated = json_dict.get('last_updated')
if last_updated:
last_updated = util.str_to_datetime(last_updated)
return cls(json_dict['entity_id'], json_dict['state'],
json_dict.get('attributes'), last_changed)
json_dict.get('attributes'), last_changed, last_updated)
def __eq__(self, other):
return (self.__class__ == other.__class__ and
@ -836,6 +878,83 @@ class Timer(threading.Thread):
self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
class Config(object):
""" Configuration settings for Home Assistant. """
# pylint: disable=too-many-instance-attributes
def __init__(self):
self.latitude = None
self.longitude = None
self.temperature_unit = None
self.location_name = None
self.time_zone = None
# List of loaded components
self.components = []
# Remote.API object pointing at local API
self.api = None
# 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/').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):
""" Returns path to the file within the config dir. """
return os.path.join(self.config_dir, path)
def temperature(self, value, unit):
""" Converts temperature to user preferred unit if set. """
if not (unit and self.temperature_unit and
unit != self.temperature_unit):
return value, unit
try:
if unit == TEMP_CELCIUS:
# Convert C to F
return round(float(value) * 1.8 + 32.0, 1), TEMP_FAHRENHEIT
# Convert F to C
return round((float(value)-32.0)/1.8, 1), TEMP_CELCIUS
except ValueError:
# Could not convert value to float
return value, unit
class HomeAssistantError(Exception):
""" General Home Assistant exception occured. """
pass

View File

@ -20,7 +20,10 @@ import homeassistant
import homeassistant.loader as loader
import homeassistant.components as core_components
import homeassistant.components.group as group
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.const import (
EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, TEMP_CELCIUS,
TEMP_FAHRENHEIT)
_LOGGER = logging.getLogger(__name__)
@ -28,21 +31,49 @@ ATTR_COMPONENT = "component"
def setup_component(hass, domain, config=None):
""" Setup a component for Home Assistant. """
# Check if already loaded
if domain in hass.components:
return
""" Setup a component and all its dependencies. """
if domain in hass.config.components:
return True
_ensure_loader_prepared(hass)
if config is None:
config = defaultdict(dict)
components = loader.load_order_component(domain)
# OrderedSet is empty if component or dependencies could not be resolved
if not components:
return False
for component in components:
if component in hass.config.components:
continue
if not _setup_component(hass, component, config):
return False
return True
def _setup_component(hass, domain, config):
""" Setup a component for Home Assistant. """
component = loader.get_component(domain)
missing_deps = [dep for dep in component.DEPENDENCIES
if dep not in hass.config.components]
if missing_deps:
_LOGGER.error(
"Not initializing %s because not all dependencies loaded: %s",
domain, ", ".join(missing_deps))
return False
try:
if component.setup(hass, config):
hass.components.append(component.DOMAIN)
hass.config.components.append(component.DOMAIN)
# Assumption: if a component does not depend on groups
# it communicates with devices
@ -73,6 +104,8 @@ def from_config_dict(config, hass=None):
if hass is None:
hass = homeassistant.HomeAssistant()
process_ha_core_config(hass, config.get(homeassistant.DOMAIN, {}))
enable_logging(hass)
_ensure_loader_prepared(hass)
@ -97,7 +130,7 @@ def from_config_dict(config, hass=None):
# Setup the components
for domain in loader.load_order_components(components):
setup_component(hass, domain, config)
_setup_component(hass, domain, config)
return hass
@ -111,8 +144,8 @@ def from_config_file(config_path, hass=None):
if hass is None:
hass = homeassistant.HomeAssistant()
# Set config dir to directory holding config file
hass.config_dir = os.path.abspath(os.path.dirname(config_path))
# 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
@ -143,13 +176,13 @@ 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.get_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
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
(not err_path_exists and os.access(hass.config_dir, os.W_OK)):
(not err_path_exists and os.access(hass.config.config_dir, os.W_OK)):
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
@ -165,6 +198,26 @@ def enable_logging(hass):
"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. """
for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name'),
(CONF_TIME_ZONE, 'time_zone')):
if key in config:
setattr(hass.config, attr, config[key])
if CONF_TEMPERATURE_UNIT in config:
unit = config[CONF_TEMPERATURE_UNIT]
if unit == 'C':
hass.config.temperature_unit = TEMP_CELCIUS
elif unit == 'F':
hass.config.temperature_unit = TEMP_FAHRENHEIT
hass.config.auto_detect()
def _ensure_loader_prepared(hass):
""" Ensure Home Assistant loader is prepared. """
if not loader.PREPARED:

View File

@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Register the API with the HTTP interface. """
if 'http' not in hass.components:
if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded')
return False
@ -311,4 +311,4 @@ def _handle_delete_api_event_forward(handler, path_match, data):
def _handle_get_api_components(handler, path_match, data):
""" Returns all the loaded components. """
handler.write_json(handler.server.hass.components)
handler.write_json(handler.server.hass.config.components)

View File

@ -83,8 +83,8 @@ def _get_instance(hass):
except KeyError:
_INSTANCES[hass] = Configurator(hass)
if DOMAIN not in hass.components:
hass.components.append(DOMAIN)
if DOMAIN not in hass.config.components:
hass.config.components.append(DOMAIN)
return _INSTANCES[hass]

View File

@ -10,8 +10,7 @@ import homeassistant as ha
import homeassistant.bootstrap as bootstrap
import homeassistant.loader as loader
from homeassistant.const import (
CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID,
CONF_LATITUDE, CONF_LONGITUDE)
CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID)
DOMAIN = "demo"
@ -33,8 +32,6 @@ def setup(hass, config):
hass.states.set('a.Demo_Mode', 'Enabled')
# Setup sun
config[ha.DOMAIN].setdefault(CONF_LATITUDE, '32.87336')
config[ha.DOMAIN].setdefault(CONF_LONGITUDE, '-117.22743')
loader.get_component('sun').setup(hass, config)
# Setup demo platforms

View File

@ -157,7 +157,8 @@ class DeviceTracker(object):
def update_devices(self, now):
""" Update device states based on the found devices. """
self.lock.acquire()
if not self.lock.acquire(False):
return
found_devices = set(dev.upper() for dev in
self.device_scanner.scan_devices())
@ -179,7 +180,7 @@ class DeviceTracker(object):
# Write new devices to known devices file
if not self.invalid_known_devices_file:
known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
try:
# If file does not exist we will write the header too
@ -214,7 +215,7 @@ class DeviceTracker(object):
# pylint: disable=too-many-branches
def _read_known_devices_file(self):
""" Parse and process the known devices file. """
known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
# Return if no known devices file exists
if not os.path.isfile(known_dev_path):

View File

@ -0,0 +1,161 @@
""" Supports scanning a DD-WRT router. """
import logging
from datetime import timedelta
import re
import threading
import requests
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a DdWrt scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = DdWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
# pylint: disable=too-many-instance-attributes
class DdWrtDeviceScanner(object):
""" This class queries a wireless router running DD-WRT firmware
for connected devices. Adapted from Tomato scanner.
"""
def __init__(self, config):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
self.mac2name = None
# Test the router is accessible
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
self.success_init = data is not None
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return self.last_results
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
with self.lock:
# if not initialised and not already scanned and not found
if self.mac2name is None or device not in self.mac2name:
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
if not data:
return
dhcp_leases = data.get('dhcp_leases', None)
if dhcp_leases:
# remove leading and trailing single quotes
cleaned_str = dhcp_leases.strip().strip('"')
elements = cleaned_str.split('","')
num_clients = int(len(elements)/5)
self.mac2name = {}
for idx in range(0, num_clients):
# this is stupid but the data is a single array
# every 5 elements represents one hosts, the MAC
# is the third element and the name is the first
mac_index = (idx * 5) + 2
if mac_index < len(elements):
mac = elements[mac_index]
self.mac2name[mac] = elements[idx * 5]
return self.mac2name.get(device, None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the DdWrt router is up to date.
Returns boolean if scanning successful. """
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
if not data:
return False
if data:
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
# regex's out values so I guess I have to do the same,
# LAME!!!
# remove leading and trailing single quotes
clean_str = active_clients.strip().strip("'")
elements = clean_str.split("','")
num_clients = int(len(elements)/9)
for idx in range(0, num_clients):
# get every 9th element which is the MAC address
index = idx * 9
if index < len(elements):
self.last_results.append(elements[index])
return True
return False
def get_ddwrt_data(self, url):
""" Retrieve data from DD-WRT and return parsed result """
try:
response = requests.get(
url,
auth=(self.username, self.password),
timeout=4)
except requests.exceptions.Timeout:
_LOGGER.exception("Connection to the router timed out")
return
if response.status_code == 200:
return _parse_ddwrt_response(response.text)
elif response.status_code == 401:
# Authentication error
_LOGGER.exception(
"Failed to authenticate, "
"please check your username and password")
return
else:
_LOGGER.error("Invalid response from ddwrt: %s", response)
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 """
return {
key: val for key, val in _DDWRT_DATA_REGEX
.findall(data_str)}

View File

@ -1,7 +1,6 @@
""" Supports scanning using nmap. """
import logging
from datetime import timedelta, datetime
import threading
from collections import namedtuple
import subprocess
import re
@ -54,7 +53,6 @@ class NmapDeviceScanner(object):
def __init__(self, config):
self.last_results = []
self.lock = threading.Lock()
self.hosts = config[CONF_HOSTS]
minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0)
self.home_interval = timedelta(minutes=minutes)
@ -116,28 +114,27 @@ class NmapDeviceScanner(object):
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Scanning")
_LOGGER.info("Scanning")
options = "-F"
exclude_targets = set()
if self.home_interval:
now = datetime.now()
for host in self.last_results:
if host.last_update + self.home_interval > now:
exclude_targets.add(host)
if len(exclude_targets) > 0:
target_list = [t.ip for t in exclude_targets]
options += " --exclude {}".format(",".join(target_list))
options = "-F --host-timeout 5"
exclude_targets = set()
if self.home_interval:
now = datetime.now()
for host in self.last_results:
if host.last_update + self.home_interval > now:
exclude_targets.add(host)
if len(exclude_targets) > 0:
target_list = [t.ip for t in exclude_targets]
options += " --exclude {}".format(",".join(target_list))
nmap = NmapProcess(targets=self.hosts, options=options)
nmap = NmapProcess(targets=self.hosts, options=options)
nmap.run()
nmap.run()
if nmap.rc == 0:
if self._parse_results(nmap.stdout):
self.last_results.extend(exclude_targets)
else:
self.last_results = []
_LOGGER.error(nmap.stderr)
return False
if nmap.rc == 0:
if self._parse_results(nmap.stdout):
self.last_results.extend(exclude_targets)
else:
self.last_results = []
_LOGGER.error(nmap.stderr)
return False

View File

@ -70,12 +70,17 @@ def setup(hass, config):
def new_service_listener(service, info):
""" Called when a new service is found. """
with lock:
component = SERVICE_HANDLERS.get(service)
logger.info("Found new service: %s %s", service, info)
if component and component not in hass.components:
bootstrap.setup_component(hass, component, config)
component = SERVICE_HANDLERS.get(service)
# We do not know how to handle this service
if not component:
return
# This component cannot be setup.
if not bootstrap.setup_component(hass, component, config):
return
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: service,

View File

@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Setup serving the frontend. """
if 'http' not in hass.components:
if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded')
return False

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "a063d1482fd49e9297d64e1329324f1c"
VERSION = "1e004712440afc642a44ad927559587e"

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../resources/moment-js.html">
<polymer-element name="display-time" attributes="dateObj">
<template>
{{ time }}
</template>
<script>
(function() {
var timeFormatOptions = {hour: 'numeric', minute: '2-digit'};
Polymer({
time: "",
dateObjChanged: function(oldVal, newVal) {
if (!newVal) {
this.time = "";
}
this.time = newVal.toLocaleTimeString([], timeFormatOptions);
},
});
})();
</script>
</polymer-element>

View File

@ -0,0 +1,17 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../components/logbook-entry.html">
<polymer-element name="ha-logbook" attributes="entries" noscript>
<template>
<style>
.logbook {
}
</style>
<div class='logbook'>
<template repeat="{{entries as entry}}">
<logbook-entry entryObj="{{entry}}"></logbook-entry>
</template>
</div>
</template>
</polymer>

View File

@ -0,0 +1,61 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="domain-icon.html">
<link rel="import" href="display-time.html">
<link rel="import" href="relative-ha-datetime.html">
<polymer-element name="logbook-entry" attributes="entryObj">
<template>
<core-style ref='ha-main'></core-style>
<style>
.logbook-entry {
line-height: 2em;
}
.time {
width: 55px;
font-size: .8em;
}
.icon {
margin: 0 8px 0 16px;
}
.name {
text-transform: capitalize;
}
.message {
}
</style>
<div horizontal layout class='logbook-entry'>
<display-time dateObj="{{entryObj.when}}" class='time secondary-text-color'></display-time>
<domain-icon domain="{{entryObj.domain}}" class='icon primary-text-color'></domain-icon>
<div class='message primary-text-color' flex>
<template if="{{!entryObj.entityId}}">
<span class='name'>{{entryObj.name}}</span>
</template>
<template if="{{entryObj.entityId}}">
<a href='#' on-click="{{entityClicked}}" class='name'>{{entryObj.name}}</a>
</template>
{{entryObj.message}}
</div>
</div>
</template>
<script>
(function() {
var uiActions = window.hass.uiActions;
Polymer({
entityClicked: function(ev) {
ev.preventDefault();
uiActions.showMoreInfoDialog(this.entryObj.entityId);
}
});
})();
</script>
</polymer-element>

View File

@ -7,12 +7,14 @@
<polymer-element name="more-info-dialog">
<template>
<ha-dialog id="dialog">
<ha-dialog id="dialog" on-core-overlay-open="{{dialogOpenChanged}}">
<div>
<state-card-content stateObj="{{stateObj}}" style='margin-bottom: 24px;'>
</state-card-content>
<state-timeline stateHistory="{{stateHistory}}"></state-timeline>
<more-info-content stateObj="{{stateObj}}"></more-info-content>
<more-info-content
stateObj="{{stateObj}}"
dialogOpen="{{dialogOpen}}"></more-info-content>
</div>
</ha-dialog>
</template>
@ -27,11 +29,16 @@ Polymer(Polymer.mixin({
stateObj: null,
stateHistory: null,
hasHistoryComponent: false,
dialogOpen: false,
observe: {
'stateObj.attributes': 'reposition'
},
created: function() {
this.dialogOpenChanged = this.dialogOpenChanged.bind(this);
},
attached: function() {
this.listenToStores(true);
},
@ -66,6 +73,13 @@ Polymer(Polymer.mixin({
}
},
dialogOpenChanged: function(ev) {
// we get CustomEvent, undefined and true/false from polymer…
if (typeof ev === 'object') {
this.dialogOpen = ev.detail;
}
},
changeEntityId: function(entityId) {
this.entityId = entityId;

View File

@ -10,6 +10,7 @@
<link rel="import" href="../layouts/partial-states.html">
<link rel="import" href="../layouts/partial-history.html">
<link rel="import" href="../layouts/partial-logbook.html">
<link rel="import" href="../layouts/partial-dev-fire-event.html">
<link rel="import" href="../layouts/partial-dev-call-service.html">
<link rel="import" href="../layouts/partial-dev-set-state.html">
@ -96,6 +97,13 @@
</paper-item>
</template>
<template if="{{hasLogbookComponent}}">
<paper-item data-panel="logbook">
<core-icon icon="list"></core-icon>
Logbook
</paper-item>
</template>
<div flex></div>
<paper-item on-click="{{handleLogOutClick}}">
@ -136,6 +144,9 @@
<template if="{{selected == 'history'}}">
<partial-history main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-history>
</template>
<template if="{{selected == 'logbook'}}">
<partial-logbook main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-logbook>
</template>
<template if="{{selected == 'fire-event'}}">
<partial-dev-fire-event main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-fire-event>
</template>
@ -161,6 +172,7 @@ Polymer(Polymer.mixin({
narrow: false,
activeFilters: [],
hasHistoryComponent: false,
hasLogbookComponent: false,
isStreaming: false,
hasStreamError: false,
@ -185,7 +197,7 @@ Polymer(Polymer.mixin({
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
this.hasScriptComponent = componentStore.isLoaded('script');
this.hasLogbookComponent = componentStore.isLoaded('logbook');
},
streamStoreChanged: function(streamStore) {

View File

@ -0,0 +1,56 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="./partial-base.html">
<link rel="import" href="../components/ha-logbook.html">
<polymer-element name="partial-logbook" attributes="narrow togglePanel">
<template>
<style>
.content {
background-color: white;
padding: 8px;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>Logbook</span>
<span header-buttons>
<paper-icon-button icon="refresh"
on-click="{{handleRefreshClick}}"></paper-icon-button>
</span>
<div flex class="{{ {content: true, narrow: narrow, wide: !narrow} | tokenList }}">
<ha-logbook entries="{{entries}}"></ha-logbook>
</div>
</partial-base>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
var logbookActions = window.hass.logbookActions;
Polymer(Polymer.mixin({
entries: null,
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
logbookStoreChanged: function(logbookStore) {
if (logbookStore.isStale()) {
logbookActions.fetch();
}
this.entries = logbookStore.all.toArray();
},
handleRefreshClick: function() {
logbookActions.fetch();
},
}, storeListenerMixIn));
</script>
</polymer>

View File

@ -8,7 +8,7 @@
<link rel="import" href="more-info-thermostat.html">
<link rel="import" href="more-info-script.html">
<polymer-element name="more-info-content" attributes="stateObj">
<polymer-element name="more-info-content" attributes="stateObj dialogOpen">
<template>
<style>
:host {
@ -20,11 +20,20 @@
<script>
Polymer({
classNames: '',
dialogOpen: false,
observe: {
'stateObj.attributes': 'stateAttributesChanged',
},
dialogOpenChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer;
if (moreInfoContainer.lastChild) {
moreInfoContainer.lastChild.dialogOpen = newVal;
}
},
stateObjChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer;
@ -42,10 +51,12 @@ Polymer({
var moreInfo = document.createElement("more-info-" + newVal.moreInfoType);
moreInfo.stateObj = newVal;
moreInfo.dialogOpen = this.dialogOpen;
moreInfoContainer.appendChild(moreInfo);
} else {
moreInfoContainer.lastChild.dialogOpen = this.dialogOpen;
moreInfoContainer.lastChild.stateObj = newVal;
}

View File

@ -51,7 +51,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
case "media_player":
var icon = "hardware:cast";
if (state !== "idle") {
if (state && state !== "idle") {
icon += "-connected";
}

View File

@ -1,5 +1,30 @@
<link rel="import" href="../bower_components/core-style/core-style.html">
<core-style id='ha-main'>
/* Palette generated by Material Palette - materialpalette.com/light-blue/orange */
.dark-primary-color { background: #0288D1; }
.default-primary-color { background: #03A9F4; }
.light-primary-color { background: #B3E5FC; }
.text-primary-color { color: #FFFFFF; }
.accent-color { background: #FF9800; }
.primary-text-color { color: #212121; }
.secondary-text-color { color: #727272; }
.divider-color { border-color: #B6B6B6; }
/* extra */
.accent-text-color { color: #FF9800; }
body {
color: #212121;
}
a {
color: #FF9800;
text-decoration: none;
}
</core-style>
<core-style id='ha-animations'>
@-webkit-keyframes ha-spin {
0% {

View File

@ -67,6 +67,10 @@ def get_states(point_in_time, entity_ids=None, run=None):
if run is None:
run = recorder.run_information(point_in_time)
# History did not run before point_in_time
if run is None:
return []
where = run.where_after_start_run + "AND created < ? "
where_data = [point_in_time]

View File

@ -135,7 +135,7 @@ def setup(hass, config=None):
threading.Thread(target=server.start, daemon=True).start())
hass.http = server
hass.local_api = rem.API(util.get_local_ip(), api_password, server_port)
hass.config.api = rem.API(util.get_local_ip(), api_password, server_port)
return True

View File

@ -52,7 +52,7 @@ import logging
import os
import csv
from homeassistant.helpers.device_component import DeviceComponent
from homeassistant.helpers.entity_component import EntityComponent
import homeassistant.util as util
from homeassistant.const import (
@ -140,7 +140,7 @@ def turn_off(hass, entity_id=None, transition=None):
def setup(hass, config):
""" Exposes light control via statemachine and services. """
component = DeviceComponent(
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_LIGHTS)
component.setup(config)
@ -148,7 +148,7 @@ def setup(hass, config):
# Load built-in profiles and custom profiles
profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE),
hass.get_config_path(LIGHT_PROFILES_FILE)]
hass.config.path(LIGHT_PROFILES_FILE)]
profiles = {}
for profile_path in profile_paths:

View File

@ -1,7 +1,7 @@
""" Provides demo lights. """
import random
from homeassistant.helpers.device import ToggleDevice
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_XY_COLOR
@ -22,7 +22,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
])
class DemoLight(ToggleDevice):
class DemoLight(ToggleEntity):
""" Provides a demo switch. """
def __init__(self, name, state, xy=None, brightness=180):
self._name = name or DEVICE_DEFAULT_NAME

View File

@ -6,7 +6,7 @@ from urllib.parse import urlparse
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.helpers.device import ToggleDevice
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import CONF_HOST
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
@ -51,7 +51,8 @@ def setup_bridge(host, hass, add_devices_callback):
try:
bridge = phue.Bridge(
host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE))
host,
config_file_path=hass.config.path(PHUE_CONFIG_FILE))
except ConnectionRefusedError: # Wrong host was given
_LOGGER.exception("Error connecting to the Hue bridge at %s", host)
@ -130,7 +131,7 @@ def request_configuration(host, hass, add_devices_callback):
)
class HueLight(ToggleDevice):
class HueLight(ToggleEntity):
""" Represents a Hue light """
def __init__(self, light_id, info, bridge, update_lights):

View File

@ -3,7 +3,7 @@ import logging
# pylint: disable=no-name-in-module, import-error
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.device import ToggleDevice
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
@ -27,7 +27,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(lights)
class TellstickLight(ToggleDevice):
class TellstickLight(ToggleEntity):
""" Represents a tellstick light """
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF |

View File

@ -0,0 +1,185 @@
"""
homeassistant.components.logbook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.components.recorder as recorder
import homeassistant.components.sun as sun
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = '/api/logbook'
QUERY_EVENTS_AFTER = "SELECT * FROM events WHERE time_fired > ?"
QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
ORDER BY time_fired
"""
GROUP_BY_MINUTES = 15
def setup(hass, config):
""" Listens for download events to download files. """
hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook)
return True
def _handle_get_logbook(handler, path_match, data):
""" Return logbook entries. """
start_today = datetime.now().date()
handler.write_json(humanify(
recorder.query_events(QUERY_EVENTS_AFTER, (start_today,))))
class Entry(object):
""" A human readable version of the log. """
# pylint: disable=too-many-arguments, too-few-public-methods
def __init__(self, when=None, name=None, message=None, domain=None,
entity_id=None):
self.when = when
self.name = name
self.message = message
self.domain = domain
self.entity_id = entity_id
def as_dict(self):
""" Convert Entry to a dict to be used within JSON. """
return {
'when': util.datetime_to_str(self.when),
'name': self.name,
'message': self.message,
'domain': self.domain,
'entity_id': self.entity_id,
}
def humanify(events):
"""
Generator that converts a list of events into Entry objects.
Will try to group events if possible:
- if 2+ sensor updates in GROUP_BY_MINUTES, show last
- if home assistant stop and start happen in same minute call it restarted
"""
# pylint: disable=too-many-branches
# Group events in batches of GROUP_BY_MINUTES
for _, g_events in groupby(
events,
lambda event: event.time_fired.minute // GROUP_BY_MINUTES):
events_batch = list(g_events)
# Keep track of last sensor states
last_sensor_event = {}
# group HA start/stop events
# Maps minute of event to 1: stop, 2: stop + start
start_stop_events = {}
# Process events
for event in events_batch:
if event.event_type == EVENT_STATE_CHANGED:
entity_id = event.data['entity_id']
if entity_id.startswith('sensor.'):
last_sensor_event[entity_id] = event
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
if event.time_fired.minute in start_stop_events:
continue
start_stop_events[event.time_fired.minute] = 1
elif event.event_type == EVENT_HOMEASSISTANT_START:
if event.time_fired.minute not in start_stop_events:
continue
start_stop_events[event.time_fired.minute] = 2
# Yield entries
for event in events_batch:
if event.event_type == EVENT_STATE_CHANGED:
# Do not report on new entities
if 'old_state' not in event.data:
continue
to_state = State.from_dict(event.data.get('new_state'))
# if last_changed == last_updated only attributes have changed
# we do not report on that yet.
if not to_state or \
to_state.last_changed != to_state.last_updated:
continue
domain = to_state.domain
# Skip all but the last sensor state
if domain == 'sensor' and \
event != last_sensor_event[to_state.entity_id]:
continue
yield Entry(
event.time_fired,
name=to_state.name,
message=_entry_message_from_state(domain, to_state),
domain=domain,
entity_id=to_state.entity_id)
elif event.event_type == EVENT_HOMEASSISTANT_START:
if start_stop_events.get(event.time_fired.minute) == 2:
continue
yield Entry(
event.time_fired, "Home Assistant", "started",
domain=HA_DOMAIN)
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
if start_stop_events.get(event.time_fired.minute) == 2:
action = "restarted"
else:
action = "stopped"
yield Entry(
event.time_fired, "Home Assistant", action,
domain=HA_DOMAIN)
def _entry_message_from_state(domain, state):
""" Convert a state to a message for the logbook. """
# We pass domain in so we don't have to split entity_id again
if domain == 'device_tracker':
return '{} home'.format(
'arrived' if state.state == STATE_HOME else 'left')
elif domain == 'sun':
if state.state == sun.STATE_ABOVE_HORIZON:
return 'has risen'
else:
return 'has set'
elif state.state == STATE_ON:
# Future: combine groups and its entity entries ?
return "turned on"
elif state.state == STATE_OFF:
return "turned off"
return "changed to {}".format(state.state)

View File

@ -7,8 +7,8 @@ Component to interface with various media players
import logging
from homeassistant.components import discovery
from homeassistant.helpers.device import Device
from homeassistant.helpers.device_component import DeviceComponent
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
@ -126,7 +126,7 @@ SERVICE_TO_METHOD = {
def setup(hass, config):
""" Track states and offer events for media_players. """
component = DeviceComponent(
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
@ -171,7 +171,7 @@ def setup(hass, config):
return True
class MediaPlayerDevice(Device):
class MediaPlayerDevice(Entity):
""" ABC for media player devices. """
def turn_off(self):

View File

@ -0,0 +1,98 @@
"""
Pushover platform for notify component.
Configuration:
To use the Pushover notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: pushover
api_key: ABCDEFGHJKLMNOPQRSTUVXYZ
user_key: ABCDEFGHJKLMNOPQRSTUVXYZ
VARIABLES:
api_key
*Required
This parameter is optional but should be configured, in order to get an API
key you should go to https://pushover.net and register a new application.
This is a quote from the pushover website regarding free/open source apps:
"If you are creating a client-side library, application, or open source project
that will be redistributed and installed by end-users, you may want to require
each of your users to register their own application rather than including your
own API token with the software."
When setting up the application I recommend using the icon located here:
https://home-assistant.io/images/favicon-192x192.png
user_key
*Required
To retrieve this value log into your account at https://pushover.net
"""
import logging
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__)
# pylint: disable=unused-variable
def get_service(hass, config):
""" Get the pushover notification service. """
if not validate_config(config,
{DOMAIN: ['user_key', CONF_API_KEY]},
_LOGGER):
return None
try:
# pylint: disable=no-name-in-module, unused-variable
from pushover import InitError
except ImportError:
_LOGGER.exception(
"Unable to import pushover. "
"Did you maybe not install the 'python-pushover.py' package?")
return None
try:
api_token = config[DOMAIN].get(CONF_API_KEY)
return PushoverNotificationService(
config[DOMAIN]['user_key'],
api_token)
except InitError:
_LOGGER.error(
"Wrong API key supplied. "
"Get it at https://pushover.net")
# pylint: disable=too-few-public-methods
class PushoverNotificationService(BaseNotificationService):
""" Implements notification service for Pushover. """
def __init__(self, user_key, api_token):
# pylint: disable=no-name-in-module, unused-variable
from pushover import Client
self._user_key = user_key
self._api_token = api_token
self.pushover = Client(
self._user_key, api_token=self._api_token)
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
# pylint: disable=no-name-in-module
from pushover import RequestError
title = kwargs.get(ATTR_TITLE)
try:
self.pushover.send_message(message, title=title)
except RequestError:
_LOGGER.exception("Could not send pushover notification")

View File

@ -9,7 +9,7 @@ import logging
import threading
import queue
import sqlite3
from datetime import datetime
from datetime import datetime, date
import time
import json
import atexit
@ -60,7 +60,8 @@ 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]))
row[1], row[2], json.loads(row[3]), datetime.fromtimestamp(row[4]),
datetime.fromtimestamp(row[5]))
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to state: %s", row)
@ -70,9 +71,10 @@ def row_to_state(row):
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()])
return Event(row[1], json.loads(row[2]), EventOrigin[row[3].lower()],
datetime.fromtimestamp(row[5]))
except ValueError:
# When json.oads fails
# When json.loads fails
_LOGGER.exception("Error converting row to event: %s", row)
return None
@ -86,7 +88,7 @@ def run_information(point_in_time=None):
return RecorderRun()
run = _INSTANCE.query(
"SELECT * FROM recorder_runs WHERE start>? AND END IS NULL OR END<?",
"SELECT * FROM recorder_runs WHERE start<? AND END>?",
(point_in_time, point_in_time), return_value=RETURN_ONE_ROW)
return RecorderRun(run) if run else None
@ -225,13 +227,13 @@ class Recorder(threading.Thread):
""" Save an event to the database. """
info = (
event.event_type, json.dumps(event.data, cls=JSONEncoder),
str(event.origin), datetime.now()
str(event.origin), datetime.now(), event.time_fired,
)
self.query(
"INSERT INTO events ("
"event_type, event_data, origin, created"
") VALUES (?, ?, ?, ?)", info)
"event_type, event_data, origin, created, time_fired"
") VALUES (?, ?, ?, ?, ?)", info)
def query(self, sql_query, data=None, return_value=None):
""" Query the database. """
@ -262,7 +264,7 @@ class Recorder(threading.Thread):
def _setup_connection(self):
""" Ensure database is ready to fly. """
db_path = self.hass.get_config_path(DB_FILE)
db_path = self.hass.config.path(DB_FILE)
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row
@ -271,6 +273,7 @@ class Recorder(threading.Thread):
atexit.register(self._close_connection)
# Have datetime objects be saved as integers
sqlite3.register_adapter(date, _adapt_datetime)
sqlite3.register_adapter(datetime, _adapt_datetime)
# Validate we are on the correct schema or that we have to migrate
@ -328,6 +331,16 @@ class Recorder(threading.Thread):
save_migration(1)
if migration_id < 2:
cur.execute("""
ALTER TABLE events
ADD COLUMN time_fired integer
""")
cur.execute('UPDATE events SET time_fired=created')
save_migration(2)
def _close_connection(self):
""" Close connection to the database. """
_LOGGER.info("Closing database")

View File

@ -19,8 +19,8 @@ import logging
from collections import namedtuple
from homeassistant import State
from homeassistant.helpers.device import ToggleDevice
from homeassistant.helpers.device_component import DeviceComponent
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.state import reproduce_state
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF)
@ -46,10 +46,10 @@ def setup(hass, config):
logger.error('Scene config should be a list of scenes')
return False
component = DeviceComponent(logger, DOMAIN, hass)
component = EntityComponent(logger, DOMAIN, hass)
component.add_devices(Scene(hass, _process_config(scene_config))
for scene_config in scene_configs)
component.add_entities(Scene(hass, _process_config(scene_config))
for scene_config in scene_configs)
def handle_scene_service(service):
""" Handles calls to the switch services. """
@ -93,7 +93,7 @@ def _process_config(scene_config):
return SceneConfig(name, states)
class Scene(ToggleDevice):
class Scene(ToggleEntity):
""" A scene is a group of entities and the states we want them to be. """
def __init__(self, hass, scene_config):

View File

@ -35,9 +35,6 @@ _SCHEDULE_FILE = 'schedule.json'
def setup(hass, config):
""" Create the schedules """
if DOMAIN in hass.components:
return True
def setup_listener(schedule, event_data):
""" Creates the event listener based on event_data """
event_type = event_data['type']
@ -47,9 +44,7 @@ def setup(hass, config):
if event_type in ['time']:
component = 'scheduler.{}'.format(event_type)
elif component not in hass.components and \
not bootstrap.setup_component(hass, component, config):
elif not bootstrap.setup_component(hass, component, config):
_LOGGER.warn("Could setup event listener for %s", component)
return None
@ -74,7 +69,7 @@ def setup(hass, config):
schedule.schedule(hass)
return True
with open(hass.get_config_path(_SCHEDULE_FILE)) as schedule_file:
with open(hass.config.path(_SCHEDULE_FILE)) as schedule_file:
schedule_descriptions = json.load(schedule_file)
for schedule_description in schedule_descriptions:

View File

@ -5,7 +5,7 @@ Component to interface with various sensors that can be monitored.
"""
import logging
from homeassistant.helpers.device_component import DeviceComponent
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import wink, zwave
DOMAIN = 'sensor'
@ -23,7 +23,7 @@ DISCOVERY_PLATFORMS = {
def setup(hass, config):
""" Track states and offer events for sensors. """
component = DeviceComponent(
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)

View File

@ -1,25 +1,25 @@
""" Support for Wink sensors. """
from homeassistant.helpers.device import Device
from homeassistant.const import (
TEMP_CELCIUS, ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME)
from homeassistant.helpers.entity import Entity
from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Demo sensors. """
add_devices([
DemoSensor('Outside Temperature', 15.6, TEMP_CELCIUS),
DemoSensor('Outside Humidity', 54, '%'),
DemoSensor('Outside Temperature', 15.6, TEMP_CELCIUS, 12),
DemoSensor('Outside Humidity', 54, '%', None),
])
class DemoSensor(Device):
class DemoSensor(Entity):
""" A Demo sensor. """
def __init__(self, name, state, unit_of_measurement):
def __init__(self, name, state, unit_of_measurement, battery):
self._name = name
self._state = state
self._unit_of_measurement = unit_of_measurement
self._battery = battery
@property
def should_poll(self):
@ -36,10 +36,15 @@ class DemoSensor(Device):
""" Returns the state of the device. """
return self._state
@property
def unit_of_measurement(self):
""" Unit this state is expressed in. """
return self._unit_of_measurement
@property
def state_attributes(self):
""" Returns the state attributes. """
return {
ATTR_FRIENDLY_NAME: self._name,
ATTR_UNIT_OF_MEASUREMENT: self._unit_of_measurement,
}
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery,
}

View File

@ -52,12 +52,11 @@ list of all available variables
from homeassistant.util import Throttle
from datetime import timedelta
from homeassistant.helpers.device import Device
from homeassistant.helpers.entity import Entity
# pylint: disable=no-name-in-module, import-error
from homeassistant.external.nzbclients.sabnzbd import SabnzbdApi
from homeassistant.external.nzbclients.sabnzbd import SabnzbdApiException
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME)
import logging
SENSOR_TYPES = {
@ -109,7 +108,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(dev)
class SabnzbdSensor(Device):
class SabnzbdSensor(Entity):
""" A Sabnzbd sensor """
def __init__(self, sensor_type, sabnzb_client, client_name):
@ -118,7 +117,7 @@ class SabnzbdSensor(Device):
self.type = sensor_type
self.client_name = client_name
self._state = None
self.unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@property
def name(self):
@ -130,12 +129,9 @@ class SabnzbdSensor(Device):
return self._state
@property
def state_attributes(self):
""" Returns the state attributes. """
return {
ATTR_FRIENDLY_NAME: self.name,
ATTR_UNIT_OF_MEASUREMENT: self.unit_of_measurement,
}
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._unit_of_measurement
def refresh_sabnzbd_data(self):
""" Calls the throttled SABnzbd refresh method. """

View File

@ -6,9 +6,8 @@ Shows system monitor values such as: disk, memory and processor use
"""
from homeassistant.helpers.device import Device
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF)
from homeassistant.helpers.entity import Entity
from homeassistant.const import STATE_ON, STATE_OFF
import psutil
import logging
@ -43,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(dev)
class SystemMonitorSensor(Device):
class SystemMonitorSensor(Entity):
""" A system monitor sensor """
def __init__(self, sensor_type, argument=''):
@ -51,7 +50,7 @@ class SystemMonitorSensor(Device):
self.argument = argument
self.type = sensor_type
self._state = None
self.unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self.update()
@property
@ -64,12 +63,8 @@ class SystemMonitorSensor(Device):
return self._state
@property
def state_attributes(self):
""" Returns the state attributes. """
return {
ATTR_FRIENDLY_NAME: self.name,
ATTR_UNIT_OF_MEASUREMENT: self.unit_of_measurement,
}
def unit_of_measurement(self):
return self._unit_of_measurement
def update(self):
if self.type == 'disk_use_percent':

View File

@ -29,9 +29,8 @@ from collections import namedtuple
import tellcore.telldus as telldus
import tellcore.constants as tellcore_constants
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELCIUS)
from homeassistant.helpers.device import Device
from homeassistant.const import TEMP_CELCIUS
from homeassistant.helpers.entity import Entity
import homeassistant.util as util
DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit'])
@ -76,7 +75,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for ts_sensor in core.sensors():
try:
sensor_name = config[str(ts_sensor.id)]
sensor_name = config[ts_sensor.id]
except KeyError:
if 'only_named' in config:
continue
@ -94,13 +93,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(sensors)
class TellstickSensor(Device):
class TellstickSensor(Entity):
""" Represents a Tellstick sensor. """
def __init__(self, name, sensor, datatype, sensor_info):
self.datatype = datatype
self.sensor = sensor
self.unit = sensor_info.unit or None
self._unit_of_measurement = sensor_info.unit or None
self._name = "{} {}".format(name, sensor_info.name)
@ -115,13 +114,5 @@ class TellstickSensor(Device):
return self.sensor.value(self.datatype).value
@property
def state_attributes(self):
""" Returns the state attributes. """
attrs = {
ATTR_FRIENDLY_NAME: self._name,
}
if self.unit:
attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit
return attrs
def unit_of_measurement(self):
return self._unit_of_measurement

View File

@ -50,9 +50,10 @@ import logging
import time
from requests.exceptions import RequestException
from homeassistant.helpers import Device
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME)
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME,
TEMP_CELCIUS, TEMP_FAHRENHEIT)
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.vera.vera as veraApi
@ -99,7 +100,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(get_devices(hass, config))
class VeraSensor(Device):
class VeraSensor(Entity):
""" Represents a Vera Sensor """
def __init__(self, vera_device, extra_data=None):
@ -110,6 +111,7 @@ class VeraSensor(Device):
else:
self._name = self.vera_device.name
self.current_value = ''
self._temperature_units = None
def __str__(self):
return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state)
@ -123,6 +125,11 @@ class VeraSensor(Device):
""" Get the mame of the sensor. """
return self._name
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._temperature_units
@property
def state_attributes(self):
attr = super().state_attributes
@ -151,7 +158,20 @@ class VeraSensor(Device):
self.vera_device.refresh_value('CurrentTemperature')
current_temp = self.vera_device.get_value('CurrentTemperature')
vera_temp_units = self.vera_device.veraController.temperature_units
self.current_value = current_temp + '°' + vera_temp_units
if vera_temp_units == 'F':
self._temperature_units = TEMP_FAHRENHEIT
else:
self._temperature_units = TEMP_CELCIUS
if self.hass:
temp = self.hass.config.temperature(
current_temp,
self._temperature_units)
current_temp, self._temperature_units = temp
self.current_value = current_temp
elif self.vera_device.category == "Light Sensor":
self.vera_device.refresh_value('CurrentLevel')
self.current_value = self.vera_device.get_value('CurrentLevel')

View File

@ -4,8 +4,8 @@ import logging
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.wink.pywink as pywink
from homeassistant.components.wink import WinkSensorDevice
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
def setup_platform(hass, config, add_devices, discovery_info=None):
@ -22,3 +22,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pywink.set_bearer_token(token)
add_devices(WinkSensorDevice(sensor) for sensor in pywink.get_sensors())
class WinkSensorDevice(Entity):
""" represents a wink sensor within home assistant. """
def __init__(self, wink):
self.wink = wink
@property
def state(self):
""" Returns the state. """
return STATE_OPEN if self.is_open else STATE_CLOSED
@property
def unique_id(self):
""" Returns the id of this wink sensor """
return "{}.{}".format(self.__class__, self.wink.deviceId())
@property
def name(self):
""" Returns the name of the sensor if any. """
return self.wink.name()
def update(self):
""" Update state of the sensor. """
self.wink.updateState()
@property
def is_open(self):
""" True if door is open. """
return self.wink.state()

View File

@ -9,9 +9,9 @@ from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
import homeassistant.components.zwave as zwave
from homeassistant.helpers.device import Device
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF,
ATTR_BATTERY_LEVEL, STATE_ON, STATE_OFF,
TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_LOCATION)
@ -33,7 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ZWaveMultilevelSensor(value)])
class ZWaveSensor(Device):
class ZWaveSensor(Entity):
""" Represents a Z-Wave sensor. """
def __init__(self, sensor_value):
self._value = sensor_value
@ -77,11 +77,6 @@ class ZWaveSensor(Device):
if battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = battery_level
unit = self.unit
if unit:
attrs[ATTR_UNIT_OF_MEASUREMENT] = unit
location = self._node.location
if location:
@ -90,8 +85,7 @@ class ZWaveSensor(Device):
return attrs
@property
def unit(self):
""" Unit if sensor has one. """
def unit_of_measurement(self):
return self._value.units
def _value_changed(self, value):
@ -126,8 +120,7 @@ class ZWaveMultilevelSensor(ZWaveSensor):
return value
@property
def unit(self):
""" Unit of this sensor. """
def unit_of_measurement(self):
unit = self._value.units
if unit == 'C':

View File

@ -24,9 +24,6 @@ which event (sunset or sunrise) and the offset.
import logging
from datetime import datetime, timedelta
import homeassistant as ha
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.helpers import validate_config
from homeassistant.util import str_to_datetime, datetime_to_str
from homeassistant.components.scheduler import ServiceEventListener
@ -83,11 +80,6 @@ def setup(hass, config):
""" Tracks the state of the sun. """
logger = logging.getLogger(__name__)
if not validate_config(config,
{ha.DOMAIN: [CONF_LATITUDE, CONF_LONGITUDE]},
logger):
return False
try:
import ephem
except ImportError:
@ -96,8 +88,8 @@ def setup(hass, config):
sun = ephem.Sun() # pylint: disable=no-member
latitude = str(config[ha.DOMAIN][CONF_LATITUDE])
longitude = str(config[ha.DOMAIN][CONF_LONGITUDE])
latitude = str(hass.config.latitude)
longitude = str(hass.config.longitude)
# Validate latitude and longitude
observer = ephem.Observer()

View File

@ -6,7 +6,7 @@ Component to interface with various switches that can be controlled remotely.
import logging
from datetime import timedelta
from homeassistant.helpers.device_component import DeviceComponent
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
@ -58,7 +58,7 @@ def turn_off(hass, entity_id=None):
def setup(hass, config):
""" Track states and offer events for switches. """
component = DeviceComponent(
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_SWITCHES)
component.setup(config)

View File

@ -1,5 +1,5 @@
""" Demo platform that has two fake switchces. """
from homeassistant.helpers.device import ToggleDevice
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
@ -12,7 +12,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
])
class DemoSwitch(ToggleDevice):
class DemoSwitch(ToggleEntity):
""" Provides a demo switch. """
def __init__(self, name, state):
self._name = name or DEVICE_DEFAULT_NAME

View File

@ -3,7 +3,7 @@ import logging
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.device import ToggleDevice
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(switches)
class TellstickSwitchDevice(ToggleDevice):
class TellstickSwitchDevice(ToggleEntity):
""" represents a Tellstick switch within home assistant. """
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF)

View File

@ -52,7 +52,7 @@ import logging
import time
from requests.exceptions import RequestException
from homeassistant.helpers import ToggleDevice
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME)
# pylint: disable=no-name-in-module, import-error
@ -100,7 +100,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(get_devices(hass, config))
class VeraSwitch(ToggleDevice):
class VeraSwitch(ToggleEntity):
""" Represents a Vera Switch """
def __init__(self, vera_device, extra_data=None):

View File

@ -1,7 +1,7 @@
""" Support for WeMo switchces. """
import logging
from homeassistant.helpers.device import ToggleDevice
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.switch import (
ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH)
@ -38,7 +38,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if isinstance(switch, pywemo.Switch)])
class WemoSwitch(ToggleDevice):
class WemoSwitch(ToggleEntity):
""" represents a WeMo switch within home assistant. """
def __init__(self, wemo):
self.wemo = wemo

View File

@ -6,13 +6,12 @@ Provides functionality to interact with thermostats.
"""
import logging
from homeassistant.helpers.device_component import DeviceComponent
from homeassistant.helpers.entity_component import EntityComponent
import homeassistant.util as util
from homeassistant.helpers.device import Device
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT,
STATE_ON, STATE_OFF)
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF)
DOMAIN = "thermostat"
DEPENDENCIES = []
@ -53,7 +52,7 @@ def set_temperature(hass, temperature, entity_id=None):
def setup(hass, config):
""" Setup thermostats. """
component = DeviceComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
def thermostat_service(service):
@ -99,7 +98,7 @@ def setup(hass, config):
return True
class ThermostatDevice(Device):
class ThermostatDevice(Entity):
""" Represents a thermostat within Home Assistant. """
# pylint: disable=no-self-use
@ -109,11 +108,6 @@ class ThermostatDevice(Device):
""" Returns the current state. """
return self.target_temperature
@property
def unit_of_measurement(self):
""" Returns the unit of measurement. """
return ""
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
@ -123,8 +117,8 @@ class ThermostatDevice(Device):
def state_attributes(self):
""" Returns optional state attributes. """
data = {
ATTR_UNIT_OF_MEASUREMENT: self.unit_of_measurement,
ATTR_CURRENT_TEMPERATURE: self.current_temperature
ATTR_CURRENT_TEMPERATURE: self.hass.config.temperature(
self.current_temperature, self.unit_of_measurement)[0]
}
is_away = self.is_away_mode_on

View File

@ -8,10 +8,10 @@ import homeassistant.external.wink.pywink as pywink
from homeassistant import bootstrap
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config, ToggleDevice, Device
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN,
STATE_OPEN, STATE_CLOSED,
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
DOMAIN = "wink"
@ -41,8 +41,7 @@ def setup(hass, config):
component = get_component(component_name)
# Ensure component is loaded
if component.DOMAIN not in hass.components:
bootstrap.setup_component(hass, component.DOMAIN, config)
bootstrap.setup_component(hass, component.DOMAIN, config)
# Fire discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
@ -53,45 +52,7 @@ def setup(hass, config):
return True
class WinkSensorDevice(Device):
""" represents a wink sensor within home assistant. """
def __init__(self, wink):
self.wink = wink
@property
def state(self):
""" Returns the state. """
return STATE_OPEN if self.is_open else STATE_CLOSED
@property
def unique_id(self):
""" Returns the id of this wink switch """
return "{}.{}".format(self.__class__, self.wink.deviceId())
@property
def name(self):
""" Returns the name of the sensor if any. """
return self.wink.name()
@property
def state_attributes(self):
""" Returns optional state attributes. """
return {
ATTR_FRIENDLY_NAME: self.wink.name()
}
def update(self):
""" Update state of the sensor. """
self.wink.updateState()
@property
def is_open(self):
""" True if door is open. """
return self.wink.state()
class WinkToggleDevice(ToggleDevice):
class WinkToggleDevice(ToggleEntity):
""" represents a Wink switch within home assistant. """
def __init__(self, wink):

View File

@ -72,7 +72,7 @@ def setup(hass, config):
# Setup options
options = ZWaveOption(
config[DOMAIN].get(CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH),
user_path=hass.config_dir)
user_path=hass.config.config_dir)
options.set_console_output(use_debug)
options.lock()
@ -96,8 +96,7 @@ def setup(hass, config):
for component, discovery_service, command_ids in DISCOVERY_COMPONENTS:
if value.command_class in command_ids:
# Ensure component is loaded
if component not in hass.components:
bootstrap.setup_component(hass, component, config)
bootstrap.setup_component(hass, component, config)
# Fire discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {

View File

@ -8,6 +8,9 @@ DEVICE_DEFAULT_NAME = "Unnamed Device"
# #### CONFIG ####
CONF_LATITUDE = "latitude"
CONF_LONGITUDE = "longitude"
CONF_TEMPERATURE_UNIT = "temperature_unit"
CONF_NAME = "name"
CONF_TIME_ZONE = "time_zone"
CONF_PLATFORM = "platform"
CONF_HOST = "host"

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

View File

@ -5,9 +5,9 @@ from homeassistant.loader import get_component
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.util import ensure_unique_string, slugify
# Deprecated 3/5/2015 - Moved to homeassistant.helpers.device
# Deprecated 3/5/2015 - Moved to homeassistant.helpers.entity
# pylint: disable=unused-import
from .device import Device, ToggleDevice # noqa
from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa
def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):

View File

@ -1,120 +1,10 @@
"""
homeassistant.helpers.device
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides ABC for devices in HA.
Deprecated since 3/21/2015 - please use helpers.entity
"""
import logging
from homeassistant import NoEntitySpecifiedError
# pylint: disable=unused-import
from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa
from homeassistant.const import (
ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME)
class Device(object):
""" ABC for Home Assistant devices. """
# pylint: disable=no-self-use
hass = None
entity_id = None
@property
def should_poll(self):
"""
Return True if device has to be polled for state.
False if device pushes its state to HA.
"""
return True
@property
def unique_id(self):
""" Returns a unique id. """
return "{}.{}".format(self.__class__, id(self))
@property
def name(self):
""" Returns the name of the device. """
return self.get_name()
@property
def state(self):
""" Returns the state of the device. """
return self.get_state()
@property
def state_attributes(self):
""" Returns the state attributes. """
return {}
# DEPRECATION NOTICE:
# Device is moving from getters to properties.
# For now the new properties will call the old functions
# This will be removed in the future.
def get_name(self):
""" Returns the name of the device if any. """
return DEVICE_DEFAULT_NAME
def get_state(self):
""" Returns state of the device. """
return "Unknown"
def get_state_attributes(self):
""" Returns optional state attributes. """
return None
def update(self):
""" Retrieve latest state from the real device. """
pass
def update_ha_state(self, force_refresh=False):
"""
Updates Home Assistant with current state of device.
If force_refresh == True will update device before setting state.
"""
if self.hass is None:
raise RuntimeError("Attribute hass is None for {}".format(self))
if self.entity_id is None:
raise NoEntitySpecifiedError(
"No entity specified for device {}".format(self.name))
if force_refresh:
self.update()
attr = self.state_attributes or {}
if ATTR_FRIENDLY_NAME not in attr and self.name:
attr[ATTR_FRIENDLY_NAME] = self.name
return self.hass.states.set(self.entity_id, self.state, attr)
def __eq__(self, other):
return (isinstance(other, Device) and
other.unique_id == self.unique_id)
def __repr__(self):
return "<Device {}: {}>".format(self.name, self.state)
class ToggleDevice(Device):
""" ABC for devices that can be turned on and off. """
# pylint: disable=no-self-use
@property
def state(self):
""" Returns the state. """
return STATE_ON if self.is_on else STATE_OFF
@property
def is_on(self):
""" True if device is on. """
return False
def turn_on(self, **kwargs):
""" Turn the device on. """
pass
def turn_off(self, **kwargs):
""" Turn the device off. """
pass
logging.getLogger(__name__).warning(
'This file is deprecated. Please use helpers.entity')

View File

@ -1,139 +1,10 @@
"""
Provides helpers for components that handle devices.
Deprecated since 3/21/2015 - please use helpers.entity_component
"""
from homeassistant.loader import get_component
from homeassistant.helpers import (
generate_entity_id, config_per_platform, extract_entity_ids)
from homeassistant.components import group, discovery
from homeassistant.const import ATTR_ENTITY_ID
import logging
DEFAULT_SCAN_INTERVAL = 15
# pylint: disable=unused-import
from .entity_component import EntityComponent as DeviceComponent # noqa
class DeviceComponent(object):
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments
"""
Helper class that will help a device component manage its devices.
"""
def __init__(self, logger, domain, hass,
scan_interval=DEFAULT_SCAN_INTERVAL,
discovery_platforms=None, group_name=None):
self.logger = logger
self.hass = hass
self.domain = domain
self.entity_id_format = domain + '.{}'
self.scan_interval = scan_interval
self.discovery_platforms = discovery_platforms
self.group_name = group_name
self.devices = {}
self.group = None
self.is_polling = False
def setup(self, config):
"""
Sets up a full device component:
- Loads the platforms from the config
- Will listen for supported discovered platforms
"""
# 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):
self._setup_platform(p_type, p_config)
if self.discovery_platforms:
discovery.listen(self.hass, self.discovery_platforms.keys(),
self._device_discovered)
def add_devices(self, new_devices):
"""
Takes in a list of new devices. For each device will see if it already
exists. If not, will add it, set it up and push the first state.
"""
for device in new_devices:
if device is not None and device not in self.devices.values():
device.hass = self.hass
device.entity_id = generate_entity_id(
self.entity_id_format, device.name, self.devices.keys())
self.devices[device.entity_id] = device
device.update_ha_state()
if self.group is None and self.group_name is not None:
self.group = group.Group(self.hass, self.group_name,
user_defined=False)
if self.group is not None:
self.group.update_tracked_entity_ids(self.devices.keys())
self._start_polling()
def extract_from_service(self, service):
"""
Takes a service and extracts all known devices.
Will return all if no entity IDs given in service.
"""
if ATTR_ENTITY_ID not in service.data:
return self.devices.values()
else:
return [self.devices[entity_id] for entity_id
in extract_entity_ids(self.hass, service)
if entity_id in self.devices]
def _update_device_states(self, now):
""" Update the states of all the lights. """
self.logger.info("Updating %s states", self.domain)
for device in self.devices.values():
if device.should_poll:
device.update_ha_state(True)
def _device_discovered(self, service, info):
""" Called when a device is discovered. """
if service not in self.discovery_platforms:
return
self._setup_platform(self.discovery_platforms[service], {}, info)
def _start_polling(self):
""" Start polling device states if necessary. """
if self.is_polling or \
not any(device.should_poll for device in self.devices.values()):
return
self.is_polling = True
self.hass.track_time_change(
self._update_device_states,
second=range(0, 60, self.scan_interval))
def _setup_platform(self, platform_type, config, discovery_info=None):
""" Tries to setup a platform for this component. """
platform_name = '{}.{}'.format(self.domain, platform_type)
platform = get_component(platform_name)
if platform is None:
self.logger.error('Unable to find platform %s', platform_type)
return
try:
platform.setup_platform(
self.hass, config, self.add_devices, discovery_info)
except AttributeError:
# Support old deprecated method for now - 3/1/2015
if hasattr(platform, 'get_devices'):
self.logger.warning(
"Please upgrade %s to return new devices using "
"setup_platform. See %s/demo.py for an example.",
platform_name, self.domain)
self.add_devices(platform.get_devices(self.hass, config))
else:
# AttributeError if setup_platform does not exist
self.logger.exception(
"Error setting up %s", platform_type)
logging.getLogger(__name__).warning(
'This file is deprecated. Please use helpers.entity_component')

View File

@ -0,0 +1,139 @@
"""
homeassistant.helpers.entity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides ABC for entities in HA.
"""
from homeassistant import NoEntitySpecifiedError
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF,
DEVICE_DEFAULT_NAME, TEMP_CELCIUS, TEMP_FAHRENHEIT)
class Entity(object):
""" ABC for Home Assistant entities. """
# pylint: disable=no-self-use
hass = None
entity_id = None
@property
def should_poll(self):
"""
Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return True
@property
def unique_id(self):
""" Returns a unique id. """
return "{}.{}".format(self.__class__, id(self))
@property
def name(self):
""" Returns the name of the entity. """
return self.get_name()
@property
def state(self):
""" Returns the state of the entity. """
return self.get_state()
@property
def state_attributes(self):
""" Returns the state attributes. """
return {}
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return None
# DEPRECATION NOTICE:
# Device is moving from getters to properties.
# For now the new properties will call the old functions
# This will be removed in the future.
def get_name(self):
""" Returns the name of the entity if any. """
return DEVICE_DEFAULT_NAME
def get_state(self):
""" Returns state of the entity. """
return "Unknown"
def get_state_attributes(self):
""" Returns optional state attributes. """
return None
def update(self):
""" Retrieve latest state. """
pass
def update_ha_state(self, force_refresh=False):
"""
Updates Home Assistant with current state of entity.
If force_refresh == True will update entity before setting state.
"""
if self.hass is None:
raise RuntimeError("Attribute hass is None for {}".format(self))
if self.entity_id is None:
raise NoEntitySpecifiedError(
"No entity id specified for entity {}".format(self.name))
if force_refresh:
self.update()
state = str(self.state)
attr = self.state_attributes or {}
if ATTR_FRIENDLY_NAME not in attr and self.name:
attr[ATTR_FRIENDLY_NAME] = self.name
if ATTR_UNIT_OF_MEASUREMENT not in attr and self.unit_of_measurement:
attr[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement
# Convert temperature if we detect one
if attr.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_CELCIUS,
TEMP_FAHRENHEIT):
state, attr[ATTR_UNIT_OF_MEASUREMENT] = \
self.hass.config.temperature(
state, attr[ATTR_UNIT_OF_MEASUREMENT])
state = str(state)
return self.hass.states.set(self.entity_id, state, attr)
def __eq__(self, other):
return (isinstance(other, Entity) and
other.unique_id == self.unique_id)
def __repr__(self):
return "<Entity {}: {}>".format(self.name, self.state)
class ToggleEntity(Entity):
""" ABC for entities that can be turned on and off. """
# pylint: disable=no-self-use
@property
def state(self):
""" Returns the state. """
return STATE_ON if self.is_on else STATE_OFF
@property
def is_on(self):
""" True if entity is on. """
return False
def turn_on(self, **kwargs):
""" Turn the entity on. """
pass
def turn_off(self, **kwargs):
""" Turn the entity off. """
pass

View File

@ -0,0 +1,149 @@
"""
homeassistant.helpers.entity_component
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides helpers for components that manage entities.
"""
from homeassistant.loader import get_component
from homeassistant.helpers import (
generate_entity_id, config_per_platform, extract_entity_ids)
from homeassistant.components import group, discovery
from homeassistant.const import ATTR_ENTITY_ID
DEFAULT_SCAN_INTERVAL = 15
class EntityComponent(object):
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments
"""
Helper class that will help a component manage its entities.
"""
def __init__(self, logger, domain, hass,
scan_interval=DEFAULT_SCAN_INTERVAL,
discovery_platforms=None, group_name=None):
self.logger = logger
self.hass = hass
self.domain = domain
self.entity_id_format = domain + '.{}'
self.scan_interval = scan_interval
self.discovery_platforms = discovery_platforms
self.group_name = group_name
self.entities = {}
self.group = None
self.is_polling = False
def setup(self, config):
"""
Sets up a full entity component:
- Loads the platforms from the config
- Will listen for supported discovered platforms
"""
# 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):
self._setup_platform(p_type, p_config)
if self.discovery_platforms:
discovery.listen(self.hass, self.discovery_platforms.keys(),
self._entity_discovered)
def add_entities(self, new_entities):
"""
Takes in a list of new entities. For each entity will see if it already
exists. If not, will add it, set it up and push the first state.
"""
for entity in new_entities:
if entity is not None and entity not in self.entities.values():
entity.hass = self.hass
entity.entity_id = generate_entity_id(
self.entity_id_format, entity.name, self.entities.keys())
self.entities[entity.entity_id] = entity
entity.update_ha_state()
if self.group is None and self.group_name is not None:
self.group = group.Group(self.hass, self.group_name,
user_defined=False)
if self.group is not None:
self.group.update_tracked_entity_ids(self.entities.keys())
self._start_polling()
def extract_from_service(self, service):
"""
Takes a service and extracts all known entities.
Will return all if no entity IDs given in service.
"""
if ATTR_ENTITY_ID not in service.data:
return self.entities.values()
else:
return [self.entities[entity_id] for entity_id
in extract_entity_ids(self.hass, service)
if entity_id in self.entities]
def _update_entity_states(self, now):
""" Update the states of all the entities. """
self.logger.info("Updating %s entities", self.domain)
for entity in self.entities.values():
if entity.should_poll:
entity.update_ha_state(True)
def _entity_discovered(self, service, info):
""" Called when a entity is discovered. """
if service not in self.discovery_platforms:
return
self._setup_platform(self.discovery_platforms[service], {}, info)
def _start_polling(self):
""" Start polling entities if necessary. """
if self.is_polling or \
not any(entity.should_poll for entity in self.entities.values()):
return
self.is_polling = True
self.hass.track_time_change(
self._update_entity_states,
second=range(0, 60, self.scan_interval))
def _setup_platform(self, platform_type, config, discovery_info=None):
""" Tries to setup a platform for this component. """
platform_name = '{}.{}'.format(self.domain, platform_type)
platform = get_component(platform_name)
if platform is None:
self.logger.error('Unable to find platform %s', platform_type)
return
try:
platform.setup_platform(
self.hass, config, self.add_entities, discovery_info)
self.hass.config.components.append(platform_name)
except AttributeError:
# AttributeError if setup_platform does not exist
# 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.",
platform_name, self.domain)
self.add_entities(platform.get_devices(self.hass, config))
else:
self.logger.exception(
"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)

View File

@ -46,11 +46,11 @@ def prepare(hass):
pkgutil.iter_modules(components.__path__, 'homeassistant.components.'))
# Look for available custom components
custom_path = hass.get_config_path("custom_components")
custom_path = hass.config.path("custom_components")
if os.path.isdir(custom_path):
# Ensure we can load custom components using Pythons import
sys.path.insert(0, hass.config_dir)
sys.path.insert(0, hass.config.config_dir)
# We cannot use the same approach as for built-in components because
# custom components might only contain a platform for a component.

View File

@ -14,7 +14,6 @@ import logging
import json
import enum
import urllib.parse
import os
import requests
@ -108,20 +107,19 @@ class HomeAssistant(ha.HomeAssistant):
remote_api.host, remote_api.port, remote_api.status))
self.remote_api = remote_api
self.local_api = local_api
self.pool = pool = ha.create_worker_pool()
self.bus = EventBus(remote_api, pool)
self.services = ha.ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus, self.remote_api)
self.components = []
self.config = ha.Config()
self.config_dir = os.path.join(os.getcwd(), 'config')
self.config.api = local_api
def start(self):
# Ensure a local API exists to connect with remote
if self.local_api is None:
if self.config.api is None:
bootstrap.setup_component(self, 'http')
bootstrap.setup_component(self, 'api')
@ -132,10 +130,10 @@ class HomeAssistant(ha.HomeAssistant):
# Setup that events from remote_api get forwarded to local_api
# Do this after we fire START, otherwise HTTP is not started
if not connect_remote_events(self.remote_api, self.local_api):
if not connect_remote_events(self.remote_api, self.config.api):
raise ha.HomeAssistantError((
'Could not setup event forwarding from api {} to '
'local api {}').format(self.remote_api, self.local_api))
'local api {}').format(self.remote_api, self.config.api))
def stop(self):
""" Stops Home Assistant and shuts down all threads. """
@ -145,7 +143,7 @@ class HomeAssistant(ha.HomeAssistant):
origin=ha.EventOrigin.remote)
# Disconnect master event forwarding
disconnect_remote_events(self.remote_api, self.local_api)
disconnect_remote_events(self.remote_api, self.config.api)
# Wait till all responses to homeassistant_stop are done
self.pool.block_till_done()
@ -264,7 +262,7 @@ class JSONEncoder(json.JSONEncoder):
def default(self, obj):
""" Converts Home Assistant objects and hands
other objects to the original method. """
if isinstance(obj, (ha.State, ha.Event)):
if hasattr(obj, 'as_dict'):
return obj.as_dict()
try:

View File

@ -36,3 +36,6 @@ pydispatcher>=2.0.5
# sensor.systemmonitor
psutil>=2.2.1
#pushover notifications
python-pushover>=0.2

View File

@ -7,14 +7,14 @@ Helper method for writing tests.
import os
import homeassistant as ha
from homeassistant.helpers.device import ToggleDevice
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
def get_test_home_assistant():
""" Returns a Home Assistant object pointing at test config dir. """
hass = ha.HomeAssistant()
hass.config_dir = os.path.join(os.path.dirname(__file__), "config")
hass.config.config_dir = os.path.join(os.path.dirname(__file__), "config")
return hass
@ -42,7 +42,7 @@ class MockModule(object):
self.setup = lambda hass, config: False if setup is None else setup
class MockToggleDevice(ToggleDevice):
class MockToggleDevice(ToggleEntity):
""" Provides a mock toggle device. """
def __init__(self, name, state):
self._name = name or DEVICE_DEFAULT_NAME

View File

@ -32,7 +32,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.hass = get_test_home_assistant()
loader.prepare(self.hass)
self.known_dev_path = self.hass.get_config_path(
self.known_dev_path = self.hass.config.path(
device_tracker.KNOWN_DEVICES_FILE)
def tearDown(self): # pylint: disable=invalid-name

View File

@ -29,7 +29,7 @@ class TestLight(unittest.TestCase):
""" Stop down stuff we started. """
self.hass.stop()
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
if os.path.isfile(user_light_file):
os.remove(user_light_file)
@ -218,7 +218,7 @@ class TestLight(unittest.TestCase):
platform = loader.get_component('light.test')
platform.init()
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
# Setup a wrong light file
with open(user_light_file, 'w') as user_file:
@ -234,7 +234,7 @@ class TestLight(unittest.TestCase):
platform = loader.get_component('light.test')
platform.init()
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
with open(user_light_file, 'w') as user_file:
user_file.write('id,x,y,brightness\n')

View File

@ -11,7 +11,6 @@ import datetime as dt
import ephem
import homeassistant as ha
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
import homeassistant.components.sun as sun
@ -35,12 +34,9 @@ class TestSun(unittest.TestCase):
def test_setting_rising(self):
""" Test retrieving sun setting and rising. """
# Compare it with the real data
self.assertTrue(sun.setup(
self.hass,
{ha.DOMAIN: {
CONF_LATITUDE: '32.87336',
CONF_LONGITUDE: '117.22743'
}}))
self.hass.config.latitude = '32.87336'
self.hass.config.longitude = '117.22743'
sun.setup(self.hass, None)
observer = ephem.Observer()
observer.lat = '32.87336' # pylint: disable=assigning-non-slot
@ -74,12 +70,9 @@ class TestSun(unittest.TestCase):
def test_state_change(self):
""" Test if the state changes at next setting/rising. """
self.assertTrue(sun.setup(
self.hass,
{ha.DOMAIN: {
CONF_LATITUDE: '32.87336',
CONF_LONGITUDE: '117.22743'
}}))
self.hass.config.latitude = '32.87336'
self.hass.config.longitude = '117.22743'
sun.setup(self.hass, None)
if sun.is_on(self.hass):
test_state = sun.STATE_BELOW_HORIZON
@ -96,30 +89,3 @@ class TestSun(unittest.TestCase):
self.hass.pool.block_till_done()
self.assertEqual(test_state, self.hass.states.get(sun.ENTITY_ID).state)
def test_setup(self):
""" Test Sun setup with empty and wrong configs. """
self.assertFalse(sun.setup(self.hass, {}))
self.assertFalse(sun.setup(self.hass, {sun.DOMAIN: {}}))
self.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: {CONF_LATITUDE: '32.87336'}}))
self.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: {CONF_LONGITUDE: '117.22743'}}))
self.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: {CONF_LATITUDE: 'hello'}}))
self.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: {CONF_LONGITUDE: 'how are you'}}))
self.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: {
CONF_LATITUDE: 'wrong', CONF_LONGITUDE: '117.22743'
}}))
self.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: {
CONF_LATITUDE: '32.87336', CONF_LONGITUDE: 'wrong'
}}))
# Test with correct config
self.assertTrue(sun.setup(
self.hass, {ha.DOMAIN: {
CONF_LATITUDE: '32.87336', CONF_LONGITUDE: '117.22743'
}}))

View File

@ -35,10 +35,10 @@ class TestHomeAssistant(unittest.TestCase):
def test_get_config_path(self):
""" Test get_config_path method. """
self.assertEqual(os.path.join(os.getcwd(), "config"),
self.hass.config_dir)
self.hass.config.config_dir)
self.assertEqual(os.path.join(os.getcwd(), "config", "test.conf"),
self.hass.get_config_path("test.conf"))
self.hass.config.path("test.conf"))
def test_block_till_stoped(self):
""" Test if we can block till stop service is called. """