Added support for multiple Netatmo thermostats/valves (#19407)
* climate/netatmo: Added support for muletiple thermostats/valves * Adjusted the update interval throttle to 10 seconds * Avoid returning 'homes' without 'therm_schedules' * Requires home to have 'modules' as well as 'therm_schedules'; Using pyatmo 1.7 * Support multiple homes * Fix nest level too deep issue * Fix crashing bug when discovery is true * Fix crashing bug when discovery is true * Modifications according to review comments * Resolve format issue * Fix mode name issue * Revisions according to review's suggestions * Revisions according to review's comments * Revisions according to review's commentspull/21608/head
parent
18491c515f
commit
18372ad81b
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue