Migrate Thermostat and HVAC component to climate component (#2825)

* First draft for climate

* Updates for thermostats
This commit is contained in:
John Arild Berentsen 2016-08-19 09:17:28 +02:00 committed by Paulus Schoutsen
parent 0abc50e844
commit ada4de3ffb
25 changed files with 3621 additions and 17 deletions

View file

@ -109,6 +109,12 @@ omit =
homeassistant/components/camera/generic.py homeassistant/components/camera/generic.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/rpi_camera.py
homeassistant/components/climate/eq3btsmart.py
homeassistant/components/climate/heatmiser.py
homeassistant/components/climate/homematic.py
homeassistant/components/climate/knx.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/asuswrt.py

View file

@ -0,0 +1,535 @@
"""
Provides functionality to interact with climate devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/climate/
"""
import logging
import os
from numbers import Number
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.config import load_yaml_config_file
import homeassistant.util as util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELSIUS)
DOMAIN = "climate"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCAN_INTERVAL = 60
SERVICE_SET_AWAY_MODE = "set_away_mode"
SERVICE_SET_AUX_HEAT = "set_aux_heat"
SERVICE_SET_TEMPERATURE = "set_temperature"
SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_OPERATION_MODE = "set_operation_mode"
SERVICE_SET_SWING_MODE = "set_swing_mode"
SERVICE_SET_HUMIDITY = "set_humidity"
STATE_HEAT = "heat"
STATE_COOL = "cool"
STATE_IDLE = "idle"
STATE_AUTO = "auto"
STATE_DRY = "dry"
STATE_FAN_ONLY = "fan_only"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_MAX_TEMP = "max_temp"
ATTR_MIN_TEMP = "min_temp"
ATTR_AWAY_MODE = "away_mode"
ATTR_AUX_HEAT = "aux_heat"
ATTR_FAN_MODE = "fan_mode"
ATTR_FAN_LIST = "fan_list"
ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity"
ATTR_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity"
ATTR_OPERATION_MODE = "operation_mode"
ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_LIST = "swing_list"
_LOGGER = logging.getLogger(__name__)
SET_AWAY_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AWAY_MODE): cv.boolean,
})
SET_AUX_HEAT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AUX_HEAT): cv.boolean,
})
SET_TEMPERATURE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_TEMPERATURE): vol.Coerce(float),
})
SET_FAN_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN_MODE): cv.string,
})
SET_OPERATION_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_OPERATION_MODE): cv.string,
})
SET_HUMIDITY_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_HUMIDITY): vol.Coerce(float),
})
SET_SWING_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_SWING_MODE): cv.string,
})
def set_away_mode(hass, away_mode, entity_id=None):
"""Turn all or specified climate devices away mode on."""
data = {
ATTR_AWAY_MODE: away_mode
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
def set_aux_heat(hass, aux_heat, entity_id=None):
"""Turn all or specified climate devices auxillary heater on."""
data = {
ATTR_AUX_HEAT: aux_heat
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, 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 set_humidity(hass, humidity, entity_id=None):
"""Set new target humidity."""
data = {ATTR_HUMIDITY: humidity}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data)
def set_fan_mode(hass, fan, entity_id=None):
"""Set all or specified climate devices fan mode on."""
data = {ATTR_FAN_MODE: fan}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
def set_operation_mode(hass, operation_mode, entity_id=None):
"""Set new target operation mode."""
data = {ATTR_OPERATION_MODE: operation_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)
def set_swing_mode(hass, swing_mode, entity_id=None):
"""Set new target swing mode."""
data = {ATTR_SWING_MODE: swing_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
# pylint: disable=too-many-branches
def setup(hass, config):
"""Setup climate devices."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def away_mode_set_service(service):
"""Set away mode on target climate devices."""
target_climate = component.extract_from_service(service)
away_mode = service.data.get(ATTR_AWAY_MODE)
if away_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE)
return
for climate in target_climate:
if away_mode:
climate.turn_away_mode_on()
else:
climate.turn_away_mode_off()
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service,
descriptions.get(SERVICE_SET_AWAY_MODE),
schema=SET_AWAY_MODE_SCHEMA)
def aux_heat_set_service(service):
"""Set auxillary heater on target climate devices."""
target_climate = component.extract_from_service(service)
aux_heat = service.data.get(ATTR_AUX_HEAT)
if aux_heat is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT)
return
for climate in target_climate:
if aux_heat:
climate.turn_aux_heat_on()
else:
climate.turn_aux_heat_off()
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service,
descriptions.get(SERVICE_SET_AUX_HEAT),
schema=SET_AUX_HEAT_SCHEMA)
def temperature_set_service(service):
"""Set temperature on the target climate devices."""
target_climate = component.extract_from_service(service)
temperature = util.convert(
service.data.get(ATTR_TEMPERATURE), float)
if temperature is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE)
return
for climate in target_climate:
climate.set_temperature(convert_temperature(
temperature, hass.config.units.temperature_unit,
climate.unit_of_measurement))
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service,
descriptions.get(SERVICE_SET_TEMPERATURE),
schema=SET_TEMPERATURE_SCHEMA)
def humidity_set_service(service):
"""Set humidity on the target climate devices."""
target_climate = component.extract_from_service(service)
humidity = service.data.get(ATTR_HUMIDITY)
if humidity is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_HUMIDITY, ATTR_HUMIDITY)
return
for climate in target_climate:
climate.set_humidity(humidity)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service,
descriptions.get(SERVICE_SET_HUMIDITY),
schema=SET_HUMIDITY_SCHEMA)
def fan_mode_set_service(service):
"""Set fan mode on target climate devices."""
target_climate = component.extract_from_service(service)
fan = service.data.get(ATTR_FAN_MODE)
if fan is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_FAN_MODE, ATTR_FAN_MODE)
return
for climate in target_climate:
climate.set_fan_mode(fan)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service,
descriptions.get(SERVICE_SET_FAN_MODE),
schema=SET_FAN_MODE_SCHEMA)
def operation_set_service(service):
"""Set operating mode on the target climate devices."""
target_climate = component.extract_from_service(service)
operation_mode = service.data.get(ATTR_OPERATION_MODE)
if operation_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE)
return
for climate in target_climate:
climate.set_operation_mode(operation_mode)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service,
descriptions.get(SERVICE_SET_OPERATION_MODE),
schema=SET_OPERATION_MODE_SCHEMA)
def swing_set_service(service):
"""Set swing mode on the target climate devices."""
target_climate = component.extract_from_service(service)
swing_mode = service.data.get(ATTR_SWING_MODE)
if swing_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_SWING_MODE, ATTR_SWING_MODE)
return
for climate in target_climate:
climate.set_swing_mode(swing_mode)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
descriptions.get(SERVICE_SET_SWING_MODE),
schema=SET_SWING_MODE_SCHEMA)
return True
class ClimateDevice(Entity):
"""Representation of a climate device."""
# pylint: disable=too-many-public-methods,no-self-use
@property
def state(self):
"""Return the current state."""
return self.current_operation or STATE_UNKNOWN
@property
def state_attributes(self):
"""Return the optional state attributes."""
data = {
ATTR_CURRENT_TEMPERATURE:
self._convert_for_display(self.current_temperature),
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp),
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
ATTR_TEMPERATURE:
self._convert_for_display(self.target_temperature),
}
humidity = self.target_humidity
if humidity is not None:
data[ATTR_HUMIDITY] = humidity
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
data[ATTR_MIN_HUMIDITY] = self.min_humidity
data[ATTR_MAX_HUMIDITY] = self.max_humidity
fan_mode = self.current_fan_mode
if fan_mode is not None:
data[ATTR_FAN_MODE] = fan_mode
data[ATTR_FAN_LIST] = self.fan_list
operation_mode = self.current_operation
if operation_mode is not None:
data[ATTR_OPERATION_MODE] = operation_mode
data[ATTR_OPERATION_LIST] = self.operation_list
swing_mode = self.current_swing_mode
if swing_mode is not None:
data[ATTR_SWING_MODE] = swing_mode
data[ATTR_SWING_LIST] = self.swing_list
is_away = self.is_away_mode_on
if is_away is not None:
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
is_aux_heat = self.is_aux_heat_on
if is_aux_heat is not None:
data[ATTR_AUX_HEAT] = STATE_ON if is_aux_heat else STATE_OFF
return data
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
raise NotImplementedError
@property
def current_humidity(self):
"""Return the current humidity."""
return None
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return None
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return None
@property
def operation_list(self):
"""List of available operation modes."""
return None
@property
def current_temperature(self):
"""Return the current temperature."""
return None
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return None
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return None
@property
def is_aux_heat_on(self):
"""Return true if aux heater."""
return None
@property
def current_fan_mode(self):
"""Return the fan setting."""
return None
@property
def fan_list(self):
"""List of available fan modes."""
return None
@property
def current_swing_mode(self):
"""Return the fan setting."""
return None
@property
def swing_list(self):
"""List of available swing modes."""
return None
def set_temperature(self, temperature):
"""Set new target temperature."""
raise NotImplementedError()
def set_humidity(self, humidity):
"""Set new target humidity."""
raise NotImplementedError()
def set_fan_mode(self, fan):
"""Set new target fan mode."""
raise NotImplementedError()
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
raise NotImplementedError()
def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
raise NotImplementedError()
def turn_away_mode_on(self):
"""Turn away mode on."""
raise NotImplementedError()
def turn_away_mode_off(self):
"""Turn away mode off."""
raise NotImplementedError()
def turn_aux_heat_on(self):
"""Turn auxillary heater on."""
raise NotImplementedError()
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
raise NotImplementedError()
@property
def min_temp(self):
"""Return the minimum temperature."""
return convert_temperature(7, TEMP_CELSIUS, self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature."""
return convert_temperature(35, TEMP_CELSIUS, self.unit_of_measurement)
@property
def min_humidity(self):
"""Return the minimum humidity."""
return 30
@property
def max_humidity(self):
"""Return the maximum humidity."""
return 99
def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes."""
if temp is None or not isinstance(temp, Number):
return temp
value = convert_temperature(temp, self.unit_of_measurement,
self.hass.config.units.temperature_unit)
if self.hass.config.units.temperature_unit is TEMP_CELSIUS:
decimal_count = 1
else:
# Users of fahrenheit generally expect integer units.
decimal_count = 0
return round(value, decimal_count)

View file

@ -0,0 +1,164 @@
"""
Demo platform that offers a fake climate device.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo climate devices."""
add_devices([
DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low",
None, None, "Auto", "Heat", None),
DemoClimate("Hvac", 21, TEMP_CELSIUS, True, 22, "On High",
67, 54, "Off", "Cool", False),
])
# pylint: disable=too-many-arguments, too-many-public-methods
class DemoClimate(ClimateDevice):
"""Representation of a demo climate device."""
# pylint: disable=too-many-instance-attributes
def __init__(self, name, target_temperature, unit_of_measurement,
away, current_temperature, current_fan_mode,
target_humidity, current_humidity, current_swing_mode,
current_operation, aux):
"""Initialize the climate device."""
self._name = name
self._target_temperature = target_temperature
self._target_humidity = target_humidity
self._unit_of_measurement = unit_of_measurement
self._away = away
self._current_temperature = current_temperature
self._current_humidity = current_humidity
self._current_fan_mode = current_fan_mode
self._current_operation = current_operation
self._aux = aux
self._current_swing_mode = current_swing_mode
self._fan_list = ["On Low", "On High", "Auto Low", "Auto High", "Off"]
self._operation_list = ["Heat", "Cool", "Auto Changeover", "Off"]
self._swing_list = ["Auto", 1, 2, 3, "Off"]
@property
def should_poll(self):
"""Polling not needed for a demo climate device."""
return False
@property
def name(self):
"""Return the name of the climate device."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def current_humidity(self):
"""Return the current humidity."""
return self._current_humidity
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return self._target_humidity
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._current_operation
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self._away
@property
def is_aux_heat_on(self):
"""Return true if away mode is on."""
return self._aux
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self._current_fan_mode
@property
def fan_list(self):
"""List of available fan modes."""
return self._fan_list
def set_temperature(self, temperature):
"""Set new target temperature."""
self._target_temperature = temperature
self.update_ha_state()
def set_humidity(self, humidity):
"""Set new target temperature."""
self._target_humidity = humidity
self.update_ha_state()
def set_swing_mode(self, swing_mode):
"""Set new target temperature."""
self._current_swing_mode = swing_mode
self.update_ha_state()
def set_fan_mode(self, fan):
"""Set new target temperature."""
self._current_fan_mode = fan
self.update_ha_state()
def set_operation_mode(self, operation_mode):
"""Set new target temperature."""
self._current_operation = operation_mode
self.update_ha_state()
@property
def current_swing_mode(self):
"""Return the swing setting."""
return self._current_swing_mode
@property
def swing_list(self):
"""List of available swing modes."""
return self._swing_list
def turn_away_mode_on(self):
"""Turn away mode on."""
self._away = True
self.update_ha_state()
def turn_away_mode_off(self):
"""Turn away mode off."""
self._away = False
self.update_ha_state()
def turn_aux_heat_on(self):
"""Turn away auxillary heater on."""
self._aux = True
self.update_ha_state()
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
self._aux = False
self.update_ha_state()

View file

@ -0,0 +1,247 @@
"""
Platform for Ecobee Thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.ecobee/
"""
import logging
from os import path
import voluptuous as vol
from homeassistant.components import ecobee
from homeassistant.components.climate import (
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['ecobee']
_LOGGER = logging.getLogger(__name__)
ECOBEE_CONFIG_FILE = 'ecobee.conf'
_CONFIGURING = {}
ATTR_FAN_MIN_ON_TIME = "fan_min_on_time"
SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time"
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Ecobee Thermostat 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 %s",
hold_temp)
devices = [Thermostat(data, index, hold_temp)
for index in range(len(data.ecobee.thermostats))]
add_devices(devices)
def fan_min_on_time_set_service(service):
"""Set the minimum fan on time on the target thermostats."""
entity_id = service.data.get('entity_id')
if entity_id:
target_thermostats = [device for device in devices
if device.entity_id == entity_id]
else:
target_thermostats = devices
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
for thermostat in target_thermostats:
thermostat.set_fan_min_on_time(str(fan_min_on_time))
thermostat.update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service,
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
# pylint: disable=too-many-public-methods, abstract-method
class Thermostat(ClimateDevice):
"""A thermostat class for Ecobee."""
def __init__(self, data, thermostat_index, hold_temp):
"""Initialize the thermostat."""
self.data = data
self.thermostat_index = thermostat_index
self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index)
self._name = self.thermostat['name']
self.hold_temp = hold_temp
def update(self):
"""Get the latest state from the thermostat."""
self.data.update()
self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index)
@property
def name(self):
"""Return the name of the Ecobee Thermostat."""
return self.thermostat['name']
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
"""Return the current temperature."""
return self.thermostat['runtime']['actualTemperature'] / 10
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if (self.operation_mode == 'heat' or
self.operation_mode == 'auxHeatOnly'):
return self.target_temperature_low
elif self.operation_mode == 'cool':
return self.target_temperature_high
else:
return (self.target_temperature_low +
self.target_temperature_high) / 2
@property
def target_temperature_low(self):
"""Return the lower bound temperature we try to reach."""
return int(self.thermostat['runtime']['desiredHeat'] / 10)
@property
def target_temperature_high(self):
"""Return the upper bound temperature we try to reach."""
return int(self.thermostat['runtime']['desiredCool'] / 10)
@property
def current_humidity(self):
"""Return the current humidity."""
return self.thermostat['runtime']['actualHumidity']
@property
def desired_fan_mode(self):
"""Return the desired fan mode of operation."""
return self.thermostat['runtime']['desiredFanMode']
@property
def fan(self):
"""Return the current fan state."""
if 'fan' in self.thermostat['equipmentStatus']:
return STATE_ON
else:
return STATE_OFF
@property
def operation_mode(self):
"""Return 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):
"""Return current mode ie. home, away, sleep."""
return self.thermostat['program']['currentClimateRef']
@property
def current_operation(self):
"""Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off."""
return self.thermostat['settings']['hvacMode']
@property
def fan_min_on_time(self):
"""Return current fan minimum on time."""
return self.thermostat['settings']['fanMinOnTime']
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
# Move these to Thermostat Device and make them global
return {
"humidity": self.current_humidity,
"fan": self.fan,
"mode": self.mode,
"operation_mode": self.current_operation,
"fan_min_on_time": self.fan_min_on_time
}
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
mode = self.mode
events = self.thermostat['events']
for event in events:
if event['running']:
mode = event['holdClimateRef']
break
return 'away' in mode
def turn_away_mode_on(self):
"""Turn away on."""
if self.hold_temp:
self.data.ecobee.set_climate_hold(self.thermostat_index,
"away", "indefinite")
else:
self.data.ecobee.set_climate_hold(self.thermostat_index, "away")
def turn_away_mode_off(self):
"""Turn away off."""
self.data.ecobee.resume_program(self.thermostat_index)
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(self.thermostat_index, low_temp,
high_temp, "indefinite")
else:
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
high_temp)
def set_operation_mode(self, operation_mode):
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode)
def set_fan_min_on_time(self, fan_min_on_time):
"""Set the minimum fan on time."""
self.data.ecobee.set_fan_min_on_time(self.thermostat_index,
fan_min_on_time)
# Home and Sleep mode aren't used in UI yet:
# def turn_home_mode_on(self):
# """ Turns home mode on. """
# self.data.ecobee.set_climate_hold(self.thermostat_index, "home")
# def turn_home_mode_off(self):
# """ Turns home mode off. """
# self.data.ecobee.resume_program(self.thermostat_index)
# def turn_sleep_mode_on(self):
# """ Turns sleep mode on. """
# self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep")
# def turn_sleep_mode_off(self):
# """ Turns sleep mode off. """
# self.data.ecobee.resume_program(self.thermostat_index)

View file

@ -0,0 +1,90 @@
"""
Support for eq3 Bluetooth Smart thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.eq3btsmart/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS
from homeassistant.util.temperature import convert
REQUIREMENTS = ['bluepy_devices==0.2.0']
CONF_MAC = 'mac'
CONF_DEVICES = 'devices'
CONF_ID = 'id'
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the eq3 BLE thermostats."""
devices = []
for name, device_cfg in config[CONF_DEVICES].items():
mac = device_cfg[CONF_MAC]
devices.append(EQ3BTSmartThermostat(mac, name))
add_devices(devices)
return True
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
class EQ3BTSmartThermostat(ClimateDevice):
"""Representation of a EQ3 Bluetooth Smart thermostat."""
def __init__(self, _mac, _name):
"""Initialize the thermostat."""
from bluepy_devices.devices import eq3btsmart
self._name = _name
self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement that is used."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Can not report temperature, so return target_temperature."""
return self.target_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._thermostat.target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
self._thermostat.target_temperature = temperature
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {"mode": self._thermostat.mode,
"mode_readable": self._thermostat.mode_readable}
@property
def min_temp(self):
"""Return the minimum temperature."""
return convert(self._thermostat.min_temp, TEMP_CELSIUS,
self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature."""
return convert(self._thermostat.max_temp, TEMP_CELSIUS,
self.unit_of_measurement)
def update(self):
"""Update the data from the thermostat."""
self._thermostat.update()

View file

@ -0,0 +1,216 @@
"""
Adds support for generic thermostat units.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.generic_thermostat/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components import switch
from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF
from homeassistant.helpers import condition
from homeassistant.helpers.event import track_state_change
DEPENDENCIES = ['switch', 'sensor']
TOL_TEMP = 0.3
CONF_NAME = 'name'
DEFAULT_NAME = 'Generic Thermostat'
CONF_HEATER = 'heater'
CONF_SENSOR = 'target_sensor'
CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp'
CONF_TARGET_TEMP = 'target_temp'
CONF_AC_MODE = 'ac_mode'
CONF_MIN_DUR = 'min_cycle_duration'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required("platform"): "generic_thermostat",
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HEATER): cv.entity_id,
vol.Required(CONF_SENSOR): cv.entity_id,
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
vol.Optional(CONF_AC_MODE): vol.Coerce(bool),
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the generic thermostat."""
name = config.get(CONF_NAME)
heater_entity_id = config.get(CONF_HEATER)
sensor_entity_id = config.get(CONF_SENSOR)
min_temp = config.get(CONF_MIN_TEMP)
max_temp = config.get(CONF_MAX_TEMP)
target_temp = config.get(CONF_TARGET_TEMP)
ac_mode = config.get(CONF_AC_MODE)
min_cycle_duration = config.get(CONF_MIN_DUR)
add_devices([GenericThermostat(hass, name, heater_entity_id,
sensor_entity_id, min_temp,
max_temp, target_temp, ac_mode,
min_cycle_duration)])
# pylint: disable=too-many-instance-attributes, abstract-method
class GenericThermostat(ClimateDevice):
"""Representation of a GenericThermostat device."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
"""Initialize the thermostat."""
self.hass = hass
self._name = name
self.heater_entity_id = heater_entity_id
self.ac_mode = ac_mode
self.min_cycle_duration = min_cycle_duration
self._active = False
self._cur_temp = None
self._min_temp = min_temp
self._max_temp = max_temp
self._target_temp = target_temp
self._unit = hass.config.units.temperature_unit
track_state_change(hass, sensor_entity_id, self._sensor_changed)
sensor_state = hass.states.get(sensor_entity_id)
if sensor_state:
self._update_temp(sensor_state)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the thermostat."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit
@property
def current_temperature(self):
"""Return the sensor temperature."""
return self._cur_temp
@property
def operation(self):
"""Return current operation ie. heat, cool, idle."""
if self.ac_mode:
cooling = self._active and self._is_device_active
return STATE_COOL if cooling else STATE_IDLE
else:
heating = self._active and self._is_device_active
return STATE_HEAT if heating else STATE_IDLE
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temp
def set_temperature(self, temperature):
"""Set new target temperature."""
self._target_temp = temperature
self._control_heating()
self.update_ha_state()
@property
def min_temp(self):
"""Return the minimum temperature."""
# pylint: disable=no-member
if self._min_temp:
return self._min_temp
else:
# get default temp from super class
return ClimateDevice.min_temp.fget(self)
@property
def max_temp(self):
"""Return the maximum temperature."""
# pylint: disable=no-member
if self._min_temp:
return self._max_temp
else:
# Get default temp from super class
return ClimateDevice.max_temp.fget(self)
def _sensor_changed(self, entity_id, old_state, new_state):
"""Called when temperature changes."""
if new_state is None:
return
self._update_temp(new_state)
self._control_heating()
self.update_ha_state()
def _update_temp(self, state):
"""Update thermostat with latest state from sensor."""
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
try:
self._cur_temp = self.hass.config.units.temperature(
float(state.state), unit)
except ValueError as ex:
_LOGGER.error('Unable to update from sensor: %s', ex)
def _control_heating(self):
"""Check if we need to turn heating on or off."""
if not self._active and None not in (self._cur_temp,
self._target_temp):
self._active = True
_LOGGER.info('Obtained current and target temperature. '
'Generic thermostat active.')
if not self._active:
return
if self.min_cycle_duration:
if self._is_device_active:
current_state = STATE_ON
else:
current_state = STATE_OFF
long_enough = condition.state(self.hass, self.heater_entity_id,
current_state,
self.min_cycle_duration)
if not long_enough:
return
if self.ac_mode:
too_hot = self._cur_temp - self._target_temp > TOL_TEMP
is_cooling = self._is_device_active
if too_hot and not is_cooling:
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id)
elif not too_hot and is_cooling:
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id)
else:
too_cold = self._target_temp - self._cur_temp > TOL_TEMP
is_heating = self._is_device_active
if too_cold and not is_heating:
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id)
elif not too_cold and is_heating:
_LOGGER.info('Turning off heater %s', self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id)
@property
def _is_device_active(self):
"""If the toggleable device is currently active."""
return switch.is_on(self.hass, self.heater_entity_id)

