From 8f3a3f89a7cf6b76df54b71f355bba7ef46dae7f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Jan 2015 00:10:33 -0800 Subject: [PATCH] Refactor nest component into thermostat component --- config/home-assistant.conf.example | 3 +- homeassistant/components/nest.py | 167 ----------------- .../components/thermostat/__init__.py | 169 ++++++++++++++++++ homeassistant/components/thermostat/nest.py | 81 +++++++++ homeassistant/const.py | 10 +- 5 files changed, 258 insertions(+), 172 deletions(-) delete mode 100644 homeassistant/components/nest.py create mode 100644 homeassistant/components/thermostat/__init__.py create mode 100644 homeassistant/components/thermostat/nest.py diff --git a/config/home-assistant.conf.example b/config/home-assistant.conf.example index 8ca1ba2485d..83a92140e3d 100644 --- a/config/home-assistant.conf.example +++ b/config/home-assistant.conf.example @@ -33,7 +33,8 @@ platform=wemo # Optional: hard code the hosts (comma seperated) to avoid scanning the network # hosts=192.168.1.9,192.168.1.12 -[nest] +[thermostat] +platform=nest # Required: username and password that are used to login to the Nest thermostat. username=myemail@mydomain.com password=mypassword diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py deleted file mode 100644 index c35cfba14bc..00000000000 --- a/homeassistant/components/nest.py +++ /dev/null @@ -1,167 +0,0 @@ -""" -homeassistant.components.nest -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Provides functionality to interact with Nest Thermostats. -""" -import logging -import homeassistant.util as util -from homeassistant.helpers import validate_config, ToggleDevice -from homeassistant.const import (ATTR_ENTITY_PICTURE, ATTR_UNIT_OF_MEASUREMENT, - ATTR_FRIENDLY_NAME, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, - ATTR_NEW_TARGET_TEMPERATURE, SERVICE_SET_TARGET_TEMPERATURE) -from datetime import datetime, timedelta - -# The domain of your component. Should be equal to the name of your component -DOMAIN = "nest" -ENTITY_AWAY_NAME = "state away" -ENTITY_TEMP_INSIDE_ID = "nest_get.temperature_inside" -ENTITY_TEMP_TARGET_ID = "nest_get.temperature_target" -ENTITY_TEMP_TARGET_SET = "nest_set.temperature_target" - -ENTITY_AWAY_ID_FORMAT = DOMAIN + '.{}' - -# Configuration key for the entity id we are targeting -CONF_USERNAME = 'username' -CONF_PASSWORD = 'password' - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = [] - -def is_on(hass, entity_id=None): - - return hass.states.is_state(entity_id, STATE_ON) - - -def turn_on(hass, entity_id=None): - """ Turns all or specified switch on. """ - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -def turn_off(hass, entity_id=None): - """ Turns all or specified switch off. """ - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - -def set_temperature(hass, entity_id=None, new_temp=None): - """ Set new target temperature. """ - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - if new_temp: - data[ATTR_NEW_TARGET_TEMPERATURE] = new_temp - - hass.services.call(DOMAIN, SERVICE_SET_TARGET_TEMPERATURE, data) - -def setup(hass, config): - """ Setup NEST thermostat. """ - - # Validate that all required config options are given - if not validate_config(config, {DOMAIN: [CONF_USERNAME, CONF_PASSWORD]}, _LOGGER): - return False - - try: - import homeassistant.external.pynest.nest as pynest - except ImportError: - logging.getLogger(__name__).exception(( - "Failed to import pynest. ")) - return False - - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] - - thermostat = NestThermostat(pynest.Nest(username, password, None)) - thermostat.entity_id = ENTITY_AWAY_ID_FORMAT.format(util.slugify(ENTITY_AWAY_NAME)) - thermostat.nest.login() - - @util.Throttle(MIN_TIME_BETWEEN_SCANS) - def update_nest_state(now): - """ Update nest state. """ - - logging.getLogger(__name__).info("Update nest state") - - thermostat.nest.get_status() - thermostat.update_ha_state(hass) - - # Update state every 30 seconds - hass.track_time_change(update_nest_state, second=[0]) - update_nest_state(None) - - def handle_nest_service(service): - """ Handles calls to the nest services. """ - if service.service == SERVICE_TURN_ON: - thermostat.turn_on() - else: - thermostat.turn_off() - - thermostat.nest.get_status() - thermostat.update_ha_state(hass) - - hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_nest_service) - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_nest_service) - - def handle_nest_set_temperature(service): - if service.data[ATTR_NEW_TARGET_TEMPERATURE]: - new_temp = float(service.data[ATTR_NEW_TARGET_TEMPERATURE]) - thermostat.nest.set_temperature(new_temp) - thermostat.nest.get_status() - nest_temp(datetime.now()) - - hass.services.register(DOMAIN, SERVICE_SET_TARGET_TEMPERATURE, handle_nest_set_temperature) - - def nest_temp(time): - """ Method to get the current inside and target temperatures. """ - - #thermostat.nest.get_status() - current_temperature = thermostat.nest.get_curtemp() - target_temperature = thermostat.nest.get_tartemp() - - - hass.states.set(ENTITY_TEMP_INSIDE_ID, current_temperature, {ATTR_UNIT_OF_MEASUREMENT: thermostat.nest.get_units(), ATTR_ENTITY_PICTURE: - "https://cdn2.iconfinder.com/data/icons/windows-8-metro-ui-weather-report/512/Temperature.png"}) - - hass.states.set(ENTITY_TEMP_TARGET_ID, target_temperature, {ATTR_UNIT_OF_MEASUREMENT: thermostat.nest.get_units(), ATTR_ENTITY_PICTURE: - "http://d1hwvnnkb0v1bo.cloudfront.net/content/art/app/icons/target_icon.jpg"}) - - hass.track_time_change(nest_temp, second=[10]) - nest_temp(datetime.now()) - - # Tells the bootstrapper that the component was succesfully initialized - return True - -class NestThermostat(ToggleDevice): - - - def __init__(self, nest): - self.nest = nest - self.state_attr = {ATTR_FRIENDLY_NAME: ENTITY_AWAY_NAME, ATTR_ENTITY_PICTURE: - "http://support-assets.nest.com/images/tpzimages/app-energy-history-basic-away-icon.png"} - - def get_name(self): - """ Returns the name of the switch if any. """ - return ENTITY_AWAY_NAME - - def turn_on(self, **kwargs): - """ Turns away on. """ - self.nest.set_away("away") - - def turn_off(self): - """ Turns away off. """ - self.nest.set_away("here") - - def is_on(self): - """ True if away is on. """ - return self.nest.is_away() - - def get_state_attributes(self): - """ Returns optional state attributes. """ - return self.state_attr - - def set_temperature(self, temperature): - """ Set new target temperature """ - self.nest.set_temperature(temperature) - diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py new file mode 100644 index 00000000000..3f2b186a95d --- /dev/null +++ b/homeassistant/components/thermostat/__init__.py @@ -0,0 +1,169 @@ +""" +homeassistant.components.thermostat +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to interact with thermostats. +""" +import logging +from datetime import timedelta + +from homeassistant.helpers import ( + extract_entity_ids, platform_devices_from_config) +import homeassistant.util as util +from homeassistant.helpers import Device +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, + STATE_ON, STATE_OFF) + +DOMAIN = "thermostat" +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +DEPENDENCIES = [] + +SERVICE_TURN_AWAY_MODE_ON = "turn_away_mode_on" +SERVICE_TURN_AWAY_MODE_OFF = "turn_away_mode_off" +SERVICE_SET_TEMPERATURE = "set_temperature" + +ATTR_TARGET_TEMPERATURE = "target_temperature" +ATTR_AWAY_MODE = "away_mode" + +_LOGGER = logging.getLogger(__name__) + + +def turn_away_mode_on(hass, entity_id=None): + """ Turn all or specified thermostat away mode on. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + + hass.services.call(DOMAIN, SERVICE_TURN_AWAY_MODE_ON, data) + + +def turn_away_mode_off(hass, entity_id=None): + """ Turn all or specified thermostat away mode off. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + + hass.services.call(DOMAIN, SERVICE_TURN_AWAY_MODE_OFF, data) + + +def set_temperature(hass, temperature, entity_id=None): + """ Set new target temperature. """ + data = {ATTR_TEMPERATURE: temperature} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data) + + +def setup(hass, config): + """ Setup thermostats. """ + + thermostats = platform_devices_from_config( + config, DOMAIN, hass, ENTITY_ID_FORMAT, _LOGGER) + + if not thermostats: + return False + + # pylint: disable=unused-argument + @util.Throttle(MIN_TIME_BETWEEN_SCANS) + def update_state(now): + """ Update thermostat state. """ + logging.getLogger(__name__).info("Update nest state") + + for thermostat in thermostats.values(): + thermostat.update_ha_state(hass, True) + + # Update state every minute + hass.track_time_change(update_state, second=[0]) + update_state(None) + + def thermostat_service(service): + """ Handles calls to the services. """ + # Convert the entity ids to valid light ids + target_thermostats = [thermostats[entity_id] for entity_id + in extract_entity_ids(hass, service) + if entity_id in thermostats] + + if not target_thermostats: + target_thermostats = thermostats.values() + + if service.service == SERVICE_TURN_AWAY_MODE_ON: + for thermostat in target_thermostats: + thermostat.turn_away_mode_on() + + elif service.service == SERVICE_TURN_AWAY_MODE_OFF: + for thermostat in target_thermostats: + thermostat.turn_away_mode_off() + + elif service.service == SERVICE_SET_TEMPERATURE: + temperature = util.convert( + service.data.get(ATTR_TEMPERATURE), float) + + if temperature is None: + return + + for thermostat in target_thermostats: + thermostat.nest.set_temperature(temperature) + + for thermostat in target_thermostats: + thermostat.update_ha_state(hass, True) + + hass.services.register( + DOMAIN, SERVICE_TURN_AWAY_MODE_OFF, thermostat_service) + + hass.services.register( + DOMAIN, SERVICE_TURN_AWAY_MODE_ON, thermostat_service) + + hass.services.register( + DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service) + + return True + + +class ThermostatDevice(Device): + """ Represents a thermostat within Home Assistant. """ + + # pylint: disable=no-self-use + + def set_temperate(self, temperature): + """ Set new target temperature. """ + pass + + def turn_away_mode_on(self): + """ Turns away mode on. """ + pass + + def turn_away_mode_off(self): + """ Turns away mode off. """ + pass + + def is_away_mode_on(self): + """ Returns if away mode is on. """ + return False + + def get_target_temperature(self): + """ Returns the temperature we try to reach. """ + return None + + def get_unit_of_measurement(self): + """ Returns the unit of measurement. """ + return "" + + def get_device_state_attributes(self): + """ Returns device specific state attributes. """ + return {} + + def get_state_attributes(self): + """ Returns optional state attributes. """ + data = { + ATTR_UNIT_OF_MEASUREMENT: self.get_unit_of_measurement(), + ATTR_AWAY_MODE: STATE_ON if self.is_away_mode_on() else STATE_OFF + } + + target_temp = self.get_target_temperature() + + if target_temp is not None: + data[ATTR_TARGET_TEMPERATURE] = target_temp + + return data diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py new file mode 100644 index 00000000000..2357053d3d0 --- /dev/null +++ b/homeassistant/components/thermostat/nest.py @@ -0,0 +1,81 @@ +""" +Adds support for Nest thermostats. +""" +import logging + +from homeassistant.components.thermostat import ThermostatDevice +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS, TEMP_FAHRENHEIT) + + +# pylint: disable=unused-argument +def get_devices(hass, config): + """ Gets Nest thermostats. """ + logger = logging.getLogger(__name__) + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + if username is None or password is None: + logger.error("Missing required configuration items %s or %s", + CONF_USERNAME, CONF_PASSWORD) + return [] + + try: + # pylint: disable=no-name-in-module, unused-variable + import homeassistant.external.pynest.nest as pynest # noqa + except ImportError: + logger.exception("Error while importing dependency phue.") + + return [] + + thermostat = NestThermostat(username, password) + + return [thermostat] + + +class NestThermostat(ThermostatDevice): + """ Represents a Nest thermostat within Home Assistant. """ + + def __init__(self, username, password): + # pylint: disable=no-name-in-module, import-error + import homeassistant.external.pynest.nest as pynest + + self.nest = pynest.Nest(username, password, None) + self.nest.login() + + def get_name(self): + """ Returns the name of the nest, if any. """ + return "Nest" # TODO Possible to get actual name from Nest device? + + def get_state(self): + """ Returns the current temperature. """ + return self.nest.get_curtemp() + + def set_temperature(self, temperature): + """ Set new target temperature """ + self.nest.set_temperature(temperature) + + def turn_away_mode_on(self): + """ Turns away on. """ + self.nest.set_away("away") + + def turn_away_mode_off(self): + """ Turns away off. """ + self.nest.set_away("here") + + def is_away_mode_on(self): + """ Returns if away mode is on. """ + return self.nest.is_away() + + def get_target_temperature(self): + """ Returns the temperature we try to reach. """ + return self.nest.get_tartemp() + + def get_unit_of_measurement(self): + """ Returns the unit of measurement. """ + return TEMP_FAHRENHEIT if self.nest.units == 'F' else TEMP_CELCIUS + + def get_device_state_attributes(self): + """ Returns device specific state attributes. """ + return {} diff --git a/homeassistant/const.py b/homeassistant/const.py index a7dae8a2886..c5f897fb63e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -53,8 +53,12 @@ ATTR_ENTITY_PICTURE = "entity_picture" # The unit of measurement if applicable ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement" -# New target temperature for thermostats -ATTR_NEW_TARGET_TEMPERATURE = "temp" +# Temperature attribute +ATTR_TEMPERATURE = "temperature" + +# #### MISC #### +TEMP_CELCIUS = "°C" +TEMP_FAHRENHEIT = "°F" # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" @@ -71,8 +75,6 @@ SERVICE_MEDIA_PAUSE = "media_pause" SERVICE_MEDIA_NEXT_TRACK = "media_next_track" SERVICE_MEDIA_PREV_TRACK = "media_prev_track" -SERVICE_SET_TARGET_TEMPERATURE = "set_target_temperature" - # #### API / REMOTE #### SERVER_PORT = 8123