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/*/zwave.py
homeassistant/components/*/tellstick.py homeassistant/components/*/tellstick.py
homeassistant/components/*/vera.py
homeassistant/components/keyboard.py homeassistant/components/keyboard.py
homeassistant/components/switch/wemo.py homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/nest.py homeassistant/components/thermostat/nest.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushover.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/light/vera.py homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/sensor/vera.py
homeassistant/components/switch/vera.py
[report] [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: 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 [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 [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) * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast)

View File

@ -1,8 +1,20 @@
homeassistant: homeassistant:
# Omitted values in this section will be auto detected using freegeoip.net
# Location required to calculate the time the sun rises and sets # Location required to calculate the time the sun rises and sets
latitude: 32.87336 latitude: 32.87336
longitude: 117.22743 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: http:
api_password: mypass api_password: mypass
# Set to 1 to enable development mode # Set to 1 to enable development mode

View File

@ -15,11 +15,14 @@ import re
import datetime as dt import datetime as dt
import functools as ft import functools as ft
import requests
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, 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 import homeassistant.util as util
DOMAIN = "homeassistant" DOMAIN = "homeassistant"
@ -49,19 +52,34 @@ class HomeAssistant(object):
self.bus = EventBus(pool) self.bus = EventBus(pool)
self.services = ServiceRegistry(self.bus, pool) self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus) self.states = StateMachine(self.bus)
self.config = Config()
# List of loaded components @property
self.components = [] 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 @property
self.local_api = None 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 @property
self.config_dir = os.path.join(os.getcwd(), 'config') 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): def get_config_path(self, path):
""" Returns path to the file within the config dir. """ """ DEPRECATED 3/18/2015. Use hass.config.path """
return os.path.join(self.config_dir, path) _LOGGER.warning(
'hass.get_config_path is deprecated. Use hass.config.path')
return self.config.path(path)
def start(self): def start(self):
""" Start home assistant. """ """ Start home assistant. """
@ -307,19 +325,23 @@ class EventOrigin(enum.Enum):
class Event(object): class Event(object):
""" Represents an event within the Bus. """ """ 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.event_type = event_type
self.data = data or {} self.data = data or {}
self.origin = origin self.origin = origin
self.time_fired = util.strip_microseconds(
time_fired or dt.datetime.now())
def as_dict(self): def as_dict(self):
""" Returns a dict representation of this Event. """ """ Returns a dict representation of this Event. """
return { return {
'event_type': self.event_type, 'event_type': self.event_type,
'data': dict(self.data), 'data': dict(self.data),
'origin': str(self.origin) 'origin': str(self.origin),
'time_fired': util.datetime_to_str(self.time_fired),
} }
def __repr__(self): def __repr__(self):
@ -441,7 +463,9 @@ class State(object):
__slots__ = ['entity_id', 'state', 'attributes', __slots__ = ['entity_id', 'state', 'attributes',
'last_changed', 'last_updated'] '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): if not ENTITY_ID_PATTERN.match(entity_id):
raise InvalidEntityFormatError(( raise InvalidEntityFormatError((
"Invalid entity id encountered: {}. " "Invalid entity id encountered: {}. "
@ -450,7 +474,7 @@ class State(object):
self.entity_id = entity_id.lower() self.entity_id = entity_id.lower()
self.state = state self.state = state
self.attributes = attributes or {} 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 # Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict()) # state == State.from_dict(state.as_dict())
@ -464,6 +488,18 @@ class State(object):
""" Returns domain of this state. """ """ Returns domain of this state. """
return util.split_entity_id(self.entity_id)[0] 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): def copy(self):
""" Creates a copy of itself. """ """ Creates a copy of itself. """
return State(self.entity_id, self.state, return State(self.entity_id, self.state,
@ -476,7 +512,8 @@ class State(object):
return {'entity_id': self.entity_id, return {'entity_id': self.entity_id,
'state': self.state, 'state': self.state,
'attributes': self.attributes, '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 @classmethod
def from_dict(cls, json_dict): def from_dict(cls, json_dict):
@ -493,8 +530,13 @@ class State(object):
if last_changed: if last_changed:
last_changed = util.str_to_datetime(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'], 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): def __eq__(self, other):
return (self.__class__ == other.__class__ and return (self.__class__ == other.__class__ and
@ -836,6 +878,83 @@ class Timer(threading.Thread):
self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) 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): class HomeAssistantError(Exception):
""" General Home Assistant exception occured. """ """ General Home Assistant exception occured. """
pass pass

View File

@ -20,7 +20,10 @@ import homeassistant
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.components as core_components import homeassistant.components as core_components
import homeassistant.components.group as group 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__) _LOGGER = logging.getLogger(__name__)
@ -28,21 +31,49 @@ ATTR_COMPONENT = "component"
def setup_component(hass, domain, config=None): def setup_component(hass, domain, config=None):
""" Setup a component for Home Assistant. """ """ Setup a component and all its dependencies. """
# Check if already loaded
if domain in hass.components: if domain in hass.config.components:
return return True
_ensure_loader_prepared(hass) _ensure_loader_prepared(hass)
if config is None: if config is None:
config = defaultdict(dict) 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) 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: try:
if component.setup(hass, config): 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 # Assumption: if a component does not depend on groups
# it communicates with devices # it communicates with devices
@ -73,6 +104,8 @@ def from_config_dict(config, hass=None):
if hass is None: if hass is None:
hass = homeassistant.HomeAssistant() hass = homeassistant.HomeAssistant()
process_ha_core_config(hass, config.get(homeassistant.DOMAIN, {}))
enable_logging(hass) enable_logging(hass)
_ensure_loader_prepared(hass) _ensure_loader_prepared(hass)
@ -97,7 +130,7 @@ def from_config_dict(config, hass=None):
# Setup the components # Setup the components
for domain in loader.load_order_components(components): for domain in loader.load_order_components(components):
setup_component(hass, domain, config) _setup_component(hass, domain, config)
return hass return hass
@ -111,8 +144,8 @@ def from_config_file(config_path, hass=None):
if hass is None: if hass is None:
hass = homeassistant.HomeAssistant() hass = homeassistant.HomeAssistant()
# Set config dir to directory holding config file # Set config dir to directory holding config file
hass.config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = os.path.abspath(os.path.dirname(config_path))
config_dict = {} config_dict = {}
# check config file type # check config file type
@ -143,13 +176,13 @@ def enable_logging(hass):
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# Log errors to a file if we have write access to file or config dir # 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) err_path_exists = os.path.isfile(err_log_path)
# Check if we can write to the error log if it exists or that # Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not. # we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \ 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_handler = logging.FileHandler(
err_log_path, mode='w', delay=True) 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) "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): def _ensure_loader_prepared(hass):
""" Ensure Home Assistant loader is prepared. """ """ Ensure Home Assistant loader is prepared. """
if not loader.PREPARED: if not loader.PREPARED:

View File

@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
""" Register the API with the HTTP interface. """ """ 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') _LOGGER.error('Dependency http is not loaded')
return False 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): def _handle_get_api_components(handler, path_match, data):
""" Returns all the loaded components. """ """ 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: except KeyError:
_INSTANCES[hass] = Configurator(hass) _INSTANCES[hass] = Configurator(hass)
if DOMAIN not in hass.components: if DOMAIN not in hass.config.components:
hass.components.append(DOMAIN) hass.config.components.append(DOMAIN)
return _INSTANCES[hass] return _INSTANCES[hass]

View File

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

View File

@ -157,7 +157,8 @@ class DeviceTracker(object):
def update_devices(self, now): def update_devices(self, now):
""" Update device states based on the found devices. """ """ 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 found_devices = set(dev.upper() for dev in
self.device_scanner.scan_devices()) self.device_scanner.scan_devices())
@ -179,7 +180,7 @@ class DeviceTracker(object):
# Write new devices to known devices file # Write new devices to known devices file
if not self.invalid_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: try:
# If file does not exist we will write the header too # If file does not exist we will write the header too
@ -214,7 +215,7 @@ class DeviceTracker(object):
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def _read_known_devices_file(self): def _read_known_devices_file(self):
""" Parse and process the known devices file. """ """ 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 # Return if no known devices file exists
if not os.path.isfile(known_dev_path): 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. """ """ Supports scanning using nmap. """
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
import threading
from collections import namedtuple from collections import namedtuple
import subprocess import subprocess
import re import re
@ -54,7 +53,6 @@ class NmapDeviceScanner(object):
def __init__(self, config): def __init__(self, config):
self.last_results = [] self.last_results = []
self.lock = threading.Lock()
self.hosts = config[CONF_HOSTS] self.hosts = config[CONF_HOSTS]
minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0) minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0)
self.home_interval = timedelta(minutes=minutes) self.home_interval = timedelta(minutes=minutes)
@ -116,28 +114,27 @@ class NmapDeviceScanner(object):
if not self.success_init: if not self.success_init:
return False return False
with self.lock: _LOGGER.info("Scanning")
_LOGGER.info("Scanning")
options = "-F" options = "-F --host-timeout 5"
exclude_targets = set() exclude_targets = set()
if self.home_interval: if self.home_interval:
now = datetime.now() now = datetime.now()
for host in self.last_results: for host in self.last_results:
if host.last_update + self.home_interval > now: if host.last_update + self.home_interval > now:
exclude_targets.add(host) exclude_targets.add(host)
if len(exclude_targets) > 0: if len(exclude_targets) > 0:
target_list = [t.ip for t in exclude_targets] target_list = [t.ip for t in exclude_targets]
options += " --exclude {}".format(",".join(target_list)) 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 nmap.rc == 0:
if self._parse_results(nmap.stdout): if self._parse_results(nmap.stdout):
self.last_results.extend(exclude_targets) self.last_results.extend(exclude_targets)
else: else:
self.last_results = [] self.last_results = []
_LOGGER.error(nmap.stderr) _LOGGER.error(nmap.stderr)
return False return False