View file

@ -0,0 +1,114 @@
"""
Support for the PRT Heatmiser themostats using the V3 protocol.
See https://github.com/andylockran/heatmiserV3 for more info on the
heatmiserV3 module dependency.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.heatmiser/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS
CONF_IPADDRESS = 'ipaddress'
CONF_PORT = 'port'
CONF_TSTATS = 'tstats'
REQUIREMENTS = ["heatmiserV3==0.9.1"]
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the heatmiser thermostat."""
from heatmiserV3 import heatmiser, connection
ipaddress = str(config[CONF_IPADDRESS])
port = str(config[CONF_PORT])
if ipaddress is None or port is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_IPADDRESS, CONF_PORT)
return False
serport = connection.connection(ipaddress, port)
serport.open()
tstats = []
if CONF_TSTATS in config:
tstats = config[CONF_TSTATS]
if tstats is None:
_LOGGER.error("No thermostats configured.")
return False
for tstat in tstats:
add_devices([
HeatmiserV3Thermostat(
heatmiser,
tstat.get("id"),
tstat.get("name"),
serport)
])
return
class HeatmiserV3Thermostat(ClimateDevice):
"""Representation of a HeatmiserV3 thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, heatmiser, device, name, serport):
"""Initialize the thermostat."""
self.heatmiser = heatmiser
self.device = device
self.serport = serport
self._current_temperature = None
self._name = name
self._id = device
self.dcb = None
self.update()
self._target_temperature = int(self.dcb.get("roomset"))
@property
def name(self):
"""Return the name of the thermostat, if any."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
if self.dcb is not None:
low = self.dcb.get("floortemplow ")
high = self.dcb.get("floortemphigh")
temp = (high*256 + low)/10.0
self._current_temperature = temp
else:
self._current_temperature = None
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
temperature = int(temperature)
self.heatmiser.hmSendAddress(
self._id,
18,
temperature,
1,
self.serport)
self._target_temperature = int(temperature)
def update(self):
"""Get the latest data."""
self.dcb = self.heatmiser.hmReadAddress(self._id, 'prt', self.serport)

