diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 9a6ccccb5e3..6b37b178a59 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -26,20 +26,11 @@ CONF_DIMMING = 'dimming' CONF_GATEWAYS = 'gateways' CONF_PLUGINS = 'plugins' CONF_RESET_COLOR = 'reset_color' - DOMAIN = 'fibaro' - FIBARO_CONTROLLERS = 'fibaro_controllers' FIBARO_DEVICES = 'fibaro_devices' - -FIBARO_COMPONENTS = [ - 'binary_sensor', - 'cover', - 'light', - 'scene', - 'sensor', - 'switch', -] +FIBARO_COMPONENTS = ['binary_sensor', 'climate', 'cover', 'light', + 'scene', 'sensor', 'switch'] FIBARO_TYPEMAP = { 'com.fibaro.multilevelSensor': "sensor", @@ -56,7 +47,11 @@ FIBARO_TYPEMAP = { 'com.fibaro.remoteSwitch': 'switch', 'com.fibaro.sensor': 'sensor', 'com.fibaro.colorController': 'light', - 'com.fibaro.securitySensor': 'binary_sensor' + 'com.fibaro.securitySensor': 'binary_sensor', + 'com.fibaro.hvac': 'climate', + 'com.fibaro.setpoint': 'climate', + 'com.fibaro.FGT001': 'climate', + 'com.fibaro.thermostatDanfoss': 'climate' } DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ @@ -174,6 +169,16 @@ class FibaroController(): """Register device with a callback for updates.""" self._callbacks[device_id] = callback + def get_children(self, device_id): + """Get a list of child devices.""" + return [ + device for device in self._device_map.values() + if device.parentId == device_id] + + def get_siblings(self, device_id): + """Get the siblings of a device.""" + return self.get_children(self._device_map[device_id].parentId) + @staticmethod def _map_device_to_type(device): """Map device to HA device type.""" @@ -229,6 +234,7 @@ class FibaroController(): devices = self._client.devices.list() self._device_map = {} self.fibaro_devices = defaultdict(list) + last_climate_parent = None for device in devices: try: device.fibaro_controller = self @@ -249,15 +255,26 @@ class FibaroController(): self._device_config.get(device.ha_id, {}) else: device.mapped_type = None - if device.mapped_type: + dtype = device.mapped_type + if dtype: device.unique_id_str = "{}.{}".format( self.hub_serial, device.id) self._device_map[device.id] = device - self.fibaro_devices[device.mapped_type].append(device) - _LOGGER.debug("%s (%s, %s) -> %s. Prop: %s Actions: %s", + if dtype != 'climate': + self.fibaro_devices[dtype].append(device) + else: + # if a sibling of this has been added, skip this one + # otherwise add the first visible device in the group + # which is a hack, but solves a problem with FGT having + # hidden compatibility devices before the real device + if last_climate_parent != device.parentId and \ + device.visible: + self.fibaro_devices[dtype].append(device) + last_climate_parent = device.parentId + _LOGGER.debug("%s (%s, %s) -> %s %s", device.ha_id, device.type, - device.baseType, device.mapped_type, - str(device.properties), str(device.actions)) + device.baseType, dtype, + str(device)) except (KeyError, ValueError): pass diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py new file mode 100644 index 00000000000..0d1ecc3a77f --- /dev/null +++ b/homeassistant/components/fibaro/climate.py @@ -0,0 +1,291 @@ +"""Support for Fibaro thermostats.""" +import logging + +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_DRY, + STATE_ECO, STATE_FAN_ONLY, STATE_HEAT, + STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE) + +from homeassistant.components.climate import ( + ClimateDevice) + +from homeassistant.const import ( + ATTR_TEMPERATURE, + STATE_OFF, + TEMP_CELSIUS, + TEMP_FAHRENHEIT) + +from . import ( + FIBARO_DEVICES, FibaroDevice) + +SPEED_LOW = 'low' +SPEED_MEDIUM = 'medium' +SPEED_HIGH = 'high' + +# State definitions missing from HA, but defined by Z-Wave standard. +# We map them to states known supported by HA here: +STATE_AUXILIARY = STATE_HEAT +STATE_RESUME = STATE_HEAT +STATE_MOIST = STATE_DRY +STATE_AUTO_CHANGEOVER = STATE_AUTO +STATE_ENERGY_HEAT = STATE_ECO +STATE_ENERGY_COOL = STATE_COOL +STATE_FULL_POWER = STATE_AUTO +STATE_FORCE_OPEN = STATE_MANUAL +STATE_AWAY = STATE_AUTO +STATE_FURNACE = STATE_HEAT + +FAN_AUTO_HIGH = 'auto_high' +FAN_AUTO_MEDIUM = 'auto_medium' +FAN_CIRCULATION = 'circulation' +FAN_HUMIDITY_CIRCULATION = 'humidity_circulation' +FAN_LEFT_RIGHT = 'left_right' +FAN_UP_DOWN = 'up_down' +FAN_QUIET = 'quiet' + +DEPENDENCIES = ['fibaro'] + +_LOGGER = logging.getLogger(__name__) + +# SDS13781-10 Z-Wave Application Command Class Specification 2019-01-04 +# Table 128, Thermostat Fan Mode Set version 4::Fan Mode encoding +FANMODES = { + 0: STATE_OFF, + 1: SPEED_LOW, + 2: FAN_AUTO_HIGH, + 3: SPEED_HIGH, + 4: FAN_AUTO_MEDIUM, + 5: SPEED_MEDIUM, + 6: FAN_CIRCULATION, + 7: FAN_HUMIDITY_CIRCULATION, + 8: FAN_LEFT_RIGHT, + 9: FAN_UP_DOWN, + 10: FAN_QUIET, + 128: STATE_AUTO +} + +# SDS13781-10 Z-Wave Application Command Class Specification 2019-01-04 +# Table 130, Thermostat Mode Set version 3::Mode encoding. +OPMODES = { + 0: STATE_OFF, + 1: STATE_HEAT, + 2: STATE_COOL, + 3: STATE_AUTO, + 4: STATE_AUXILIARY, + 5: STATE_RESUME, + 6: STATE_FAN_ONLY, + 7: STATE_FURNACE, + 8: STATE_DRY, + 9: STATE_MOIST, + 10: STATE_AUTO_CHANGEOVER, + 11: STATE_ENERGY_HEAT, + 12: STATE_ENERGY_COOL, + 13: STATE_AWAY, + 15: STATE_FULL_POWER, + 31: STATE_FORCE_OPEN +} + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Perform the setup for Fibaro controller devices.""" + if discovery_info is None: + return + + add_entities( + [FibaroThermostat(device) + for device in hass.data[FIBARO_DEVICES]['climate']], True) + + +class FibaroThermostat(FibaroDevice, ClimateDevice): + """Representation of a Fibaro Thermostat.""" + + def __init__(self, fibaro_device): + """Initialize the Fibaro device.""" + super().__init__(fibaro_device) + self._temp_sensor_device = None + self._target_temp_device = None + self._op_mode_device = None + self._fan_mode_device = None + self._support_flags = 0 + self.entity_id = 'climate.{}'.format(self.ha_id) + self._fan_mode_to_state = {} + self._fan_state_to_mode = {} + self._op_mode_to_state = {} + self._op_state_to_mode = {} + + siblings = fibaro_device.fibaro_controller.get_siblings( + fibaro_device.id) + tempunit = 'C' + for device in siblings: + if device.type == 'com.fibaro.temperatureSensor': + self._temp_sensor_device = FibaroDevice(device) + tempunit = device.properties.unit + if 'setTargetLevel' in device.actions or \ + 'setThermostatSetpoint' in device.actions: + self._target_temp_device = FibaroDevice(device) + self._support_flags |= SUPPORT_TARGET_TEMPERATURE + tempunit = device.properties.unit + if 'setMode' in device.actions or \ + 'setOperatingMode' in device.actions: + self._op_mode_device = FibaroDevice(device) + self._support_flags |= SUPPORT_OPERATION_MODE + if 'setFanMode' in device.actions: + self._fan_mode_device = FibaroDevice(device) + self._support_flags |= SUPPORT_FAN_MODE + + if tempunit == 'F': + self._unit_of_temp = TEMP_FAHRENHEIT + else: + self._unit_of_temp = TEMP_CELSIUS + + if self._fan_mode_device: + fan_modes = self._fan_mode_device.fibaro_device.\ + properties.supportedModes.split(",") + for mode in fan_modes: + try: + self._fan_mode_to_state[int(mode)] = FANMODES[int(mode)] + self._fan_state_to_mode[FANMODES[int(mode)]] = int(mode) + except KeyError: + self._fan_mode_to_state[int(mode)] = 'unknown' + + if self._op_mode_device: + prop = self._op_mode_device.fibaro_device.properties + if "supportedOperatingModes" in prop: + op_modes = prop.supportedOperatingModes.split(",") + elif "supportedModes" in prop: + op_modes = prop.supportedModes.split(",") + for mode in op_modes: + try: + self._op_mode_to_state[int(mode)] = OPMODES[int(mode)] + self._op_state_to_mode[OPMODES[int(mode)]] = int(mode) + except KeyError: + self._op_mode_to_state[int(mode)] = 'unknown' + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + _LOGGER.debug("Climate %s\n" + "- _temp_sensor_device %s\n" + "- _target_temp_device %s\n" + "- _op_mode_device %s\n" + "- _fan_mode_device %s", + self.ha_id, + self._temp_sensor_device.ha_id + if self._temp_sensor_device else "None", + self._target_temp_device.ha_id + if self._target_temp_device else "None", + self._op_mode_device.ha_id + if self._op_mode_device else "None", + self._fan_mode_device.ha_id + if self._fan_mode_device else "None") + await super().async_added_to_hass() + + # Register update callback for child devices + siblings = self.fibaro_device.fibaro_controller.get_siblings( + self.fibaro_device.id) + for device in siblings: + if device != self.fibaro_device: + self.controller.register(device.id, + self._update_callback) + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._support_flags + + @property + def fan_list(self): + """Return the list of available fan modes.""" + if self._fan_mode_device is None: + return None + return list(self._fan_state_to_mode) + + @property + def current_fan_mode(self): + """Return the fan setting.""" + if self._fan_mode_device is None: + return None + + mode = int(self._fan_mode_device.fibaro_device.properties.mode) + return self._fan_mode_to_state[mode] + + def set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + if self._fan_mode_device is None: + return + self._fan_mode_device.action( + "setFanMode", self._fan_state_to_mode[fan_mode]) + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + if self._op_mode_device is None: + return None + + if "operatingMode" in self._op_mode_device.fibaro_device.properties: + mode = int(self._op_mode_device.fibaro_device. + properties.operatingMode) + else: + mode = int(self._op_mode_device.fibaro_device.properties.mode) + return self._op_mode_to_state.get(mode) + + @property + def operation_list(self): + """Return the list of available operation modes.""" + if self._op_mode_device is None: + return None + return list(self._op_state_to_mode) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + if self._op_mode_device is None: + return + if "setOperatingMode" in self._op_mode_device.fibaro_device.actions: + self._op_mode_device.action( + "setOperatingMode", self._op_state_to_mode[operation_mode]) + elif "setMode" in self._op_mode_device.fibaro_device.actions: + self._op_mode_device.action( + "setMode", self._op_state_to_mode[operation_mode]) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._unit_of_temp + + @property + def current_temperature(self): + """Return the current temperature.""" + if self._temp_sensor_device: + device = self._temp_sensor_device.fibaro_device + return float(device.properties.value) + return None + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self._target_temp_device: + device = self._target_temp_device.fibaro_device + return float(device.properties.targetLevel) + return None + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + target = self._target_temp_device + if temperature is not None: + if "setThermostatSetpoint" in target.fibaro_device.actions: + target.action("setThermostatSetpoint", + self._op_state_to_mode[self.current_operation], + temperature) + else: + target.action("setTargetLevel", + temperature) + + @property + def is_on(self): + """Return true if on.""" + if self.current_operation == STATE_OFF: + return False + return True