From 9b643d57f0d79232e81e930d602d3718753bcb38 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Mar 2015 23:02:58 -0700 Subject: [PATCH 01/47] ps: Add a global config object to Home Assistant --- config/configuration.yaml.example | 12 +++ homeassistant/__init__.py | 85 +++++++++++++++++-- homeassistant/bootstrap.py | 35 ++++++-- homeassistant/components/demo.py | 5 +- .../components/device_tracker/__init__.py | 5 +- homeassistant/components/light/__init__.py | 2 +- homeassistant/components/light/hue.py | 3 +- .../components/scheduler/__init__.py | 2 +- homeassistant/components/sensor/demo.py | 23 +++-- .../components/sensor/systemmonitor.py | 11 +-- homeassistant/components/sensor/tellstick.py | 17 +--- homeassistant/components/sensor/wink.py | 35 +++++++- homeassistant/components/sensor/zwave.py | 13 +-- homeassistant/components/sun.py | 12 +-- .../components/thermostat/__init__.py | 12 +-- homeassistant/components/wink.py | 41 +-------- homeassistant/components/zwave.py | 2 +- homeassistant/const.py | 3 + homeassistant/helpers/device.py | 23 ++++- homeassistant/loader.py | 4 +- homeassistant/remote.py | 4 +- tests/helpers.py | 2 +- tests/test_component_sun.py | 46 ++-------- 23 files changed, 224 insertions(+), 173 deletions(-) diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index 6c0d9f64a1f..125be6e2d05 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -1,8 +1,20 @@ homeassistant: + # Omitted values in this section will be auto detected using freegeoip.net + # Location required to calculate the time the sun rises and sets latitude: 32.87336 longitude: 117.22743 + # C for Celcius, F for Fahrenheit + temperature_unit: C + + # Pick yours from here: + # http://en.wikipedia.org/wiki/List_of_tz_database_time_zones + time_zone: America/Los_Angeles + + # Name of the location where Home Assistant is running + name: Home + http: api_password: mypass # Set to 1 to enable development mode diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index bc6dca95113..062616bf0b4 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -15,11 +15,14 @@ import re import datetime as dt import functools as ft +import requests + from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, - EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED) + EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, + TEMP_CELCIUS, TEMP_FAHRENHEIT) import homeassistant.util as util DOMAIN = "homeassistant" @@ -49,6 +52,7 @@ class HomeAssistant(object): self.bus = EventBus(pool) self.services = ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus) + self.config = Config() # List of loaded components self.components = [] @@ -56,12 +60,14 @@ class HomeAssistant(object): # Remote.API object pointing at local API self.local_api = None - # Directory that holds the configuration - self.config_dir = os.path.join(os.getcwd(), 'config') + @property + def config_dir(self): + """ DEPRECATED 3/18/2015. Use hass.config.config_dir """ + return self.config.config_dir def get_config_path(self, path): - """ Returns path to the file within the config dir. """ - return os.path.join(self.config_dir, path) + """ DEPRECATED 3/18/2015. Use hass.config.get_config_path """ + return self.config.get_config_path(path) def start(self): """ Start home assistant. """ @@ -836,6 +842,75 @@ class Timer(threading.Thread): self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) +class Config(object): + """ Configuration settings for Home Assistant. """ + def __init__(self): + self.latitude = None + self.longitude = None + self.temperature_unit = None + self.location_name = None + self.time_zone = None + + # Directory that holds the configuration + self.config_dir = os.path.join(os.getcwd(), 'config') + + def get_config_path(self, path): + """ Returns path to the file within the config dir. """ + return os.path.join(self.config_dir, path) + + 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 temperature(self, value, unit): + """ Converts temperature to user preferred unit if set. """ + if not (unit and self.temperature_unit and + unit != self.temperature_unit): + return value, unit + + try: + if unit == TEMP_CELCIUS: + # Convert C to F + return round(float(value) * 1.8 + 32.0, 1), TEMP_FAHRENHEIT + + # Convert F to C + return round((float(value)-32.0)/1.8, 1), TEMP_CELCIUS + + except ValueError: + # Could not convert value to float + return value, unit + + class HomeAssistantError(Exception): """ General Home Assistant exception occured. """ pass diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f9a90664ae7..fc08c6565be 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -20,7 +20,10 @@ import homeassistant import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group -from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.const import ( + EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, + CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, TEMP_CELCIUS, + TEMP_FAHRENHEIT) _LOGGER = logging.getLogger(__name__) @@ -73,6 +76,8 @@ def from_config_dict(config, hass=None): if hass is None: hass = homeassistant.HomeAssistant() + process_ha_core_config(hass, config.get(homeassistant.DOMAIN, {})) + enable_logging(hass) _ensure_loader_prepared(hass) @@ -111,8 +116,8 @@ def from_config_file(config_path, hass=None): if hass is None: hass = homeassistant.HomeAssistant() - # Set config dir to directory holding config file - hass.config_dir = os.path.abspath(os.path.dirname(config_path)) + # Set config dir to directory holding config file + hass.config.config_dir = os.path.abspath(os.path.dirname(config_path)) config_dict = {} # check config file type @@ -143,13 +148,13 @@ def enable_logging(hass): logging.basicConfig(level=logging.INFO) # Log errors to a file if we have write access to file or config dir - err_log_path = hass.get_config_path("home-assistant.log") + err_log_path = hass.config.get_config_path("home-assistant.log") err_path_exists = os.path.isfile(err_log_path) # Check if we can write to the error log if it exists or that # we can create files in the containing directory if not. if (err_path_exists and os.access(err_log_path, os.W_OK)) or \ - (not err_path_exists and os.access(hass.config_dir, os.W_OK)): + (not err_path_exists and os.access(hass.config.config_dir, os.W_OK)): err_handler = logging.FileHandler( err_log_path, mode='w', delay=True) @@ -165,6 +170,26 @@ def enable_logging(hass): "Unable to setup error log %s (access denied)", err_log_path) +def process_ha_core_config(hass, config): + """ Processes the [homeassistant] section from the config. """ + for key, attr in ((CONF_LATITUDE, 'latitude'), + (CONF_LONGITUDE, 'longitude'), + (CONF_NAME, 'location_name'), + (CONF_TIME_ZONE, 'time_zone')): + if key in config: + setattr(hass.config, attr, config[key]) + + if CONF_TEMPERATURE_UNIT in config: + unit = config[CONF_TEMPERATURE_UNIT] + + if unit == 'C': + hass.config.temperature_unit = TEMP_CELCIUS + elif unit == 'F': + hass.config.temperature_unit = TEMP_FAHRENHEIT + + hass.config.auto_detect() + + def _ensure_loader_prepared(hass): """ Ensure Home Assistant loader is prepared. """ if not loader.PREPARED: diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 53774210868..48d8476759f 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -10,8 +10,7 @@ import homeassistant as ha import homeassistant.bootstrap as bootstrap import homeassistant.loader as loader from homeassistant.const import ( - CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, - CONF_LATITUDE, CONF_LONGITUDE) + CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID) DOMAIN = "demo" @@ -33,8 +32,6 @@ def setup(hass, config): hass.states.set('a.Demo_Mode', 'Enabled') # Setup sun - config[ha.DOMAIN].setdefault(CONF_LATITUDE, '32.87336') - config[ha.DOMAIN].setdefault(CONF_LONGITUDE, '-117.22743') loader.get_component('sun').setup(hass, config) # Setup demo platforms diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 85e8add47fc..c0e29532b99 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -179,7 +179,8 @@ class DeviceTracker(object): # Write new devices to known devices file if not self.invalid_known_devices_file: - known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE) + known_dev_path = self.hass.config.get_config_path( + KNOWN_DEVICES_FILE) try: # If file does not exist we will write the header too @@ -214,7 +215,7 @@ class DeviceTracker(object): # pylint: disable=too-many-branches def _read_known_devices_file(self): """ Parse and process the known devices file. """ - known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE) + known_dev_path = self.hass.config.get_config_path(KNOWN_DEVICES_FILE) # Return if no known devices file exists if not os.path.isfile(known_dev_path): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a0bcd742aa2..04c187e2d85 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -148,7 +148,7 @@ def setup(hass, config): # Load built-in profiles and custom profiles profile_paths = [os.path.join(os.path.dirname(__file__), LIGHT_PROFILES_FILE), - hass.get_config_path(LIGHT_PROFILES_FILE)] + hass.config.get_config_path(LIGHT_PROFILES_FILE)] profiles = {} for profile_path in profile_paths: diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 1c5e6048f47..80fdb47c82c 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -51,7 +51,8 @@ def setup_bridge(host, hass, add_devices_callback): try: bridge = phue.Bridge( - host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE)) + host, + config_file_path=hass.config.get_config_path(PHUE_CONFIG_FILE)) except ConnectionRefusedError: # Wrong host was given _LOGGER.exception("Error connecting to the Hue bridge at %s", host) diff --git a/homeassistant/components/scheduler/__init__.py b/homeassistant/components/scheduler/__init__.py index 120f90b6ccf..18689b2d7b1 100644 --- a/homeassistant/components/scheduler/__init__.py +++ b/homeassistant/components/scheduler/__init__.py @@ -74,7 +74,7 @@ def setup(hass, config): schedule.schedule(hass) return True - with open(hass.get_config_path(_SCHEDULE_FILE)) as schedule_file: + with open(hass.config.get_config_path(_SCHEDULE_FILE)) as schedule_file: schedule_descriptions = json.load(schedule_file) for schedule_description in schedule_descriptions: diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index 3dc5dbd1998..57b905152c2 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -1,25 +1,25 @@ """ Support for Wink sensors. """ from homeassistant.helpers.device import Device -from homeassistant.const import ( - TEMP_CELCIUS, ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME) +from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Demo sensors. """ add_devices([ - DemoSensor('Outside Temperature', 15.6, TEMP_CELCIUS), - DemoSensor('Outside Humidity', 54, '%'), + DemoSensor('Outside Temperature', 15.6, TEMP_CELCIUS, 12), + DemoSensor('Outside Humidity', 54, '%', None), ]) class DemoSensor(Device): """ A Demo sensor. """ - def __init__(self, name, state, unit_of_measurement): + def __init__(self, name, state, unit_of_measurement, battery): self._name = name self._state = state self._unit_of_measurement = unit_of_measurement + self._battery = battery @property def should_poll(self): @@ -36,10 +36,15 @@ class DemoSensor(Device): """ Returns the state of the device. """ return self._state + @property + def unit_of_measurement(self): + """ Unit this state is expressed in. """ + return self._unit_of_measurement + @property def state_attributes(self): """ Returns the state attributes. """ - return { - ATTR_FRIENDLY_NAME: self._name, - ATTR_UNIT_OF_MEASUREMENT: self._unit_of_measurement, - } + if self._battery: + return { + ATTR_BATTERY_LEVEL: self._battery, + } diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 3e560c95250..11a5f4dcefd 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -7,8 +7,7 @@ Shows system monitor values such as: disk, memory and processor use """ from homeassistant.helpers.device import Device -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF) +from homeassistant.const import STATE_ON, STATE_OFF import psutil import logging @@ -63,14 +62,6 @@ class SystemMonitorSensor(Device): """ Returns the state of the device. """ return self._state - @property - def state_attributes(self): - """ Returns the state attributes. """ - return { - ATTR_FRIENDLY_NAME: self.name, - ATTR_UNIT_OF_MEASUREMENT: self.unit_of_measurement, - } - def update(self): if self.type == 'disk_use_percent': self._state = psutil.disk_usage(self.argument).percent diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 4786ddd7850..7464837afdb 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -29,8 +29,7 @@ from collections import namedtuple import tellcore.telldus as telldus import tellcore.constants as tellcore_constants -from homeassistant.const import ( - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELCIUS) +from homeassistant.const import TEMP_CELCIUS from homeassistant.helpers.device import Device DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit']) @@ -99,7 +98,7 @@ class TellstickSensor(Device): def __init__(self, name, sensor, datatype, sensor_info): self.datatype = datatype self.sensor = sensor - self.unit = sensor_info.unit or None + self.unit_of_measurement = sensor_info.unit or None self._name = "{} {}".format(name, sensor_info.name) @@ -112,15 +111,3 @@ class TellstickSensor(Device): def state(self): """ Returns the state of the device. """ return self.sensor.value(self.datatype).value - - @property - def state_attributes(self): - """ Returns the state attributes. """ - attrs = { - ATTR_FRIENDLY_NAME: self._name, - } - - if self.unit: - attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit - - return attrs diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 984e218e967..bdabb676b8f 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -4,8 +4,8 @@ import logging # pylint: disable=no-name-in-module, import-error import homeassistant.external.wink.pywink as pywink -from homeassistant.components.wink import WinkSensorDevice -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.device import Device +from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED def setup_platform(hass, config, add_devices, discovery_info=None): @@ -22,3 +22,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pywink.set_bearer_token(token) add_devices(WinkSensorDevice(sensor) for sensor in pywink.get_sensors()) + + +class WinkSensorDevice(Device): + """ represents a wink sensor within home assistant. """ + + def __init__(self, wink): + self.wink = wink + + @property + def state(self): + """ Returns the state. """ + return STATE_OPEN if self.is_open else STATE_CLOSED + + @property + def unique_id(self): + """ Returns the id of this wink 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() diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index 7edf3962c8c..7e05cfb9624 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -11,7 +11,7 @@ from pydispatch import dispatcher import homeassistant.components.zwave as zwave from homeassistant.helpers.device import Device 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) @@ -77,11 +77,6 @@ class ZWaveSensor(Device): if battery_level is not None: attrs[ATTR_BATTERY_LEVEL] = battery_level - unit = self.unit - - if unit: - attrs[ATTR_UNIT_OF_MEASUREMENT] = unit - location = self._node.location if location: @@ -90,8 +85,7 @@ class ZWaveSensor(Device): return attrs @property - def unit(self): - """ Unit if sensor has one. """ + def unit_of_measurement(self): return self._value.units def _value_changed(self, value): @@ -126,8 +120,7 @@ class ZWaveMultilevelSensor(ZWaveSensor): return value @property - def unit(self): - """ Unit of this sensor. """ + def unit_of_measurement(self): unit = self._value.units if unit == 'C': diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index fbfff4e226f..52baf430579 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -24,9 +24,6 @@ which event (sunset or sunrise) and the offset. import logging from datetime import datetime, timedelta -import homeassistant as ha -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.helpers import validate_config from homeassistant.util import str_to_datetime, datetime_to_str from homeassistant.components.scheduler import ServiceEventListener @@ -83,11 +80,6 @@ def setup(hass, config): """ Tracks the state of the sun. """ logger = logging.getLogger(__name__) - if not validate_config(config, - {ha.DOMAIN: [CONF_LATITUDE, CONF_LONGITUDE]}, - logger): - return False - try: import ephem except ImportError: @@ -96,8 +88,8 @@ def setup(hass, config): sun = ephem.Sun() # pylint: disable=no-member - latitude = str(config[ha.DOMAIN][CONF_LATITUDE]) - longitude = str(config[ha.DOMAIN][CONF_LONGITUDE]) + latitude = str(hass.config.latitude) + longitude = str(hass.config.longitude) # Validate latitude and longitude observer = ephem.Observer() diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 47b5f61700d..183d47eb7fe 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -11,8 +11,7 @@ from homeassistant.helpers.device_component import DeviceComponent import homeassistant.util as util from homeassistant.helpers.device import Device from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, - STATE_ON, STATE_OFF) + ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF) DOMAIN = "thermostat" DEPENDENCIES = [] @@ -109,11 +108,6 @@ class ThermostatDevice(Device): """ Returns the current state. """ return self.target_temperature - @property - def unit_of_measurement(self): - """ Returns the unit of measurement. """ - return "" - @property def device_state_attributes(self): """ Returns device specific state attributes. """ @@ -123,8 +117,8 @@ class ThermostatDevice(Device): def state_attributes(self): """ Returns optional state attributes. """ data = { - ATTR_UNIT_OF_MEASUREMENT: self.unit_of_measurement, - ATTR_CURRENT_TEMPERATURE: self.current_temperature + ATTR_CURRENT_TEMPERATURE: self.hass.config.temperature( + self.current_temperature, self.unit_of_measurement)[0] } is_away = self.is_away_mode_on diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 643af935f94..239f308affb 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -8,10 +8,9 @@ import homeassistant.external.wink.pywink as pywink from homeassistant import bootstrap from homeassistant.loader import get_component -from homeassistant.helpers import validate_config, ToggleDevice, Device +from homeassistant.helpers import validate_config, ToggleDevice from homeassistant.const import ( EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN, - STATE_OPEN, STATE_CLOSED, ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) DOMAIN = "wink" @@ -53,44 +52,6 @@ def setup(hass, config): return True -class WinkSensorDevice(Device): - """ represents a wink sensor within home assistant. """ - - def __init__(self, wink): - self.wink = wink - - @property - def state(self): - """ Returns the state. """ - return STATE_OPEN if self.is_open else STATE_CLOSED - - @property - def unique_id(self): - """ Returns the id of this wink switch """ - return "{}.{}".format(self.__class__, self.wink.deviceId()) - - @property - def name(self): - """ Returns the name of the sensor if any. """ - return self.wink.name() - - @property - def state_attributes(self): - """ Returns optional state attributes. """ - return { - ATTR_FRIENDLY_NAME: self.wink.name() - } - - def update(self): - """ Update state of the sensor. """ - self.wink.updateState() - - @property - def is_open(self): - """ True if door is open. """ - return self.wink.state() - - class WinkToggleDevice(ToggleDevice): """ represents a Wink switch within home assistant. """ diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 41bcfb876ae..15e436d7f4d 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -72,7 +72,7 @@ def setup(hass, config): # Setup options options = ZWaveOption( config[DOMAIN].get(CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH), - user_path=hass.config_dir) + user_path=hass.config.config_dir) options.set_console_output(use_debug) options.lock() diff --git a/homeassistant/const.py b/homeassistant/const.py index ed74f1edf4f..467bb692399 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,6 +8,9 @@ DEVICE_DEFAULT_NAME = "Unnamed Device" # #### CONFIG #### CONF_LATITUDE = "latitude" CONF_LONGITUDE = "longitude" +CONF_TEMPERATURE_UNIT = "temperature_unit" +CONF_NAME = "name" +CONF_TIME_ZONE = "time_zone" CONF_PLATFORM = "platform" CONF_HOST = "host" diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py index 017d2673f70..5d400124089 100644 --- a/homeassistant/helpers/device.py +++ b/homeassistant/helpers/device.py @@ -8,7 +8,8 @@ Provides ABC for devices in HA. from homeassistant import NoEntitySpecifiedError from homeassistant.const import ( - ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME) + ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, + DEVICE_DEFAULT_NAME, TEMP_CELCIUS, TEMP_FAHRENHEIT) class Device(object): @@ -46,6 +47,11 @@ class Device(object): """ 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 @@ -82,12 +88,25 @@ class Device(object): 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 - return self.hass.states.set(self.entity_id, self.state, attr) + 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, Device) and diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 4bbab3c1ac6..14f1632fad3 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -46,11 +46,11 @@ def prepare(hass): pkgutil.iter_modules(components.__path__, 'homeassistant.components.')) # Look for available custom components - custom_path = hass.get_config_path("custom_components") + custom_path = hass.config.get_config_path("custom_components") if os.path.isdir(custom_path): # Ensure we can load custom components using Pythons import - sys.path.insert(0, hass.config_dir) + sys.path.insert(0, hass.config.config_dir) # We cannot use the same approach as for built-in components because # custom components might only contain a platform for a component. diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 9065c359c19..3c2ffe69f29 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -14,7 +14,6 @@ import logging import json import enum import urllib.parse -import os import requests @@ -115,10 +114,9 @@ class HomeAssistant(ha.HomeAssistant): self.bus = EventBus(remote_api, pool) self.services = ha.ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus, self.remote_api) + self.config = ha.Config() self.components = [] - self.config_dir = os.path.join(os.getcwd(), 'config') - def start(self): # Ensure a local API exists to connect with remote if self.local_api is None: diff --git a/tests/helpers.py b/tests/helpers.py index f08df8c4e80..2157b46d835 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -14,7 +14,7 @@ from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME def get_test_home_assistant(): """ Returns a Home Assistant object pointing at test config dir. """ hass = ha.HomeAssistant() - hass.config_dir = os.path.join(os.path.dirname(__file__), "config") + hass.config.config_dir = os.path.join(os.path.dirname(__file__), "config") return hass diff --git a/tests/test_component_sun.py b/tests/test_component_sun.py index 33570cdcc4e..a4ff19429f3 100644 --- a/tests/test_component_sun.py +++ b/tests/test_component_sun.py @@ -11,7 +11,6 @@ import datetime as dt import ephem import homeassistant as ha -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE import homeassistant.components.sun as sun @@ -35,12 +34,9 @@ class TestSun(unittest.TestCase): def test_setting_rising(self): """ Test retrieving sun setting and rising. """ # Compare it with the real data - self.assertTrue(sun.setup( - self.hass, - {ha.DOMAIN: { - CONF_LATITUDE: '32.87336', - CONF_LONGITUDE: '117.22743' - }})) + self.hass.config.latitude = '32.87336' + self.hass.config.longitude = '117.22743' + sun.setup(self.hass, None) observer = ephem.Observer() observer.lat = '32.87336' # pylint: disable=assigning-non-slot @@ -74,12 +70,9 @@ class TestSun(unittest.TestCase): def test_state_change(self): """ Test if the state changes at next setting/rising. """ - self.assertTrue(sun.setup( - self.hass, - {ha.DOMAIN: { - CONF_LATITUDE: '32.87336', - CONF_LONGITUDE: '117.22743' - }})) + self.hass.config.latitude = '32.87336' + self.hass.config.longitude = '117.22743' + sun.setup(self.hass, None) if sun.is_on(self.hass): test_state = sun.STATE_BELOW_HORIZON @@ -96,30 +89,3 @@ class TestSun(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(test_state, self.hass.states.get(sun.ENTITY_ID).state) - - def test_setup(self): - """ Test Sun setup with empty and wrong configs. """ - self.assertFalse(sun.setup(self.hass, {})) - self.assertFalse(sun.setup(self.hass, {sun.DOMAIN: {}})) - self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: {CONF_LATITUDE: '32.87336'}})) - self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: {CONF_LONGITUDE: '117.22743'}})) - self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: {CONF_LATITUDE: 'hello'}})) - self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: {CONF_LONGITUDE: 'how are you'}})) - self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: { - CONF_LATITUDE: 'wrong', CONF_LONGITUDE: '117.22743' - }})) - self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: { - CONF_LATITUDE: '32.87336', CONF_LONGITUDE: 'wrong' - }})) - - # Test with correct config - self.assertTrue(sun.setup( - self.hass, {ha.DOMAIN: { - CONF_LATITUDE: '32.87336', CONF_LONGITUDE: '117.22743' - }})) From 7a7f486cb2de2d13090ce9dff65d99896af88704 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Mar 2015 12:27:56 -0700 Subject: [PATCH 02/47] Rename config.get_config_path to config.path --- homeassistant/__init__.py | 16 ++++++++++------ homeassistant/bootstrap.py | 2 +- .../components/device_tracker/__init__.py | 5 ++--- homeassistant/components/light/__init__.py | 2 +- homeassistant/components/light/hue.py | 2 +- homeassistant/components/recorder.py | 2 +- homeassistant/components/scheduler/__init__.py | 2 +- homeassistant/loader.py | 2 +- tests/test_component_device_scanner.py | 2 +- tests/test_component_light.py | 6 +++--- tests/test_core.py | 4 ++-- 11 files changed, 24 insertions(+), 21 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 062616bf0b4..8ed3d7bc1b6 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -63,11 +63,15 @@ class HomeAssistant(object): @property def config_dir(self): """ DEPRECATED 3/18/2015. Use hass.config.config_dir """ + _LOGGER.warning( + 'hass.config_dir is deprecated. Use hass.config.config_dir') return self.config.config_dir def get_config_path(self, path): - """ DEPRECATED 3/18/2015. Use hass.config.get_config_path """ - return self.config.get_config_path(path) + """ DEPRECATED 3/18/2015. Use hass.config.path """ + _LOGGER.warning( + 'hass.get_config_path is deprecated. Use hass.config.path') + return self.config.path(path) def start(self): """ Start home assistant. """ @@ -854,10 +858,6 @@ class Config(object): # Directory that holds the configuration self.config_dir = os.path.join(os.getcwd(), 'config') - def get_config_path(self, path): - """ Returns path to the file within the config dir. """ - return os.path.join(self.config_dir, path) - def auto_detect(self): """ Will attempt to detect config of Home Assistant. """ # Only detect if location or temp unit missing @@ -892,6 +892,10 @@ class Config(object): 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 diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index fc08c6565be..83d966731cd 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -148,7 +148,7 @@ def enable_logging(hass): logging.basicConfig(level=logging.INFO) # Log errors to a file if we have write access to file or config dir - err_log_path = hass.config.get_config_path("home-assistant.log") + err_log_path = hass.config.path("home-assistant.log") err_path_exists = os.path.isfile(err_log_path) # Check if we can write to the error log if it exists or that diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index c0e29532b99..ac0c8d483ff 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -179,8 +179,7 @@ class DeviceTracker(object): # Write new devices to known devices file if not self.invalid_known_devices_file: - known_dev_path = self.hass.config.get_config_path( - KNOWN_DEVICES_FILE) + known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE) try: # If file does not exist we will write the header too @@ -215,7 +214,7 @@ class DeviceTracker(object): # pylint: disable=too-many-branches def _read_known_devices_file(self): """ Parse and process the known devices file. """ - known_dev_path = self.hass.config.get_config_path(KNOWN_DEVICES_FILE) + known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE) # Return if no known devices file exists if not os.path.isfile(known_dev_path): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 04c187e2d85..e7951afe31a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -148,7 +148,7 @@ def setup(hass, config): # Load built-in profiles and custom profiles profile_paths = [os.path.join(os.path.dirname(__file__), LIGHT_PROFILES_FILE), - hass.config.get_config_path(LIGHT_PROFILES_FILE)] + hass.config.path(LIGHT_PROFILES_FILE)] profiles = {} for profile_path in profile_paths: diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 80fdb47c82c..def43df4fe2 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -52,7 +52,7 @@ def setup_bridge(host, hass, add_devices_callback): try: bridge = phue.Bridge( host, - config_file_path=hass.config.get_config_path(PHUE_CONFIG_FILE)) + config_file_path=hass.config.path(PHUE_CONFIG_FILE)) except ConnectionRefusedError: # Wrong host was given _LOGGER.exception("Error connecting to the Hue bridge at %s", host) diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index e31f933e421..84d4c1ecccd 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -262,7 +262,7 @@ class Recorder(threading.Thread): def _setup_connection(self): """ Ensure database is ready to fly. """ - db_path = self.hass.get_config_path(DB_FILE) + db_path = self.hass.config.path(DB_FILE) self.conn = sqlite3.connect(db_path, check_same_thread=False) self.conn.row_factory = sqlite3.Row diff --git a/homeassistant/components/scheduler/__init__.py b/homeassistant/components/scheduler/__init__.py index 18689b2d7b1..d05d90a903b 100644 --- a/homeassistant/components/scheduler/__init__.py +++ b/homeassistant/components/scheduler/__init__.py @@ -74,7 +74,7 @@ def setup(hass, config): schedule.schedule(hass) return True - with open(hass.config.get_config_path(_SCHEDULE_FILE)) as schedule_file: + with open(hass.config.path(_SCHEDULE_FILE)) as schedule_file: schedule_descriptions = json.load(schedule_file) for schedule_description in schedule_descriptions: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 14f1632fad3..d38e0df4465 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -46,7 +46,7 @@ def prepare(hass): pkgutil.iter_modules(components.__path__, 'homeassistant.components.')) # Look for available custom components - custom_path = hass.config.get_config_path("custom_components") + custom_path = hass.config.path("custom_components") if os.path.isdir(custom_path): # Ensure we can load custom components using Pythons import diff --git a/tests/test_component_device_scanner.py b/tests/test_component_device_scanner.py index 3b43126681f..2bd392c21d0 100644 --- a/tests/test_component_device_scanner.py +++ b/tests/test_component_device_scanner.py @@ -32,7 +32,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.hass = get_test_home_assistant() loader.prepare(self.hass) - self.known_dev_path = self.hass.get_config_path( + self.known_dev_path = self.hass.config.path( device_tracker.KNOWN_DEVICES_FILE) def tearDown(self): # pylint: disable=invalid-name diff --git a/tests/test_component_light.py b/tests/test_component_light.py index c5ccb687e07..eb8a17361bf 100644 --- a/tests/test_component_light.py +++ b/tests/test_component_light.py @@ -29,7 +29,7 @@ class TestLight(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE) + user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) if os.path.isfile(user_light_file): os.remove(user_light_file) @@ -218,7 +218,7 @@ class TestLight(unittest.TestCase): platform = loader.get_component('light.test') platform.init() - user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE) + user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) # Setup a wrong light file with open(user_light_file, 'w') as user_file: @@ -234,7 +234,7 @@ class TestLight(unittest.TestCase): platform = loader.get_component('light.test') platform.init() - user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE) + user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) with open(user_light_file, 'w') as user_file: user_file.write('id,x,y,brightness\n') diff --git a/tests/test_core.py b/tests/test_core.py index 61ac776fba8..a5c37f753b9 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -35,10 +35,10 @@ class TestHomeAssistant(unittest.TestCase): def test_get_config_path(self): """ Test get_config_path method. """ self.assertEqual(os.path.join(os.getcwd(), "config"), - self.hass.config_dir) + self.hass.config.config_dir) self.assertEqual(os.path.join(os.getcwd(), "config", "test.conf"), - self.hass.get_config_path("test.conf")) + self.hass.config.path("test.conf")) def test_block_till_stoped(self): """ Test if we can block till stop service is called. """ From d3f0210b1abbc917c01ab8ee9b61e9bb5e378d9c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 18:49:30 -0700 Subject: [PATCH 03/47] Refactor helper.device to helper.entity Introduces a minor backwards compatible change: device_component function add_devices is renamed to add_entities. --- homeassistant/components/scene.py | 4 +- homeassistant/helpers/__init__.py | 4 +- homeassistant/helpers/device.py | 141 +-------------------- homeassistant/helpers/device_component.py | 141 +-------------------- homeassistant/helpers/entity.py | 139 +++++++++++++++++++++ homeassistant/helpers/entity_component.py | 142 ++++++++++++++++++++++ 6 files changed, 297 insertions(+), 274 deletions(-) create mode 100644 homeassistant/helpers/entity.py create mode 100644 homeassistant/helpers/entity_component.py diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index 889d7271ef1..28296859f80 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -48,8 +48,8 @@ def setup(hass, config): component = DeviceComponent(logger, DOMAIN, hass) - component.add_devices(Scene(hass, _process_config(scene_config)) - for scene_config in scene_configs) + component.add_entities(Scene(hass, _process_config(scene_config)) + for scene_config in scene_configs) def handle_scene_service(service): """ Handles calls to the switch services. """ diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index a677ef8d0c7..4d92df43282 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -5,9 +5,9 @@ from homeassistant.loader import get_component from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.util import ensure_unique_string, slugify -# Deprecated 3/5/2015 - Moved to homeassistant.helpers.device +# Deprecated 3/5/2015 - Moved to homeassistant.helpers.entity # pylint: disable=unused-import -from .device import Device, ToggleDevice # noqa +from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py index 5d400124089..4c713693c43 100644 --- a/homeassistant/helpers/device.py +++ b/homeassistant/helpers/device.py @@ -1,139 +1,10 @@ """ -homeassistant.helpers.device -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Provides ABC for devices in HA. +Deprecated since 3/21/2015 - please use helpers.entity """ +import logging -from homeassistant import NoEntitySpecifiedError +# pylint: disable=unused-import +from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa -from homeassistant.const import ( - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, - DEVICE_DEFAULT_NAME, TEMP_CELCIUS, TEMP_FAHRENHEIT) - - -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 {} - - @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 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() - - 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, Device) and - other.unique_id == self.unique_id) - - def __repr__(self): - return "".format(self.name, self.state) - - -class ToggleDevice(Device): - """ ABC for devices that can be turned on and off. """ - # pylint: disable=no-self-use - - @property - def state(self): - """ Returns the state. """ - return STATE_ON if self.is_on else STATE_OFF - - @property - def is_on(self): - """ True if device is on. """ - return False - - def turn_on(self, **kwargs): - """ Turn the device on. """ - pass - - def turn_off(self, **kwargs): - """ Turn the device off. """ - pass +logging.getLogger(__name__).warning( + 'This file is deprecated. Please use helpers.entity') diff --git a/homeassistant/helpers/device_component.py b/homeassistant/helpers/device_component.py index a0b61924080..248297a9694 100644 --- a/homeassistant/helpers/device_component.py +++ b/homeassistant/helpers/device_component.py @@ -1,139 +1,10 @@ """ -Provides helpers for components that handle devices. +Deprecated since 3/21/2015 - please use helpers.entity_component """ -from homeassistant.loader import get_component -from homeassistant.helpers import ( - generate_entity_id, config_per_platform, extract_entity_ids) -from homeassistant.components import group, discovery -from homeassistant.const import ATTR_ENTITY_ID +import logging -DEFAULT_SCAN_INTERVAL = 15 +# pylint: disable=unused-import +from .entity_component import EntityComponent as DeviceComponent # noqa - -class DeviceComponent(object): - # pylint: disable=too-many-instance-attributes - # pylint: disable=too-many-arguments - """ - Helper class that will help a device component manage its devices. - """ - def __init__(self, logger, domain, hass, - scan_interval=DEFAULT_SCAN_INTERVAL, - discovery_platforms=None, group_name=None): - self.logger = logger - self.hass = hass - - self.domain = domain - self.entity_id_format = domain + '.{}' - self.scan_interval = scan_interval - self.discovery_platforms = discovery_platforms - self.group_name = group_name - - self.devices = {} - self.group = None - self.is_polling = False - - def setup(self, config): - """ - Sets up a full device component: - - Loads the platforms from the config - - Will listen for supported discovered platforms - """ - # Look in config for Domain, Domain 2, Domain 3 etc and load them - for p_type, p_config in \ - config_per_platform(config, self.domain, self.logger): - - self._setup_platform(p_type, p_config) - - if self.discovery_platforms: - discovery.listen(self.hass, self.discovery_platforms.keys(), - self._device_discovered) - - def add_devices(self, new_devices): - """ - Takes in a list of new devices. For each device will see if it already - exists. If not, will add it, set it up and push the first state. - """ - for device in new_devices: - if device is not None and device not in self.devices.values(): - device.hass = self.hass - - device.entity_id = generate_entity_id( - self.entity_id_format, device.name, self.devices.keys()) - - self.devices[device.entity_id] = device - - device.update_ha_state() - - if self.group is None and self.group_name is not None: - self.group = group.Group(self.hass, self.group_name, - user_defined=False) - - if self.group is not None: - self.group.update_tracked_entity_ids(self.devices.keys()) - - self._start_polling() - - def extract_from_service(self, service): - """ - Takes a service and extracts all known devices. - Will return all if no entity IDs given in service. - """ - if ATTR_ENTITY_ID not in service.data: - return self.devices.values() - else: - return [self.devices[entity_id] for entity_id - in extract_entity_ids(self.hass, service) - if entity_id in self.devices] - - def _update_device_states(self, now): - """ Update the states of all the lights. """ - self.logger.info("Updating %s states", self.domain) - - for device in self.devices.values(): - if device.should_poll: - device.update_ha_state(True) - - def _device_discovered(self, service, info): - """ Called when a device is discovered. """ - if service not in self.discovery_platforms: - return - - self._setup_platform(self.discovery_platforms[service], {}, info) - - def _start_polling(self): - """ Start polling device states if necessary. """ - if self.is_polling or \ - not any(device.should_poll for device in self.devices.values()): - return - - self.is_polling = True - - self.hass.track_time_change( - self._update_device_states, - second=range(0, 60, self.scan_interval)) - - def _setup_platform(self, platform_type, config, discovery_info=None): - """ Tries to setup a platform for this component. """ - platform_name = '{}.{}'.format(self.domain, platform_type) - platform = get_component(platform_name) - - if platform is None: - self.logger.error('Unable to find platform %s', platform_type) - return - - try: - platform.setup_platform( - self.hass, config, self.add_devices, discovery_info) - except AttributeError: - # Support old deprecated method for now - 3/1/2015 - if hasattr(platform, 'get_devices'): - self.logger.warning( - "Please upgrade %s to return new devices using " - "setup_platform. See %s/demo.py for an example.", - platform_name, self.domain) - self.add_devices(platform.get_devices(self.hass, config)) - - else: - # AttributeError if setup_platform does not exist - self.logger.exception( - "Error setting up %s", platform_type) +logging.getLogger(__name__).warning( + 'This file is deprecated. Please use helpers.entity_component') diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py new file mode 100644 index 00000000000..a8ee712b0f7 --- /dev/null +++ b/homeassistant/helpers/entity.py @@ -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 "".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 diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py new file mode 100644 index 00000000000..21093d89804 --- /dev/null +++ b/homeassistant/helpers/entity_component.py @@ -0,0 +1,142 @@ +""" +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) + except AttributeError: + # Support old deprecated method for now - 3/1/2015 + if hasattr(platform, 'get_entities'): + 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_devices(platform.get_entities(self.hass, config)) + + else: + # AttributeError if setup_platform does not exist + self.logger.exception( + "Error setting up %s", platform_type) From a9324ba9d4c9310488acfd189a29b9972dc9706d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 19:16:13 -0700 Subject: [PATCH 04/47] Update components to use Entity instead of Device --- homeassistant/components/light/demo.py | 4 ++-- homeassistant/components/light/hue.py | 4 ++-- homeassistant/components/light/tellstick.py | 4 ++-- homeassistant/components/media_player/__init__.py | 4 ++-- homeassistant/components/scene.py | 4 ++-- homeassistant/components/sensor/demo.py | 4 ++-- homeassistant/components/sensor/sabnzbd.py | 4 ++-- homeassistant/components/sensor/systemmonitor.py | 4 ++-- homeassistant/components/sensor/tellstick.py | 4 ++-- homeassistant/components/sensor/vera.py | 4 ++-- homeassistant/components/sensor/wink.py | 4 ++-- homeassistant/components/sensor/zwave.py | 4 ++-- homeassistant/components/switch/demo.py | 4 ++-- homeassistant/components/switch/tellstick.py | 4 ++-- homeassistant/components/switch/vera.py | 4 ++-- homeassistant/components/switch/wemo.py | 4 ++-- homeassistant/components/thermostat/__init__.py | 4 ++-- homeassistant/components/wink.py | 5 +++-- homeassistant/helpers/entity_component.py | 2 +- 19 files changed, 38 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index 4f25fef999d..73fb1580e60 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -1,7 +1,7 @@ """ Provides demo lights. """ import random -from homeassistant.helpers.device import ToggleDevice +from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_XY_COLOR @@ -22,7 +22,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): ]) -class DemoLight(ToggleDevice): +class DemoLight(ToggleEntity): """ Provides a demo switch. """ def __init__(self, name, state, xy=None, brightness=180): self._name = name or DEVICE_DEFAULT_NAME diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index def43df4fe2..683d8a1a4c9 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -6,7 +6,7 @@ from urllib.parse import urlparse from homeassistant.loader import get_component import homeassistant.util as util -from homeassistant.helpers.device import ToggleDevice +from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import CONF_HOST from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION, @@ -131,7 +131,7 @@ def request_configuration(host, hass, add_devices_callback): ) -class HueLight(ToggleDevice): +class HueLight(ToggleEntity): """ Represents a Hue light """ def __init__(self, light_id, info, bridge, update_lights): diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index c7648a6902a..6d28c196326 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -3,7 +3,7 @@ import logging # pylint: disable=no-name-in-module, import-error from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.helpers.device import ToggleDevice +from homeassistant.helpers.entity import ToggleEntity import tellcore.constants as tellcore_constants @@ -27,7 +27,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): add_devices_callback(lights) -class TellstickLight(ToggleDevice): +class TellstickLight(ToggleEntity): """ Represents a tellstick light """ last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | tellcore_constants.TELLSTICK_TURNOFF | diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d0d1e7713bb..4623c538e62 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -7,7 +7,7 @@ Component to interface with various media players import logging 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.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, @@ -171,7 +171,7 @@ def setup(hass, config): return True -class MediaPlayerDevice(Device): +class MediaPlayerDevice(Entity): """ ABC for media player devices. """ def turn_off(self): diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index 28296859f80..df7d26e8b3f 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -19,7 +19,7 @@ import logging from collections import namedtuple 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.state import reproduce_state from homeassistant.const import ( @@ -93,7 +93,7 @@ def _process_config(scene_config): return SceneConfig(name, states) -class Scene(ToggleDevice): +class Scene(ToggleEntity): """ A scene is a group of entities and the states we want them to be. """ def __init__(self, hass, scene_config): diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index 57b905152c2..71b6cf2b8fe 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -1,5 +1,5 @@ """ Support for Wink sensors. """ -from homeassistant.helpers.device import Device +from homeassistant.helpers.entity import Entity from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL @@ -12,7 +12,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) -class DemoSensor(Device): +class DemoSensor(Entity): """ A Demo sensor. """ def __init__(self, name, state, unit_of_measurement, battery): diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index c85dabf057d..431752f544c 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -52,7 +52,7 @@ list of all available variables from homeassistant.util import Throttle from datetime import timedelta -from homeassistant.helpers.device import Device +from homeassistant.helpers.entity import Entity # pylint: disable=no-name-in-module, import-error from homeassistant.external.nzbclients.sabnzbd import SabnzbdApi from homeassistant.external.nzbclients.sabnzbd import SabnzbdApiException @@ -109,7 +109,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -class SabnzbdSensor(Device): +class SabnzbdSensor(Entity): """ A Sabnzbd sensor """ def __init__(self, sensor_type, sabnzb_client, client_name): diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 11a5f4dcefd..99e2453c487 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -6,7 +6,7 @@ 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 STATE_ON, STATE_OFF import psutil import logging @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -class SystemMonitorSensor(Device): +class SystemMonitorSensor(Entity): """ A system monitor sensor """ def __init__(self, sensor_type, argument=''): diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 6070e6f2e68..47e3e2ade3a 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -30,7 +30,7 @@ import tellcore.telldus as telldus import tellcore.constants as tellcore_constants from homeassistant.const import TEMP_CELCIUS -from homeassistant.helpers.device import Device +from homeassistant.helpers.entity import Entity import homeassistant.util as util DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit']) @@ -93,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors) -class TellstickSensor(Device): +class TellstickSensor(Entity): """ Represents a Tellstick sensor. """ def __init__(self, name, sensor, datatype, sensor_info): diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 9ec0b272856..daa2acf5004 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -50,7 +50,7 @@ import logging import time from requests.exceptions import RequestException -from homeassistant.helpers import Device +from homeassistant.helpers.entity import Entity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) # pylint: disable=no-name-in-module, import-error @@ -99,7 +99,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(get_devices(hass, config)) -class VeraSensor(Device): +class VeraSensor(Entity): """ Represents a Vera Sensor """ def __init__(self, vera_device, extra_data=None): diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index bdabb676b8f..ff61f02d041 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -4,7 +4,7 @@ import logging # pylint: disable=no-name-in-module, import-error import homeassistant.external.wink.pywink as pywink -from homeassistant.helpers.device import Device +from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED @@ -24,7 +24,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkSensorDevice(sensor) for sensor in pywink.get_sensors()) -class WinkSensorDevice(Device): +class WinkSensorDevice(Entity): """ represents a wink sensor within home assistant. """ def __init__(self, wink): diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index 7e05cfb9624..d057635d9f6 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -9,7 +9,7 @@ from openzwave.network import ZWaveNetwork from pydispatch import dispatcher import homeassistant.components.zwave as zwave -from homeassistant.helpers.device import Device +from homeassistant.helpers.entity import Entity from homeassistant.const import ( ATTR_BATTERY_LEVEL, STATE_ON, STATE_OFF, TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_LOCATION) @@ -33,7 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ZWaveMultilevelSensor(value)]) -class ZWaveSensor(Device): +class ZWaveSensor(Entity): """ Represents a Z-Wave sensor. """ def __init__(self, sensor_value): self._value = sensor_value diff --git a/homeassistant/components/switch/demo.py b/homeassistant/components/switch/demo.py index a2009b0ec6e..998597c3e8c 100644 --- a/homeassistant/components/switch/demo.py +++ b/homeassistant/components/switch/demo.py @@ -1,5 +1,5 @@ """ Demo platform that has two fake switchces. """ -from homeassistant.helpers.device import ToggleDevice +from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME @@ -12,7 +12,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): ]) -class DemoSwitch(ToggleDevice): +class DemoSwitch(ToggleEntity): """ Provides a demo switch. """ def __init__(self, name, state): self._name = name or DEVICE_DEFAULT_NAME diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index 17a8128927b..fccd2ba5c08 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -3,7 +3,7 @@ import logging from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.helpers.device import ToggleDevice +from homeassistant.helpers.entity import ToggleEntity import tellcore.constants as tellcore_constants @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): add_devices_callback(switches) -class TellstickSwitchDevice(ToggleDevice): +class TellstickSwitchDevice(ToggleEntity): """ represents a Tellstick switch within home assistant. """ last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | tellcore_constants.TELLSTICK_TURNOFF) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index e302e707a72..7a9f8f96b79 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -52,7 +52,7 @@ import logging import time from requests.exceptions import RequestException -from homeassistant.helpers import ToggleDevice +from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) # pylint: disable=no-name-in-module, import-error @@ -100,7 +100,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(get_devices(hass, config)) -class VeraSwitch(ToggleDevice): +class VeraSwitch(ToggleEntity): """ Represents a Vera Switch """ def __init__(self, vera_device, extra_data=None): diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index b90c008a7d0..2baf10f53d8 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -1,7 +1,7 @@ """ Support for WeMo switchces. """ import logging -from homeassistant.helpers.device import ToggleDevice +from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.switch import ( ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH) @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if isinstance(switch, pywemo.Switch)]) -class WemoSwitch(ToggleDevice): +class WemoSwitch(ToggleEntity): """ represents a WeMo switch within home assistant. """ def __init__(self, wemo): self.wemo = wemo diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 183d47eb7fe..d91c54efa7f 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -9,7 +9,7 @@ import logging from homeassistant.helpers.device_component import DeviceComponent import homeassistant.util as util -from homeassistant.helpers.device import Device +from homeassistant.helpers.entity import Entity from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF) @@ -98,7 +98,7 @@ def setup(hass, config): return True -class ThermostatDevice(Device): +class ThermostatDevice(Entity): """ Represents a thermostat within Home Assistant. """ # pylint: disable=no-self-use diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 239f308affb..059aac4363c 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -8,7 +8,8 @@ import homeassistant.external.wink.pywink as pywink from homeassistant import bootstrap from homeassistant.loader import get_component -from homeassistant.helpers import validate_config, ToggleDevice +from homeassistant.helpers import validate_config +from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN, ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) @@ -52,7 +53,7 @@ def setup(hass, config): return True -class WinkToggleDevice(ToggleDevice): +class WinkToggleDevice(ToggleEntity): """ represents a Wink switch within home assistant. """ def __init__(self, wink): diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 21093d89804..dd98149eaf8 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -134,7 +134,7 @@ class EntityComponent(object): "Please upgrade %s to return new entities using " "setup_platform. See %s/demo.py for an example.", platform_name, self.domain) - self.add_devices(platform.get_entities(self.hass, config)) + self.add_entities(platform.get_devices(self.hass, config)) else: # AttributeError if setup_platform does not exist From bbfd97e2b803a4cf69d315bf9c4308612633d368 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 19:37:18 -0700 Subject: [PATCH 05/47] Migrate components to use EntityComponent --- homeassistant/components/light/__init__.py | 4 ++-- homeassistant/components/media_player/__init__.py | 4 ++-- homeassistant/components/scene.py | 4 ++-- homeassistant/components/sensor/__init__.py | 4 ++-- homeassistant/components/switch/__init__.py | 4 ++-- homeassistant/components/thermostat/__init__.py | 4 ++-- tests/helpers.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index e7951afe31a..16020f1ecb1 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -52,7 +52,7 @@ import logging import os import csv -from homeassistant.helpers.device_component import DeviceComponent +from homeassistant.helpers.entity_component import EntityComponent import homeassistant.util as util from homeassistant.const import ( @@ -140,7 +140,7 @@ def turn_off(hass, entity_id=None, transition=None): def setup(hass, config): """ Exposes light control via statemachine and services. """ - component = DeviceComponent( + component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS, GROUP_NAME_ALL_LIGHTS) component.setup(config) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 4623c538e62..00fee802397 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -8,7 +8,7 @@ import logging from homeassistant.components import discovery from homeassistant.helpers.entity import Entity -from homeassistant.helpers.device_component import DeviceComponent +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, @@ -126,7 +126,7 @@ SERVICE_TO_METHOD = { def setup(hass, config): """ Track states and offer events for media_players. """ - component = DeviceComponent( + component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS) diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index df7d26e8b3f..979b3281300 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -20,7 +20,7 @@ from collections import namedtuple from homeassistant import State 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.const import ( ATTR_ENTITY_ID, STATE_OFF, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF) @@ -46,7 +46,7 @@ def setup(hass, config): logger.error('Scene config should be a list of scenes') return False - component = DeviceComponent(logger, DOMAIN, hass) + component = EntityComponent(logger, DOMAIN, hass) component.add_entities(Scene(hass, _process_config(scene_config)) for scene_config in scene_configs) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 1aebf7f59ad..8248651710a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -5,7 +5,7 @@ Component to interface with various sensors that can be monitored. """ import logging -from homeassistant.helpers.device_component import DeviceComponent +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.components import wink, zwave DOMAIN = 'sensor' @@ -23,7 +23,7 @@ DISCOVERY_PLATFORMS = { def setup(hass, config): """ Track states and offer events for sensors. """ - component = DeviceComponent( + component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index a93ba1b95df..f246692560d 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -6,7 +6,7 @@ Component to interface with various switches that can be controlled remotely. import logging from datetime import timedelta -from homeassistant.helpers.device_component import DeviceComponent +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) @@ -58,7 +58,7 @@ def turn_off(hass, entity_id=None): def setup(hass, config): """ Track states and offer events for switches. """ - component = DeviceComponent( + component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS, GROUP_NAME_ALL_SWITCHES) component.setup(config) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index d91c54efa7f..08940b977c9 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -6,7 +6,7 @@ Provides functionality to interact with thermostats. """ import logging -from homeassistant.helpers.device_component import DeviceComponent +from homeassistant.helpers.entity_component import EntityComponent import homeassistant.util as util from homeassistant.helpers.entity import Entity @@ -52,7 +52,7 @@ def set_temperature(hass, temperature, entity_id=None): def setup(hass, config): """ Setup thermostats. """ - component = DeviceComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) component.setup(config) def thermostat_service(service): diff --git a/tests/helpers.py b/tests/helpers.py index 2157b46d835..d98c549346d 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -7,7 +7,7 @@ Helper method for writing tests. import os import homeassistant as ha -from homeassistant.helpers.device import ToggleDevice +from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME @@ -42,7 +42,7 @@ class MockModule(object): self.setup = lambda hass, config: False if setup is None else setup -class MockToggleDevice(ToggleDevice): +class MockToggleDevice(ToggleEntity): """ Provides a mock toggle device. """ def __init__(self, name, state): self._name = name or DEVICE_DEFAULT_NAME From e8724358708da1fc1c51844ad0b510db3570d60a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 19:38:43 -0700 Subject: [PATCH 06/47] Tellstick sensor: Lookup sensor ids as ints --- homeassistant/components/sensor/tellstick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 47e3e2ade3a..699821fa6b6 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -75,7 +75,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for ts_sensor in core.sensors(): try: - sensor_name = config[str(ts_sensor.id)] + sensor_name = config[ts_sensor.id] except KeyError: if 'only_named' in config: continue From a0f1c1d17a02ea8f07da084662ef779af06c3c97 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 22 Mar 2015 14:36:58 +1100 Subject: [PATCH 07/47] Added support for pushover notifications --- homeassistant/components/notify/pushover.py | 72 +++++++++++++++++++++ requirements.txt | 3 + 2 files changed, 75 insertions(+) create mode 100644 homeassistant/components/notify/pushover.py diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py new file mode 100644 index 00000000000..3ebd9030683 --- /dev/null +++ b/homeassistant/components/notify/pushover.py @@ -0,0 +1,72 @@ +""" +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 + user_key: ABCDEFGHJKLMNOPQRSTUVXYZ + +VARIABLES: + +user_key +*Required +To retrieve this value log into your account at http://pushover.com + +""" +import logging + +from homeassistant.helpers import validate_config +from homeassistant.components.notify import ( + DOMAIN, ATTR_TITLE, BaseNotificationService) + +_LOGGER = logging.getLogger(__name__) + +_API_TOKEN = 'a99PmRoFWhchykrsS6MQwgM3mPdhEv' + +def get_service(hass, config): + """ Get the pushover notification service. """ + + if not validate_config(config, + {DOMAIN: ['user_key']}, + _LOGGER): + return None + + try: + # pylint: disable=unused-variable + from pushover import Client + + except ImportError: + _LOGGER.exception( + "Unable to import pushover. " + "Did you maybe not install the 'python-pushover.py' package?") + + return None + + try: + return PushoverNotificationService(config[DOMAIN]['user_key']) + + except InvalidKeyError: + _LOGGER.error( + "Wrong API key supplied. " + "Get it at https://www.pushover.com") + + +# pylint: disable=too-few-public-methods +class PushoverNotificationService(BaseNotificationService): + """ Implements notification service for Pushover. """ + + def __init__(self, user_key): + from pushover import Client + self.user_key = user_key + self.pushover = client = Client(self.user_key, api_token=_API_TOKEN) + + def send_message(self, message="", **kwargs): + """ Send a message to a user. """ + + title = kwargs.get(ATTR_TITLE) + + self.pushover.send_message(message, title=title) diff --git a/requirements.txt b/requirements.txt index c4b2160c38c..9f0352ccef1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,3 +36,6 @@ pydispatcher>=2.0.5 # sensor.systemmonitor psutil>=2.2.1 + +#pushover notifications +python-pushover>=0.2 \ No newline at end of file From e877ed01afca576a405041af78de23b53a19ee86 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 22 Mar 2015 14:43:20 +1100 Subject: [PATCH 08/47] Fixed attribute error with new unit_of_measurement changes --- homeassistant/components/sensor/sabnzbd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 431752f544c..d33c9603c01 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -118,7 +118,7 @@ class SabnzbdSensor(Entity): self.type = sensor_type self.client_name = client_name self._state = None - self.unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @property def name(self): @@ -134,7 +134,7 @@ class SabnzbdSensor(Entity): """ Returns the state attributes. """ return { ATTR_FRIENDLY_NAME: self.name, - ATTR_UNIT_OF_MEASUREMENT: self.unit_of_measurement, + ATTR_UNIT_OF_MEASUREMENT: self._unit_of_measurement, } def refresh_sabnzbd_data(self): From 20f52a7fb1a18620c54382abb1421761804503a6 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 22 Mar 2015 15:13:57 +1100 Subject: [PATCH 09/47] Made API Key a required config variable --- homeassistant/components/notify/pushover.py | 44 +++++++++++++++------ 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 3ebd9030683..a82c8439779 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -8,10 +8,25 @@ 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 pushover.com 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 http://pushover.com @@ -22,22 +37,22 @@ 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__) -_API_TOKEN = 'a99PmRoFWhchykrsS6MQwgM3mPdhEv' def get_service(hass, config): """ Get the pushover notification service. """ if not validate_config(config, - {DOMAIN: ['user_key']}, + {DOMAIN: ['user_key', CONF_API_KEY]}, _LOGGER): return None try: # pylint: disable=unused-variable - from pushover import Client + from pushover import Client, InitError, RequestError except ImportError: _LOGGER.exception( @@ -47,9 +62,13 @@ def get_service(hass, config): return None try: - return PushoverNotificationService(config[DOMAIN]['user_key']) + api_token = config[DOMAIN].get(CONF_API_KEY) + return PushoverNotificationService( + config[DOMAIN]['user_key'], + api_token + ) - except InvalidKeyError: + except InitError: _LOGGER.error( "Wrong API key supplied. " "Get it at https://www.pushover.com") @@ -59,14 +78,17 @@ def get_service(hass, config): class PushoverNotificationService(BaseNotificationService): """ Implements notification service for Pushover. """ - def __init__(self, user_key): - from pushover import Client - self.user_key = user_key - self.pushover = client = Client(self.user_key, api_token=_API_TOKEN) + def __init__(self, user_key, api_token): + from pushover import Client, RequestError + self._user_key = user_key + self._api_token = api_token + self.pushover = client = Client(self._user_key, api_token=self._api_token) def send_message(self, message="", **kwargs): """ Send a message to a user. """ title = kwargs.get(ATTR_TITLE) - - self.pushover.send_message(message, title=title) + try: + self.pushover.send_message(message, title=title) + except RequestError: + _LOGGER.exception("Could not send pushover notification") From 04d16d76076a3ffff7ca7706ac5347c47f86de6f Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 22 Mar 2015 15:18:58 +1100 Subject: [PATCH 10/47] removed unused state attributes that are no longer required after upstream changes --- homeassistant/components/sensor/sabnzbd.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index d33c9603c01..974c7d05fbe 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -130,12 +130,9 @@ class SabnzbdSensor(Entity): return self._state @property - def state_attributes(self): - """ Returns the state attributes. """ - return { - ATTR_FRIENDLY_NAME: self.name, - ATTR_UNIT_OF_MEASUREMENT: self._unit_of_measurement, - } + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + return self._unit_of_measurement def refresh_sabnzbd_data(self): """ Calls the throttled SABnzbd refresh method. """ From 58fd07e4c650964ca6bb7dc1a05fbdc81c17fea9 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 22 Mar 2015 15:32:47 +1100 Subject: [PATCH 11/47] fixed the flake8 and pylint warnings --- homeassistant/components/notify/pushover.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index a82c8439779..ca3bf47819f 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -42,6 +42,7 @@ 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. """ @@ -51,8 +52,8 @@ def get_service(hass, config): return None try: - # pylint: disable=unused-variable - from pushover import Client, InitError, RequestError + # pylint: disable=no-name-in-module, unused-variable + from pushover import InitError except ImportError: _LOGGER.exception( @@ -64,9 +65,8 @@ def get_service(hass, config): try: api_token = config[DOMAIN].get(CONF_API_KEY) return PushoverNotificationService( - config[DOMAIN]['user_key'], - api_token - ) + config[DOMAIN]['user_key'], + api_token) except InitError: _LOGGER.error( @@ -79,14 +79,18 @@ class PushoverNotificationService(BaseNotificationService): """ Implements notification service for Pushover. """ def __init__(self, user_key, api_token): - from pushover import Client, RequestError + # 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 = Client(self._user_key, api_token=self._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) From 7a2aa43caab56fc499085fafb6c15e5cbe129b12 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 22 Mar 2015 15:37:43 +1100 Subject: [PATCH 12/47] Fixed incorrect URL in config instructions --- homeassistant/components/notify/pushover.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index ca3bf47819f..466cfce8ea0 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -16,7 +16,7 @@ VARIABLES: api_key *Required This parameter is optional but should be configured, in order to get an API -key you should go to pushover.com and register a new application. +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 @@ -29,7 +29,7 @@ https://home-assistant.io/images/favicon-192x192.png user_key *Required -To retrieve this value log into your account at http://pushover.com +To retrieve this value log into your account at https://pushover.net """ import logging @@ -71,7 +71,7 @@ def get_service(hass, config): except InitError: _LOGGER.error( "Wrong API key supplied. " - "Get it at https://www.pushover.com") + "Get it at https://pushover.net") # pylint: disable=too-few-public-methods From 470096047b8a9979655823d964fad15c7518d1dc Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 22 Mar 2015 15:43:59 +1100 Subject: [PATCH 13/47] removed unused imports --- homeassistant/components/sensor/sabnzbd.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 974c7d05fbe..a1230def614 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -56,8 +56,7 @@ from homeassistant.helpers.entity import Entity # pylint: disable=no-name-in-module, import-error from homeassistant.external.nzbclients.sabnzbd import SabnzbdApi from homeassistant.external.nzbclients.sabnzbd import SabnzbdApiException -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME) + import logging SENSOR_TYPES = { From 58812b326c0662c8e1fa997d37bdaab4bf35b721 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 21:10:46 -0700 Subject: [PATCH 14/47] Move hass.local_api and hass.components to config object --- homeassistant/__init__.py | 24 +++++++++++++++---- homeassistant/bootstrap.py | 4 ++-- homeassistant/components/api.py | 4 ++-- homeassistant/components/configurator.py | 4 ++-- homeassistant/components/discovery.py | 2 +- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/http.py | 2 +- .../components/scheduler/__init__.py | 4 ++-- homeassistant/components/wink.py | 2 +- homeassistant/components/zwave.py | 2 +- homeassistant/remote.py | 12 +++++----- 11 files changed, 39 insertions(+), 23 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 8ed3d7bc1b6..b93a8ee99be 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -54,11 +54,19 @@ class HomeAssistant(object): self.states = StateMachine(self.bus) self.config = Config() - # List of loaded components - self.components = [] + @property + def components(self): + """ DEPRECATED 3/21/2015. Use hass.config.components """ + _LOGGER.warning( + 'hass.components is deprecated. Use hass.config.components') + return self.config.components - # Remote.API object pointing at local API - self.local_api = None + @property + def local_api(self): + """ DEPRECATED 3/21/2015. Use hass.config.api """ + _LOGGER.warning( + 'hass.local_api is deprecated. Use hass.config.api') + return self.config.api @property def config_dir(self): @@ -848,6 +856,8 @@ class Timer(threading.Thread): class Config(object): """ Configuration settings for Home Assistant. """ + + # pylint: disable=too-many-instance-attributes def __init__(self): self.latitude = None self.longitude = None @@ -855,6 +865,12 @@ class Config(object): 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') diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 83d966731cd..01093bdbded 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -33,7 +33,7 @@ ATTR_COMPONENT = "component" def setup_component(hass, domain, config=None): """ Setup a component for Home Assistant. """ # Check if already loaded - if domain in hass.components: + if domain in hass.config.components: return _ensure_loader_prepared(hass) @@ -45,7 +45,7 @@ def setup_component(hass, domain, config=None): try: if component.setup(hass, config): - hass.components.append(component.DOMAIN) + hass.config.components.append(component.DOMAIN) # Assumption: if a component does not depend on groups # it communicates with devices diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ebb632b95ab..b5cdb9cae6c 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Register the API with the HTTP interface. """ - if 'http' not in hass.components: + if 'http' not in hass.config.components: _LOGGER.error('Dependency http is not loaded') return False @@ -311,4 +311,4 @@ def _handle_delete_api_event_forward(handler, path_match, data): def _handle_get_api_components(handler, path_match, data): """ Returns all the loaded components. """ - handler.write_json(handler.server.hass.components) + handler.write_json(handler.server.hass.config.components) diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index fdd3c571601..8bec580abf9 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -83,8 +83,8 @@ def _get_instance(hass): except KeyError: _INSTANCES[hass] = Configurator(hass) - if DOMAIN not in hass.components: - hass.components.append(DOMAIN) + if DOMAIN not in hass.config.components: + hass.config.components.append(DOMAIN) return _INSTANCES[hass] diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index abd2aed6957..c17a20f9414 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -74,7 +74,7 @@ def setup(hass, config): logger.info("Found new service: %s %s", service, info) - if component and component not in hass.components: + if component and component not in hass.config.components: bootstrap.setup_component(hass, component, config) hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index e757ac43e69..8ff722e41b2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Setup serving the frontend. """ - if 'http' not in hass.components: + if 'http' not in hass.config.components: _LOGGER.error('Dependency http is not loaded') return False diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 5528267686f..fed43cb43de 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -135,7 +135,7 @@ def setup(hass, config=None): threading.Thread(target=server.start, daemon=True).start()) hass.http = server - hass.local_api = rem.API(util.get_local_ip(), api_password, server_port) + hass.config.api = rem.API(util.get_local_ip(), api_password, server_port) return True diff --git a/homeassistant/components/scheduler/__init__.py b/homeassistant/components/scheduler/__init__.py index d05d90a903b..ac990543a30 100644 --- a/homeassistant/components/scheduler/__init__.py +++ b/homeassistant/components/scheduler/__init__.py @@ -35,7 +35,7 @@ _SCHEDULE_FILE = 'schedule.json' def setup(hass, config): """ Create the schedules """ - if DOMAIN in hass.components: + if DOMAIN in hass.config.components: return True def setup_listener(schedule, event_data): @@ -47,7 +47,7 @@ def setup(hass, config): if event_type in ['time']: component = 'scheduler.{}'.format(event_type) - elif component not in hass.components and \ + elif component not in hass.config.components and \ not bootstrap.setup_component(hass, component, config): _LOGGER.warn("Could setup event listener for %s", component) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 059aac4363c..daa6c92a893 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -41,7 +41,7 @@ def setup(hass, config): component = get_component(component_name) # Ensure component is loaded - if component.DOMAIN not in hass.components: + if component.DOMAIN not in hass.config.components: bootstrap.setup_component(hass, component.DOMAIN, config) # Fire discovery event diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 15e436d7f4d..9304ba81a72 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -96,7 +96,7 @@ def setup(hass, config): for component, discovery_service, command_ids in DISCOVERY_COMPONENTS: if value.command_class in command_ids: # Ensure component is loaded - if component not in hass.components: + if component not in hass.config.components: bootstrap.setup_component(hass, component, config) # Fire discovery event diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 3c2ffe69f29..19aa86f67b9 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -107,7 +107,6 @@ class HomeAssistant(ha.HomeAssistant): remote_api.host, remote_api.port, remote_api.status)) self.remote_api = remote_api - self.local_api = local_api self.pool = pool = ha.create_worker_pool() @@ -115,11 +114,12 @@ class HomeAssistant(ha.HomeAssistant): self.services = ha.ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus, self.remote_api) self.config = ha.Config() - self.components = [] + + self.config.api = local_api def start(self): # Ensure a local API exists to connect with remote - if self.local_api is None: + if self.config.api is None: bootstrap.setup_component(self, 'http') bootstrap.setup_component(self, 'api') @@ -130,10 +130,10 @@ class HomeAssistant(ha.HomeAssistant): # Setup that events from remote_api get forwarded to local_api # Do this after we fire START, otherwise HTTP is not started - if not connect_remote_events(self.remote_api, self.local_api): + if not connect_remote_events(self.remote_api, self.config.api): raise ha.HomeAssistantError(( 'Could not setup event forwarding from api {} to ' - 'local api {}').format(self.remote_api, self.local_api)) + 'local api {}').format(self.remote_api, self.config.api)) def stop(self): """ Stops Home Assistant and shuts down all threads. """ @@ -143,7 +143,7 @@ class HomeAssistant(ha.HomeAssistant): origin=ha.EventOrigin.remote) # Disconnect master event forwarding - disconnect_remote_events(self.remote_api, self.local_api) + disconnect_remote_events(self.remote_api, self.config.api) # Wait till all responses to homeassistant_stop are done self.pool.block_till_done() From 2863c2d593184ba4edfda242892b50b466cedcf6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 22:02:47 -0700 Subject: [PATCH 15/47] Made bootstrap.setup_component more robust --- homeassistant/bootstrap.py | 36 ++++++++++++++++--- homeassistant/components/discovery.py | 13 ++++--- .../components/scheduler/__init__.py | 7 +--- homeassistant/components/wink.py | 3 +- homeassistant/components/zwave.py | 3 +- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 01093bdbded..4a6aab53483 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -31,18 +31,46 @@ ATTR_COMPONENT = "component" def setup_component(hass, domain, config=None): - """ Setup a component for Home Assistant. """ - # Check if already loaded + """ Setup a component and all its dependencies. """ + if domain in hass.config.components: - return + return True _ensure_loader_prepared(hass) if config is None: config = defaultdict(dict) + components = loader.load_order_component(domain) + + # OrderedSet is empty if component or dependencies could not be resolved + if not components: + return False + + for component in components: + if component in hass.config.components: + continue + + if not _setup_component(hass, component, config): + return False + + return True + + +def _setup_component(hass, domain, config): + """ Setup a component for Home Assistant. """ component = loader.get_component(domain) + missing_deps = [dep for dep in component.DEPENDENCIES + if dep not in hass.config.components] + + if missing_deps: + _LOGGER.error( + "Not initializing %s because not all dependencies loaded: %s", + domain, ", ".join(missing_deps)) + + return False + try: if component.setup(hass, config): hass.config.components.append(component.DOMAIN) @@ -102,7 +130,7 @@ def from_config_dict(config, hass=None): # Setup the components for domain in loader.load_order_components(components): - setup_component(hass, domain, config) + _setup_component(hass, domain, config) return hass diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index c17a20f9414..2a9cb64f6ec 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -70,12 +70,17 @@ def setup(hass, config): def new_service_listener(service, info): """ Called when a new service is found. """ with lock: - component = SERVICE_HANDLERS.get(service) - logger.info("Found new service: %s %s", service, info) - if component and component not in hass.config.components: - bootstrap.setup_component(hass, component, config) + component = SERVICE_HANDLERS.get(service) + + # We do not know how to handle this service + if not component: + return + + # This component cannot be setup. + if not bootstrap.setup_component(hass, component, config): + return hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { ATTR_SERVICE: service, diff --git a/homeassistant/components/scheduler/__init__.py b/homeassistant/components/scheduler/__init__.py index ac990543a30..f84dafd5ec3 100644 --- a/homeassistant/components/scheduler/__init__.py +++ b/homeassistant/components/scheduler/__init__.py @@ -35,9 +35,6 @@ _SCHEDULE_FILE = 'schedule.json' def setup(hass, config): """ Create the schedules """ - if DOMAIN in hass.config.components: - return True - def setup_listener(schedule, event_data): """ Creates the event listener based on event_data """ event_type = event_data['type'] @@ -47,9 +44,7 @@ def setup(hass, config): if event_type in ['time']: component = 'scheduler.{}'.format(event_type) - elif component not in hass.config.components and \ - not bootstrap.setup_component(hass, component, config): - + elif not bootstrap.setup_component(hass, component, config): _LOGGER.warn("Could setup event listener for %s", component) return None diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index daa6c92a893..cfdbf9b1a92 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -41,8 +41,7 @@ def setup(hass, config): component = get_component(component_name) # Ensure component is loaded - if component.DOMAIN not in hass.config.components: - bootstrap.setup_component(hass, component.DOMAIN, config) + bootstrap.setup_component(hass, component.DOMAIN, config) # Fire discovery event hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 9304ba81a72..1d798746a63 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -96,8 +96,7 @@ def setup(hass, config): for component, discovery_service, command_ids in DISCOVERY_COMPONENTS: if value.command_class in command_ids: # Ensure component is loaded - if component not in hass.config.components: - bootstrap.setup_component(hass, component, config) + bootstrap.setup_component(hass, component, config) # Fire discovery event hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { From 06d85595a058d5c6012e604a6693af6efe72a234 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 22:09:25 -0700 Subject: [PATCH 16/47] Update coveragerc --- .coveragerc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index f0953f49cd5..cccd815df2d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,21 +12,21 @@ omit = homeassistant/components/*/zwave.py homeassistant/components/*/tellstick.py + homeassistant/components/*/vera.py homeassistant/components/keyboard.py homeassistant/components/switch/wemo.py homeassistant/components/thermostat/nest.py homeassistant/components/light/hue.py homeassistant/components/sensor/systemmonitor.py + homeassistant/components/sensor/sabnzbd.py homeassistant/components/notify/pushbullet.py + homeassistant/components/notify/pushover.py homeassistant/components/media_player/cast.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py - homeassistant/components/light/vera.py - homeassistant/components/sensor/vera.py - homeassistant/components/switch/vera.py [report] From cdd5d1196af79104f64f10c17c9adc1579682cfa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 22:14:30 -0700 Subject: [PATCH 17/47] Fix regression from refactoring EntityComponent --- homeassistant/helpers/entity_component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index dd98149eaf8..644c06d8901 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -129,7 +129,7 @@ class EntityComponent(object): self.hass, config, self.add_entities, discovery_info) except AttributeError: # Support old deprecated method for now - 3/1/2015 - if hasattr(platform, 'get_entities'): + 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.", From 49d7901585654dfc1658a73ee223002202acd880 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 22:21:57 -0700 Subject: [PATCH 18/47] Tweaks to EntityComponent.setup_platform --- homeassistant/helpers/entity_component.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 644c06d8901..1723091d5c2 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -127,7 +127,11 @@ class EntityComponent(object): 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( @@ -137,6 +141,9 @@ class EntityComponent(object): self.add_entities(platform.get_devices(self.hass, config)) else: - # AttributeError if setup_platform does not exist self.logger.exception( - "Error setting up %s", platform_type) + "Error while setting up platform %s", platform_type) + + except Exception: # pylint: disable=broad-except + self.logger.exception( + "Error while setting up platform %s", platform_type) From ec557f8d44609f72d07500157741858b44555ca2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Mar 2015 22:26:41 -0700 Subject: [PATCH 19/47] Fix broken tellstick and systemmonitor sensors --- homeassistant/components/sensor/systemmonitor.py | 6 +++++- homeassistant/components/sensor/tellstick.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 99e2453c487..e03e987802b 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -50,7 +50,7 @@ class SystemMonitorSensor(Entity): self.argument = argument self.type = sensor_type self._state = None - self.unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self.update() @property @@ -62,6 +62,10 @@ class SystemMonitorSensor(Entity): """ Returns the state of the device. """ return self._state + @property + def unit_of_measurement(self): + return self._unit_of_measurement + def update(self): if self.type == 'disk_use_percent': self._state = psutil.disk_usage(self.argument).percent diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 699821fa6b6..5720b65a669 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -99,7 +99,7 @@ class TellstickSensor(Entity): def __init__(self, name, sensor, datatype, sensor_info): self.datatype = datatype self.sensor = sensor - self.unit_of_measurement = sensor_info.unit or None + self._unit_of_measurement = sensor_info.unit or None self._name = "{} {}".format(name, sensor_info.name) @@ -112,3 +112,7 @@ class TellstickSensor(Entity): def state(self): """ Returns the state of the device. """ return self.sensor.value(self.datatype).value + + @property + def unit_of_measurement(self): + return self._unit_of_measurement From c3fc19353bb8c3e498f51ef1b91a20b6e16afd0b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Mar 2015 22:50:20 -0700 Subject: [PATCH 20/47] Fix device tracker waiting forever when platform gets stuck --- homeassistant/components/device_tracker/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index ac0c8d483ff..6d4db7ad7ed 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -157,7 +157,8 @@ class DeviceTracker(object): def update_devices(self, now): """ Update device states based on the found devices. """ - self.lock.acquire() + if not self.lock.acquire(False): + return found_devices = set(dev.upper() for dev in self.device_scanner.scan_devices()) From 4484baa8666febec62fb0af1340626381b1a8e84 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Mar 2015 22:50:51 -0700 Subject: [PATCH 21/47] Remove lock and add host timeout to NMAP scanner --- .../components/device_tracker/nmap_tracker.py | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 860ba3b45fb..b221a815fb8 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -1,7 +1,6 @@ """ Supports scanning using nmap. """ import logging from datetime import timedelta, datetime -import threading from collections import namedtuple import subprocess import re @@ -54,7 +53,6 @@ class NmapDeviceScanner(object): def __init__(self, config): self.last_results = [] - self.lock = threading.Lock() self.hosts = config[CONF_HOSTS] minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0) self.home_interval = timedelta(minutes=minutes) @@ -116,28 +114,27 @@ class NmapDeviceScanner(object): if not self.success_init: return False - with self.lock: - _LOGGER.info("Scanning") + _LOGGER.info("Scanning") - options = "-F" - exclude_targets = set() - if self.home_interval: - now = datetime.now() - for host in self.last_results: - if host.last_update + self.home_interval > now: - exclude_targets.add(host) - if len(exclude_targets) > 0: - target_list = [t.ip for t in exclude_targets] - options += " --exclude {}".format(",".join(target_list)) + options = "-F --host-timeout 5" + exclude_targets = set() + if self.home_interval: + now = datetime.now() + for host in self.last_results: + if host.last_update + self.home_interval > now: + exclude_targets.add(host) + if len(exclude_targets) > 0: + target_list = [t.ip for t in exclude_targets] + options += " --exclude {}".format(",".join(target_list)) - nmap = NmapProcess(targets=self.hosts, options=options) + nmap = NmapProcess(targets=self.hosts, options=options) - nmap.run() + nmap.run() - if nmap.rc == 0: - if self._parse_results(nmap.stdout): - self.last_results.extend(exclude_targets) - else: - self.last_results = [] - _LOGGER.error(nmap.stderr) - return False + if nmap.rc == 0: + if self._parse_results(nmap.stdout): + self.last_results.extend(exclude_targets) + else: + self.last_results = [] + _LOGGER.error(nmap.stderr) + return False From c8c38e498a54d96c7c63f219afbe33332fbf9db7 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 03:51:33 +1100 Subject: [PATCH 22/47] Added a device tracker for dd-wrt routers --- .../components/device_tracker/ddwrt.py | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 homeassistant/components/device_tracker/ddwrt.py diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py new file mode 100644 index 00000000000..a055d2d496f --- /dev/null +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -0,0 +1,159 @@ +""" Supports scanning a DD-WRT router. """ +import logging +import json +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__) + + +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 x 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 = (x * 5) + 2 + if mac_index < len(elements): + self.mac2name[elements[mac_index]] = elements[x * 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 x in range(0, num_clients): + # get every 9th element which is the MAC address + index = x * 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 res.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", res) + + +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 """ + data = {} + pattern = re.compile(r'\{(\w+)::([^\}]*)\}') + for (key, val) in re.findall(pattern, data_str): + data[key] = val + + return data \ No newline at end of file From 5a0251c3cd99e7aa937ef55de64bc4d4c186cfe0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Mar 2015 23:11:07 -0700 Subject: [PATCH 23/47] ps: Fix recorder.py fetching wrong run information --- homeassistant/components/recorder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 84d4c1ecccd..367af7d2d73 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -86,7 +86,7 @@ def run_information(point_in_time=None): return RecorderRun() run = _INSTANCE.query( - "SELECT * FROM recorder_runs WHERE start>? AND END IS NULL OR END?", (point_in_time, point_in_time), return_value=RETURN_ONE_ROW) return RecorderRun(run) if run else None From 7e6af57186cb702a32cf5fa87557c26fa6ef0c60 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:29:45 +1100 Subject: [PATCH 24/47] FIxed some linting issues --- .../components/device_tracker/ddwrt.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index a055d2d496f..baebe484a27 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -1,6 +1,5 @@ """ Supports scanning a DD-WRT router. """ import logging -import json from datetime import timedelta import re import threading @@ -38,7 +37,7 @@ class DdWrtDeviceScanner(object): def __init__(self, config): self.host = config[CONF_HOST] self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] + self.password = config[CONF_PASSWORD] self.lock = threading.Lock() @@ -73,20 +72,20 @@ class DdWrtDeviceScanner(object): 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 x in range(0, num_clients): + 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 = (x * 5) + 2 - if mac_index < len(elements): - self.mac2name[elements[mac_index]] = elements[x * 5] - + 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) @@ -109,21 +108,22 @@ class DdWrtDeviceScanner(object): self.last_results = [] active_clients = data.get('active_wireless', None) if active_clients: - # This is really lame, instead of using JSON the ddwrt UI uses - # it's own data format for some reason and then regex's out - # values so I guess I have to do the same, LAME!!! - + # 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 x in range(0, num_clients): + + num_clients = int(len(elements)/9) + for idx in range(0, num_clients): # get every 9th element which is the MAC address - index = x * 9 - if index < len(elements): + index = idx * 9 + if index < len(elements): self.last_results.append(elements[index]) - + return True return False @@ -131,29 +131,29 @@ class DdWrtDeviceScanner(object): def get_ddwrt_data(self, url): """ Retrieve data from DD-WRT and return parsed result """ try: - response = requests.get(url, auth=(self.username, + 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 res.status_code == 401: + 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", res) + _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????. + """ 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 """ data = {} pattern = re.compile(r'\{(\w+)::([^\}]*)\}') for (key, val) in re.findall(pattern, data_str): data[key] = val - return data \ No newline at end of file + return data From fc07032d35bcccd217d02149a96d85a3338a0e31 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:43:41 +1100 Subject: [PATCH 25/47] Fixed some code formatting and added dd-wrt to the readme --- homeassistant/components/device_tracker/ddwrt.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index baebe484a27..20db671fbc7 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -16,6 +16,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +# pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a DdWrt scanner. """ if not validate_config(config, @@ -131,8 +132,10 @@ class DdWrtDeviceScanner(object): 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) + response = requests.get(url, auth=( + self.username, + self.password), + timeout=4) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return From 05239c26f9ecfaa6a671e822ed24c2a230e53f18 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:50:24 +1100 Subject: [PATCH 26/47] Forgot to add README.md file changes to last commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f848d551adb..43c4d58a33d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho It offers the following functionality through built-in components: - * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com)) + * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index)) * Track and control [Philips Hue](http://meethue.com) lights * Track and control [WeMo switches](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) From a9ce12be34cb04299cd63bbdeb9075c3307592e5 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:59:12 +1100 Subject: [PATCH 27/47] Fixed travis CI indenting error --- homeassistant/components/device_tracker/ddwrt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 20db671fbc7..fa7d332feb8 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -132,9 +132,9 @@ class DdWrtDeviceScanner(object): 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), + response = requests.get( + url, + auth=(self.username, self.password), timeout=4) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") From a959c48708caaf7f22a8cd6a4e112aa37dd5fc82 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 19:17:51 +1100 Subject: [PATCH 28/47] Fixed travis another CI indenting error --- homeassistant/components/device_tracker/ddwrt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index fa7d332feb8..3d6af407ded 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -46,7 +46,7 @@ class DdWrtDeviceScanner(object): self.mac2name = None - #test the router is accessible + # 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 @@ -142,7 +142,7 @@ class DdWrtDeviceScanner(object): if response.status_code == 200: return _parse_ddwrt_response(response.text) elif response.status_code == 401: - # Authentication error + # Authentication error _LOGGER.exception( "Failed to authenticate, " "please check your username and password") From 522bbfb716568cc30bf0baaa05aa73194e44d1fe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Mar 2015 13:41:07 -0700 Subject: [PATCH 29/47] Expose to more info content if dialog is open --- .../polymer/dialogs/more-info-dialog.html | 18 ++++++++++++++++-- .../polymer/more-infos/more-info-content.html | 13 ++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/www_static/polymer/dialogs/more-info-dialog.html b/homeassistant/components/frontend/www_static/polymer/dialogs/more-info-dialog.html index 05a183bfd1b..98c88b7db35 100644 --- a/homeassistant/components/frontend/www_static/polymer/dialogs/more-info-dialog.html +++ b/homeassistant/components/frontend/www_static/polymer/dialogs/more-info-dialog.html @@ -7,12 +7,14 @@ @@ -27,11 +29,16 @@ Polymer(Polymer.mixin({ stateObj: null, stateHistory: null, hasHistoryComponent: false, + dialogOpen: false, observe: { 'stateObj.attributes': 'reposition' }, + created: function() { + this.dialogOpenChanged = this.dialogOpenChanged.bind(this); + }, + attached: function() { this.listenToStores(true); }, @@ -66,6 +73,13 @@ Polymer(Polymer.mixin({ } }, + dialogOpenChanged: function(ev) { + // we get CustomEvent, undefined and true/false from polymer… + if (typeof ev === 'object') { + this.dialogOpen = ev.detail; + } + }, + changeEntityId: function(entityId) { this.entityId = entityId; diff --git a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-content.html b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-content.html index a06dd06c93f..b80a016686b 100644 --- a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-content.html +++ b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-content.html @@ -8,7 +8,7 @@ - + +
Streaming updates
Developer Tools
diff --git a/homeassistant/components/frontend/www_static/polymer/components/display-time.html b/homeassistant/components/frontend/www_static/polymer/components/display-time.html new file mode 100644 index 00000000000..ff2f0a6dd8f --- /dev/null +++ b/homeassistant/components/frontend/www_static/polymer/components/display-time.html @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html b/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html new file mode 100644 index 00000000000..4b6177a6949 --- /dev/null +++ b/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html @@ -0,0 +1,17 @@ + + + + + + + diff --git a/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html new file mode 100644 index 00000000000..e454fee9ed7 --- /dev/null +++ b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html @@ -0,0 +1,60 @@ + + + + + + + + + + + diff --git a/homeassistant/components/frontend/www_static/polymer/home-assistant-js b/homeassistant/components/frontend/www_static/polymer/home-assistant-js index e048bf6ece9..282004e3e27 160000 --- a/homeassistant/components/frontend/www_static/polymer/home-assistant-js +++ b/homeassistant/components/frontend/www_static/polymer/home-assistant-js @@ -1 +1 @@ -Subproject commit e048bf6ece91983b9f03aafeb414ae5c535288a2 +Subproject commit 282004e3e27134a3de1b9c0e6c264ce811f3e510 diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html b/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html index ade9c9d166b..cee51c78998 100644 --- a/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html +++ b/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html @@ -10,6 +10,7 @@ + @@ -96,6 +97,13 @@ + +
@@ -136,6 +144,9 @@ + @@ -161,6 +172,7 @@ Polymer(Polymer.mixin({ narrow: false, activeFilters: [], hasHistoryComponent: false, + hasLogbookComponent: false, isStreaming: false, hasStreamError: false, @@ -185,7 +197,7 @@ Polymer(Polymer.mixin({ componentStoreChanged: function(componentStore) { this.hasHistoryComponent = componentStore.isLoaded('history'); - this.hasScriptComponent = componentStore.isLoaded('script'); + this.hasLogbookComponent = componentStore.isLoaded('logbook'); }, streamStoreChanged: function(streamStore) { diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/partial-logbook.html b/homeassistant/components/frontend/www_static/polymer/layouts/partial-logbook.html new file mode 100644 index 00000000000..faa7f93fea1 --- /dev/null +++ b/homeassistant/components/frontend/www_static/polymer/layouts/partial-logbook.html @@ -0,0 +1,56 @@ + + + + + + + + + + diff --git a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html index 2d8b6d6e536..0567c7a5300 100644 --- a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html +++ b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html @@ -51,7 +51,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) { case "media_player": var icon = "hardware:cast"; - if (state !== "idle") { + if (state && state !== "idle") { icon += "-connected"; } diff --git a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html index 17049015294..94abddfabb7 100644 --- a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html +++ b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html @@ -1,5 +1,30 @@ + +/* 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-colo { color: #FF9800; } + +body { + color: #212121; +} + +a { + color: #FF9800; + text-decoration: none; +} + + @-webkit-keyframes ha-spin { 0% { From 9fb634ed3aeca8aba900da41ba6540a0f9c1425f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Mar 2015 15:23:50 -0700 Subject: [PATCH 39/47] Fix type in CSS class name --- .../www_static/polymer/resources/home-assistant-style.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html index 94abddfabb7..cb0dbe8e414 100644 --- a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html +++ b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html @@ -13,7 +13,7 @@ .divider-color { border-color: #B6B6B6; } /* extra */ -.accent-text-colo { color: #FF9800; } +.accent-text-color { color: #FF9800; } body { color: #212121; From 6455f1388ae48d0adfe33edd96b569f112a21f4d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Mar 2015 23:57:52 -0700 Subject: [PATCH 40/47] Have logbook only report each sensor every 15 minutes --- homeassistant/components/logbook.py | 115 ++++++++++++++++++---------- 1 file changed, 74 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index b5739638a43..8cf750fb932 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -5,6 +5,7 @@ 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 ( @@ -25,6 +26,8 @@ QUERY_EVENTS_BETWEEN = """ ORDER BY time_fired """ +GROUP_BY_MINUTES = 15 + def setup(hass, config): """ Listens for download events to download files. """ @@ -72,56 +75,86 @@ class Entry(object): def humanify(events): - """ Generator that converts a list of events into Entry objects. """ + """ + 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 + """ # pylint: disable=too-many-branches - for event in events: - if event.event_type == EVENT_STATE_CHANGED: - # Do not report on new entities - if 'old_state' not in event.data: - continue + # Group events in batches of GROUP_BY_MINUTES + for _, g_events in groupby( + events, + lambda event: event.time_fired.minute // GROUP_BY_MINUTES): - to_state = State.from_dict(event.data.get('new_state')) + events_batch = list(g_events) - if not to_state: - continue + # Keep track of last sensor states + last_sensor_event = {} - domain = to_state.domain + # Process events + for event in events_batch: + if event.event_type == EVENT_STATE_CHANGED: + entity_id = event.data['entity_id'] - entry = Entry( - event.time_fired, domain=domain, - name=to_state.name, entity_id=to_state.entity_id) + if entity_id.startswith('sensor.'): + last_sensor_event[entity_id] = event - if domain == 'device_tracker': - entry.message = '{} home'.format( - 'arrived' if to_state.state == STATE_HOME else 'left') + # 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 not to_state: + 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 + + entry = Entry( + event.time_fired, domain=domain, + name=to_state.name, entity_id=to_state.entity_id) + + if domain == 'device_tracker': + entry.message = '{} home'.format( + 'arrived' if to_state.state == STATE_HOME else 'left') + + elif domain == 'sun': + if to_state.state == sun.STATE_ABOVE_HORIZON: + entry.message = 'has risen' + else: + entry.message = 'has set' + + elif to_state.state == STATE_ON: + # Future: combine groups and its entity entries ? + entry.message = "turned on" + + elif to_state.state == STATE_OFF: + entry.message = "turned off" - elif domain == 'sun': - if to_state.state == sun.STATE_ABOVE_HORIZON: - entry.message = 'has risen' else: - entry.message = 'has set' + entry.message = "changed to {}".format(to_state.state) - elif to_state.state == STATE_ON: - # Future: combine groups and its entity entries ? - entry.message = "turned on" + if entry.is_valid: + yield entry - elif to_state.state == STATE_OFF: - entry.message = "turned off" + elif event.event_type == EVENT_HOMEASSISTANT_START: + # Future: look for sequence stop/start and rewrite as restarted + yield Entry( + event.time_fired, "Home Assistant", "started", + domain=HA_DOMAIN) - else: - entry.message = "changed to {}".format(to_state.state) - - if entry.is_valid: - yield entry - - elif event.event_type == EVENT_HOMEASSISTANT_START: - # Future: look for sequence stop/start and rewrite as restarted - yield Entry( - event.time_fired, "Home Assistant", "started", - domain=HA_DOMAIN) - - elif event.event_type == EVENT_HOMEASSISTANT_STOP: - yield Entry( - event.time_fired, "Home Assistant", "stopped", - domain=HA_DOMAIN) + elif event.event_type == EVENT_HOMEASSISTANT_STOP: + yield Entry( + event.time_fired, "Home Assistant", "stopped", + domain=HA_DOMAIN) From 742479f8bd9f035add48483da3658d0a3ca5cca0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Mar 2015 00:11:24 -0700 Subject: [PATCH 41/47] Have logbook group HA stop + start --- homeassistant/components/logbook.py | 30 ++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 8cf750fb932..dd7d3275d84 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -80,8 +80,9 @@ def humanify(events): 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 + # pylint: disable=too-many-branches, too-many-statements # Group events in batches of GROUP_BY_MINUTES for _, g_events in groupby( @@ -93,6 +94,10 @@ def humanify(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: @@ -101,6 +106,18 @@ def humanify(events): 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: @@ -149,12 +166,19 @@ def humanify(events): yield entry elif event.event_type == EVENT_HOMEASSISTANT_START: - # Future: look for sequence stop/start and rewrite as restarted + 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", "stopped", + event.time_fired, "Home Assistant", action, domain=HA_DOMAIN) From 30e7f09000c78039114caca50658588bf80ecb62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Mar 2015 00:19:56 -0700 Subject: [PATCH 42/47] Clean up logbook component --- homeassistant/components/logbook.py | 65 ++++++++++++++--------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index dd7d3275d84..810b06b25da 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -48,7 +48,7 @@ def _handle_get_logbook(handler, path_match, data): class Entry(object): """ A human readable version of the log. """ - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, too-few-public-methods def __init__(self, when=None, name=None, message=None, domain=None, entity_id=None): @@ -58,11 +58,6 @@ class Entry(object): self.domain = domain self.entity_id = entity_id - @property - def is_valid(self): - """ Returns if this entry contains all the needed fields. """ - return self.when and self.name and self.message - def as_dict(self): """ Convert Entry to a dict to be used within JSON. """ return { @@ -82,7 +77,7 @@ def humanify(events): - 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, too-many-statements + # pylint: disable=too-many-branches # Group events in batches of GROUP_BY_MINUTES for _, g_events in groupby( @@ -138,32 +133,12 @@ def humanify(events): event != last_sensor_event[to_state.entity_id]: continue - entry = Entry( - event.time_fired, domain=domain, - name=to_state.name, entity_id=to_state.entity_id) - - if domain == 'device_tracker': - entry.message = '{} home'.format( - 'arrived' if to_state.state == STATE_HOME else 'left') - - elif domain == 'sun': - if to_state.state == sun.STATE_ABOVE_HORIZON: - entry.message = 'has risen' - else: - entry.message = 'has set' - - elif to_state.state == STATE_ON: - # Future: combine groups and its entity entries ? - entry.message = "turned on" - - elif to_state.state == STATE_OFF: - entry.message = "turned off" - - else: - entry.message = "changed to {}".format(to_state.state) - - if entry.is_valid: - yield entry + 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: @@ -182,3 +157,27 @@ def humanify(events): 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) From 00bbc17e11753cd10f57f07e437aab6f22f87f70 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Mar 2015 23:08:38 -0700 Subject: [PATCH 43/47] Add State.last_updated to JSON obj --- homeassistant/__init__.py | 15 +++++++++++---- homeassistant/components/recorder.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index d1cb17f22c6..898b66c4ef9 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -463,7 +463,8 @@ class State(object): __slots__ = ['entity_id', 'state', 'attributes', 'last_changed', 'last_updated'] - def __init__(self, entity_id, state, attributes=None, last_changed=None): + def __init__(self, entity_id, state, attributes=None, last_changed=None, + last_updated=None): if not ENTITY_ID_PATTERN.match(entity_id): raise InvalidEntityFormatError(( "Invalid entity id encountered: {}. " @@ -472,7 +473,7 @@ class State(object): self.entity_id = entity_id.lower() self.state = state self.attributes = attributes or {} - self.last_updated = dt.datetime.now() + self.last_updated = last_updated or dt.datetime.now() # Strip microsecond from last_changed else we cannot guarantee # state == State.from_dict(state.as_dict()) @@ -510,7 +511,8 @@ class State(object): return {'entity_id': self.entity_id, 'state': self.state, 'attributes': self.attributes, - 'last_changed': util.datetime_to_str(self.last_changed)} + 'last_changed': util.datetime_to_str(self.last_changed), + 'last_updated': util.datetime_to_str(self.last_updated)} @classmethod def from_dict(cls, json_dict): @@ -527,8 +529,13 @@ class State(object): if last_changed: last_changed = util.str_to_datetime(last_changed) + last_updated = json_dict.get('last_updated') + + if last_updated: + last_updated = util.str_to_datetime(last_updated) + return cls(json_dict['entity_id'], json_dict['state'], - json_dict.get('attributes'), last_changed) + json_dict.get('attributes'), last_changed, last_updated) def __eq__(self, other): return (self.__class__ == other.__class__ and diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index f2e5fa35ad4..6856ce4d7b5 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -60,7 +60,8 @@ def row_to_state(row): """ Convert a databsae row to a state. """ try: return State( - row[1], row[2], json.loads(row[3]), datetime.fromtimestamp(row[4])) + row[1], row[2], json.loads(row[3]), datetime.fromtimestamp(row[4]), + datetime.fromtimestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to state: %s", row) From 57b3e8018b1d8e1829df62b5688bd1ebb4b741ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Mar 2015 23:09:08 -0700 Subject: [PATCH 44/47] Logbook bug fixes --- .../www_static/polymer/components/logbook-entry.html | 3 ++- homeassistant/components/logbook.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html index e454fee9ed7..6d5bd917fb5 100644 --- a/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html +++ b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html @@ -50,7 +50,8 @@ var uiActions = window.hass.uiActions; Polymer({ - entityClicked: function() { + entityClicked: function(ev) { + ev.preventDefault(); uiActions.showMoreInfoDialog(this.entryObj.entityId); } }); diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 810b06b25da..3b42ffdee57 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -39,8 +39,7 @@ def setup(hass, config): def _handle_get_logbook(handler, path_match, data): """ Return logbook entries. """ start_today = datetime.now().date() - import time - print(time.mktime(start_today.timetuple())) + handler.write_json(humanify( recorder.query_events(QUERY_EVENTS_AFTER, (start_today,)))) @@ -123,7 +122,7 @@ def humanify(events): to_state = State.from_dict(event.data.get('new_state')) - if not to_state: + if not to_state or to_state.last_changed != to_state.last_updated: continue domain = to_state.domain From e43eee2eb13a00b7e7ab4f9e190e8dfc0e9266db Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Apr 2015 07:18:03 -0700 Subject: [PATCH 45/47] Style fixes --- homeassistant/__init__.py | 1 + homeassistant/components/logbook.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 898b66c4ef9..5e54cbd5d0a 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -463,6 +463,7 @@ class State(object): __slots__ = ['entity_id', 'state', 'attributes', 'last_changed', 'last_updated'] + # 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): diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 3b42ffdee57..a8299fbd6ed 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -122,7 +122,10 @@ def humanify(events): to_state = State.from_dict(event.data.get('new_state')) - if not to_state or to_state.last_changed != to_state.last_updated: + # 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 From b0bf775da8a6dc2fab6cfd1f5cf1e8fffd709416 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Apr 2015 21:49:03 -0700 Subject: [PATCH 46/47] Compile new version frontend --- homeassistant/components/frontend/version.py | 2 +- homeassistant/components/frontend/www_static/frontend.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index ea306279316..095769c9c28 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "b06d3667e9e461173029ded9c0c9b815" +VERSION = "1e004712440afc642a44ad927559587e" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 32b16c2fb6a..10ac2336a3c 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -17,7 +17,7 @@ b.events&&Object.keys(a).length>0&&console.log("[%s] addHostListeners:",this.loc .divider-color { border-color: #B6B6B6; } /* extra */ -.accent-text-colo { color: #FF9800; } +.accent-text-color { color: #FF9800; } body { color: #212121; @@ -213,7 +213,7 @@ return pickBy("isBefore",args)};moment.max=function(){var args=[].slice.call(arg {{ time }}