View file

@ -0,0 +1,90 @@
"""
Support for Homematic thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homematic/
"""
import logging
import homeassistant.components.homematic as homematic
from homeassistant.components.climate import ClimateDevice
from homeassistant.util.temperature import convert
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN
DEPENDENCIES = ['homematic']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the Homematic thermostat platform."""
if discovery_info is None:
return
return homematic.setup_hmdevice_discovery_helper(HMThermostat,
discovery_info,
add_callback_devices)
# pylint: disable=abstract-method
class HMThermostat(homematic.HMDevice, ClimateDevice):
"""Representation of a Homematic thermostat."""
@property
def unit_of_measurement(self):
"""Return the unit of measurement that is used."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
if not self.available:
return None
return self._data["ACTUAL_TEMPERATURE"]
@property
def target_temperature(self):
"""Return the target temperature."""
if not self.available:
return None
return self._data["SET_TEMPERATURE"]
def set_temperature(self, temperature):
"""Set new target temperature."""
if not self.available:
return None
self._hmdevice.set_temperature(temperature)
@property
def min_temp(self):
"""Return the minimum temperature - 4.5 means off."""
return convert(4.5, TEMP_CELSIUS, self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature - 30.5 means on."""
return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement)
def _check_hm_to_ha_object(self):
"""Check if possible to use the Homematic object as this HA type."""
from pyhomematic.devicetypes.thermostats import HMThermostat\
as pyHMThermostat
# Check compatibility from HMDevice
if not super()._check_hm_to_ha_object():
return False
# Check if the Homematic device correct for this HA device
if isinstance(self._hmdevice, pyHMThermostat):
return True
_LOGGER.critical("This %s can't be use as thermostat", self._name)
return False
def _init_data_struct(self):
"""Generate a data dict (self._data) from the Homematic metadata."""
super()._init_data_struct()
# Add state to data dict
self._data.update({"CONTROL_MODE": STATE_UNKNOWN,
"SET_TEMPERATURE": STATE_UNKNOWN,
"ACTUAL_TEMPERATURE": STATE_UNKNOWN})

View file

@ -0,0 +1,266 @@
"""
Support for Honeywell Round Connected and Honeywell Evohome thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.honeywell/
"""
import logging
import socket
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['evohomeclient==0.2.5',
'somecomfort==0.2.1']
_LOGGER = logging.getLogger(__name__)
CONF_AWAY_TEMP = "away_temperature"
DEFAULT_AWAY_TEMP = 16
def _setup_round(username, password, config, add_devices):
"""Setup rounding function."""
from evohomeclient import EvohomeClient
try:
away_temp = float(config.get(CONF_AWAY_TEMP, DEFAULT_AWAY_TEMP))
except ValueError:
_LOGGER.error("value entered for item %s should convert to a number",
CONF_AWAY_TEMP)
return False
evo_api = EvohomeClient(username, password)
try:
zones = evo_api.temperatures(force_refresh=True)
for i, zone in enumerate(zones):
add_devices([RoundThermostat(evo_api,
zone['id'],
i == 0,
away_temp)])
except socket.error:
_LOGGER.error(
"Connection error logging into the honeywell evohome web service"
)
return False
return True
# config will be used later
def _setup_us(username, password, config, add_devices):
"""Setup user."""
import somecomfort
try:
client = somecomfort.SomeComfort(username, password)
except somecomfort.AuthError:
_LOGGER.error('Failed to login to honeywell account %s', username)
return False
except somecomfort.SomeComfortError as ex:
_LOGGER.error('Failed to initialize honeywell client: %s', str(ex))
return False
dev_id = config.get('thermostat')
loc_id = config.get('location')
add_devices([HoneywellUSThermostat(client, device)
for location in client.locations_by_id.values()
for device in location.devices_by_id.values()
if ((not loc_id or location.locationid == loc_id) and
(not dev_id or device.deviceid == dev_id))])
return True
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the honeywel thermostat."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
region = config.get('region', 'eu').lower()
if username is None or password is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_USERNAME, CONF_PASSWORD)
return False
if region not in ('us', 'eu'):
_LOGGER.error('Region `%s` is invalid (use either us or eu)', region)
return False
if region == 'us':
return _setup_us(username, password, config, add_devices)
else:
return _setup_round(username, password, config, add_devices)
class RoundThermostat(ClimateDevice):
"""Representation of a Honeywell Round Connected thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, device, zone_id, master, away_temp):
"""Initialize the thermostat."""
self.device = device
self._current_temperature = None
self._target_temperature = None
self._name = "round connected"
self._id = zone_id
self._master = master
self._is_dhw = False
self._away_temp = away_temp
self._away = False
self.update()
@property
def name(self):
"""Return the name of the honeywell, if any."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self._is_dhw:
return None
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
self.device.set_temperature(self._name, temperature)
@property
def current_operation(self: ClimateDevice) -> str:
"""Get the current operation of the system."""
return getattr(self.device, 'system_mode', None)
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
"""Set the HVAC mode for the thermostat."""
if hasattr(self.device, 'system_mode'):
self.device.system_mode = operation_mode
def turn_away_mode_on(self):
"""Turn away on.
Evohome does have a proprietary away mode, but it doesn't really work
the way it should. For example: If you set a temperature manually
it doesn't get overwritten when away mode is switched on.
"""
self._away = True
self.device.set_temperature(self._name, self._away_temp)
def turn_away_mode_off(self):
"""Turn away off."""
self._away = False
self.device.cancel_temp_override(self._name)
def update(self):
"""Get the latest date."""
try:
# Only refresh if this is the "master" device,
# others will pick up the cache
for val in self.device.temperatures(force_refresh=self._master):
if val['id'] == self._id:
data = val
except StopIteration:
_LOGGER.error("Did not receive any temperature data from the "
"evohomeclient API.")
return
self._current_temperature = data['temp']
self._target_temperature = data['setpoint']
if data['thermostat'] == "DOMESTIC_HOT_WATER":
self._name = "Hot Water"
self._is_dhw = True
else:
self._name = data['name']
self._is_dhw = False
# pylint: disable=abstract-method
class HoneywellUSThermostat(ClimateDevice):
"""Representation of a Honeywell US Thermostat."""
def __init__(self, client, device):
"""Initialize the thermostat."""
self._client = client
self._device = device
@property
def is_fan_on(self):
"""Return true if fan is on."""
return self._device.fan_running
@property
def name(self):
"""Return the name of the honeywell, if any."""
return self._device.name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return (TEMP_CELSIUS if self._device.temperature_unit == 'C'
else TEMP_FAHRENHEIT)
@property
def current_temperature(self):
"""Return the current temperature."""
self._device.refresh()
return self._device.current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self._device.system_mode == 'cool':
return self._device.setpoint_cool
else:
return self._device.setpoint_heat
@property
def current_operation(self: ClimateDevice) -> str:
"""Return current operation ie. heat, cool, idle."""
return getattr(self._device, 'system_mode', None)
def set_temperature(self, temperature):
"""Set target temperature."""
import somecomfort
try:
if self._device.system_mode == 'cool':
self._device.setpoint_cool = temperature
else:
self._device.setpoint_heat = temperature
except somecomfort.SomeComfortError:
_LOGGER.error('Temperature %.1f out of range', temperature)
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {'fan': (self.is_fan_on and 'running' or 'idle'),
'fanmode': self._device.fan_mode,
'system_mode': self._device.system_mode}
def turn_away_mode_on(self):
"""Turn away on."""
pass
def turn_away_mode_off(self):
"""Turn away off."""
pass
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
"""Set the system mode (Cool, Heat, etc)."""
if hasattr(self._device, 'system_mode'):
self._device.system_mode = operation_mode

View file

@ -0,0 +1,83 @@
"""
Support for KNX thermostats.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/knx/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.knx import (
KNXConfig, KNXMultiAddressDevice)
DEPENDENCIES = ["knx"]
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create and add an entity based on the configuration."""
add_entities([
KNXThermostat(hass, KNXConfig(config))
])
class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
"""Representation of a KNX thermostat.
A KNX thermostat will has the following parameters:
- temperature (current temperature)
- setpoint (target temperature in HASS terms)
- operation mode selection (comfort/night/frost protection)
This version supports only polling. Messages from the KNX bus do not
automatically update the state of the thermostat (to be implemented
in future releases)
"""
def __init__(self, hass, config):
"""Initialize the thermostat based on the given configuration."""
KNXMultiAddressDevice.__init__(self, hass, config,
["temperature", "setpoint"],
["mode"])
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
self._away = False # not yet supported
self._is_fan_on = False # not yet supported
@property
def should_poll(self):
"""Polling is needed for the KNX thermostat."""
return True
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value("temperature"))
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value("setpoint"))
def set_temperature(self, temperature):
"""Set new target temperature."""
from knxip.conversion import float_to_knx2
self.set_value("setpoint", float_to_knx2(temperature))
_LOGGER.debug("Set target temperature to %s", temperature)
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
raise NotImplementedError()

View file

@ -0,0 +1,189 @@
"""
Support for Nest thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.nest/
"""
import voluptuous as vol
import homeassistant.components.nest as nest
from homeassistant.components.climate import (
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
from homeassistant.const import TEMP_CELSIUS, CONF_PLATFORM, CONF_SCAN_INTERVAL
DEPENDENCIES = ['nest']
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): nest.DOMAIN,
vol.Optional(CONF_SCAN_INTERVAL):
vol.All(vol.Coerce(int), vol.Range(min=1)),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Nest thermostat."""
add_devices([NestThermostat(structure, device)
for structure, device in nest.devices()])
# pylint: disable=abstract-method
class NestThermostat(ClimateDevice):
"""Representation of a Nest thermostat."""
def __init__(self, structure, device):
"""Initialize the thermostat."""
self.structure = structure
self.device = device
@property
def name(self):
"""Return the name of the nest, if any."""
location = self.device.where
name = self.device.name
if location is None:
return name
else:
if name == '':
return location.capitalize()
else:
return location.capitalize() + '(' + name + ')'
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
# Move these to Thermostat Device and make them global
return {
"humidity": self.device.humidity,
"target_humidity": self.device.target_humidity,
"mode": self.device.mode
}
@property
def current_temperature(self):
"""Return the current temperature."""
return self.device.temperature
@property
def operation(self):
"""Return current operation ie. heat, cool, idle."""
if self.device.hvac_ac_state is True:
return STATE_COOL
elif self.device.hvac_heater_state is True:
return STATE_HEAT
else:
return STATE_IDLE
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self.device.mode == 'range':
low, high = self.target_temperature_low, \
self.target_temperature_high
if self.operation == STATE_COOL:
temp = high
elif self.operation == STATE_HEAT:
temp = low
else:
# If the outside temp is lower than the current temp, consider
# the 'low' temp to the target, otherwise use the high temp
if (self.device.structure.weather.current.temperature <
self.current_temperature):
temp = low
else:
temp = high
else:
if self.is_away_mode_on:
# away_temperature is a low, high tuple. Only one should be set
# if not in range mode, the other will be None
temp = self.device.away_temperature[0] or \
self.device.away_temperature[1]
else:
temp = self.device.target
return temp
@property
def target_temperature_low(self):
"""Return the lower bound temperature we try to reach."""
if self.is_away_mode_on and self.device.away_temperature[0]:
# away_temperature is always a low, high tuple
return self.device.away_temperature[0]
if self.device.mode == 'range':
return self.device.target[0]
return self.target_temperature
@property
def target_temperature_high(self):
"""Return the upper bound temperature we try to reach."""
if self.is_away_mode_on and self.device.away_temperature[1]:
# away_temperature is always a low, high tuple
return self.device.away_temperature[1]
if self.device.mode == 'range':
return self.device.target[1]
return self.target_temperature
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self.structure.away
def set_temperature(self, temperature):
"""Set new target temperature."""
if self.device.mode == 'range':
if self.target_temperature == self.target_temperature_low:
temperature = (temperature, self.target_temperature_high)
elif self.target_temperature == self.target_temperature_high:
temperature = (self.target_temperature_low, temperature)
self.device.target = temperature
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
self.device.mode = operation_mode
def turn_away_mode_on(self):
"""Turn away on."""
self.structure.away = True
def turn_away_mode_off(self):
"""Turn away off."""
self.structure.away = False
@property
def is_fan_on(self):
"""Return whether the fan is on."""
return self.device.fan
def turn_fan_on(self):
"""Turn fan on."""
self.device.fan = True
def turn_fan_off(self):
"""Turn fan off."""
self.device.fan = False
@property
def min_temp(self):
"""Identify min_temp in Nest API or defaults if not available."""
temp = self.device.away_temperature.low
if temp is None:
return super().min_temp
else:
return temp
@property
def max_temp(self):
"""Identify max_temp in Nest API or defaults if not available."""
temp = self.device.away_temperature.high
if temp is None:
return super().max_temp
else:
return temp
def update(self):
"""Python-nest has its own mechanism for staying up to date."""
pass

