Migrate Thermostat and HVAC component to climate component (#2825)
* First draft for climate * Updates for thermostats
This commit is contained in:
parent
0abc50e844
commit
ada4de3ffb
25 changed files with 3621 additions and 17 deletions
|
@ -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
|
||||||
|
|
535
homeassistant/components/climate/__init__.py
Normal file
535
homeassistant/components/climate/__init__.py
Normal 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)
|
164
homeassistant/components/climate/demo.py
Normal file
164
homeassistant/components/climate/demo.py
Normal 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()
|
247
homeassistant/components/climate/ecobee.py
Normal file
247
homeassistant/components/climate/ecobee.py
Normal 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)
|
90
homeassistant/components/climate/eq3btsmart.py
Normal file
90
homeassistant/components/climate/eq3btsmart.py
Normal 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()
|
216
homeassistant/components/climate/generic_thermostat.py
Normal file
216
homeassistant/components/climate/generic_thermostat.py
Normal 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)
|
114
homeassistant/components/climate/heatmiser.py
Normal file
114
homeassistant/components/climate/heatmiser.py
Normal 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)
|
90
homeassistant/components/climate/homematic.py
Normal file
90
homeassistant/components/climate/homematic.py
Normal 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})
|
266
homeassistant/components/climate/honeywell.py
Normal file
266
homeassistant/components/climate/honeywell.py
Normal 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
|
83
homeassistant/components/climate/knx.py
Normal file
83
homeassistant/components/climate/knx.py
Normal 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()
|
189
homeassistant/components/climate/nest.py
Normal file
189
homeassistant/components/climate/nest.py
Normal 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
|
90
homeassistant/components/climate/proliphix.py
Normal file
90
homeassistant/components/climate/proliphix.py
Normal 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
|
136
homeassistant/components/climate/radiotherm.py
Normal file
136
homeassistant/components/climate/radiotherm.py
Normal 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
|
84
homeassistant/components/climate/services.yaml
Normal file
84
homeassistant/components/climate/services.yaml
Normal 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
|
253
homeassistant/components/climate/zwave.py
Executable file
253
homeassistant/components/climate/zwave.py
Executable 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
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
1
tests/components/climate/__init__.py
Normal file
1
tests/components/climate/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""The tests for climate component."""
|
166
tests/components/climate/test_demo.py
Normal file
166
tests/components/climate/test_demo.py
Normal 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'))
|
493
tests/components/climate/test_generic_thermostat.py
Normal file
493
tests/components/climate/test_generic_thermostat.py
Normal 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)
|
377
tests/components/climate/test_honeywell.py
Normal file
377
tests/components/climate/test_honeywell.py
Normal 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)
|
Loading…
Add table
Reference in a new issue