View File

@ -70,12 +70,17 @@ def setup(hass, config):
def new_service_listener(service, info): def new_service_listener(service, info):
""" Called when a new service is found. """ """ Called when a new service is found. """
with lock: with lock:
component = SERVICE_HANDLERS.get(service)
logger.info("Found new service: %s %s", service, info) logger.info("Found new service: %s %s", service, info)
if component and component not in hass.components: component = SERVICE_HANDLERS.get(service)
bootstrap.setup_component(hass, component, config)
# 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, { hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: service, ATTR_SERVICE: service,

View File

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

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ 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"> <polymer-element name="more-info-dialog">
<template> <template>
<ha-dialog id="dialog"> <ha-dialog id="dialog" on-core-overlay-open="{{dialogOpenChanged}}">
<div> <div>
<state-card-content stateObj="{{stateObj}}" style='margin-bottom: 24px;'> <state-card-content stateObj="{{stateObj}}" style='margin-bottom: 24px;'>
</state-card-content> </state-card-content>
<state-timeline stateHistory="{{stateHistory}}"></state-timeline> <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> </div>
</ha-dialog> </ha-dialog>
</template> </template>
@ -27,11 +29,16 @@ Polymer(Polymer.mixin({
stateObj: null, stateObj: null,
stateHistory: null, stateHistory: null,
hasHistoryComponent: false, hasHistoryComponent: false,
dialogOpen: false,
observe: { observe: {
'stateObj.attributes': 'reposition' 'stateObj.attributes': 'reposition'
}, },
created: function() {
this.dialogOpenChanged = this.dialogOpenChanged.bind(this);
},
attached: function() { attached: function() {
this.listenToStores(true); 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) { changeEntityId: function(entityId) {
this.entityId = entityId; this.entityId = entityId;

View File

@ -10,6 +10,7 @@
<link rel="import" href="../layouts/partial-states.html"> <link rel="import" href="../layouts/partial-states.html">
<link rel="import" href="../layouts/partial-history.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-fire-event.html">
<link rel="import" href="../layouts/partial-dev-call-service.html"> <link rel="import" href="../layouts/partial-dev-call-service.html">
<link rel="import" href="../layouts/partial-dev-set-state.html"> <link rel="import" href="../layouts/partial-dev-set-state.html">
@ -96,6 +97,13 @@
</paper-item> </paper-item>
</template> </template>
<template if="{{hasLogbookComponent}}">
<paper-item data-panel="logbook">
<core-icon icon="list"></core-icon>
Logbook
</paper-item>
</template>
<div flex></div> <div flex></div>
<paper-item on-click="{{handleLogOutClick}}"> <paper-item on-click="{{handleLogOutClick}}">
@ -136,6 +144,9 @@
<template if="{{selected == 'history'}}"> <template if="{{selected == 'history'}}">
<partial-history main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-history> <partial-history main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-history>
</template> </template>
<template if="{{selected == 'logbook'}}">
<partial-logbook main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-logbook>
</template>
<template if="{{selected == 'fire-event'}}"> <template if="{{selected == 'fire-event'}}">
<partial-dev-fire-event main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-fire-event> <partial-dev-fire-event main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-fire-event>
</template> </template>
@ -161,6 +172,7 @@ Polymer(Polymer.mixin({
narrow: false, narrow: false,
activeFilters: [], activeFilters: [],
hasHistoryComponent: false, hasHistoryComponent: false,
hasLogbookComponent: false,
isStreaming: false, isStreaming: false,
hasStreamError: false, hasStreamError: false,
@ -185,7 +197,7 @@ Polymer(Polymer.mixin({
componentStoreChanged: function(componentStore) { componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history'); this.hasHistoryComponent = componentStore.isLoaded('history');
this.hasScriptComponent = componentStore.isLoaded('script'); this.hasLogbookComponent = componentStore.isLoaded('logbook');
}, },
streamStoreChanged: function(streamStore) { 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-thermostat.html">
<link rel="import" href="more-info-script.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> <template>
<style> <style>
:host { :host {
@ -20,11 +20,20 @@
<script> <script>
Polymer({ Polymer({
classNames: '', classNames: '',
dialogOpen: false,
observe: { observe: {
'stateObj.attributes': 'stateAttributesChanged', 'stateObj.attributes': 'stateAttributesChanged',
}, },
dialogOpenChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer;
if (moreInfoContainer.lastChild) {
moreInfoContainer.lastChild.dialogOpen = newVal;
}
},
stateObjChanged: function(oldVal, newVal) { stateObjChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer; var moreInfoContainer = this.$.moreInfoContainer;
@ -42,10 +51,12 @@ Polymer({
var moreInfo = document.createElement("more-info-" + newVal.moreInfoType); var moreInfo = document.createElement("more-info-" + newVal.moreInfoType);
moreInfo.stateObj = newVal; moreInfo.stateObj = newVal;
moreInfo.dialogOpen = this.dialogOpen;
moreInfoContainer.appendChild(moreInfo); moreInfoContainer.appendChild(moreInfo);
} else { } else {
moreInfoContainer.lastChild.dialogOpen = this.dialogOpen;
moreInfoContainer.lastChild.stateObj = newVal; moreInfoContainer.lastChild.stateObj = newVal;
} }

View File

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

View File

@ -1,5 +1,30 @@
<link rel="import" href="../bower_components/core-style/core-style.html"> <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'> <core-style id='ha-animations'>
@-webkit-keyframes ha-spin { @-webkit-keyframes ha-spin {
0% { 0% {

View File

@ -67,6 +67,10 @@ def get_states(point_in_time, entity_ids=None, run=None):
if run is None: if run is None:
run = recorder.run_information(point_in_time) 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 = run.where_after_start_run + "AND created < ? "
where_data = [point_in_time] where_data = [point_in_time]

View File

@ -135,7 +135,7 @@ def setup(hass, config=None):
threading.Thread(target=server.start, daemon=True).start()) threading.Thread(target=server.start, daemon=True).start())
hass.http = server 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 return True

View File

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

View File

@ -1,7 +1,7 @@
""" Provides demo lights. """ """ Provides demo lights. """
import random 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.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_XY_COLOR 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. """ """ Provides a demo switch. """
def __init__(self, name, state, xy=None, brightness=180): def __init__(self, name, state, xy=None, brightness=180):
self._name = name or DEVICE_DEFAULT_NAME self._name = name or DEVICE_DEFAULT_NAME

View File

@ -6,7 +6,7 @@ from urllib.parse import urlparse
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.util as util 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.const import CONF_HOST
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
@ -51,7 +51,8 @@ def setup_bridge(host, hass, add_devices_callback):
try: try:
bridge = phue.Bridge( 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 except ConnectionRefusedError: # Wrong host was given
_LOGGER.exception("Error connecting to the Hue bridge at %s", host) _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 """ """ Represents a Hue light """
def __init__(self, light_id, info, bridge, update_lights): 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 # pylint: disable=no-name-in-module, import-error
from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import ATTR_FRIENDLY_NAME 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 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) add_devices_callback(lights)
class TellstickLight(ToggleDevice): class TellstickLight(ToggleEntity):
""" Represents a tellstick light """ """ Represents a tellstick light """
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF | 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 import logging
from homeassistant.components import discovery from homeassistant.components import discovery
from homeassistant.helpers.device import Device from homeassistant.helpers.entity import Entity
from homeassistant.helpers.device_component import DeviceComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
@ -126,7 +126,7 @@ SERVICE_TO_METHOD = {
def setup(hass, config): def setup(hass, config):
""" Track states and offer events for media_players. """ """ Track states and offer events for media_players. """
component = DeviceComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS) DISCOVERY_PLATFORMS)
@ -171,7 +171,7 @@ def setup(hass, config):
return True return True
class MediaPlayerDevice(Device): class MediaPlayerDevice(Entity):
""" ABC for media player devices. """ """ ABC for media player devices. """
def turn_off(self): 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 threading
import queue import queue
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime, date
import time import time
import json import json
import atexit import atexit
@ -60,7 +60,8 @@ def row_to_state(row):
""" Convert a databsae row to a state. """ """ Convert a databsae row to a state. """
try: try:
return State( 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: except ValueError:
# When json.loads fails # When json.loads fails
_LOGGER.exception("Error converting row to state: %s", row) _LOGGER.exception("Error converting row to state: %s", row)
@ -70,9 +71,10 @@ def row_to_state(row):
def row_to_event(row): def row_to_event(row):
""" Convert a databse row to an event. """ """ Convert a databse row to an event. """
try: try:
return Event(row[1], json.loads(row[2]), EventOrigin[row[3].lower()]) return Event(row[1], json.loads(row[2]), EventOrigin[row[3].lower()],
datetime.fromtimestamp(row[5]))
except ValueError: except ValueError:
# When json.oads fails # When json.loads fails
_LOGGER.exception("Error converting row to event: %s", row) _LOGGER.exception("Error converting row to event: %s", row)
return None return None
@ -86,7 +88,7 @@ def run_information(point_in_time=None):
return RecorderRun() return RecorderRun()
run = _INSTANCE.query( 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) (point_in_time, point_in_time), return_value=RETURN_ONE_ROW)
return RecorderRun(run) if run else None return RecorderRun(run) if run else None
@ -225,13 +227,13 @@ class Recorder(threading.Thread):
""" Save an event to the database. """ """ Save an event to the database. """
info = ( info = (
event.event_type, json.dumps(event.data, cls=JSONEncoder), event.event_type, json.dumps(event.data, cls=JSONEncoder),
str(event.origin), datetime.now() str(event.origin), datetime.now(), event.time_fired,
) )
self.query( self.query(
"INSERT INTO events (" "INSERT INTO events ("
"event_type, event_data, origin, created" "event_type, event_data, origin, created, time_fired"
") VALUES (?, ?, ?, ?)", info) ") VALUES (?, ?, ?, ?, ?)", info)
def query(self, sql_query, data=None, return_value=None): def query(self, sql_query, data=None, return_value=None):
""" Query the database. """ """ Query the database. """
@ -262,7 +264,7 @@ class Recorder(threading.Thread):
def _setup_connection(self): def _setup_connection(self):
""" Ensure database is ready to fly. """ """ 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 = sqlite3.connect(db_path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row self.conn.row_factory = sqlite3.Row
@ -271,6 +273,7 @@ class Recorder(threading.Thread):
atexit.register(self._close_connection) atexit.register(self._close_connection)
# Have datetime objects be saved as integers # Have datetime objects be saved as integers
sqlite3.register_adapter(date, _adapt_datetime)
sqlite3.register_adapter(datetime, _adapt_datetime) sqlite3.register_adapter(datetime, _adapt_datetime)
# Validate we are on the correct schema or that we have to migrate # Validate we are on the correct schema or that we have to migrate
@ -328,6 +331,16 @@ class Recorder(threading.Thread):
save_migration(1) 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): def _close_connection(self):
""" Close connection to the database. """ """ Close connection to the database. """
_LOGGER.info("Closing database") _LOGGER.info("Closing database")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,9 +50,10 @@ import logging
import time import time
from requests.exceptions import RequestException from requests.exceptions import RequestException
from homeassistant.helpers import Device from homeassistant.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME,
TEMP_CELCIUS, TEMP_FAHRENHEIT)
# pylint: disable=no-name-in-module, import-error # pylint: disable=no-name-in-module, import-error
import homeassistant.external.vera.vera as veraApi 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)) add_devices(get_devices(hass, config))
class VeraSensor(Device): class VeraSensor(Entity):
""" Represents a Vera Sensor """ """ Represents a Vera Sensor """
def __init__(self, vera_device, extra_data=None): def __init__(self, vera_device, extra_data=None):
@ -110,6 +111,7 @@ class VeraSensor(Device):
else: else:
self._name = self.vera_device.name self._name = self.vera_device.name
self.current_value = '' self.current_value = ''
self._temperature_units = None
def __str__(self): def __str__(self):
return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state) return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state)
@ -123,6 +125,11 @@ class VeraSensor(Device):
""" Get the mame of the sensor. """ """ Get the mame of the sensor. """
return self._name return self._name
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._temperature_units
@property @property
def state_attributes(self): def state_attributes(self):
attr = super().state_attributes attr = super().state_attributes
@ -151,7 +158,20 @@ class VeraSensor(Device):
self.vera_device.refresh_value('CurrentTemperature') self.vera_device.refresh_value('CurrentTemperature')
current_temp = self.vera_device.get_value('CurrentTemperature') current_temp = self.vera_device.get_value('CurrentTemperature')
vera_temp_units = self.vera_device.veraController.temperature_units 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": elif self.vera_device.category == "Light Sensor":
self.vera_device.refresh_value('CurrentLevel') self.vera_device.refresh_value('CurrentLevel')
self.current_value = self.vera_device.get_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 # pylint: disable=no-name-in-module, import-error
import homeassistant.external.wink.pywink as pywink import homeassistant.external.wink.pywink as pywink
from homeassistant.components.wink import WinkSensorDevice from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
def setup_platform(hass, config, add_devices, discovery_info=None): 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) pywink.set_bearer_token(token)
add_devices(WinkSensorDevice(sensor) for sensor in pywink.get_sensors()) 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 from pydispatch import dispatcher
import homeassistant.components.zwave as zwave import homeassistant.components.zwave as zwave
from homeassistant.helpers.device import Device from homeassistant.helpers.entity import Entity
from homeassistant.const import ( 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) TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_LOCATION)
@ -33,7 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ZWaveMultilevelSensor(value)]) add_devices([ZWaveMultilevelSensor(value)])
class ZWaveSensor(Device): class ZWaveSensor(Entity):
""" Represents a Z-Wave sensor. """ """ Represents a Z-Wave sensor. """
def __init__(self, sensor_value): def __init__(self, sensor_value):
self._value = sensor_value self._value = sensor_value
@ -77,11 +77,6 @@ class ZWaveSensor(Device):
if battery_level is not None: if battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = battery_level attrs[ATTR_BATTERY_LEVEL] = battery_level
unit = self.unit
if unit:
attrs[ATTR_UNIT_OF_MEASUREMENT] = unit
location = self._node.location location = self._node.location
if location: if location:
@ -90,8 +85,7 @@ class ZWaveSensor(Device):
return attrs return attrs
@property @property
def unit(self): def unit_of_measurement(self):
""" Unit if sensor has one. """
return self._value.units return self._value.units
def _value_changed(self, value): def _value_changed(self, value):
@ -126,8 +120,7 @@ class ZWaveMultilevelSensor(ZWaveSensor):
return value return value
@property @property
def unit(self): def unit_of_measurement(self):
""" Unit of this sensor. """
unit = self._value.units unit = self._value.units
if unit == 'C': if unit == 'C':

View File

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

View File

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

View File

@ -1,5 +1,5 @@
""" Demo platform that has two fake switchces. """ """ 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 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. """ """ Provides a demo switch. """
def __init__(self, name, state): def __init__(self, name, state):
self._name = name or DEVICE_DEFAULT_NAME self._name = name or DEVICE_DEFAULT_NAME

View File

@ -3,7 +3,7 @@ import logging
from homeassistant.const import ATTR_FRIENDLY_NAME 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 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) add_devices_callback(switches)
class TellstickSwitchDevice(ToggleDevice): 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 | last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF) tellcore_constants.TELLSTICK_TURNOFF)

View File

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

View File

@ -1,7 +1,7 @@
""" Support for WeMo switchces. """ """ Support for WeMo switchces. """
import logging import logging
from homeassistant.helpers.device import ToggleDevice from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.switch import ( from homeassistant.components.switch import (
ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH) 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)]) if isinstance(switch, pywemo.Switch)])
class WemoSwitch(ToggleDevice): class WemoSwitch(ToggleEntity):
""" represents a WeMo switch within home assistant. """ """ represents a WeMo switch within home assistant. """
def __init__(self, wemo): def __init__(self, wemo):
self.wemo = wemo self.wemo = wemo

View File

@ -6,13 +6,12 @@ Provides functionality to interact with thermostats.
""" """
import logging import logging
from homeassistant.helpers.device_component import DeviceComponent from homeassistant.helpers.entity_component import EntityComponent
import homeassistant.util as util import homeassistant.util as util
from homeassistant.helpers.device import Device from homeassistant.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF)
STATE_ON, STATE_OFF)
DOMAIN = "thermostat" DOMAIN = "thermostat"
DEPENDENCIES = [] DEPENDENCIES = []
@ -53,7 +52,7 @@ def set_temperature(hass, temperature, entity_id=None):
def setup(hass, config): def setup(hass, config):
""" Setup thermostats. """ """ Setup thermostats. """
component = DeviceComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config) component.setup(config)
def thermostat_service(service): def thermostat_service(service):
@ -99,7 +98,7 @@ def setup(hass, config):
return True return True
class ThermostatDevice(Device): class ThermostatDevice(Entity):
""" Represents a thermostat within Home Assistant. """ """ Represents a thermostat within Home Assistant. """
# pylint: disable=no-self-use # pylint: disable=no-self-use
@ -109,11 +108,6 @@ class ThermostatDevice(Device):
""" Returns the current state. """ """ Returns the current state. """
return self.target_temperature return self.target_temperature
@property
def unit_of_measurement(self):
""" Returns the unit of measurement. """
return ""
@property @property
def device_state_attributes(self): def device_state_attributes(self):
""" Returns device specific state attributes. """ """ Returns device specific state attributes. """
@ -123,8 +117,8 @@ class ThermostatDevice(Device):
def state_attributes(self): def state_attributes(self):
""" Returns optional state attributes. """ """ Returns optional state attributes. """
data = { data = {
ATTR_UNIT_OF_MEASUREMENT: self.unit_of_measurement, ATTR_CURRENT_TEMPERATURE: self.hass.config.temperature(
ATTR_CURRENT_TEMPERATURE: self.current_temperature self.current_temperature, self.unit_of_measurement)[0]
} }
is_away = self.is_away_mode_on 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 import bootstrap
from homeassistant.loader import get_component 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 ( from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN, EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN,
STATE_OPEN, STATE_CLOSED,
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
DOMAIN = "wink" DOMAIN = "wink"
@ -41,8 +41,7 @@ def setup(hass, config):
component = get_component(component_name) component = get_component(component_name)
# Ensure component is loaded # 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 # Fire discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
@ -53,45 +52,7 @@ def setup(hass, config):
return True return True
class WinkSensorDevice(Device): class WinkToggleDevice(ToggleEntity):
""" 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):
""" represents a Wink switch within home assistant. """ """ represents a Wink switch within home assistant. """
def __init__(self, wink): def __init__(self, wink):

View File

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

View File

@ -8,6 +8,9 @@ DEVICE_DEFAULT_NAME = "Unnamed Device"
# #### CONFIG #### # #### CONFIG ####
CONF_LATITUDE = "latitude" CONF_LATITUDE = "latitude"
CONF_LONGITUDE = "longitude" CONF_LONGITUDE = "longitude"
CONF_TEMPERATURE_UNIT = "temperature_unit"
CONF_NAME = "name"
CONF_TIME_ZONE = "time_zone"
CONF_PLATFORM = "platform" CONF_PLATFORM = "platform"
CONF_HOST = "host" 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.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.util import ensure_unique_string, slugify 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 # 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): def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):

View File

@ -1,120 +1,10 @@
""" """
homeassistant.helpers.device Deprecated since 3/21/2015 - please use helpers.entity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides ABC for devices in HA.
""" """
import logging
from homeassistant import NoEntitySpecifiedError # pylint: disable=unused-import
from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa
from homeassistant.const import ( logging.getLogger(__name__).warning(
ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME) 'This file is deprecated. Please use helpers.entity')
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

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 import logging
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 # pylint: disable=unused-import
from .entity_component import EntityComponent as DeviceComponent # noqa
logging.getLogger(__name__).warning(
class DeviceComponent(object): 'This file is deprecated. Please use helpers.entity_component')
# 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)

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.')) pkgutil.iter_modules(components.__path__, 'homeassistant.components.'))
# Look for available custom 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): if os.path.isdir(custom_path):
# Ensure we can load custom components using Pythons import # 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 # We cannot use the same approach as for built-in components because
# custom components might only contain a platform for a component. # custom components might only contain a platform for a component.

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
loader.prepare(self.hass) 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) device_tracker.KNOWN_DEVICES_FILE)
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name

View File

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

View File

@ -11,7 +11,6 @@ import datetime as dt
import ephem import ephem
import homeassistant as ha import homeassistant as ha
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
import homeassistant.components.sun as sun import homeassistant.components.sun as sun
@ -35,12 +34,9 @@ class TestSun(unittest.TestCase):
def test_setting_rising(self): def test_setting_rising(self):
""" Test retrieving sun setting and rising. """ """ Test retrieving sun setting and rising. """
# Compare it with the real data # Compare it with the real data
self.assertTrue(sun.setup( self.hass.config.latitude = '32.87336'
self.hass, self.hass.config.longitude = '117.22743'
{ha.DOMAIN: { sun.setup(self.hass, None)
CONF_LATITUDE: '32.87336',
CONF_LONGITUDE: '117.22743'
}}))
observer = ephem.Observer() observer = ephem.Observer()
observer.lat = '32.87336' # pylint: disable=assigning-non-slot observer.lat = '32.87336' # pylint: disable=assigning-non-slot
@ -74,12 +70,9 @@ class TestSun(unittest.TestCase):
def test_state_change(self): def test_state_change(self):
""" Test if the state changes at next setting/rising. """ """ Test if the state changes at next setting/rising. """
self.assertTrue(sun.setup( self.hass.config.latitude = '32.87336'
self.hass, self.hass.config.longitude = '117.22743'
{ha.DOMAIN: { sun.setup(self.hass, None)
CONF_LATITUDE: '32.87336',
CONF_LONGITUDE: '117.22743'
}}))
if sun.is_on(self.hass): if sun.is_on(self.hass):
test_state = sun.STATE_BELOW_HORIZON test_state = sun.STATE_BELOW_HORIZON
@ -96,30 +89,3 @@ class TestSun(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(test_state, self.hass.states.get(sun.ENTITY_ID).state) 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): def test_get_config_path(self):
""" Test get_config_path method. """ """ Test get_config_path method. """
self.assertEqual(os.path.join(os.getcwd(), "config"), 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.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): def test_block_till_stoped(self):
""" Test if we can block till stop service is called. """ """ Test if we can block till stop service is called. """