diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1e16f2d3e05..2b093ee4bc0 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -3,71 +3,140 @@ import logging from datetime import timedelta import voluptuous as vol -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.const import ( + STATE_OFF, TEMP_CELSIUS, ATTR_TEMPERATURE, STATE_UNKNOWN, CONF_NAME) from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( - STATE_HEAT, STATE_IDLE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) + STATE_HEAT, STATE_IDLE, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, STATE_MANUAL, STATE_AUTO, + STATE_ECO, STATE_COOL) from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv + DEPENDENCIES = ['netatmo'] _LOGGER = logging.getLogger(__name__) -CONF_RELAY = 'relay' -CONF_THERMOSTAT = 'thermostat' +CONF_HOMES = 'homes' +CONF_ROOMS = 'rooms' -DEFAULT_AWAY_TEMPERATURE = 14 -# # The default offset is 2 hours (when you use the thermostat itself) -DEFAULT_TIME_OFFSET = 7200 -# # Return cached results if last scan was less then this time ago -# # NetAtmo Data is uploaded to server every hour -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) + +HOME_CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, [cv.string]) +}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_RELAY): cv.string, - vol.Optional(CONF_THERMOSTAT, default=[]): - vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_HOMES): vol.All(cv.ensure_list, [HOME_CONFIG_SCHEMA]) }) +STATE_NETATMO_SCHEDULE = 'schedule' +STATE_NETATMO_HG = 'hg' +STATE_NETATMO_MAX = 'max' +STATE_NETATMO_AWAY = 'away' +STATE_NETATMO_OFF = STATE_OFF +STATE_NETATMO_MANUAL = STATE_MANUAL + +DICT_NETATMO_TO_HA = { + STATE_NETATMO_SCHEDULE: STATE_AUTO, + STATE_NETATMO_HG: STATE_COOL, + STATE_NETATMO_MAX: STATE_HEAT, + STATE_NETATMO_AWAY: STATE_ECO, + STATE_NETATMO_OFF: STATE_OFF, + STATE_NETATMO_MANUAL: STATE_MANUAL +} + +DICT_HA_TO_NETATMO = { + STATE_AUTO: STATE_NETATMO_SCHEDULE, + STATE_COOL: STATE_NETATMO_HG, + STATE_HEAT: STATE_NETATMO_MAX, + STATE_ECO: STATE_NETATMO_AWAY, + STATE_OFF: STATE_NETATMO_OFF, + STATE_MANUAL: STATE_NETATMO_MANUAL +} + SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE) +NA_THERM = 'NATherm1' +NA_VALVE = 'NRV' + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NetAtmo Thermostat.""" netatmo = hass.components.netatmo - device = config.get(CONF_RELAY) import pyatmo + homes_conf = config.get(CONF_HOMES) try: - data = ThermostatData(netatmo.NETATMO_AUTH, device) - for module_name in data.get_module_names(): - if CONF_THERMOSTAT in config: - if config[CONF_THERMOSTAT] != [] and \ - module_name not in config[CONF_THERMOSTAT]: - continue - add_entities([NetatmoThermostat(data, module_name)], True) + home_data = HomeData(netatmo.NETATMO_AUTH) except pyatmo.NoDevice: - return None + return + + homes = [] + rooms = {} + if homes_conf is not None: + for home_conf in homes_conf: + home = home_conf[CONF_NAME] + if home_conf[CONF_ROOMS] != []: + rooms[home] = home_conf[CONF_ROOMS] + homes.append(home) + else: + homes = home_data.get_home_names() + + for home in homes: + _LOGGER.debug("Setting up %s ...", home) + try: + room_data = ThermostatData(netatmo.NETATMO_AUTH, home) + except pyatmo.NoDevice: + continue + for room_id in room_data.get_room_ids(): + room_name = room_data.homedata.rooms[home][room_id]['name'] + _LOGGER.debug("Setting up %s (%s) ...", room_name, room_id) + if home in rooms and room_name not in rooms[home]: + _LOGGER.debug("Excluding %s ...", room_name) + continue + _LOGGER.debug("Adding devices for room %s (%s) ...", + room_name, room_id) + add_entities([NetatmoThermostat(room_data, room_id)], True) class NetatmoThermostat(ClimateDevice): """Representation a Netatmo thermostat.""" - def __init__(self, data, module_name, away_temp=None): + def __init__(self, data, room_id): """Initialize the sensor.""" self._data = data self._state = None - self._name = module_name + self._room_id = room_id + room_name = self._data.homedata.rooms[self._data.home][room_id]['name'] + self._name = 'netatmo_{}'.format(room_name) self._target_temperature = None self._away = None + self._module_type = self._data.room_status[room_id]['module_type'] + if self._module_type == NA_VALVE: + self._operation_list = [DICT_NETATMO_TO_HA[STATE_NETATMO_SCHEDULE], + DICT_NETATMO_TO_HA[STATE_NETATMO_MANUAL], + DICT_NETATMO_TO_HA[STATE_NETATMO_AWAY], + DICT_NETATMO_TO_HA[STATE_NETATMO_HG]] + self._support_flags = SUPPORT_FLAGS + elif self._module_type == NA_THERM: + self._operation_list = [DICT_NETATMO_TO_HA[STATE_NETATMO_SCHEDULE], + DICT_NETATMO_TO_HA[STATE_NETATMO_MANUAL], + DICT_NETATMO_TO_HA[STATE_NETATMO_AWAY], + DICT_NETATMO_TO_HA[STATE_NETATMO_HG], + DICT_NETATMO_TO_HA[STATE_NETATMO_MAX], + DICT_NETATMO_TO_HA[STATE_NETATMO_OFF]] + self._support_flags = SUPPORT_FLAGS | SUPPORT_ON_OFF + self._operation_mode = None + self.update_without_throttle = False @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + return self._support_flags @property def name(self): @@ -82,90 +151,264 @@ class NetatmoThermostat(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - return self._data.current_temperature + return self._data.room_status[self._room_id]['current_temperature'] @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._target_temperature + return self._data.room_status[self._room_id]['target_temperature'] @property def current_operation(self): """Return the current state of the thermostat.""" - state = self._data.thermostatdata.relay_cmd - if state == 0: + state = self._data.room_status[self._room_id]['heating_status'] + if state is False: return STATE_IDLE - if state == 100: + if state is True: return STATE_HEAT + return STATE_UNKNOWN + + @property + def operation_list(self): + """Return the operation modes list.""" + return self._operation_list + + @property + def operation_mode(self): + """Return current operation ie. heat, cool, idle.""" + return self._operation_mode + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + module_type = self._data.room_status[self._room_id]['module_type'] + if module_type not in (NA_THERM, NA_VALVE): + return {} + state_attributes = { + "home_id": self._data.homedata.gethomeId(self._data.home), + "room_id": self._room_id, + "setpoint_default_duration": self._data.setpoint_duration, + "away_temperature": self._data.away_temperature, + "hg_temperature": self._data.hg_temperature, + "operation_mode": self._operation_mode, + "module_type": module_type, + "module_id": self._data.room_status[self._room_id]['module_id'] + } + if module_type == NA_THERM: + state_attributes["boiler_status"] = self.current_operation + elif module_type == NA_VALVE: + state_attributes["heating_power_request"] = \ + self._data.room_status[self._room_id]['heating_power_request'] + return state_attributes @property def is_away_mode_on(self): """Return true if away mode is on.""" return self._away + @property + def is_on(self): + """Return true if on.""" + return self.target_temperature > 0 + def turn_away_mode_on(self): """Turn away on.""" - mode = "away" - temp = None - self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) - self._away = True + self.set_operation_mode(DICT_NETATMO_TO_HA[STATE_NETATMO_AWAY]) def turn_away_mode_off(self): """Turn away off.""" - mode = "program" - temp = None - self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) - self._away = False + self.set_operation_mode(DICT_NETATMO_TO_HA[STATE_NETATMO_SCHEDULE]) + + def turn_off(self): + """Turn Netatmo off.""" + _LOGGER.debug("Switching off ...") + self.set_operation_mode(DICT_NETATMO_TO_HA[STATE_NETATMO_OFF]) + self.update_without_throttle = True + self.schedule_update_ha_state() + + def turn_on(self): + """Turn Netatmo on.""" + _LOGGER.debug("Switching on ...") + _LOGGER.debug("Setting temperature first to %d ...", + self._data.hg_temperature) + self._data.homestatus.setroomThermpoint( + self._data.homedata.gethomeId(self._data.home), + self._room_id, STATE_NETATMO_MANUAL, self._data.hg_temperature) + _LOGGER.debug("Setting operation mode to schedule ...") + self._data.homestatus.setThermmode( + self._data.homedata.gethomeId(self._data.home), + STATE_NETATMO_SCHEDULE) + self.update_without_throttle = True + self.schedule_update_ha_state() + + def set_operation_mode(self, operation_mode): + """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" + if not self.is_on: + self.turn_on() + if operation_mode in [DICT_NETATMO_TO_HA[STATE_NETATMO_MAX], + DICT_NETATMO_TO_HA[STATE_NETATMO_OFF]]: + self._data.homestatus.setroomThermpoint( + self._data.homedata.gethomeId(self._data.home), + self._room_id, DICT_HA_TO_NETATMO[operation_mode]) + elif operation_mode in [DICT_NETATMO_TO_HA[STATE_NETATMO_HG], + DICT_NETATMO_TO_HA[STATE_NETATMO_SCHEDULE], + DICT_NETATMO_TO_HA[STATE_NETATMO_AWAY]]: + self._data.homestatus.setThermmode( + self._data.homedata.gethomeId(self._data.home), + DICT_HA_TO_NETATMO[operation_mode]) + self.update_without_throttle = True + self.schedule_update_ha_state() def set_temperature(self, **kwargs): """Set new target temperature for 2 hours.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is None: return - mode = "manual" - self._data.thermostatdata.setthermpoint( - mode, temperature, DEFAULT_TIME_OFFSET) - self._target_temperature = temperature - self._away = False + mode = STATE_NETATMO_MANUAL + self._data.homestatus.setroomThermpoint( + self._data.homedata.gethomeId(self._data.home), + self._room_id, DICT_HA_TO_NETATMO[mode], temp) + self.update_without_throttle = True + self.schedule_update_ha_state() - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from NetAtmo API and updates the states.""" - self._data.update() - self._target_temperature = self._data.thermostatdata.setpoint_temp - self._away = self._data.setpoint_mode == 'away' + try: + if self.update_without_throttle: + self._data.update(no_throttle=True) + self.update_without_throttle = False + else: + self._data.update() + except AttributeError: + _LOGGER.error("NetatmoThermostat::update() " + "got exception.") + return + self._target_temperature = \ + self._data.room_status[self._room_id]['target_temperature'] + self._operation_mode = DICT_NETATMO_TO_HA[ + self._data.room_status[self._room_id]['setpoint_mode']] + self._away = self._operation_mode == DICT_NETATMO_TO_HA[ + STATE_NETATMO_AWAY] + + +class HomeData: + """Representation Netatmo homes.""" + + def __init__(self, auth, home=None): + """Initialize the HomeData object.""" + self.auth = auth + self.homedata = None + self.home_names = [] + self.room_names = [] + self.schedules = [] + self.home = home + self.home_id = None + + def get_home_names(self): + """Get all the home names returned by NetAtmo API.""" + self.setup() + for home in self.homedata.homes: + if 'therm_schedules' in self.homedata.homes[home] and 'modules' \ + in self.homedata.homes[home]: + self.home_names.append(self.homedata.homes[home]['name']) + return self.home_names + + def setup(self): + """Retrieve HomeData by NetAtmo API.""" + import pyatmo + try: + self.homedata = pyatmo.HomeData(self.auth) + self.home_id = self.homedata.gethomeId(self.home) + except TypeError: + _LOGGER.error("Error when getting homedata.") + except pyatmo.NoDevice: + _LOGGER.error("Error when getting homestatus response.") class ThermostatData: """Get the latest data from Netatmo.""" - def __init__(self, auth, device=None): + def __init__(self, auth, home=None): """Initialize the data object.""" self.auth = auth - self.thermostatdata = None - self.module_names = [] - self.device = device - self.current_temperature = None - self.target_temperature = None - self.setpoint_mode = None + self.homedata = None + self.homestatus = None + self.room_ids = [] + self.room_status = {} + self.schedules = [] + self.home = home + self.away_temperature = None + self.hg_temperature = None + self.boilerstatus = None + self.setpoint_duration = None + self.home_id = None - def get_module_names(self): + def get_room_ids(self): """Return all module available on the API as a list.""" - self.update() - if not self.device: - for device in self.thermostatdata.modules: - for module in self.thermostatdata.modules[device].values(): - self.module_names.append(module['module_name']) - else: - for module in self.thermostatdata.modules[self.device].values(): - self.module_names.append(module['module_name']) - return self.module_names + if self.setup(): + for key in self.homestatus.rooms: + self.room_ids.append(key) + return self.room_ids + return [] + + def setup(self): + """Retrieve HomeData and HomeStatus by NetAtmo API.""" + import pyatmo + try: + self.homedata = pyatmo.HomeData(self.auth) + self.homestatus = pyatmo.HomeStatus(self.auth, home=self.home) + self.home_id = self.homedata.gethomeId(self.home) + self.update() + except TypeError: + _LOGGER.error("ThermostatData::setup() got error.") + return False + return True @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the NetAtmo API to update the data.""" import pyatmo - self.thermostatdata = pyatmo.ThermostatData(self.auth) - self.target_temperature = self.thermostatdata.setpoint_temp - self.setpoint_mode = self.thermostatdata.setpoint_mode - self.current_temperature = self.thermostatdata.temp + try: + self.homestatus = pyatmo.HomeStatus(self.auth, home=self.home) + except TypeError: + _LOGGER.error("Error when getting homestatus.") + return + _LOGGER.debug("Following is the debugging output for homestatus:") + _LOGGER.debug(self.homestatus.rawData) + for key in self.homestatus.rooms: + roomstatus = {} + homestatus_room = self.homestatus.rooms[key] + homedata_room = self.homedata.rooms[self.home][key] + roomstatus['roomID'] = homestatus_room['id'] + roomstatus['roomname'] = homedata_room['name'] + roomstatus['target_temperature'] = \ + homestatus_room['therm_setpoint_temperature'] + roomstatus['setpoint_mode'] = \ + homestatus_room['therm_setpoint_mode'] + roomstatus['current_temperature'] = \ + homestatus_room['therm_measured_temperature'] + roomstatus['module_type'] = \ + self.homestatus.thermostatType(self.home, key) + roomstatus['module_id'] = None + roomstatus['heating_status'] = None + roomstatus['heating_power_request'] = None + for module_id in homedata_room['module_ids']: + if self.homedata.modules[self.home][module_id]['type'] == \ + NA_THERM or roomstatus['module_id'] is None: + roomstatus['module_id'] = module_id + if roomstatus['module_type'] == NA_THERM: + self.boilerstatus = self.homestatus.boilerStatus( + rid=roomstatus['module_id']) + roomstatus['heating_status'] = self.boilerstatus + elif roomstatus['module_type'] == NA_VALVE: + roomstatus['heating_power_request'] = \ + homestatus_room['heating_power_request'] + roomstatus['heating_status'] = \ + roomstatus['heating_power_request'] > 0 + if self.boilerstatus is not None: + roomstatus['heating_status'] = \ + self.boilerstatus and roomstatus['heating_status'] + self.room_status[key] = roomstatus + self.away_temperature = self.homestatus.getAwaytemp(self.home) + self.hg_temperature = self.homestatus.getHgtemp(self.home) + self.setpoint_duration = self.homedata.setpoint_duration[self.home]