Refactor nest component into thermostat component

This commit is contained in:
Paulus Schoutsen 2015-01-06 00:10:33 -08:00
parent 68b712adfd
commit 8f3a3f89a7
5 changed files with 258 additions and 172 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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 {}

View File

@ -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