diff --git a/.coveragerc b/.coveragerc index 22e8116693c..46a945e760b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -17,6 +17,9 @@ omit = homeassistant/components/*/tellstick.py homeassistant/components/*/vera.py + homeassistant/components/ecobee.py + homeassistant/components/*/ecobee.py + homeassistant/components/verisure.py homeassistant/components/*/verisure.py diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py new file mode 100644 index 00000000000..a41bf7f25cc --- /dev/null +++ b/homeassistant/components/ecobee.py @@ -0,0 +1,154 @@ +""" +homeassistant.components.ecobee +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ecobee Component + +This component adds support for Ecobee3 Wireless Thermostats. +You will need to setup developer access to your thermostat, +and create and API key on the ecobee website. + +The first time you run this component you will see a configuration +component card in Home Assistant. This card will contain a PIN code +that you will need to use to authorize access to your thermostat. You +can do this at https://www.ecobee.com/consumerportal/index.html +Click My Apps, Add application, Enter Pin and click Authorize. + +After authorizing the application click the button in the configuration +card. Now your thermostat and sensors should shown in home-assistant. + +You can use the optional hold_temp parameter to set whether or not holds +are set indefintely or until the next scheduled event. + +ecobee: + api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf + hold_temp: True + +""" + +from homeassistant.loader import get_component +from homeassistant import bootstrap +from homeassistant.util import Throttle +from homeassistant.const import ( + EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_API_KEY) +from datetime import timedelta +import logging +import os + +DOMAIN = "ecobee" +DISCOVER_THERMOSTAT = "ecobee.thermostat" +DISCOVER_SENSORS = "ecobee.sensor" +NETWORK = None +HOLD_TEMP = 'hold_temp' + +REQUIREMENTS = [ + 'https://github.com/nkgilley/python-ecobee-api/archive/' + 'd35596b67c75451fa47001c493a15eebee195e93.zip#python-ecobee==0.0.1'] + +_LOGGER = logging.getLogger(__name__) + +ECOBEE_CONFIG_FILE = 'ecobee.conf' +_CONFIGURING = {} + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) + + +def request_configuration(network, hass, config): + """ Request configuration steps from the user. """ + configurator = get_component('configurator') + if 'ecobee' in _CONFIGURING: + configurator.notify_errors( + _CONFIGURING['ecobee'], "Failed to register, please try again.") + + return + + # pylint: disable=unused-argument + def ecobee_configuration_callback(callback_data): + """ Actions to do when our configuration callback is called. """ + network.request_tokens() + network.update() + setup_ecobee(hass, network, config) + + _CONFIGURING['ecobee'] = configurator.request_config( + hass, "Ecobee", ecobee_configuration_callback, + description=( + 'Please authorize this app at https://www.ecobee.com/consumer' + 'portal/index.html with pin code: ' + network.pin), + description_image="/static/images/config_ecobee_thermostat.png", + submit_caption="I have authorized the app." + ) + + +def setup_ecobee(hass, network, config): + """ Setup ecobee thermostat """ + # If ecobee has a PIN then it needs to be configured. + if network.pin is not None: + request_configuration(network, hass, config) + return + + if 'ecobee' in _CONFIGURING: + configurator = get_component('configurator') + configurator.request_done(_CONFIGURING.pop('ecobee')) + + # Ensure component is loaded + bootstrap.setup_component(hass, 'thermostat', config) + bootstrap.setup_component(hass, 'sensor', config) + + hold_temp = config[DOMAIN].get(HOLD_TEMP, False) + + # Fire thermostat discovery event + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: DISCOVER_THERMOSTAT, + ATTR_DISCOVERED: {'hold_temp': hold_temp} + }) + + # Fire sensor discovery event + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: DISCOVER_SENSORS, + ATTR_DISCOVERED: {} + }) + + +# pylint: disable=too-few-public-methods +class EcobeeData(object): + """ Gets the latest data and update the states. """ + + def __init__(self, config_file): + from pyecobee import Ecobee + self.ecobee = Ecobee(config_file) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Get the latest data from pyecobee. """ + self.ecobee.update() + _LOGGER.info("ecobee data updated successfully.") + + +def setup(hass, config): + """ + Setup Ecobee. + Will automatically load thermostat and sensor components to support + devices discovered on the network. + """ + # pylint: disable=global-statement, import-error + global NETWORK + + if 'ecobee' in _CONFIGURING: + return + + from pyecobee import config_from_file + + # Create ecobee.conf if it doesn't exist + if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): + if config[DOMAIN].get(CONF_API_KEY) is None: + _LOGGER.error("No ecobee api_key found in config.") + return + jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} + config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) + + NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) + + setup_ecobee(hass, NETWORK.ecobee, config) + + return True diff --git a/homeassistant/components/frontend/www_static/images/config_ecobee_thermostat.png b/homeassistant/components/frontend/www_static/images/config_ecobee_thermostat.png new file mode 100644 index 00000000000..e62a4165c9b Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/config_ecobee_thermostat.png differ diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 95cb331b91f..04770ced241 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -9,7 +9,7 @@ https://home-assistant.io/components/sensor/ import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.components import wink, zwave, isy994, verisure +from homeassistant.components import wink, zwave, isy994, verisure, ecobee DOMAIN = 'sensor' SCAN_INTERVAL = 30 @@ -21,7 +21,8 @@ DISCOVERY_PLATFORMS = { wink.DISCOVER_SENSORS: 'wink', zwave.DISCOVER_SENSORS: 'zwave', isy994.DISCOVER_SENSORS: 'isy994', - verisure.DISCOVER_SENSORS: 'verisure' + verisure.DISCOVER_SENSORS: 'verisure', + ecobee.DISCOVER_SENSORS: 'ecobee' } diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py new file mode 100644 index 00000000000..a6499949015 --- /dev/null +++ b/homeassistant/components/sensor/ecobee.py @@ -0,0 +1,94 @@ +""" +homeassistant.components.sensor.ecobee +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ecobee Thermostat Component + +This component adds support for Ecobee3 Wireless Thermostats. +You will need to setup developer access to your thermostat, +and create and API key on the ecobee website. + +The first time you run this component you will see a configuration +component card in Home Assistant. This card will contain a PIN code +that you will need to use to authorize access to your thermostat. You +can do this at https://www.ecobee.com/consumerportal/index.html +Click My Apps, Add application, Enter Pin and click Authorize. + +After authorizing the application click the button in the configuration +card. Now your thermostat and sensors should shown in home-assistant. + +You can use the optional hold_temp parameter to set whether or not holds +are set indefintely or until the next scheduled event. + +ecobee: + api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf + hold_temp: True + +""" +from homeassistant.helpers.entity import Entity +from homeassistant.components import ecobee +from homeassistant.const import TEMP_FAHRENHEIT +import logging + +DEPENDENCIES = ['ecobee'] + +SENSOR_TYPES = { + 'temperature': ['Temperature', TEMP_FAHRENHEIT], + 'humidity': ['Humidity', '%'], + 'occupancy': ['Occupancy', ''] +} + +_LOGGER = logging.getLogger(__name__) + +ECOBEE_CONFIG_FILE = 'ecobee.conf' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the sensors. """ + if discovery_info is None: + return + dev = list() + for name, data in ecobee.NETWORK.ecobee.sensors.items(): + if 'temp' in data: + dev.append(EcobeeSensor(name, 'temperature')) + if 'humidity' in data: + dev.append(EcobeeSensor(name, 'humidity')) + if 'occupancy' in data: + dev.append(EcobeeSensor(name, 'occupancy')) + + add_devices(dev) + + +class EcobeeSensor(Entity): + """ An ecobee sensor. """ + + def __init__(self, sensor_name, sensor_type): + self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0] + self.sensor_name = sensor_name + self.type = sensor_type + self._state = None + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self.update() + + @property + def name(self): + return self._name.rstrip() + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def unit_of_measurement(self): + return self._unit_of_measurement + + def update(self): + ecobee.NETWORK.update() + data = ecobee.NETWORK.ecobee.sensors[self.sensor_name] + if self.type == 'temperature': + self._state = data['temp'] + elif self.type == 'humidity': + self._state = data['humidity'] + elif self.type == 'occupancy': + self._state = data['occupancy'] diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index b475fce39d8..8dc4f1a6c98 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -15,6 +15,7 @@ from homeassistant.config import load_yaml_config_file import homeassistant.util as util from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import convert +from homeassistant.components import ecobee from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELCIUS) @@ -41,6 +42,10 @@ ATTR_OPERATION = "current_operation" _LOGGER = logging.getLogger(__name__) +DISCOVERY_PLATFORMS = { + ecobee.DISCOVER_THERMOSTAT: 'ecobee', +} + def set_away_mode(hass, away_mode, entity_id=None): """ Turn all or specified thermostat away mode on. """ @@ -66,7 +71,8 @@ def set_temperature(hass, temperature, entity_id=None): def setup(hass, config): """ Setup thermostats. """ - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = EntityComponent(_LOGGER, DOMAIN, hass, + SCAN_INTERVAL, DISCOVERY_PLATFORMS) component.setup(config) def thermostat_service(service): diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py new file mode 100644 index 00000000000..78f4d555c9c --- /dev/null +++ b/homeassistant/components/thermostat/ecobee.py @@ -0,0 +1,209 @@ +""" +homeassistant.components.thermostat.ecobee +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ecobee Thermostat Component + +This component adds support for Ecobee3 Wireless Thermostats. +You will need to setup developer access to your thermostat, +and create and API key on the ecobee website. + +The first time you run this component you will see a configuration +component card in Home Assistant. This card will contain a PIN code +that you will need to use to authorize access to your thermostat. You +can do this at https://www.ecobee.com/consumerportal/index.html +Click My Apps, Add application, Enter Pin and click Authorize. + +After authorizing the application click the button in the configuration +card. Now your thermostat and sensors should shown in home-assistant. + +You can use the optional hold_temp parameter to set whether or not holds +are set indefintely or until the next scheduled event. + +ecobee: + api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf + hold_temp: True + +""" +from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL, + STATE_IDLE, STATE_HEAT) +from homeassistant.const import (TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) +from homeassistant.components import ecobee +import logging + +DEPENDENCIES = ['ecobee'] + +_LOGGER = logging.getLogger(__name__) + +ECOBEE_CONFIG_FILE = 'ecobee.conf' +_CONFIGURING = {} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup Platform """ + if discovery_info is None: + return + data = ecobee.NETWORK + hold_temp = discovery_info['hold_temp'] + _LOGGER.info("Loading ecobee thermostat component with hold_temp set to " + + str(hold_temp)) + add_devices(Thermostat(data, index, hold_temp) + for index in range(len(data.ecobee.thermostats))) + + +class Thermostat(ThermostatDevice): + """ Thermostat class for Ecobee """ + + def __init__(self, data, thermostat_index, hold_temp): + self.data = data + self.thermostat_index = thermostat_index + self.thermostat = self.data.ecobee.get_thermostat( + self.thermostat_index) + self._name = self.thermostat['name'] + self._away = 'away' in self.thermostat['program']['currentClimateRef'] + self.hold_temp = hold_temp + + def update(self): + self.data.update() + self.thermostat = self.data.ecobee.get_thermostat( + self.thermostat_index) + + @property + def name(self): + """ Returns the name of the Ecobee Thermostat. """ + return self.thermostat['name'] + + @property + def unit_of_measurement(self): + """ Unit of measurement this thermostat expresses itself in. """ + return TEMP_FAHRENHEIT + + @property + def current_temperature(self): + """ Returns the current temperature. """ + return self.thermostat['runtime']['actualTemperature'] / 10 + + @property + def target_temperature(self): + """ Returns the temperature we try to reach. """ + return (self.target_temperature_low + self.target_temperature_high) / 2 + + @property + def target_temperature_low(self): + """ Returns the lower bound temperature we try to reach. """ + return int(self.thermostat['runtime']['desiredHeat'] / 10) + + @property + def target_temperature_high(self): + """ Returns the upper bound temperature we try to reach. """ + return int(self.thermostat['runtime']['desiredCool'] / 10) + + @property + def humidity(self): + """ Returns the current humidity. """ + return self.thermostat['runtime']['actualHumidity'] + + @property + def desired_fan_mode(self): + """ Returns the desired fan mode of operation. """ + return self.thermostat['runtime']['desiredFanMode'] + + @property + def fan(self): + """ Returns the current fan state. """ + if 'fan' in self.thermostat['equipmentStatus']: + return STATE_ON + else: + return STATE_OFF + + @property + def operation(self): + """ Returns current operation ie. heat, cool, idle """ + status = self.thermostat['equipmentStatus'] + if status == '': + return STATE_IDLE + elif 'Cool' in status: + return STATE_COOL + elif 'auxHeat' in status: + return STATE_HEAT + elif 'heatPump' in status: + return STATE_HEAT + else: + return status + + @property + def mode(self): + """ Returns current mode ie. home, away, sleep """ + mode = self.thermostat['program']['currentClimateRef'] + self._away = 'away' in mode + return mode + + @property + def hvac_mode(self): + """ Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off """ + return self.thermostat['settings']['hvacMode'] + + @property + def device_state_attributes(self): + """ Returns device specific state attributes. """ + # Move these to Thermostat Device and make them global + return { + "humidity": self.humidity, + "fan": self.fan, + "mode": self.mode, + "hvac_mode": self.hvac_mode + } + + @property + def is_away_mode_on(self): + """ Returns if away mode is on. """ + return self._away + + def turn_away_mode_on(self): + """ Turns away on. """ + self._away = True + if self.hold_temp: + self.data.ecobee.set_climate_hold("away", "indefinite") + else: + self.data.ecobee.set_climate_hold("away") + + def turn_away_mode_off(self): + """ Turns away off. """ + self._away = False + self.data.ecobee.resume_program() + + def set_temperature(self, temperature): + """ Set new target temperature """ + temperature = int(temperature) + low_temp = temperature - 1 + high_temp = temperature + 1 + if self.hold_temp: + self.data.ecobee.set_hold_temp(low_temp, high_temp, "indefinite") + else: + self.data.ecobee.set_hold_temp(low_temp, high_temp) + + def set_hvac_mode(self, mode): + """ Set HVAC mode (auto, auxHeatOnly, cool, heat, off) """ + self.data.ecobee.set_hvac_mode(mode) + + # Home and Sleep mode aren't used in UI yet: + + # def turn_home_mode_on(self): + # """ Turns home mode on. """ + # self._away = False + # self.data.ecobee.set_climate_hold("home") + + # def turn_home_mode_off(self): + # """ Turns home mode off. """ + # self._away = False + # self.data.ecobee.resume_program() + + # def turn_sleep_mode_on(self): + # """ Turns sleep mode on. """ + # self._away = False + # self.data.ecobee.set_climate_hold("sleep") + + # def turn_sleep_mode_off(self): + # """ Turns sleep mode off. """ + # self._away = False + # self.data.ecobee.resume_program() diff --git a/requirements_all.txt b/requirements_all.txt index 982cf125572..3f15ecf2e99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -20,6 +20,9 @@ pysnmp==4.2.5 # homeassistant.components.discovery netdisco==0.5.1 +# homeassistant.components.ecobee +https://github.com/nkgilley/python-ecobee-api/archive/d35596b67c75451fa47001c493a15eebee195e93.zip#python-ecobee==0.0.1 + # homeassistant.components.ifttt pyfttt==0.3 @@ -75,6 +78,8 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6 # homeassistant.components.mqtt paho-mqtt==1.1 + +# homeassistant.components.mqtt jsonpath-rw==1.4.0 # homeassistant.components.notify.pushbullet