View file

@ -0,0 +1,90 @@
"""
Support for Proliphix NT10e Thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.proliphix/
"""
from homeassistant.components.climate import (
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT)
REQUIREMENTS = ['proliphix==0.3.1']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Proliphix thermostats."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
import proliphix
pdp = proliphix.PDP(host, username, password)
add_devices([
ProliphixThermostat(pdp)
])
# pylint: disable=abstract-method
class ProliphixThermostat(ClimateDevice):
"""Representation a Proliphix thermostat."""
def __init__(self, pdp):
"""Initialize the thermostat."""
self._pdp = pdp
# initial data
self._pdp.update()
self._name = self._pdp.name
@property
def should_poll(self):
"""Polling needed for thermostat."""
return True
def update(self):
"""Update the data from the thermostat."""
self._pdp.update()
@property
def name(self):
"""Return the name of the thermostat."""
return self._name
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
"fan": self._pdp.fan_state
}
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
"""Return the current temperature."""
return self._pdp.cur_temp
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._pdp.setback
@property
def current_operation(self):
"""Return the current state of the thermostat."""
state = self._pdp.hvac_state
if state in (1, 2):
return STATE_IDLE
elif state == 3:
return STATE_HEAT
elif state == 6:
return STATE_COOL
def set_temperature(self, temperature):
"""Set new target temperature."""
self._pdp.setback = temperature

View file

@ -0,0 +1,136 @@
"""
Support for Radio Thermostat wifi-enabled home thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.radiotherm/
"""
import datetime
import logging
from urllib.error import URLError
from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF,
ClimateDevice)
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT
REQUIREMENTS = ['radiotherm==1.2']
HOLD_TEMP = 'hold_temp'
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Radio Thermostat."""
import radiotherm
hosts = []
if CONF_HOST in config:
hosts = config[CONF_HOST]
else:
hosts.append(radiotherm.discover.discover_address())
if hosts is None:
_LOGGER.error("No radiotherm thermostats detected.")
return False
hold_temp = config.get(HOLD_TEMP, False)
tstats = []
for host in hosts:
try:
tstat = radiotherm.get_thermostat(host)
tstats.append(RadioThermostat(tstat, hold_temp))
except (URLError, OSError):
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
host)
add_devices(tstats)
# pylint: disable=abstract-method
class RadioThermostat(ClimateDevice):
"""Representation of a Radio Thermostat."""
def __init__(self, device, hold_temp):
"""Initialize the thermostat."""
self.device = device
self.set_time()
self._target_temperature = None
self._current_temperature = None
self._current_operation = STATE_IDLE
self._name = None
self.hold_temp = hold_temp
self.update()
@property
def name(self):
"""Return the name of the Radio Thermostat."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
"fan": self.device.fmode['human'],
"mode": self.device.tmode['human']
}
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def current_operation(self):
"""Return the current operation. head, cool idle."""
return self._current_operation
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def update(self):
"""Update the data from the thermostat."""
self._current_temperature = self.device.temp['raw']
self._name = self.device.name['raw']
if self.device.tmode['human'] == 'Cool':
self._target_temperature = self.device.t_cool['raw']
self._current_operation = STATE_COOL
elif self.device.tmode['human'] == 'Heat':
self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_HEAT
else:
self._current_operation = STATE_IDLE
def set_temperature(self, temperature):
"""Set new target temperature."""
if self._current_operation == STATE_COOL:
self.device.t_cool = temperature
elif self._current_operation == STATE_HEAT:
self.device.t_heat = temperature
if self.hold_temp:
self.device.hold = 1
else:
self.device.hold = 0
def set_time(self):
"""Set device time."""
now = datetime.datetime.now()
self.device.time = {'day': now.weekday(),
'hour': now.hour, 'minute': now.minute}
def set_operation_mode(self, operation_mode):
"""Set operation mode (auto, cool, heat, off)."""
if operation_mode == STATE_OFF:
self.device.tmode = 0
elif operation_mode == STATE_AUTO:
self.device.tmode = 3
elif operation_mode == STATE_COOL:
self.device.t_cool = self._target_temperature
elif operation_mode == STATE_HEAT:
self.device.t_heat = self._target_temperature

