diff --git a/homeassistant/components/hvac/__init__.py b/homeassistant/components/hvac/__init__.py new file mode 100644 index 00000000000..d514db364b8 --- /dev/null +++ b/homeassistant/components/hvac/__init__.py @@ -0,0 +1,491 @@ +""" +Provides functionality to interact with hvacs. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/hvac/ +""" +import logging +import os + +from homeassistant.helpers.entity_component import EntityComponent + +from homeassistant.config import load_yaml_config_file +import homeassistant.util as util +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.temperature import convert +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.components import zwave +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, + TEMP_CELCIUS) + +DOMAIN = "hvac" + +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 = "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_CURRENT_HUMIDITY = "current_humidity" +ATTR_HUMIDITY = "humidity" +ATTR_AWAY_MODE = "away_mode" +ATTR_AUX_HEAT = "aux_heat" +ATTR_FAN = "fan" +ATTR_FAN_LIST = "fan_list" +ATTR_MAX_TEMP = "max_temp" +ATTR_MIN_TEMP = "min_temp" +ATTR_MAX_HUMIDITY = "max_humidity" +ATTR_MIN_HUMIDITY = "min_humidity" +ATTR_OPERATION = "operation_mode" +ATTR_OPERATION_LIST = "operation_list" +ATTR_SWING_MODE = "swing_mode" +ATTR_SWING_LIST = "swing_list" + +_LOGGER = logging.getLogger(__name__) + +DISCOVERY_PLATFORMS = { + zwave.DISCOVER_HVAC: 'zwave' +} + + +def set_away_mode(hass, away_mode, entity_id=None): + """Turn all or specified hvac 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 hvac 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): + """Turn all or specified hvac fan mode on.""" + data = {ATTR_FAN: 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: 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, data) + + +# pylint: disable=too-many-branches +def setup(hass, config): + """Setup hvacs.""" + component = EntityComponent(_LOGGER, DOMAIN, hass, + SCAN_INTERVAL, DISCOVERY_PLATFORMS) + 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 hvacs.""" + target_hvacs = 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 hvac in target_hvacs: + if away_mode: + hvac.turn_away_mode_on() + else: + hvac.turn_away_mode_off() + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service, + descriptions.get(SERVICE_SET_AWAY_MODE)) + + def aux_heat_set_service(service): + """Set auxillary heater on target hvacs.""" + target_hvacs = 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 hvac in target_hvacs: + if aux_heat: + hvac.turn_aux_heat_on() + else: + hvac.turn_aux_heat_off() + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service, + descriptions.get(SERVICE_SET_AUX_HEAT)) + + def temperature_set_service(service): + """Set temperature on the target hvacs.""" + target_hvacs = 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 hvac in target_hvacs: + hvac.set_temperature(convert( + temperature, hass.config.temperature_unit, + hvac.unit_of_measurement)) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service, + descriptions.get(SERVICE_SET_TEMPERATURE)) + + def humidity_set_service(service): + """Set humidity on the target hvacs.""" + target_hvacs = 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 hvac in target_hvacs: + hvac.set_humidity(humidity) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service, + descriptions.get(SERVICE_SET_HUMIDITY)) + + def fan_mode_set_service(service): + """Set fan mode on target hvacs.""" + target_hvacs = component.extract_from_service(service) + + fan = service.data.get(ATTR_FAN) + + if fan is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_FAN_MODE, ATTR_FAN) + return + + for hvac in target_hvacs: + hvac.set_fan_mode(fan) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service, + descriptions.get(SERVICE_SET_FAN_MODE)) + + def operation_set_service(service): + """Set operating mode on the target hvacs.""" + target_hvacs = component.extract_from_service(service) + + operation_mode = service.data.get(ATTR_OPERATION) + + if operation_mode is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_OPERATION_MODE, ATTR_OPERATION) + return + + for hvac in target_hvacs: + hvac.set_operation(operation_mode) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service, + descriptions.get(SERVICE_SET_OPERATION_MODE)) + + def swing_set_service(service): + """Set swing mode on the target hvacs.""" + target_hvacs = 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, ATTR_SWING_MODE) + return + + for hvac in target_hvacs: + hvac.set_swing(swing_mode) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_SWING, swing_set_service, + descriptions.get(SERVICE_SET_SWING)) + return True + + +class HvacDevice(Entity): + """Representation of a hvac.""" + + # 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), + ATTR_HUMIDITY: self.target_humidity, + ATTR_CURRENT_HUMIDITY: self.current_humidity, + ATTR_MIN_HUMIDITY: self.min_humidity, + ATTR_MAX_HUMIDITY: self.max_humidity, + ATTR_FAN_LIST: self.fan_list, + ATTR_OPERATION_LIST: self.operation_list, + ATTR_SWING_LIST: self.swing_list, + ATTR_OPERATION: self.current_operation, + ATTR_FAN: self.current_fan_mode, + ATTR_SWING_MODE: self.current_swing_mode, + + } + + 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.""" + raise NotImplementedError + + @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 away mode is on.""" + 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.""" + pass + + def set_humidity(self, humidity): + """Set new target humidity.""" + pass + + def set_fan_mode(self, fan): + """Set new target fan mode.""" + pass + + def set_operation(self, operation_mode): + """Set new target operation mode.""" + pass + + def set_swing(self, swing_mode): + """Set new target swing operation.""" + pass + + def turn_away_mode_on(self): + """Turn away mode on.""" + pass + + def turn_away_mode_off(self): + """Turn away mode off.""" + pass + + def turn_aux_heat_on(self): + """Turn auxillary heater on.""" + pass + + def turn_aux_heat_off(self): + """Turn auxillary heater off.""" + pass + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._convert_for_display(7) + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._convert_for_display(35) + + @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: + return None + + value = convert(temp, self.unit_of_measurement, + self.hass.config.temperature_unit) + + if self.hass.config.temperature_unit is TEMP_CELCIUS: + decimal_count = 1 + else: + # Users of fahrenheit generally expect integer units. + decimal_count = 0 + + return round(value, decimal_count) diff --git a/homeassistant/components/hvac/demo.py b/homeassistant/components/hvac/demo.py new file mode 100644 index 00000000000..cb2f0c4b364 --- /dev/null +++ b/homeassistant/components/hvac/demo.py @@ -0,0 +1,164 @@ +""" +Demo platform that offers a fake hvac. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +from homeassistant.components.hvac import HvacDevice +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Demo hvacs.""" + add_devices([ + DemoHvac("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low", + None, None, "Auto", "Heat", None), + DemoHvac("Hvac", 21, TEMP_CELSIUS, True, 22, "On High", + 67, 54, "Off", "Cool", False), + ]) + + +# pylint: disable=too-many-arguments, too-many-public-methods +class DemoHvac(HvacDevice): + """Representation of a demo hvac.""" + + # 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 hvac.""" + 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 hvac.""" + return False + + @property + def name(self): + """Return the name of the hvac.""" + 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(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(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() diff --git a/homeassistant/components/hvac/services.yaml b/homeassistant/components/hvac/services.yaml new file mode 100644 index 00000000000..5d9f7463399 --- /dev/null +++ b/homeassistant/components/hvac/services.yaml @@ -0,0 +1,84 @@ +set_aux_heat: + description: Turn auxillary heater on/off for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.kitchen' + + aux_heat: + description: New value of axillary heater + example: true + +set_away_mode: + description: Turn away mode on/off for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.kitchen' + + away_mode: + description: New value of away mode + example: true + +set_temperature: + description: Set target temperature of hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.kitchen' + + temperature: + description: New target temperature for hvac + example: 25 + +set_humidity: + description: Set target humidity of hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.kitchen' + + humidity: + description: New target humidity for hvac + example: 60 + +set_fan_mode: + description: Set fan operation for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.nest' + + fan: + description: New value of fan mode + example: On Low + +set_operation_mode: + description: Set operation mode for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.nest' + + operation_mode: + description: New value of operation mode + example: Heat + + +set_swing_mode: + description: Set swing operation for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.nest' + + swing_mode: + description: New value of swing mode + example: 1 diff --git a/homeassistant/components/hvac/zwave.py b/homeassistant/components/hvac/zwave.py new file mode 100644 index 00000000000..f02a3e74f98 --- /dev/null +++ b/homeassistant/components/hvac/zwave.py @@ -0,0 +1,228 @@ +"""ZWave Hvac device.""" + +# Because we do not compile openzwave on CI +# pylint: disable=import-error +import logging +from homeassistant.components.hvac import DOMAIN +from homeassistant.components.hvac import HvacDevice +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 Hvac' + +REMOTEC = 0x5254 +REMOTEC_ZXT_120 = 0x8377 +REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0) + +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 Hvac 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([ZWaveHvac(value)]) + _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", + discovery_info, zwave.NETWORK) + + +# pylint: disable=too-many-arguments +class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): + """Represents a HeatControl hvac.""" + + # pylint: disable=too-many-public-methods, too-many-instance-attributes + def __init__(self, value): + """Initialize the zwave hvac.""" + 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_operation_state = None + self._current_fan_mode = None + self._fan_list = None + self._current_swing_mode = None + self._swing_list = None + self._unit = 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), + value.index) + + if specific_sensor_key in DEVICE_MAPPINGS: + if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120: + _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat as HVAC") + self._zxt_120 = 1 + + def value_changed(self, value): + """Called when a value has changed on the network.""" + if self._value.node == value.node: + self.update_properties() + self.update_ha_state(True) + _LOGGER.debug("Value changed on network %s", value) + + def update_properties(self): + """Callback on data change for the registered node/value pair.""" + # Set point + for value in self._node.get_values(class_id=0x43).values(): + if int(value.data) != 0: + self._target_temperature = int(value.data) + # Operation Mode + for value in self._node.get_values(class_id=0x40).values(): + self._current_operation = value.data + self._operation_list = list(value.data_items) + _LOGGER.debug("self._operation_list=%s", self._operation_list) + # Current Temp + for value in self._node.get_values(class_id=0x31).values(): + self._current_temperature = int(value.data) + self._unit = value.units + # Fan Mode + fan_class_id = 0x44 if self._zxt_120 else 0x42 + _LOGGER.debug("fan_class_id=%s", fan_class_id) + for value in self._node.get_values(class_id=fan_class_id).values(): + self._current_operation_state = value.data + self._fan_list = list(value.data_items) + _LOGGER.debug("self._fan_list=%s", self._fan_list) + _LOGGER.debug("self._current_operation_state=%s", + self._current_operation_state) + # Swing mode + if self._zxt_120 == 1: + for value in self._node.get_values(class_id=0x70).values(): + if value.command_class == 112 and value.index == 33: + self._current_swing_mode = value.data + self._swing_list = [0, 1] + _LOGGER.debug("self._swing_list=%s", self._swing_list) + + @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_operation_state + + @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=0x43).values(): + if value.command_class != 67: + 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 + # ZXT-120 responds only to whole int + value.data = int(round(temperature, 0)) + else: + value.data = int(temperature) + + def set_fan_mode(self, fan): + """Set new target fan mode.""" + for value in self._node.get_values(class_id=0x44).values(): + if value.command_class == 68 and value.index == 0: + value.data = bytes(fan, 'utf-8') + + def set_operation(self, operation_mode): + """Set new target operation mode.""" + for value in self._node.get_values(class_id=0x40).values(): + if value.command_class == 64 and value.index == 0: + value.data = bytes(operation_mode, 'utf-8') + + def set_swing(self, swing_mode): + """Set new target swing mode.""" + if self._zxt_120 == 1: + for value in self._node.get_values(class_id=0x70).values(): + if value.command_class == 112 and value.index == 33: + value.data = int(swing_mode) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._convert_for_display(19) + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._convert_for_display(30) diff --git a/homeassistant/components/thermostat/zwave.py b/homeassistant/components/thermostat/zwave.py index c5f00fbd457..ef72fc55c10 100644 --- a/homeassistant/components/thermostat/zwave.py +++ b/homeassistant/components/thermostat/zwave.py @@ -2,6 +2,7 @@ # Because we do not compile openzwave on CI # pylint: disable=import-error +import logging from homeassistant.components.thermostat import DOMAIN from homeassistant.components.thermostat import ( ThermostatDevice, @@ -9,19 +10,46 @@ from homeassistant.components.thermostat import ( from homeassistant.components import zwave from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS +_LOGGER = logging.getLogger(__name__) + CONF_NAME = 'name' DEFAULT_NAME = 'ZWave Thermostat' +REMOTEC = 0x5254 +REMOTEC_ZXT_120 = 0x8377 +REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0) + +WORKAROUND_IGNORE = 'ignore' + +DEVICE_MAPPINGS = { + REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_IGNORE +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the ZWave thermostats.""" 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[zwave.ATTR_NODE_ID]] value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] value.set_change_verified(False) - add_devices([ZWaveThermostat(value)]) + # 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), + value.index) + if specific_sensor_key in DEVICE_MAPPINGS: + if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_IGNORE: + _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat, ignoring") + return + else: + add_devices([ZWaveThermostat(value)]) + _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", + discovery_info, zwave.NETWORK) # pylint: disable=too-many-arguments diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 94e8b85df6b..1a15c1e3baf 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -39,6 +39,7 @@ DISCOVER_SWITCHES = "zwave.switch" DISCOVER_LIGHTS = "zwave.light" DISCOVER_BINARY_SENSORS = 'zwave.binary_sensor' DISCOVER_THERMOSTATS = 'zwave.thermostat' +DISCOVER_HVAC = 'zwave.hvac' EVENT_SCENE_ACTIVATED = "zwave.scene_activated" @@ -51,6 +52,7 @@ COMMAND_CLASS_METER = 50 COMMAND_CLASS_BATTERY = 128 COMMAND_CLASS_ALARM = 113 # 0x71 COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43 +COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 # 0x44 GENRE_WHATEVER = None GENRE_USER = "User" @@ -91,6 +93,11 @@ DISCOVERY_COMPONENTS = [ [COMMAND_CLASS_THERMOSTAT_SETPOINT], TYPE_WHATEVER, GENRE_WHATEVER), + ('hvac', + DISCOVER_HVAC, + [COMMAND_CLASS_THERMOSTAT_FAN_MODE], + TYPE_WHATEVER, + GENRE_WHATEVER), ] diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 1a3421cecaf..b8585621913 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -15,6 +15,10 @@ from homeassistant.components.sun import ( from homeassistant.components.thermostat import ( ATTR_AWAY_MODE, ATTR_FAN, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE, SERVICE_SET_TEMPERATURE) +from homeassistant.components.hvac import ( + ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION, ATTR_AUX_HEAT, + SERVICE_SET_HUMIDITY, SERVICE_SET_SWING, + SERVICE_SET_OPERATION_MODE, SERVICE_SET_AUX_HEAT) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, @@ -43,6 +47,10 @@ SERVICE_ATTRIBUTES = { SERVICE_SET_AWAY_MODE: [ATTR_AWAY_MODE], SERVICE_SET_FAN_MODE: [ATTR_FAN], SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE], + SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY], + SERVICE_SET_SWING: [ATTR_SWING_MODE], + SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION], + SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT], SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], } diff --git a/tests/components/hvac/__init__.py b/tests/components/hvac/__init__.py new file mode 100644 index 00000000000..6a5696cfb62 --- /dev/null +++ b/tests/components/hvac/__init__.py @@ -0,0 +1 @@ +"""The tests for hvac component.""" diff --git a/tests/components/hvac/test_demo.py b/tests/components/hvac/test_demo.py new file mode 100644 index 00000000000..59a51d52011 --- /dev/null +++ b/tests/components/hvac/test_demo.py @@ -0,0 +1,166 @@ +"""The tests for the demo hvac.""" +import unittest + +from homeassistant.const import ( + TEMP_CELSIUS, +) +from homeassistant.components import hvac + +from tests.common import get_test_home_assistant + + +ENTITY_HVAC = 'hvac.hvac' + + +class TestDemoHvac(unittest.TestCase): + """Test the demo 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.temperature_unit = TEMP_CELSIUS + self.assertTrue(hvac.setup(self.hass, {'hvac': { + '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_HVAC) + 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')) + 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_HVAC) + 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_HVAC) + self.assertEqual(21, state.attributes.get('temperature')) + hvac.set_temperature(self.hass, None, ENTITY_HVAC) + 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.""" + hvac.set_temperature(self.hass, 30, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + 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_HVAC) + self.assertEqual(67, state.attributes.get('humidity')) + hvac.set_humidity(self.hass, None, ENTITY_HVAC) + 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.""" + hvac.set_humidity(self.hass, 64, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + 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_HVAC) + self.assertEqual("On High", state.attributes.get('fan')) + hvac.set_fan_mode(self.hass, None, ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual("On High", state.attributes.get('fan')) + + def test_set_fan_mode(self): + """Test setting of new fan mode.""" + hvac.set_fan_mode(self.hass, "On Low", ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual("On Low", state.attributes.get('fan')) + + def test_set_swing_mode_bad_attr(self): + """Test setting swing mode without required attribute.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual("Off", state.attributes.get('swing_mode')) + hvac.set_swing_mode(self.hass, None, ENTITY_HVAC) + 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.""" + hvac.set_swing_mode(self.hass, "Auto", ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + 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_HVAC).state) + hvac.set_operation_mode(self.hass, None, ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual("Cool", self.hass.states.get(ENTITY_HVAC).state) + + def test_set_operation(self): + """Test setting of new operation mode.""" + hvac.set_operation_mode(self.hass, "Heat", ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual("Heat", self.hass.states.get(ENTITY_HVAC).state) + + def test_set_away_mode_bad_attr(self): + """Test setting the away mode without required attribute.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('on', state.attributes.get('away_mode')) + hvac.set_away_mode(self.hass, None, ENTITY_HVAC) + 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.""" + hvac.set_away_mode(self.hass, True, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('on', state.attributes.get('away_mode')) + + def test_set_away_mode_off(self): + """Test setting the away mode off/false.""" + hvac.set_away_mode(self.hass, False, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + 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_HVAC) + self.assertEqual('off', state.attributes.get('aux_heat')) + hvac.set_aux_heat(self.hass, None, ENTITY_HVAC) + 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.""" + hvac.set_aux_heat(self.hass, True, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('on', state.attributes.get('aux_heat')) + + def test_set_aux_heat_off(self): + """Test setting the auxillary heater off/false.""" + hvac.set_aux_heat(self.hass, False, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('off', state.attributes.get('aux_heat'))