From e317e0798b3814f9adf4ac514f0b04c20b0da9ff Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Tue, 17 Nov 2015 19:14:29 -0500 Subject: [PATCH 01/12] initial commit for ecobee thermostat component. --- homeassistant/components/sensor/ecobee.py | 101 +++++++ homeassistant/components/thermostat/ecobee.py | 263 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 367 insertions(+) create mode 100644 homeassistant/components/sensor/ecobee.py create mode 100644 homeassistant/components/thermostat/ecobee.py diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py new file mode 100644 index 00000000000..ba4db55b842 --- /dev/null +++ b/homeassistant/components/sensor/ecobee.py @@ -0,0 +1,101 @@ +""" +homeassistant.components.sensor.ecobee +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This sensor component requires that the Ecobee Thermostat +component be setup first. This component shows remote +ecobee sensor data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.ecobee/ +""" +from homeassistant.helpers.entity import Entity +import json +import logging +import os + +SENSOR_TYPES = { + 'temperature': ['Temperature', '°F'], + 'humidity': ['Humidity', '%'], + 'occupancy': ['Occupancy', ''] +} + +_LOGGER = logging.getLogger(__name__) + +ECOBEE_CONFIG_FILE = 'ecobee.conf' + + +def config_from_file(filename, config=None): + ''' Small configuration file management function ''' + if config: + # We're writing configuration + try: + with open(filename, 'w') as fdesc: + fdesc.write(json.dumps(config)) + except IOError as error: + print(error) + return False + return True + else: + # We're reading config + if os.path.isfile(filename): + try: + with open(filename, 'r') as fdesc: + return json.loads(fdesc.read()) + except IOError as error: + return False + else: + return {} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the sensors. """ + config = config_from_file(hass.config.path(ECOBEE_CONFIG_FILE)) + dev = list() + for name, data in config['sensors'].items(): + if 'temp' in data: + dev.append(EcobeeSensor(name, 'temperature', hass)) + if 'humidity' in data: + dev.append(EcobeeSensor(name, 'humidity', hass)) + if 'occupancy' in data: + dev.append(EcobeeSensor(name, 'occupancy', hass)) + + add_devices(dev) + + +class EcobeeSensor(Entity): + """ An ecobee sensor. """ + + def __init__(self, sensor_name, sensor_type, hass): + self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0] + self.sensor_name = sensor_name + self.hass = hass + 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): + config = config_from_file(self.hass.config.path(ECOBEE_CONFIG_FILE)) + try: + data = config['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'] + except KeyError: + print("Error updating ecobee sensors.") diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py new file mode 100644 index 00000000000..6792c34a421 --- /dev/null +++ b/homeassistant/components/thermostat/ecobee.py @@ -0,0 +1,263 @@ +#!/usr/local/bin/python3 +""" +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 should shown in home-assistant. You will need +to restart home assistant to get rid of the configuration card. Once the +thermostat has been added you can add the ecobee sensor component +to your configuration.yaml. + +thermostat: + platform: ecobee + api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf +""" +from homeassistant.loader import get_component +from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL, + STATE_IDLE, STATE_HEAT) +from homeassistant.const import ( + CONF_API_KEY, TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) +import logging +import os + +REQUIREMENTS = [ + 'https://github.com/nkgilley/home-assistant-ecobee-api/archive/' + 'c61ee6d456bb5f4ab0c9598804aa9231c3d06f8e.zip#python-ecobee==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +ECOBEE_CONFIG_FILE = 'ecobee.conf' +_CONFIGURING = {} + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Setup Platform """ + # Only act if we are not already configuring this host + if 'ecobee' in _CONFIGURING: + return + + setup_ecobee(hass, config, add_devices_callback) + + +def setup_ecobee(hass, config, add_devices_callback): + """ Setup ecobee thermostat """ + from pyecobee import Ecobee, config_from_file + # Create ecobee.conf if it doesn't exist + if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): + jsonconfig = {"API_KEY": config[CONF_API_KEY]} + config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) + + ecobee = Ecobee(hass.config.path(ECOBEE_CONFIG_FILE)) + + # If ecobee has a PIN then it needs to be configured. + if ecobee.pin is not None: + # ecobee.request_pin() + request_configuration(ecobee, hass, add_devices_callback) + return + + if 'ecobee' in _CONFIGURING: + _CONFIGURING.pop('ecobee') + configurator = get_component('configurator') + configurator.request_done('ecobee') + + devices = [] + for index in range(0, len(ecobee.thermostats)): + devices.append(Thermostat(ecobee, index)) + + add_devices_callback(devices) + + +def request_configuration(ecobee, hass, add_devices_callback): + """ 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(data): + """ Actions to do when our configuration callback is called. """ + ecobee.request_tokens() + ecobee.update() + setup_ecobee(hass, None, add_devices_callback) + + _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: ' + ecobee.pin), + description_image='https://goo.gl/6tBkbH', + submit_caption="I have authorized the app." + ) + + +class Thermostat(ThermostatDevice): + """docstring for Thermostat""" + + def __init__(self, ecobee, thermostat_index): + self.ecobee = ecobee + self.thermostat_index = thermostat_index + self.thermostat_data = self.ecobee.get_thermostat( + self.thermostat_index) + self._name = self.thermostat_data['name'] + if 'away' in self.thermostat_data['program']['currentClimateRef']: + self._away = True + else: + self._away = False + + def update(self): + self.thermostat_data = self.ecobee.get_thermostat( + self.thermostat_index) + _LOGGER.info("ecobee data updated successfully.") + + @property + def name(self): + """ Returns the name of the Ecobee Thermostat. """ + return self.thermostat_data['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_data['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_data['runtime']['desiredHeat'] / 10) + + @property + def target_temperature_high(self): + """ Returns the upper bound temperature we try to reach. """ + return int(self.thermostat_data['runtime']['desiredCool'] / 10) + + @property + def humidity(self): + """ Returns the current humidity. """ + return self.thermostat_data['runtime']['actualHumidity'] + + @property + def desired_fan_mode(self): + """ Returns the desired fan mode of operation. """ + return self.thermostat_data['runtime']['desiredFanMode'] + + @property + def fan(self): + """ Returns the current fan state. """ + if 'fan' in self.thermostat_data['equipmentStatus']: + return STATE_ON + else: + return STATE_OFF + + @property + def operation(self): + """ Returns current operation ie. heat, cool, idle """ + status = self.thermostat_data['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_data['program']['currentClimateRef'] + if 'away' in mode: + self._away = True + else: + self._away = False + return mode + + @property + def hvac_mode(self): + """ Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off """ + return self.thermostat_data['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 + self.ecobee.set_climate_hold("away") + + def turn_away_mode_off(self): + """ Turns away off. """ + self._away = False + self.ecobee.resume_program() + + def set_temperature(self, temperature): + """ Set new target temperature """ + temperature = int(temperature) + low_temp = temperature - 1 + high_temp = temperature + 1 + self.ecobee.set_hold_temp(low_temp, high_temp) + + def set_hvac_mode(self, mode): + """ Set HVAC mode (auto, auxHeatOnly, cool, heat, off) """ + self.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.ecobee.set_climate_hold("home") + + # def turn_home_mode_off(self): + # """ Turns home mode off. """ + # self._away = False + # self.ecobee.resume_program() + + # def turn_sleep_mode_on(self): + # """ Turns sleep mode on. """ + # self._away = False + # self.ecobee.set_climate_hold("sleep") + + # def turn_sleep_mode_off(self): + # """ Turns sleep mode off. """ + # self._away = False + # self.ecobee.resume_program() diff --git a/requirements_all.txt b/requirements_all.txt index ce6cbfabc96..db7ca1ada2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -159,3 +159,6 @@ pushetta==1.0.15 # Orvibo S10 orvibo==1.0.0 + +# Ecobee (*.ecobee) +https://github.com/nkgilley/home-assistant-ecobee-api/archive/e0388659a0f2fc7266485affbd398350cc0b5c58.zip#python-ecobee==0.1.1 From 22fcbc67cfa61ca8f372a4ce978c687c19d5346d Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Tue, 17 Nov 2015 19:20:56 -0500 Subject: [PATCH 02/12] fix req --- homeassistant/components/thermostat/ecobee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index 6792c34a421..cbdd52ba7e9 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -35,7 +35,7 @@ import os REQUIREMENTS = [ 'https://github.com/nkgilley/home-assistant-ecobee-api/archive/' - 'c61ee6d456bb5f4ab0c9598804aa9231c3d06f8e.zip#python-ecobee==0.1.1'] + 'e0388659a0f2fc7266485affbd398350cc0b5c58.zip#python-ecobee==0.1.1'] _LOGGER = logging.getLogger(__name__) From c6d1a4bdaf572407cfbfe79dd5a259c6290a3780 Mon Sep 17 00:00:00 2001 From: "nkgilley@gmail.com" Date: Wed, 18 Nov 2015 10:13:46 -0500 Subject: [PATCH 03/12] Fix configurator, rename repo, cleanup code. --- homeassistant/components/sensor/ecobee.py | 26 ++++----- homeassistant/components/thermostat/ecobee.py | 53 ++++++++----------- requirements_all.txt | 2 +- 3 files changed, 32 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index ba4db55b842..a8d9e41acb1 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -13,6 +13,8 @@ import json import logging import os +DEPENDENCIES = ['thermostat'] + SENSOR_TYPES = { 'temperature': ['Temperature', '°F'], 'humidity': ['Humidity', '%'], @@ -24,27 +26,17 @@ _LOGGER = logging.getLogger(__name__) ECOBEE_CONFIG_FILE = 'ecobee.conf' -def config_from_file(filename, config=None): - ''' Small configuration file management function ''' - if config: - # We're writing configuration +def config_from_file(filename): + ''' Small configuration file reading function ''' + if os.path.isfile(filename): try: - with open(filename, 'w') as fdesc: - fdesc.write(json.dumps(config)) + with open(filename, 'r') as fdesc: + return json.loads(fdesc.read()) except IOError as error: - print(error) + _LOGGER.error("ecobee sensor couldn't read config file: " + error) return False - return True else: - # We're reading config - if os.path.isfile(filename): - try: - with open(filename, 'r') as fdesc: - return json.loads(fdesc.read()) - except IOError as error: - return False - else: - return {} + return {} def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index cbdd52ba7e9..0c00fb0de46 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -16,8 +16,7 @@ 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 should shown in home-assistant. You will need -to restart home assistant to get rid of the configuration card. Once the +card. Now your thermostat should shown in home-assistant. Once the thermostat has been added you can add the ecobee sensor component to your configuration.yaml. @@ -34,8 +33,8 @@ import logging import os REQUIREMENTS = [ - 'https://github.com/nkgilley/home-assistant-ecobee-api/archive/' - 'e0388659a0f2fc7266485affbd398350cc0b5c58.zip#python-ecobee==0.1.1'] + 'https://github.com/nkgilley/python-ecobee-api/archive/' + '824a7dfabe7ef6975b2864f33e6ae0b48fb6ea3f.zip#python-ecobee==0.0.1'] _LOGGER = logging.getLogger(__name__) @@ -64,20 +63,15 @@ def setup_ecobee(hass, config, add_devices_callback): # If ecobee has a PIN then it needs to be configured. if ecobee.pin is not None: - # ecobee.request_pin() request_configuration(ecobee, hass, add_devices_callback) return if 'ecobee' in _CONFIGURING: - _CONFIGURING.pop('ecobee') configurator = get_component('configurator') - configurator.request_done('ecobee') + configurator.request_done(_CONFIGURING.pop('ecobee')) - devices = [] - for index in range(0, len(ecobee.thermostats)): - devices.append(Thermostat(ecobee, index)) - - add_devices_callback(devices) + add_devices_callback(Thermostat(ecobee, index) + for index in range(len(ecobee.thermostats))) def request_configuration(ecobee, hass, add_devices_callback): @@ -101,34 +95,31 @@ def request_configuration(ecobee, hass, add_devices_callback): description=( 'Please authorize this app at https://www.ecobee.com/consumer' 'portal/index.html with pin code: ' + ecobee.pin), - description_image='https://goo.gl/6tBkbH', + description_image="/static/images/config_ecobee_thermostat.png", submit_caption="I have authorized the app." ) class Thermostat(ThermostatDevice): - """docstring for Thermostat""" + """ Thermostat class for Ecobee """ def __init__(self, ecobee, thermostat_index): self.ecobee = ecobee self.thermostat_index = thermostat_index - self.thermostat_data = self.ecobee.get_thermostat( + self.thermostat = self.ecobee.get_thermostat( self.thermostat_index) - self._name = self.thermostat_data['name'] - if 'away' in self.thermostat_data['program']['currentClimateRef']: - self._away = True - else: - self._away = False + self._name = self.thermostat['name'] + self._away = 'away' in self.thermostat['program']['currentClimateRef'] def update(self): - self.thermostat_data = self.ecobee.get_thermostat( + self.thermostat = self.ecobee.get_thermostat( self.thermostat_index) _LOGGER.info("ecobee data updated successfully.") @property def name(self): """ Returns the name of the Ecobee Thermostat. """ - return self.thermostat_data['name'] + return self.thermostat['name'] @property def unit_of_measurement(self): @@ -138,7 +129,7 @@ class Thermostat(ThermostatDevice): @property def current_temperature(self): """ Returns the current temperature. """ - return self.thermostat_data['runtime']['actualTemperature'] / 10 + return self.thermostat['runtime']['actualTemperature'] / 10 @property def target_temperature(self): @@ -148,27 +139,27 @@ class Thermostat(ThermostatDevice): @property def target_temperature_low(self): """ Returns the lower bound temperature we try to reach. """ - return int(self.thermostat_data['runtime']['desiredHeat'] / 10) + 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_data['runtime']['desiredCool'] / 10) + return int(self.thermostat['runtime']['desiredCool'] / 10) @property def humidity(self): """ Returns the current humidity. """ - return self.thermostat_data['runtime']['actualHumidity'] + return self.thermostat['runtime']['actualHumidity'] @property def desired_fan_mode(self): """ Returns the desired fan mode of operation. """ - return self.thermostat_data['runtime']['desiredFanMode'] + return self.thermostat['runtime']['desiredFanMode'] @property def fan(self): """ Returns the current fan state. """ - if 'fan' in self.thermostat_data['equipmentStatus']: + if 'fan' in self.thermostat['equipmentStatus']: return STATE_ON else: return STATE_OFF @@ -176,7 +167,7 @@ class Thermostat(ThermostatDevice): @property def operation(self): """ Returns current operation ie. heat, cool, idle """ - status = self.thermostat_data['equipmentStatus'] + status = self.thermostat['equipmentStatus'] if status == '': return STATE_IDLE elif 'Cool' in status: @@ -191,7 +182,7 @@ class Thermostat(ThermostatDevice): @property def mode(self): """ Returns current mode ie. home, away, sleep """ - mode = self.thermostat_data['program']['currentClimateRef'] + mode = self.thermostat['program']['currentClimateRef'] if 'away' in mode: self._away = True else: @@ -201,7 +192,7 @@ class Thermostat(ThermostatDevice): @property def hvac_mode(self): """ Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off """ - return self.thermostat_data['settings']['hvacMode'] + return self.thermostat['settings']['hvacMode'] @property def device_state_attributes(self): diff --git a/requirements_all.txt b/requirements_all.txt index db7ca1ada2d..4e34b6029ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,4 +161,4 @@ pushetta==1.0.15 orvibo==1.0.0 # Ecobee (*.ecobee) -https://github.com/nkgilley/home-assistant-ecobee-api/archive/e0388659a0f2fc7266485affbd398350cc0b5c58.zip#python-ecobee==0.1.1 +https://github.com/nkgilley/python-ecobee-api/archive/824a7dfabe7ef6975b2864f33e6ae0b48fb6ea3f.zip#python-ecobee==0.0.1 From 18d0f4461f0fade2e919f16ae18c826932b9e749 Mon Sep 17 00:00:00 2001 From: "nkgilley@gmail.com" Date: Wed, 18 Nov 2015 10:16:16 -0500 Subject: [PATCH 04/12] add config png to images dir. --- .../images/config_ecobee_thermostat.png | Bin 0 -> 31083 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 homeassistant/components/frontend/www_static/images/config_ecobee_thermostat.png 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 0000000000000000000000000000000000000000..e62a4165c9becb8c974faa54523cd1bad9b4ef32 GIT binary patch literal 31083 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 xU6?KYcZ>A7BL5HbU&-%mneX{5e$8K|?t}7cwC# Date: Wed, 18 Nov 2015 10:43:52 -0500 Subject: [PATCH 05/12] point to updated python-ecobee library --- homeassistant/components/thermostat/ecobee.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index 0c00fb0de46..472c0e60b60 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -34,7 +34,7 @@ import os REQUIREMENTS = [ 'https://github.com/nkgilley/python-ecobee-api/archive/' - '824a7dfabe7ef6975b2864f33e6ae0b48fb6ea3f.zip#python-ecobee==0.0.1'] + '730009b9593899d42e98c81a0544f91e65b2bc15.zip#python-ecobee==0.0.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4e34b6029ac..3a930c145dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,4 +161,4 @@ pushetta==1.0.15 orvibo==1.0.0 # Ecobee (*.ecobee) -https://github.com/nkgilley/python-ecobee-api/archive/824a7dfabe7ef6975b2864f33e6ae0b48fb6ea3f.zip#python-ecobee==0.0.1 +https://github.com/nkgilley/python-ecobee-api/archive/730009b9593899d42e98c81a0544f91e65b2bc15.zip#python-ecobee==0.0.1 From d05af626802231bf1398298a5e1cb9d0753a1820 Mon Sep 17 00:00:00 2001 From: "nkgilley@gmail.com" Date: Wed, 18 Nov 2015 14:57:27 -0500 Subject: [PATCH 06/12] use Throttle like the BitCoin component. --- .coveragerc | 1 + homeassistant/components/thermostat/ecobee.py | 82 +++++++++++-------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/.coveragerc b/.coveragerc index f19e37d00a1..9fafe443dcf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -16,6 +16,7 @@ omit = homeassistant/components/*/tellstick.py homeassistant/components/*/vera.py + homeassistant/components/*/ecobee.py homeassistant/components/verisure.py homeassistant/components/*/verisure.py diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index 472c0e60b60..7258a3b65a1 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -29,18 +29,23 @@ from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL, STATE_IDLE, STATE_HEAT) from homeassistant.const import ( CONF_API_KEY, TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) +from homeassistant.util import Throttle +from datetime import timedelta import logging import os REQUIREMENTS = [ 'https://github.com/nkgilley/python-ecobee-api/archive/' - '730009b9593899d42e98c81a0544f91e65b2bc15.zip#python-ecobee==0.0.1'] + '790c20d820dbb727af2dbfb3ef0f79231e19a503.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 setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Setup Platform """ @@ -48,33 +53,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if 'ecobee' in _CONFIGURING: return - setup_ecobee(hass, config, add_devices_callback) + from pyecobee import config_from_file - -def setup_ecobee(hass, config, add_devices_callback): - """ Setup ecobee thermostat """ - from pyecobee import Ecobee, config_from_file # Create ecobee.conf if it doesn't exist if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): jsonconfig = {"API_KEY": config[CONF_API_KEY]} config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) + data = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) + setup_ecobee(hass, data, config, add_devices_callback) - ecobee = Ecobee(hass.config.path(ECOBEE_CONFIG_FILE)) +def setup_ecobee(hass, data, config, add_devices_callback): + """ Setup ecobee thermostat """ # If ecobee has a PIN then it needs to be configured. - if ecobee.pin is not None: - request_configuration(ecobee, hass, add_devices_callback) + if data.ecobee.pin is not None: + request_configuration(data, hass, add_devices_callback) return if 'ecobee' in _CONFIGURING: configurator = get_component('configurator') configurator.request_done(_CONFIGURING.pop('ecobee')) - add_devices_callback(Thermostat(ecobee, index) - for index in range(len(ecobee.thermostats))) + add_devices_callback(Thermostat(data, index) + for index in range(len(data.ecobee.thermostats))) -def request_configuration(ecobee, hass, add_devices_callback): +def request_configuration(data, hass, add_devices_callback): """ Request configuration steps from the user. """ configurator = get_component('configurator') if 'ecobee' in _CONFIGURING: @@ -84,35 +88,50 @@ def request_configuration(ecobee, hass, add_devices_callback): return # pylint: disable=unused-argument - def ecobee_configuration_callback(data): + def ecobee_configuration_callback(callback_data): """ Actions to do when our configuration callback is called. """ - ecobee.request_tokens() - ecobee.update() - setup_ecobee(hass, None, add_devices_callback) + data.ecobee.request_tokens() + data.ecobee.update() + setup_ecobee(hass, data, None, add_devices_callback) _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: ' + ecobee.pin), + 'portal/index.html with pin code: ' + data.ecobee.pin), description_image="/static/images/config_ecobee_thermostat.png", submit_caption="I have authorized the app." ) +# pylint: disable=too-few-public-methods +class EcobeeData(object): + """ Gets the latest data and update the states. """ + + def __init__(self, config_filename): + from pyecobee import Ecobee + self.ecobee = Ecobee(config_filename) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Get the latest data from pyecobee. """ + self.ecobee.update() + + class Thermostat(ThermostatDevice): """ Thermostat class for Ecobee """ - def __init__(self, ecobee, thermostat_index): - self.ecobee = ecobee + def __init__(self, data, thermostat_index): + self.data = data self.thermostat_index = thermostat_index - self.thermostat = self.ecobee.get_thermostat( + self.thermostat = self.data.ecobee.get_thermostat( self.thermostat_index) self._name = self.thermostat['name'] self._away = 'away' in self.thermostat['program']['currentClimateRef'] def update(self): - self.thermostat = self.ecobee.get_thermostat( + self.data.update() + self.thermostat = self.data.ecobee.get_thermostat( self.thermostat_index) _LOGGER.info("ecobee data updated successfully.") @@ -183,10 +202,7 @@ class Thermostat(ThermostatDevice): def mode(self): """ Returns current mode ie. home, away, sleep """ mode = self.thermostat['program']['currentClimateRef'] - if 'away' in mode: - self._away = True - else: - self._away = False + self._away = 'away' in mode return mode @property @@ -213,42 +229,42 @@ class Thermostat(ThermostatDevice): def turn_away_mode_on(self): """ Turns away on. """ self._away = True - self.ecobee.set_climate_hold("away") + self.data.ecobee.set_climate_hold("away") def turn_away_mode_off(self): """ Turns away off. """ self._away = False - self.ecobee.resume_program() + 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 - self.ecobee.set_hold_temp(low_temp, high_temp) + 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.ecobee.set_hvac_mode(mode) + 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.ecobee.set_climate_hold("home") + # self.data.ecobee.set_climate_hold("home") # def turn_home_mode_off(self): # """ Turns home mode off. """ # self._away = False - # self.ecobee.resume_program() + # self.data.ecobee.resume_program() # def turn_sleep_mode_on(self): # """ Turns sleep mode on. """ # self._away = False - # self.ecobee.set_climate_hold("sleep") + # self.data.ecobee.set_climate_hold("sleep") # def turn_sleep_mode_off(self): # """ Turns sleep mode off. """ # self._away = False - # self.ecobee.resume_program() + # self.data.ecobee.resume_program() From 44abc31057c174968939beb940d48e55b326895c Mon Sep 17 00:00:00 2001 From: "nkgilley@gmail.com" Date: Fri, 20 Nov 2015 17:47:25 -0500 Subject: [PATCH 07/12] work in progress: configurator is now in it's own component. configurator seems to work but the thermostat is now broken. --- .coveragerc | 2 + homeassistant/components/ecobee.py | 127 ++++++++++++++++++ homeassistant/components/thermostat/ecobee.py | 72 ++-------- requirements_all.txt | 2 +- 4 files changed, 142 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/ecobee.py diff --git a/.coveragerc b/.coveragerc index 9fafe443dcf..b8016fa7624 100644 --- a/.coveragerc +++ b/.coveragerc @@ -16,6 +16,8 @@ omit = homeassistant/components/*/tellstick.py homeassistant/components/*/vera.py + + homeassistant/components/ecobee.py homeassistant/components/*/ecobee.py homeassistant/components/verisure.py diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py new file mode 100644 index 00000000000..99db73b6a90 --- /dev/null +++ b/homeassistant/components/ecobee.py @@ -0,0 +1,127 @@ +""" +homeassistant.components.zwave +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Connects Home Assistant to the Ecobee API and maintains tokens. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ecobee/ + +[ecobee] +api_key: asdflaksf +""" + +from homeassistant.loader import get_component +from homeassistant import bootstrap +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_API_KEY) +from datetime import timedelta +import logging +import os + +DOMAIN = "ecobee" +DISCOVER_THERMOSTAT = "ecobee.thermostat" +DEPENDENCIES = [] +NETWORK = None + +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): + """ 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) + + _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): + """ Setup ecobee thermostat """ + # If ecobee has a PIN then it needs to be configured. + if network.pin is not None: + request_configuration(network, hass) + return + + if 'ecobee' in _CONFIGURING: + configurator = get_component('configurator') + configurator.request_done(_CONFIGURING.pop('ecobee')) + + +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 Ecobee, 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 = Ecobee(hass.config.path(ECOBEE_CONFIG_FILE)) + + setup_ecobee(hass, NETWORK) + + # Ensure component is loaded + bootstrap.setup_component(hass, 'thermostat', config) + + # Fire discovery event + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: DISCOVER_THERMOSTAT, + ATTR_DISCOVERED: { + 'network': NETWORK, + } + }) + + def stop_ecobee(event): + """ Stop Ecobee. """ + + pass + + def start_ecobee(event): + """ Called when Home Assistant starts up. """ + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_ecobee) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_ecobee) + + return True diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index 7258a3b65a1..5b377be4907 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -1,4 +1,3 @@ -#!/usr/local/bin/python3 """ homeassistant.components.thermostat.ecobee ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -24,19 +23,14 @@ thermostat: platform: ecobee api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf """ -from homeassistant.loader import get_component from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL, STATE_IDLE, STATE_HEAT) -from homeassistant.const import ( - CONF_API_KEY, TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) +from homeassistant.const import (TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) from homeassistant.util import Throttle from datetime import timedelta import logging -import os -REQUIREMENTS = [ - 'https://github.com/nkgilley/python-ecobee-api/archive/' - '790c20d820dbb727af2dbfb3ef0f79231e19a503.zip#python-ecobee==0.0.1'] +DEPENDENCIES = ['ecobee'] _LOGGER = logging.getLogger(__name__) @@ -47,70 +41,28 @@ _CONFIGURING = {} MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """ Setup Platform """ - # Only act if we are not already configuring this host - if 'ecobee' in _CONFIGURING: + _LOGGER.error("ecobee !!!!") + if discovery_info is None: 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)): - jsonconfig = {"API_KEY": config[CONF_API_KEY]} - config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) - data = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) - setup_ecobee(hass, data, config, add_devices_callback) + data = EcobeeData(discovery_info[0]) + setup_ecobee(hass, data, add_devices) -def setup_ecobee(hass, data, config, add_devices_callback): +def setup_ecobee(hass, data, add_devices): """ Setup ecobee thermostat """ - # If ecobee has a PIN then it needs to be configured. - if data.ecobee.pin is not None: - request_configuration(data, hass, add_devices_callback) - return - if 'ecobee' in _CONFIGURING: - configurator = get_component('configurator') - configurator.request_done(_CONFIGURING.pop('ecobee')) - - add_devices_callback(Thermostat(data, index) - for index in range(len(data.ecobee.thermostats))) - - -def request_configuration(data, hass, add_devices_callback): - """ 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. """ - data.ecobee.request_tokens() - data.ecobee.update() - setup_ecobee(hass, data, None, add_devices_callback) - - _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: ' + data.ecobee.pin), - description_image="/static/images/config_ecobee_thermostat.png", - submit_caption="I have authorized the app." - ) + add_devices(Thermostat(data, index) + for index in range(len(data.ecobee.thermostats))) # pylint: disable=too-few-public-methods class EcobeeData(object): """ Gets the latest data and update the states. """ - def __init__(self, config_filename): - from pyecobee import Ecobee - self.ecobee = Ecobee(config_filename) + def __init__(self, network): + self.ecobee = network @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): diff --git a/requirements_all.txt b/requirements_all.txt index 3a930c145dd..b74226f4991 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,4 +161,4 @@ pushetta==1.0.15 orvibo==1.0.0 # Ecobee (*.ecobee) -https://github.com/nkgilley/python-ecobee-api/archive/730009b9593899d42e98c81a0544f91e65b2bc15.zip#python-ecobee==0.0.1 +https://github.com/nkgilley/python-ecobee-api/archive/d35596b67c75451fa47001c493a15eebee195e93.zip#python-ecobee==0.0.1 From 8dc0de1d05cda0f218b882ccf4013bbfa405824d Mon Sep 17 00:00:00 2001 From: "nkgilley@gmail.com" Date: Sat, 21 Nov 2015 12:24:06 -0500 Subject: [PATCH 08/12] move EcobeeData class and Throttle to the main ecobee component, this way the sensor and thermostat will use the same throttled updating object. --- homeassistant/components/ecobee.py | 25 ++++++++++++++---- homeassistant/components/thermostat/ecobee.py | 26 +------------------ 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 99db73b6a90..ef982a15e63 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -1,5 +1,5 @@ """ -homeassistant.components.zwave +homeassistant.components.ecobee ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Connects Home Assistant to the Ecobee API and maintains tokens. @@ -12,6 +12,7 @@ api_key: asdflaksf from homeassistant.loader import get_component from homeassistant import bootstrap +from homeassistant.util import Throttle from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_API_KEY) @@ -57,7 +58,7 @@ def request_configuration(network, hass): hass, "Ecobee", ecobee_configuration_callback, description=( 'Please authorize this app at https://www.ecobee.com/consumer' - 'portal/index.html with pin code: ' + NETWORK.pin), + 'portal/index.html with pin code: ' + network.pin), description_image="/static/images/config_ecobee_thermostat.png", submit_caption="I have authorized the app." ) @@ -75,6 +76,20 @@ def setup_ecobee(hass, network): configurator.request_done(_CONFIGURING.pop('ecobee')) +# 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() + + def setup(hass, config): """ Setup Ecobee. @@ -87,7 +102,7 @@ def setup(hass, config): if 'ecobee' in _CONFIGURING: return - from pyecobee import Ecobee, config_from_file + 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)): @@ -97,9 +112,9 @@ def setup(hass, config): jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) - NETWORK = Ecobee(hass.config.path(ECOBEE_CONFIG_FILE)) + NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) - setup_ecobee(hass, NETWORK) + setup_ecobee(hass, NETWORK.ecobee) # Ensure component is loaded bootstrap.setup_component(hass, 'thermostat', config) diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index 5b377be4907..7f95aa7d1c6 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -26,8 +26,6 @@ thermostat: from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL, STATE_IDLE, STATE_HEAT) from homeassistant.const import (TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) -from homeassistant.util import Throttle -from datetime import timedelta import logging DEPENDENCIES = ['ecobee'] @@ -37,39 +35,17 @@ _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 setup_platform(hass, config, add_devices, discovery_info=None): """ Setup Platform """ _LOGGER.error("ecobee !!!!") if discovery_info is None: return - data = EcobeeData(discovery_info[0]) - setup_ecobee(hass, data, add_devices) - - -def setup_ecobee(hass, data, add_devices): - """ Setup ecobee thermostat """ - + data = discovery_info[0] add_devices(Thermostat(data, index) for index in range(len(data.ecobee.thermostats))) -# pylint: disable=too-few-public-methods -class EcobeeData(object): - """ Gets the latest data and update the states. """ - - def __init__(self, network): - self.ecobee = network - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Get the latest data from pyecobee. """ - self.ecobee.update() - - class Thermostat(ThermostatDevice): """ Thermostat class for Ecobee """ From cc196d988867fe7e7d08c062c3ca06bf3383bdcf Mon Sep 17 00:00:00 2001 From: "nkgilley@gmail.com" Date: Mon, 23 Nov 2015 11:15:19 -0500 Subject: [PATCH 09/12] fixed sensors and thermostat. discovery working for both now. --- homeassistant/components/ecobee.py | 70 ++++++++++++------ homeassistant/components/sensor/__init__.py | 5 +- homeassistant/components/sensor/ecobee.py | 73 ++++++++++--------- .../components/thermostat/__init__.py | 8 +- homeassistant/components/thermostat/ecobee.py | 35 ++++++--- 5 files changed, 118 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index ef982a15e63..8b73a9969ef 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -1,13 +1,29 @@ """ homeassistant.components.ecobee -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Connects Home Assistant to the Ecobee API and maintains tokens. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For more details about this component, please refer to the documentation at -https://home-assistant.io/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 -[ecobee] -api_key: asdflaksf """ from homeassistant.loader import get_component @@ -22,8 +38,10 @@ import os DOMAIN = "ecobee" DISCOVER_THERMOSTAT = "ecobee.thermostat" +DISCOVER_SENSORS = "ecobee.sensor" DEPENDENCIES = [] NETWORK = None +HOLD_TEMP = 'hold_temp' REQUIREMENTS = [ 'https://github.com/nkgilley/python-ecobee-api/archive/' @@ -38,7 +56,7 @@ _CONFIGURING = {} MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) -def request_configuration(network, hass): +def request_configuration(network, hass, config): """ Request configuration steps from the user. """ configurator = get_component('configurator') if 'ecobee' in _CONFIGURING: @@ -52,7 +70,7 @@ def request_configuration(network, hass): """ Actions to do when our configuration callback is called. """ network.request_tokens() network.update() - setup_ecobee(hass, network) + setup_ecobee(hass, network, config) _CONFIGURING['ecobee'] = configurator.request_config( hass, "Ecobee", ecobee_configuration_callback, @@ -64,17 +82,35 @@ def request_configuration(network, hass): ) -def setup_ecobee(hass, network): +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) + 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') + bootstrap.setup_component(hass, 'sensor') + + 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): @@ -88,6 +124,7 @@ class EcobeeData(object): def update(self): """ Get the latest data from pyecobee. """ self.ecobee.update() + _LOGGER.info("ecobee data updated successfully.") def setup(hass, config): @@ -114,18 +151,7 @@ def setup(hass, config): NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) - setup_ecobee(hass, NETWORK.ecobee) - - # Ensure component is loaded - bootstrap.setup_component(hass, 'thermostat', config) - - # Fire discovery event - hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: DISCOVER_THERMOSTAT, - ATTR_DISCOVERED: { - 'network': NETWORK, - } - }) + setup_ecobee(hass, NETWORK.ecobee, config) def stop_ecobee(event): """ Stop Ecobee. """ diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 32ee59a6fa9..0d214475358 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' DEPENDENCIES = [] @@ -22,7 +22,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 index a8d9e41acb1..b7663a70d6a 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -1,22 +1,39 @@ """ homeassistant.components.sensor.ecobee -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This sensor component requires that the Ecobee Thermostat -component be setup first. This component shows remote -ecobee sensor data. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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 -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ecobee/ """ from homeassistant.helpers.entity import Entity -import json +from homeassistant.components.ecobee import NETWORK +from homeassistant.const import TEMP_FAHRENHEIT import logging -import os -DEPENDENCIES = ['thermostat'] +DEPENDENCIES = ['ecobee'] SENSOR_TYPES = { - 'temperature': ['Temperature', '°F'], + 'temperature': ['Temperature', TEMP_FAHRENHEIT], 'humidity': ['Humidity', '%'], 'occupancy': ['Occupancy', ''] } @@ -26,24 +43,12 @@ _LOGGER = logging.getLogger(__name__) ECOBEE_CONFIG_FILE = 'ecobee.conf' -def config_from_file(filename): - ''' Small configuration file reading function ''' - if os.path.isfile(filename): - try: - with open(filename, 'r') as fdesc: - return json.loads(fdesc.read()) - except IOError as error: - _LOGGER.error("ecobee sensor couldn't read config file: " + error) - return False - else: - return {} - - def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the sensors. """ - config = config_from_file(hass.config.path(ECOBEE_CONFIG_FILE)) + if discovery_info is None: + return dev = list() - for name, data in config['sensors'].items(): + for name, data in NETWORK.ecobee.sensors.items(): if 'temp' in data: dev.append(EcobeeSensor(name, 'temperature', hass)) if 'humidity' in data: @@ -80,14 +85,10 @@ class EcobeeSensor(Entity): return self._unit_of_measurement def update(self): - config = config_from_file(self.hass.config.path(ECOBEE_CONFIG_FILE)) - try: - data = config['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'] - except KeyError: - print("Error updating ecobee sensors.") + data = 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 480e3e4805e..f1a82a4c989 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) @@ -42,6 +43,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. """ @@ -67,7 +72,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 index 7f95aa7d1c6..51d21cb9991 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -15,17 +15,20 @@ 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 should shown in home-assistant. Once the -thermostat has been added you can add the ecobee sensor component -to your configuration.yaml. +card. Now your thermostat and sensors should shown in home-assistant. -thermostat: - platform: ecobee +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.ecobee import NETWORK import logging DEPENDENCIES = ['ecobee'] @@ -38,30 +41,32 @@ _CONFIGURING = {} def setup_platform(hass, config, add_devices, discovery_info=None): """ Setup Platform """ - _LOGGER.error("ecobee !!!!") if discovery_info is None: return - data = discovery_info[0] - add_devices(Thermostat(data, index) + data = 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): + 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) - _LOGGER.info("ecobee data updated successfully.") @property def name(self): @@ -157,7 +162,10 @@ class Thermostat(ThermostatDevice): def turn_away_mode_on(self): """ Turns away on. """ self._away = True - self.data.ecobee.set_climate_hold("away") + 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. """ @@ -169,7 +177,10 @@ class Thermostat(ThermostatDevice): temperature = int(temperature) low_temp = temperature - 1 high_temp = temperature + 1 - self.data.ecobee.set_hold_temp(low_temp, high_temp) + 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) """ From 27bc4c582bc2fa7ce3b7467f2823525879d5ee12 Mon Sep 17 00:00:00 2001 From: "nkgilley@gmail.com" Date: Mon, 23 Nov 2015 11:40:54 -0500 Subject: [PATCH 10/12] update network data before sensor setup. --- homeassistant/components/sensor/ecobee.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index b7663a70d6a..1ef40bcca89 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -48,13 +48,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return dev = list() + NETWORK.update() for name, data in NETWORK.ecobee.sensors.items(): if 'temp' in data: - dev.append(EcobeeSensor(name, 'temperature', hass)) + dev.append(EcobeeSensor(name, 'temperature')) if 'humidity' in data: - dev.append(EcobeeSensor(name, 'humidity', hass)) + dev.append(EcobeeSensor(name, 'humidity')) if 'occupancy' in data: - dev.append(EcobeeSensor(name, 'occupancy', hass)) + dev.append(EcobeeSensor(name, 'occupancy')) add_devices(dev) @@ -62,10 +63,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class EcobeeSensor(Entity): """ An ecobee sensor. """ - def __init__(self, sensor_name, sensor_type, hass): + def __init__(self, sensor_name, sensor_type): self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0] self.sensor_name = sensor_name - self.hass = hass self.type = sensor_type self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @@ -85,6 +85,7 @@ class EcobeeSensor(Entity): return self._unit_of_measurement def update(self): + NETWORK.update() data = NETWORK.ecobee.sensors[self.sensor_name] if self.type == 'temperature': self._state = data['temp'] From 80e829f53a1482757be9671b3ac864e2ced0f5ab Mon Sep 17 00:00:00 2001 From: "nkgilley@gmail.com" Date: Mon, 23 Nov 2015 11:52:02 -0500 Subject: [PATCH 11/12] was getting errors for NETWORK being None. looked like it was being loaded too early, so this will wait until it's ready --- homeassistant/components/sensor/ecobee.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index 1ef40bcca89..524ac912405 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -48,7 +48,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return dev = list() - NETWORK.update() + while NETWORK is None: + continue for name, data in NETWORK.ecobee.sensors.items(): if 'temp' in data: dev.append(EcobeeSensor(name, 'temperature')) From 067b5862c01731df43093085bbee2a514ff96667 Mon Sep 17 00:00:00 2001 From: "nkgilley@gmail.com" Date: Tue, 24 Nov 2015 09:29:33 -0500 Subject: [PATCH 12/12] bug fixes --- homeassistant/components/ecobee.py | 13 ------------- homeassistant/components/sensor/ecobee.py | 10 ++++------ homeassistant/components/thermostat/ecobee.py | 4 ++-- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 8b73a9969ef..03f17133501 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -30,7 +30,6 @@ from homeassistant.loader import get_component from homeassistant import bootstrap from homeassistant.util import Throttle from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_API_KEY) from datetime import timedelta import logging @@ -153,16 +152,4 @@ def setup(hass, config): setup_ecobee(hass, NETWORK.ecobee, config) - def stop_ecobee(event): - """ Stop Ecobee. """ - - pass - - def start_ecobee(event): - """ Called when Home Assistant starts up. """ - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_ecobee) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_ecobee) - return True diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index 524ac912405..a6499949015 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -26,7 +26,7 @@ ecobee: """ from homeassistant.helpers.entity import Entity -from homeassistant.components.ecobee import NETWORK +from homeassistant.components import ecobee from homeassistant.const import TEMP_FAHRENHEIT import logging @@ -48,9 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return dev = list() - while NETWORK is None: - continue - for name, data in NETWORK.ecobee.sensors.items(): + for name, data in ecobee.NETWORK.ecobee.sensors.items(): if 'temp' in data: dev.append(EcobeeSensor(name, 'temperature')) if 'humidity' in data: @@ -86,8 +84,8 @@ class EcobeeSensor(Entity): return self._unit_of_measurement def update(self): - NETWORK.update() - data = NETWORK.ecobee.sensors[self.sensor_name] + ecobee.NETWORK.update() + data = ecobee.NETWORK.ecobee.sensors[self.sensor_name] if self.type == 'temperature': self._state = data['temp'] elif self.type == 'humidity': diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index 51d21cb9991..78f4d555c9c 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -28,7 +28,7 @@ ecobee: 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.ecobee import NETWORK +from homeassistant.components import ecobee import logging DEPENDENCIES = ['ecobee'] @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """ Setup Platform """ if discovery_info is None: return - data = NETWORK + data = ecobee.NETWORK hold_temp = discovery_info['hold_temp'] _LOGGER.info("Loading ecobee thermostat component with hold_temp set to " + str(hold_temp))