View file

@ -0,0 +1,84 @@
set_aux_heat:
description: Turn auxillary heater on/off for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
aux_heat:
description: New value of axillary heater
example: true
set_away_mode:
description: Turn away mode on/off for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
away_mode:
description: New value of away mode
example: true
set_temperature:
description: Set target temperature of climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
temperature:
description: New target temperature for hvac
example: 25
set_humidity:
description: Set target humidity of climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
humidity:
description: New target humidity for climate device
example: 60
set_fan_mode:
description: Set fan operation for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.nest'
fan:
description: New value of fan mode
example: On Low
set_operation_mode:
description: Set operation mode for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climet.nest'
operation_mode:
description: New value of operation mode
example: Heat
set_swing_mode:
description: Set swing operation for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: '.nest'
swing_mode:
description: New value of swing mode
example: 1

View file

@ -0,0 +1,253 @@
"""
Support for ZWave climate devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
from homeassistant.components.climate import DOMAIN
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.zwave import (
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
from homeassistant.components import zwave
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
_LOGGER = logging.getLogger(__name__)
CONF_NAME = 'name'
DEFAULT_NAME = 'ZWave Climate'
REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
COMMAND_CLASS_CONFIGURATION = 0x70
WORKAROUND_ZXT_120 = 'zxt_120'
DEVICE_MAPPINGS = {
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
}
ZXT_120_SET_TEMP = {
'Heat': 1,
'Cool': 2,
'Dry Air': 8,
'Auto Changeover': 10
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ZWave Climate devices."""
if discovery_info is None or zwave.NETWORK is None:
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
discovery_info, zwave.NETWORK)
return
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
value = node.values[discovery_info[ATTR_VALUE_ID]]
value.set_change_verified(False)
add_devices([ZWaveClimate(value)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK)
# pylint: disable=too-many-arguments, abstract-method
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Represents a ZWave Climate device."""
# pylint: disable=too-many-public-methods, too-many-instance-attributes
def __init__(self, value):
"""Initialize the zwave climate device."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
self._target_temperature = None
self._current_temperature = None
self._current_operation = None
self._operation_list = None
self._current_fan_mode = None
self._fan_list = None
self._current_swing_mode = None
self._swing_list = None
self._unit = None
self._index = None
self._zxt_120 = None
self.update_properties()
# register listener
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
" workaround")
self._zxt_120 = 1
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties()
self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Set point
temps = []
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
temps.append(int(value.data))
if value.index == self._index:
self._target_temperature = int(value.data)
self._target_temperature_high = max(temps)
self._target_temperature_low = min(temps)
# Operation Mode
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
self._current_operation = value.data
self._operation_list = list(value.data_items)
_LOGGER.debug("self._operation_list=%s", self._operation_list)
_LOGGER.debug("self._current_operation=%s",
self._current_operation)
# Current Temp
for value in self._node.get_values(
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
if value.label == 'Temperature':
self._current_temperature = int(value.data)
self._unit = value.units
# Fan Mode
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
self._current_fan_mode = value.data
self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list)
_LOGGER.debug("self._current_fan_mode=%s",
self._current_fan_mode)
# Swing mode
if self._zxt_120 == 1:
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data
self._swing_list = list(value.data_items)
_LOGGER.debug("self._swing_list=%s", self._swing_list)
_LOGGER.debug("self._current_swing_mode=%s",
self._current_swing_mode)
@property
def should_poll(self):
"""No polling on ZWave."""
return False
@property
def current_fan_mode(self):
"""Return the fan speed set."""
return self._current_fan_mode
@property
def fan_list(self):
"""List of available fan modes."""
return self._fan_list
@property
def current_swing_mode(self):
"""Return the swing mode set."""
return self._current_swing_mode
@property
def swing_list(self):
"""List of available swing modes."""
return self._swing_list
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
unit = self._unit
if unit == 'C':
return TEMP_CELSIUS
elif unit == 'F':
return TEMP_FAHRENHEIT
else:
_LOGGER.exception("unit_of_measurement=%s is not valid",
unit)
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def current_operation(self):
"""Return the current operation mode."""
return self._current_operation
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
if value.command_class != 67 and value.index != self._index:
continue
if self._zxt_120:
# ZXT-120 does not support get setpoint
self._target_temperature = temperature
if ZXT_120_SET_TEMP.get(self._current_operation) \
!= value.index:
continue
_LOGGER.debug("ZXT_120_SET_TEMP=%s and"
" self._current_operation=%s",
ZXT_120_SET_TEMP.get(self._current_operation),
self._current_operation)
# ZXT-120 responds only to whole int
value.data = int(round(temperature, 0))
else:
value.data = int(temperature)
break
def set_fan_mode(self, fan):
"""Set new target fan mode."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
if value.command_class == 68 and value.index == 0:
value.data = bytes(fan, 'utf-8')
break
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
if value.command_class == 64 and value.index == 0:
value.data = bytes(operation_mode, 'utf-8')
break
def set_swing_mode(self, swing_mode):
"""Set new target swing mode."""
if self._zxt_120 == 1:
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
value.data = bytes(swing_mode, 'utf-8')
break

View file

@ -69,7 +69,7 @@ def setup_ecobee(hass, network, config):
hold_temp = config[DOMAIN].get(HOLD_TEMP, False) hold_temp = config[DOMAIN].get(HOLD_TEMP, False)
discovery.load_platform(hass, 'thermostat', DOMAIN, discovery.load_platform(hass, 'climate', DOMAIN,
{'hold_temp': hold_temp}, config) {'hold_temp': hold_temp}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)

View file

@ -28,7 +28,7 @@ DISCOVER_LIGHTS = 'homematic.light'
DISCOVER_SENSORS = 'homematic.sensor' DISCOVER_SENSORS = 'homematic.sensor'
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
DISCOVER_ROLLERSHUTTER = 'homematic.rollershutter' DISCOVER_ROLLERSHUTTER = 'homematic.rollershutter'
DISCOVER_THERMOSTATS = 'homematic.thermostat' DISCOVER_THERMOSTATS = 'homematic.climate'
ATTR_DISCOVER_DEVICES = 'devices' ATTR_DISCOVER_DEVICES = 'devices'
ATTR_PARAM = 'param' ATTR_PARAM = 'param'
@ -214,7 +214,7 @@ def system_callback_handler(hass, config, src, *args):
('rollershutter', DISCOVER_ROLLERSHUTTER), ('rollershutter', DISCOVER_ROLLERSHUTTER),
('binary_sensor', DISCOVER_BINARY_SENSORS), ('binary_sensor', DISCOVER_BINARY_SENSORS),
('sensor', DISCOVER_SENSORS), ('sensor', DISCOVER_SENSORS),
('thermostat', DISCOVER_THERMOSTATS)): ('climate', DISCOVER_THERMOSTATS)):
# Get all devices of a specific type # Get all devices of a specific type
found_devices = _get_devices(discovery_type, key_dict) found_devices = _get_devices(discovery_type, key_dict)

View file

@ -2,7 +2,7 @@
Support for Nest thermostats and protect smoke alarms. Support for Nest thermostats and protect smoke alarms.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.nest/ https://home-assistant.io/components/climate.nest/
""" """
import logging import logging
import socket import socket

View file

@ -57,6 +57,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
COMMAND_CLASS_THERMOSTAT_SETPOINT)): COMMAND_CLASS_THERMOSTAT_SETPOINT)):
return return
if value.command_class != COMMAND_CLASS_SENSOR_MULTILEVEL and \
value.command_class != COMMAND_CLASS_THERMOSTAT_SETPOINT:
return
add_devices([ZWaveThermostat(value)]) add_devices([ZWaveThermostat(value)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK) discovery_info, zwave.NETWORK)

View file

@ -151,18 +151,6 @@ DISCOVERY_COMPONENTS = [
[COMMAND_CLASS_SENSOR_BINARY], [COMMAND_CLASS_SENSOR_BINARY],
TYPE_BOOL, TYPE_BOOL,
GENRE_USER), GENRE_USER),
('thermostat',
[GENERIC_COMMAND_CLASS_THERMOSTAT],
[SPECIFIC_DEVICE_CLASS_WHATEVER],
[COMMAND_CLASS_THERMOSTAT_SETPOINT],
TYPE_WHATEVER,
GENRE_WHATEVER),
('hvac',
[GENERIC_COMMAND_CLASS_THERMOSTAT],
[SPECIFIC_DEVICE_CLASS_WHATEVER],
[COMMAND_CLASS_THERMOSTAT_FAN_MODE],
TYPE_WHATEVER,
GENRE_WHATEVER),
('lock', ('lock',
[GENERIC_COMMAND_CLASS_ENTRY_CONTROL], [GENERIC_COMMAND_CLASS_ENTRY_CONTROL],
[SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK, [SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK,
@ -186,7 +174,13 @@ DISCOVERY_COMPONENTS = [
[COMMAND_CLASS_SWITCH_BINARY, [COMMAND_CLASS_SWITCH_BINARY,
COMMAND_CLASS_BARRIER_OPERATOR], COMMAND_CLASS_BARRIER_OPERATOR],
TYPE_BOOL, TYPE_BOOL,
GENRE_USER) GENRE_USER),
('climate',
[GENERIC_COMMAND_CLASS_THERMOSTAT],
[SPECIFIC_DEVICE_CLASS_WHATEVER],
[COMMAND_CLASS_THERMOSTAT_SETPOINT],
TYPE_WHATEVER,
GENRE_WHATEVER),
] ]

View file

@ -40,6 +40,7 @@ blinkstick==1.1.7
# homeassistant.components.sensor.bitcoin # homeassistant.components.sensor.bitcoin
blockchain==1.3.3 blockchain==1.3.3
# homeassistant.components.climate.eq3btsmart
# homeassistant.components.thermostat.eq3btsmart # homeassistant.components.thermostat.eq3btsmart
# bluepy_devices==0.2.0 # bluepy_devices==0.2.0
@ -67,6 +68,7 @@ eliqonline==1.0.12
# homeassistant.components.enocean # homeassistant.components.enocean
enocean==0.31 enocean==0.31
# homeassistant.components.climate.honeywell
# homeassistant.components.thermostat.honeywell # homeassistant.components.thermostat.honeywell
evohomeclient==0.2.5 evohomeclient==0.2.5
@ -103,6 +105,7 @@ ha-ffmpeg==0.4
# homeassistant.components.mqtt.server # homeassistant.components.mqtt.server
hbmqtt==0.7.1 hbmqtt==0.7.1
# homeassistant.components.climate.heatmiser
# homeassistant.components.thermostat.heatmiser # homeassistant.components.thermostat.heatmiser
heatmiserV3==0.9.1 heatmiserV3==0.9.1
@ -254,6 +257,7 @@ plexapi==2.0.2
# homeassistant.components.sensor.serial_pm # homeassistant.components.sensor.serial_pm
pmsensor==0.2 pmsensor==0.2
# homeassistant.components.climate.proliphix
# homeassistant.components.thermostat.proliphix # homeassistant.components.thermostat.proliphix
proliphix==0.3.1 proliphix==0.3.1
@ -388,6 +392,7 @@ pyvera==0.2.15
# homeassistant.components.wemo # homeassistant.components.wemo
pywemo==0.4.5 pywemo==0.4.5
# homeassistant.components.climate.radiotherm
# homeassistant.components.thermostat.radiotherm # homeassistant.components.thermostat.radiotherm
radiotherm==1.2 radiotherm==1.2
@ -418,6 +423,7 @@ sleekxmpp==1.3.1
# homeassistant.components.media_player.snapcast # homeassistant.components.media_player.snapcast
snapcast==1.2.1 snapcast==1.2.1
# homeassistant.components.climate.honeywell
# homeassistant.components.thermostat.honeywell # homeassistant.components.thermostat.honeywell
somecomfort==0.2.1 somecomfort==0.2.1

View file

@ -0,0 +1 @@
"""The tests for climate component."""

View file

@ -0,0 +1,166 @@
"""The tests for the demo climate component."""
import unittest
from homeassistant.util.unit_system import (
METRIC_SYSTEM,
)
from homeassistant.components import climate
from tests.common import get_test_home_assistant
ENTITY_CLIMATE = 'climate.hvac'
class TestDemoClimate(unittest.TestCase):
"""Test the demo climate hvac."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.config.units = METRIC_SYSTEM
self.assertTrue(climate.setup(self.hass, {'climate': {
'platform': 'demo',
}}))
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
def test_setup_params(self):
"""Test the inititial parameters."""
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(21, state.attributes.get('temperature'))
self.assertEqual('on', state.attributes.get('away_mode'))
self.assertEqual(22, state.attributes.get('current_temperature'))
self.assertEqual("On High", state.attributes.get('fan_mode'))
self.assertEqual(67, state.attributes.get('humidity'))
self.assertEqual(54, state.attributes.get('current_humidity'))
self.assertEqual("Off", state.attributes.get('swing_mode'))
self.assertEqual("Cool", state.attributes.get('operation_mode'))
self.assertEqual('off', state.attributes.get('aux_heat'))
def test_default_setup_params(self):
"""Test the setup with default parameters."""
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(7, state.attributes.get('min_temp'))
self.assertEqual(35, state.attributes.get('max_temp'))
self.assertEqual(30, state.attributes.get('min_humidity'))
self.assertEqual(99, state.attributes.get('max_humidity'))
def test_set_target_temp_bad_attr(self):
"""Test setting the target temperature without required attribute."""
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(21, state.attributes.get('temperature'))
climate.set_temperature(self.hass, None, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual(21, state.attributes.get('temperature'))
def test_set_target_temp(self):
"""Test the setting of the target temperature."""
climate.set_temperature(self.hass, 30, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(30.0, state.attributes.get('temperature'))
def test_set_target_humidity_bad_attr(self):
"""Test setting the target humidity without required attribute."""
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(67, state.attributes.get('humidity'))
climate.set_humidity(self.hass, None, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual(67, state.attributes.get('humidity'))
def test_set_target_humidity(self):
"""Test the setting of the target humidity."""
climate.set_humidity(self.hass, 64, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(64.0, state.attributes.get('humidity'))
def test_set_fan_mode_bad_attr(self):
"""Test setting fan mode without required attribute."""
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("On High", state.attributes.get('fan_mode'))
climate.set_fan_mode(self.hass, None, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual("On High", state.attributes.get('fan_mode'))
def test_set_fan_mode(self):
"""Test setting of new fan mode."""
climate.set_fan_mode(self.hass, "On Low", ENTITY_CLIMATE)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("On Low", state.attributes.get('fan_mode'))
def test_set_swing_mode_bad_attr(self):
"""Test setting swing mode without required attribute."""
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("Off", state.attributes.get('swing_mode'))
climate.set_swing_mode(self.hass, None, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual("Off", state.attributes.get('swing_mode'))
def test_set_swing(self):
"""Test setting of new swing mode."""
climate.set_swing_mode(self.hass, "Auto", ENTITY_CLIMATE)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("Auto", state.attributes.get('swing_mode'))
def test_set_operation_bad_attr(self):
"""Test setting operation mode without required attribute."""
self.assertEqual("Cool", self.hass.states.get(ENTITY_CLIMATE).state)
climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual("Cool", self.hass.states.get(ENTITY_CLIMATE).state)
def test_set_operation(self):
"""Test setting of new operation mode."""
climate.set_operation_mode(self.hass, "Heat", ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual("Heat", self.hass.states.get(ENTITY_CLIMATE).state)
def test_set_away_mode_bad_attr(self):
"""Test setting the away mode without required attribute."""
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('on', state.attributes.get('away_mode'))
climate.set_away_mode(self.hass, None, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual('on', state.attributes.get('away_mode'))
def test_set_away_mode_on(self):
"""Test setting the away mode on/true."""
climate.set_away_mode(self.hass, True, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('on', state.attributes.get('away_mode'))
def test_set_away_mode_off(self):
"""Test setting the away mode off/false."""
climate.set_away_mode(self.hass, False, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('off', state.attributes.get('away_mode'))
def test_set_aux_heat_bad_attr(self):
"""Test setting the auxillary heater without required attribute."""
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('off', state.attributes.get('aux_heat'))
climate.set_aux_heat(self.hass, None, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual('off', state.attributes.get('aux_heat'))
def test_set_aux_heat_on(self):
"""Test setting the axillary heater on/true."""
climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('on', state.attributes.get('aux_heat'))
def test_set_aux_heat_off(self):
"""Test setting the auxillary heater off/false."""
climate.set_aux_heat(self.hass, False, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('off', state.attributes.get('aux_heat'))

View file

@ -0,0 +1,493 @@
"""The tests for the generic_thermostat."""
import datetime
import unittest
from unittest import mock
from homeassistant.bootstrap import _setup_component
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
STATE_OFF,
TEMP_CELSIUS,
)
from homeassistant.util.unit_system import METRIC_SYSTEM
from homeassistant.components import climate
from tests.common import get_test_home_assistant
ENTITY = 'climate.test'
ENT_SENSOR = 'sensor.test'
ENT_SWITCH = 'switch.test'
MIN_TEMP = 3.0
MAX_TEMP = 65.0
TARGET_TEMP = 42.0
class TestSetupClimateGenericThermostat(unittest.TestCase):
"""Test the Generic thermostat with custom config."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
def test_setup_missing_conf(self):
"""Test set up heat_control with missing config values."""
config = {
'name': 'test',
'target_sensor': ENT_SENSOR
}
self.assertFalse(_setup_component(self.hass, 'climate', {
'climate': config}))
def test_valid_conf(self):
"""Test set up genreic_thermostat with valid config values."""
self.assertTrue(_setup_component(self.hass, 'climate',
{'climate': {
'platform': 'generic_thermostat',
'name': 'test',
'heater': ENT_SWITCH,
'target_sensor': ENT_SENSOR}}))
def test_setup_with_sensor(self):
"""Test set up heat_control with sensor to trigger update at init."""
self.hass.states.set(ENT_SENSOR, 22.0, {
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS
})
climate.setup(self.hass, {'climate': {
'platform': 'generic_thermostat',
'name': 'test',
'heater': ENT_SWITCH,
'target_sensor': ENT_SENSOR
}})
state = self.hass.states.get(ENTITY)
self.assertEqual(
TEMP_CELSIUS, state.attributes.get('unit_of_measurement'))
self.assertEqual(22.0, state.attributes.get('current_temperature'))
class TestClimateGenericThermostat(unittest.TestCase):
"""Test the Generic thermostat."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.config.units = METRIC_SYSTEM
climate.setup(self.hass, {'climate': {
'platform': 'generic_thermostat',
'name': 'test',
'heater': ENT_SWITCH,
'target_sensor': ENT_SENSOR
}})
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
def test_setup_defaults_to_unknown(self):
"""Test the setting of defaults to unknown."""
self.assertEqual('unknown', self.hass.states.get(ENTITY).state)
def test_default_setup_params(self):
"""Test the setup with default parameters."""
state = self.hass.states.get(ENTITY)
self.assertEqual(7, state.attributes.get('min_temp'))
self.assertEqual(35, state.attributes.get('max_temp'))
self.assertEqual(None, state.attributes.get('temperature'))
def test_custom_setup_params(self):
"""Test the setup with custom parameters."""
climate.setup(self.hass, {'climate': {
'platform': 'generic_thermostat',
'name': 'test',
'heater': ENT_SWITCH,
'target_sensor': ENT_SENSOR,
'min_temp': MIN_TEMP,
'max_temp': MAX_TEMP,
'target_temp': TARGET_TEMP
}})
state = self.hass.states.get(ENTITY)
self.assertEqual(MIN_TEMP, state.attributes.get('min_temp'))
self.assertEqual(MAX_TEMP, state.attributes.get('max_temp'))
self.assertEqual(TARGET_TEMP, state.attributes.get('temperature'))
def test_set_target_temp(self):
"""Test the setting of the target temperature."""
climate.set_temperature(self.hass, 30)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY)
self.assertEqual(30.0, state.attributes.get('temperature'))
def test_sensor_bad_unit(self):
"""Test sensor that have bad unit."""
state = self.hass.states.get(ENTITY)
temp = state.attributes.get('current_temperature')
unit = state.attributes.get('unit_of_measurement')
self._setup_sensor(22.0, unit='bad_unit')
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY)
self.assertEqual(unit, state.attributes.get('unit_of_measurement'))
self.assertEqual(temp, state.attributes.get('current_temperature'))
def test_sensor_bad_value(self):
"""Test sensor that have None as state."""
state = self.hass.states.get(ENTITY)
temp = state.attributes.get('current_temperature')
unit = state.attributes.get('unit_of_measurement')
self._setup_sensor(None)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY)
self.assertEqual(unit, state.attributes.get('unit_of_measurement'))
self.assertEqual(temp, state.attributes.get('current_temperature'))
def test_set_target_temp_heater_on(self):
"""Test if target temperature turn heater on."""
self._setup_switch(False)
self._setup_sensor(25)
self.hass.pool.block_till_done()
climate.set_temperature(self.hass, 30)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_set_target_temp_heater_off(self):
"""Test if target temperature turn heater off."""
self._setup_switch(True)
self._setup_sensor(30)
self.hass.pool.block_till_done()
climate.set_temperature(self.hass, 25)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_set_temp_change_heater_on(self):
"""Test if temperature change turn heater on."""
self._setup_switch(False)
climate.set_temperature(self.hass, 30)
self.hass.pool.block_till_done()
self._setup_sensor(25)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_temp_change_heater_off(self):
"""Test if temperature change turn heater off."""
self._setup_switch(True)
climate.set_temperature(self.hass, 25)
self.hass.pool.block_till_done()
self._setup_sensor(30)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def _setup_sensor(self, temp, unit=TEMP_CELSIUS):
"""Setup the test sensor."""
self.hass.states.set(ENT_SENSOR, temp, {
ATTR_UNIT_OF_MEASUREMENT: unit
})
def _setup_switch(self, is_on):
"""Setup the test switch."""
self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF)
self.calls = []
def log_call(call):
"""Log service calls."""
self.calls.append(call)
self.hass.services.register('switch', SERVICE_TURN_ON, log_call)
self.hass.services.register('switch', SERVICE_TURN_OFF, log_call)
class TestClimateGenericThermostatACMode(unittest.TestCase):
"""Test the Generic thermostat."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.config.temperature_unit = TEMP_CELSIUS
climate.setup(self.hass, {'climate': {
'platform': 'generic_thermostat',
'name': 'test',
'heater': ENT_SWITCH,
'target_sensor': ENT_SENSOR,
'ac_mode': True
}})
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
def test_set_target_temp_ac_off(self):
"""Test if target temperature turn ac off."""
self._setup_switch(True)
self._setup_sensor(25)
self.hass.pool.block_till_done()
climate.set_temperature(self.hass, 30)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_set_target_temp_ac_on(self):
"""Test if target temperature turn ac on."""
self._setup_switch(False)
self._setup_sensor(30)
self.hass.pool.block_till_done()
climate.set_temperature(self.hass, 25)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_set_temp_change_ac_off(self):
"""Test if temperature change turn ac off."""
self._setup_switch(True)
climate.set_temperature(self.hass, 30)
self.hass.pool.block_till_done()
self._setup_sensor(25)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_temp_change_ac_on(self):
"""Test if temperature change turn ac on."""
self._setup_switch(False)
climate.set_temperature(self.hass, 25)
self.hass.pool.block_till_done()
self._setup_sensor(30)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def _setup_sensor(self, temp, unit=TEMP_CELSIUS):
"""Setup the test sensor."""
self.hass.states.set(ENT_SENSOR, temp, {
ATTR_UNIT_OF_MEASUREMENT: unit
})
def _setup_switch(self, is_on):
"""Setup the test switch."""
self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF)
self.calls = []
def log_call(call):
"""Log service calls."""
self.calls.append(call)
self.hass.services.register('switch', SERVICE_TURN_ON, log_call)
self.hass.services.register('switch', SERVICE_TURN_OFF, log_call)
class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase):
"""Test the Generic Thermostat."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.config.temperature_unit = TEMP_CELSIUS
climate.setup(self.hass, {'climate': {
'platform': 'generic_thermostat',
'name': 'test',
'heater': ENT_SWITCH,
'target_sensor': ENT_SENSOR,
'ac_mode': True,
'min_cycle_duration': datetime.timedelta(minutes=10)
}})
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
def test_temp_change_ac_trigger_on_not_long_enough(self):
"""Test if temperature change turn ac on."""
self._setup_switch(False)
climate.set_temperature(self.hass, 25)
self.hass.pool.block_till_done()
self._setup_sensor(30)
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_temp_change_ac_trigger_on_long_enough(self):
"""Test if temperature change turn ac on."""
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11,
tzinfo=datetime.timezone.utc)
with mock.patch('homeassistant.helpers.condition.dt_util.utcnow',
return_value=fake_changed):
self._setup_switch(False)
climate.set_temperature(self.hass, 25)
self.hass.pool.block_till_done()
self._setup_sensor(30)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_temp_change_ac_trigger_off_not_long_enough(self):
"""Test if temperature change turn ac on."""
self._setup_switch(True)
climate.set_temperature(self.hass, 30)
self.hass.pool.block_till_done()
self._setup_sensor(25)
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_temp_change_ac_trigger_off_long_enough(self):
"""Test if temperature change turn ac on."""
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11,
tzinfo=datetime.timezone.utc)
with mock.patch('homeassistant.helpers.condition.dt_util.utcnow',
return_value=fake_changed):
self._setup_switch(True)
climate.set_temperature(self.hass, 30)
self.hass.pool.block_till_done()
self._setup_sensor(25)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def _setup_sensor(self, temp, unit=TEMP_CELSIUS):
"""Setup the test sensor."""
self.hass.states.set(ENT_SENSOR, temp, {
ATTR_UNIT_OF_MEASUREMENT: unit
})
def _setup_switch(self, is_on):
"""Setup the test switch."""
self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF)
self.calls = []
def log_call(call):
"""Log service calls."""
self.calls.append(call)
self.hass.services.register('switch', SERVICE_TURN_ON, log_call)
self.hass.services.register('switch', SERVICE_TURN_OFF, log_call)
class TestClimateGenericThermostatMinCycle(unittest.TestCase):
"""Test the Generic thermostat."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.config.temperature_unit = TEMP_CELSIUS
climate.setup(self.hass, {'climate': {
'platform': 'generic_thermostat',
'name': 'test',
'heater': ENT_SWITCH,
'target_sensor': ENT_SENSOR,
'min_cycle_duration': datetime.timedelta(minutes=10)
}})
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
def test_temp_change_heater_trigger_off_not_long_enough(self):
"""Test if temp change doesn't turn heater off because of time."""
self._setup_switch(True)
climate.set_temperature(self.hass, 25)
self.hass.pool.block_till_done()
self._setup_sensor(30)
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_temp_change_heater_trigger_on_not_long_enough(self):
"""Test if temp change doesn't turn heater on because of time."""
self._setup_switch(False)
climate.set_temperature(self.hass, 30)
self.hass.pool.block_till_done()
self._setup_sensor(25)
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_temp_change_heater_trigger_on_long_enough(self):
"""Test if temperature change turn heater on after min cycle."""
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11,
tzinfo=datetime.timezone.utc)
with mock.patch('homeassistant.helpers.condition.dt_util.utcnow',
return_value=fake_changed):
self._setup_switch(False)
climate.set_temperature(self.hass, 30)
self.hass.pool.block_till_done()
self._setup_sensor(25)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_temp_change_heater_trigger_off_long_enough(self):
"""Test if temperature change turn heater off after min cycle."""
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11,
tzinfo=datetime.timezone.utc)
with mock.patch('homeassistant.helpers.condition.dt_util.utcnow',
return_value=fake_changed):
self._setup_switch(True)
climate.set_temperature(self.hass, 25)
self.hass.pool.block_till_done()
self._setup_sensor(30)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def _setup_sensor(self, temp, unit=TEMP_CELSIUS):
"""Setup the test sensor."""
self.hass.states.set(ENT_SENSOR, temp, {
ATTR_UNIT_OF_MEASUREMENT: unit
})
def _setup_switch(self, is_on):
"""Setup the test switch."""
self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF)
self.calls = []
def log_call(call):
"""Log service calls."""
self.calls.append(call)
self.hass.services.register('switch', SERVICE_TURN_ON, log_call)
self.hass.services.register('switch', SERVICE_TURN_OFF, log_call)

View file

@ -0,0 +1,377 @@
"""The test the Honeywell thermostat module."""
import socket
import unittest
from unittest import mock
import somecomfort
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
import homeassistant.components.climate.honeywell as honeywell
class TestHoneywell(unittest.TestCase):
"""A test class for Honeywell themostats."""
@mock.patch('somecomfort.SomeComfort')
@mock.patch('homeassistant.components.climate.'
'honeywell.HoneywellUSThermostat')
def test_setup_us(self, mock_ht, mock_sc):
"""Test for the US setup."""
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
'region': 'us',
}
bad_pass_config = {
CONF_USERNAME: 'user',
'region': 'us',
}
bad_region_config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
'region': 'un',
}
hass = mock.MagicMock()
add_devices = mock.MagicMock()
locations = [
mock.MagicMock(),
mock.MagicMock(),
]
devices_1 = [mock.MagicMock()]
devices_2 = [mock.MagicMock(), mock.MagicMock]
mock_sc.return_value.locations_by_id.values.return_value = \
locations
locations[0].devices_by_id.values.return_value = devices_1
locations[1].devices_by_id.values.return_value = devices_2
result = honeywell.setup_platform(hass, bad_pass_config, add_devices)
self.assertFalse(result)
result = honeywell.setup_platform(hass, bad_region_config, add_devices)
self.assertFalse(result)
result = honeywell.setup_platform(hass, config, add_devices)
self.assertTrue(result)
mock_sc.assert_called_once_with('user', 'pass')
mock_ht.assert_has_calls([
mock.call(mock_sc.return_value, devices_1[0]),
mock.call(mock_sc.return_value, devices_2[0]),
mock.call(mock_sc.return_value, devices_2[1]),
])
@mock.patch('somecomfort.SomeComfort')
def test_setup_us_failures(self, mock_sc):
"""Test the US setup."""
hass = mock.MagicMock()
add_devices = mock.MagicMock()
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
'region': 'us',
}
mock_sc.side_effect = somecomfort.AuthError
result = honeywell.setup_platform(hass, config, add_devices)
self.assertFalse(result)
self.assertFalse(add_devices.called)
mock_sc.side_effect = somecomfort.SomeComfortError
result = honeywell.setup_platform(hass, config, add_devices)
self.assertFalse(result)
self.assertFalse(add_devices.called)
@mock.patch('somecomfort.SomeComfort')
@mock.patch('homeassistant.components.climate.'
'honeywell.HoneywellUSThermostat')
def _test_us_filtered_devices(self, mock_ht, mock_sc, loc=None, dev=None):
"""Test for US filtered thermostats."""
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
'region': 'us',
'location': loc,
'thermostat': dev,
}
locations = {
1: mock.MagicMock(locationid=mock.sentinel.loc1,
devices_by_id={
11: mock.MagicMock(
deviceid=mock.sentinel.loc1dev1),
12: mock.MagicMock(
deviceid=mock.sentinel.loc1dev2),
}),
2: mock.MagicMock(locationid=mock.sentinel.loc2,
devices_by_id={
21: mock.MagicMock(
deviceid=mock.sentinel.loc2dev1),
}),
3: mock.MagicMock(locationid=mock.sentinel.loc3,
devices_by_id={
31: mock.MagicMock(
deviceid=mock.sentinel.loc3dev1),
}),
}
mock_sc.return_value = mock.MagicMock(locations_by_id=locations)
hass = mock.MagicMock()
add_devices = mock.MagicMock()
self.assertEqual(True,
honeywell.setup_platform(hass, config, add_devices))
return mock_ht.call_args_list, mock_sc
def test_us_filtered_thermostat_1(self):
"""Test for US filtered thermostats."""
result, client = self._test_us_filtered_devices(
dev=mock.sentinel.loc1dev1)
devices = [x[0][1].deviceid for x in result]
self.assertEqual([mock.sentinel.loc1dev1], devices)
def test_us_filtered_thermostat_2(self):
"""Test for US filtered location."""
result, client = self._test_us_filtered_devices(
dev=mock.sentinel.loc2dev1)
devices = [x[0][1].deviceid for x in result]
self.assertEqual([mock.sentinel.loc2dev1], devices)
def test_us_filtered_location_1(self):
"""Test for US filtered locations."""
result, client = self._test_us_filtered_devices(
loc=mock.sentinel.loc1)
devices = [x[0][1].deviceid for x in result]
self.assertEqual([mock.sentinel.loc1dev1,
mock.sentinel.loc1dev2], devices)
def test_us_filtered_location_2(self):
"""Test for US filtered locations."""
result, client = self._test_us_filtered_devices(
loc=mock.sentinel.loc2)
devices = [x[0][1].deviceid for x in result]
self.assertEqual([mock.sentinel.loc2dev1], devices)
@mock.patch('evohomeclient.EvohomeClient')
@mock.patch('homeassistant.components.climate.honeywell.'
'RoundThermostat')
def test_eu_setup_full_config(self, mock_round, mock_evo):
"""Test the EU setup wwith complete configuration."""
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
honeywell.CONF_AWAY_TEMP: 20,
'region': 'eu',
}
mock_evo.return_value.temperatures.return_value = [
{'id': 'foo'}, {'id': 'bar'}]
hass = mock.MagicMock()
add_devices = mock.MagicMock()
self.assertTrue(honeywell.setup_platform(hass, config, add_devices))
mock_evo.assert_called_once_with('user', 'pass')
mock_evo.return_value.temperatures.assert_called_once_with(
force_refresh=True)
mock_round.assert_has_calls([
mock.call(mock_evo.return_value, 'foo', True, 20),
mock.call(mock_evo.return_value, 'bar', False, 20),
])
self.assertEqual(2, add_devices.call_count)
@mock.patch('evohomeclient.EvohomeClient')
@mock.patch('homeassistant.components.climate.honeywell.'
'RoundThermostat')
def test_eu_setup_partial_config(self, mock_round, mock_evo):
"""Test the EU setup with partial configuration."""
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
'region': 'eu',
}
mock_evo.return_value.temperatures.return_value = [
{'id': 'foo'}, {'id': 'bar'}]
hass = mock.MagicMock()
add_devices = mock.MagicMock()
self.assertTrue(honeywell.setup_platform(hass, config, add_devices))
default = honeywell.DEFAULT_AWAY_TEMP
mock_round.assert_has_calls([
mock.call(mock_evo.return_value, 'foo', True, default),
mock.call(mock_evo.return_value, 'bar', False, default),
])
@mock.patch('evohomeclient.EvohomeClient')
@mock.patch('homeassistant.components.climate.honeywell.'
'RoundThermostat')
def test_eu_setup_bad_temp(self, mock_round, mock_evo):
"""Test the EU setup with invalid temperature."""
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
honeywell.CONF_AWAY_TEMP: 'ponies',
'region': 'eu',
}
self.assertFalse(honeywell.setup_platform(None, config, None))
@mock.patch('evohomeclient.EvohomeClient')
@mock.patch('homeassistant.components.climate.honeywell.'
'RoundThermostat')
def test_eu_setup_error(self, mock_round, mock_evo):
"""Test the EU setup with errors."""
config = {
CONF_USERNAME: 'user',
CONF_PASSWORD: 'pass',
honeywell.CONF_AWAY_TEMP: 20,
'region': 'eu',
}
mock_evo.return_value.temperatures.side_effect = socket.error
add_devices = mock.MagicMock()
hass = mock.MagicMock()
self.assertFalse(honeywell.setup_platform(hass, config, add_devices))
class TestHoneywellRound(unittest.TestCase):
"""A test class for Honeywell Round thermostats."""
def setup_method(self, method):
"""Test the setup method."""
def fake_temperatures(force_refresh=None):
"""Create fake temperatures."""
temps = [
{'id': '1', 'temp': 20, 'setpoint': 21,
'thermostat': 'main', 'name': 'House'},
{'id': '2', 'temp': 21, 'setpoint': 22,
'thermostat': 'DOMESTIC_HOT_WATER'},
]
return temps
self.device = mock.MagicMock()
self.device.temperatures.side_effect = fake_temperatures
self.round1 = honeywell.RoundThermostat(self.device, '1',
True, 16)
self.round2 = honeywell.RoundThermostat(self.device, '2',
False, 17)
def test_attributes(self):
"""Test the attributes."""
self.assertEqual('House', self.round1.name)
self.assertEqual(TEMP_CELSIUS, self.round1.unit_of_measurement)
self.assertEqual(20, self.round1.current_temperature)
self.assertEqual(21, self.round1.target_temperature)
self.assertFalse(self.round1.is_away_mode_on)
self.assertEqual('Hot Water', self.round2.name)
self.assertEqual(TEMP_CELSIUS, self.round2.unit_of_measurement)
self.assertEqual(21, self.round2.current_temperature)
self.assertEqual(None, self.round2.target_temperature)
self.assertFalse(self.round2.is_away_mode_on)
def test_away_mode(self):
"""Test setting the away mode."""
self.assertFalse(self.round1.is_away_mode_on)
self.round1.turn_away_mode_on()
self.assertTrue(self.round1.is_away_mode_on)
self.device.set_temperature.assert_called_once_with('House', 16)
self.device.set_temperature.reset_mock()
self.round1.turn_away_mode_off()
self.assertFalse(self.round1.is_away_mode_on)
self.device.cancel_temp_override.assert_called_once_with('House')
def test_set_temperature(self):
"""Test setting the temperature."""
self.round1.set_temperature(25)
self.device.set_temperature.assert_called_once_with('House', 25)
def test_set_operation_mode(self: unittest.TestCase) -> None:
"""Test setting the system operation."""
self.round1.set_operation_mode('cool')
self.assertEqual('cool', self.round1.current_operation)
self.assertEqual('cool', self.device.system_mode)
self.round1.set_operation_mode('heat')
self.assertEqual('heat', self.round1.current_operation)
self.assertEqual('heat', self.device.system_mode)
class TestHoneywellUS(unittest.TestCase):
"""A test class for Honeywell US thermostats."""
def setup_method(self, method):
"""Test the setup method."""
self.client = mock.MagicMock()
self.device = mock.MagicMock()
self.honeywell = honeywell.HoneywellUSThermostat(
self.client, self.device)
self.device.fan_running = True
self.device.name = 'test'
self.device.temperature_unit = 'F'
self.device.current_temperature = 72
self.device.setpoint_cool = 78
self.device.setpoint_heat = 65
self.device.system_mode = 'heat'
self.device.fan_mode = 'auto'
def test_properties(self):
"""Test the properties."""
self.assertTrue(self.honeywell.is_fan_on)
self.assertEqual('test', self.honeywell.name)
self.assertEqual(72, self.honeywell.current_temperature)
def test_unit_of_measurement(self):
"""Test the unit of measurement."""
self.assertEqual(TEMP_FAHRENHEIT, self.honeywell.unit_of_measurement)
self.device.temperature_unit = 'C'
self.assertEqual(TEMP_CELSIUS, self.honeywell.unit_of_measurement)
def test_target_temp(self):
"""Test the target temperature."""
self.assertEqual(65, self.honeywell.target_temperature)
self.device.system_mode = 'cool'
self.assertEqual(78, self.honeywell.target_temperature)
def test_set_temp(self):
"""Test setting the temperature."""
self.honeywell.set_temperature(70)
self.assertEqual(70, self.device.setpoint_heat)
self.assertEqual(70, self.honeywell.target_temperature)
self.device.system_mode = 'cool'
self.assertEqual(78, self.honeywell.target_temperature)
self.honeywell.set_temperature(74)
self.assertEqual(74, self.device.setpoint_cool)
self.assertEqual(74, self.honeywell.target_temperature)
def test_set_operation_mode(self: unittest.TestCase) -> None:
"""Test setting the operation mode."""
self.honeywell.set_operation_mode('cool')
self.assertEqual('cool', self.honeywell.current_operation)
self.assertEqual('cool', self.device.system_mode)
self.honeywell.set_operation_mode('heat')
self.assertEqual('heat', self.honeywell.current_operation)
self.assertEqual('heat', self.device.system_mode)
def test_set_temp_fail(self):
"""Test if setting the temperature fails."""
self.device.setpoint_heat = mock.MagicMock(
side_effect=somecomfort.SomeComfortError)
self.honeywell.set_temperature(123)
def test_attributes(self):
"""Test the attributes."""
expected = {
'fan': 'running',
'fanmode': 'auto',
'system_mode': 'heat',
}
self.assertEqual(expected, self.honeywell.device_state_attributes)
expected['fan'] = 'idle'
self.device.fan_running = False
self.assertEqual(expected, self.honeywell.device_state_attributes)
def test_with_no_fan(self):
"""Test if there is on fan."""
self.device.fan_running = False
self.device.fan_mode = None
expected = {
'fan': 'idle',
'fanmode': None,
'system_mode': 'heat',
}
self.assertEqual(expected, self.honeywell.device_state_attributes)