diff --git a/.gitignore b/.gitignore index fd20fdd..dc38629 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,11 @@ +# Hide some OS X stuff +.DS_Store +.AppleDouble +.LSOverride +Icon -*.pyc +# GITHUB Proposed Python stuff: +*.py[cod] + +# Visual Studio Code +.vscode diff --git a/README.md b/README.md index 2cf1cd0..f9b747f 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ Custom home-assistant component for tado (using my fork of PyTado for a py3 comp It is highly inspired by https://community.home-assistant.io/t/tado-api-json/272/5 and the comments by diplix (https://community.home-assistant.io/users/diplix) -It is called `tado_v1` because it is build upon the unofficial API used by the myTado.com-Webapp. - +It is called `tado_v1` because it is build upon the unofficial API used by the myTado.com-Webapp. It will be merged as `tado` component in hass main repository, but I will leave this here for those willing to use a custom component. # Howto use I created a new custom_component which adds multiple sensors for every zone in myTado.com (not for every device) @@ -19,8 +18,8 @@ For hass manual installation it is: ## Edit configuration.yaml ``` tado_v1: - mytado_username: <.. your username ..> - mytado_password: <.. your password ..> + username: <.. your username ..> + password: <.. your password ..> ``` ## Use the new sensors in home-assistant diff --git a/custom_components/climate/tado_v1.py b/custom_components/climate/tado_v1.py old mode 100644 new mode 100755 index f801973..9c57dec --- a/custom_components/climate/tado_v1.py +++ b/custom_components/climate/tado_v1.py @@ -1,67 +1,112 @@ """ -tado component to create a climate device for each zone -""" +Tado component to create a climate device for each zone. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.tado/ +""" import logging from homeassistant.const import TEMP_CELSIUS -from homeassistant.helpers.event import track_state_change +from homeassistant.components.climate import ClimateDevice +from homeassistant.const import ATTR_TEMPERATURE +DATA_TADO = 'tado_v1_data' -from homeassistant.components.climate import ( - ClimateDevice) -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_TEMPERATURE) +_LOGGER = logging.getLogger(__name__) -CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode -CONST_MODE_OFF = "OFF" # Switch off heating in a zone +CONST_MODE_SMART_SCHEDULE = 'SMART_SCHEDULE' # Default mytado mode +CONST_MODE_OFF = 'OFF' # Switch off heating in a zone # When we change the temperature setting, we need an overlay mode -CONST_OVERLAY_TADO_MODE = "TADO_MODE" # wait until tado changes the mode automatic -CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually -CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan - -CONST_DEFAULT_OPERATION_MODE = CONST_OVERLAY_TADO_MODE # will be used when changing temperature -CONST_DEFAULT_OFF_MODE = CONST_OVERLAY_MANUAL # will be used when switching to CONST_MODE_OFF +# wait until tado changes the mode automatic +CONST_OVERLAY_TADO_MODE = 'TADO_MODE' +# the user has change the temperature or mode manually +CONST_OVERLAY_MANUAL = 'MANUAL' +# the temperature will be reset after a timespan +CONST_OVERLAY_TIMER = 'TIMER' + +CONST_MODE_FAN_HIGH = 'HIGH' +CONST_MODE_FAN_MIDDLE = 'MIDDLE' +CONST_MODE_FAN_LOW = 'LOW' + +FAN_MODES_LIST = { + CONST_MODE_FAN_HIGH: 'High', + CONST_MODE_FAN_MIDDLE: 'Middle', + CONST_MODE_FAN_LOW: 'Low', + CONST_MODE_OFF: 'Off', +} + +OPERATION_LIST = { + CONST_OVERLAY_MANUAL: 'Manual', + CONST_OVERLAY_TIMER: 'Timer', + CONST_OVERLAY_TADO_MODE: 'Tado mode', + CONST_MODE_SMART_SCHEDULE: 'Smart schedule', + CONST_MODE_OFF: 'Off', +} -# DOMAIN = 'tado_v1' - -_LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = ['temperature', 'humidity', 'tado mode', 'power', 'overlay'] def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the climate platform.""" - # pylint: disable=W0613 - - # get the PyTado object from the hub component - tado = hass.data['Mytado'] + """Set up the Tado climate platform.""" + tado = hass.data[DATA_TADO] try: - zones = tado.getZones() + zones = tado.get_zones() except RuntimeError: _LOGGER.error("Unable to get zone info from mytado") return False - tado_data = TadoData(tado) - climate_devices = [] for zone in zones: - climate_devices.append(tado_data.create_climate_device(hass, zone['name'], zone['id'])) + climate_devices.append(create_climate_device( + tado, hass, zone, zone['name'], zone['id'])) if len(climate_devices) > 0: - add_devices(climate_devices) - tado_data.activate_tracking(hass) + add_devices(climate_devices, True) return True else: return False + +def create_climate_device(tado, hass, zone, name, zone_id): + """Create a Tado climate device.""" + capabilities = tado.get_capabilities(zone_id) + + unit = TEMP_CELSIUS + ac_mode = capabilities['type'] == 'AIR_CONDITIONING' + + if ac_mode: + min_temp = float(capabilities['HEAT']['temperatures']['celsius']['min']) + max_temp = float(capabilities['HEAT']['temperatures']['celsius']['max']) + else: + min_temp = float(capabilities['temperatures']['celsius']['min']) + max_temp = float(capabilities['temperatures']['celsius']['max']) + + data_id = 'zone {} {}'.format(name, zone_id) + device = TadoClimate(tado, + name, zone_id, data_id, + hass.config.units.temperature(min_temp, unit), + hass.config.units.temperature(max_temp, unit), + ac_mode) + + tado.add_sensor(data_id, { + 'id': zone_id, + 'zone': zone, + 'name': name, + 'climate': device + }) + + return device + + class TadoClimate(ClimateDevice): """Representation of a tado climate device.""" - # pylint: disable=R0902, R0913 - def __init__(self, tado, zone_name, zone_id, - min_temp, max_temp, target_temp, ac_mode, + def __init__(self, store, zone_name, zone_id, data_id, + min_temp, max_temp, ac_mode, tolerance=0.3): - self._tado = tado + """Initialization of Tado climate device.""" + self._store = store + self._data_id = data_id + self.zone_name = zone_name self.zone_id = zone_id @@ -70,40 +115,25 @@ def __init__(self, tado, zone_name, zone_id, self._active = False self._device_is_active = False + self._unit = TEMP_CELSIUS self._cur_temp = None self._cur_humidity = None self._is_away = False self._min_temp = min_temp self._max_temp = max_temp - self._target_temp = target_temp + self._target_temp = None self._tolerance = tolerance - self._unit = TEMP_CELSIUS + self._cooling = False - self._operation_list = [CONST_OVERLAY_MANUAL, CONST_OVERLAY_TIMER, CONST_OVERLAY_TADO_MODE, - CONST_MODE_SMART_SCHEDULE, CONST_MODE_OFF] + self._current_fan = CONST_MODE_OFF self._current_operation = CONST_MODE_SMART_SCHEDULE - self._overlay_mode = self._current_operation - - @property - def should_poll(self): - """No Polling needed for tado climate device (because it reuses sensors).""" - return False + self._overlay_mode = CONST_MODE_SMART_SCHEDULE @property def name(self): """Return the name of the sensor.""" return self.zone_name - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._unit - - @property - def state(self): - """Return the current temperature as the state, instead of operation_mode""" - return self._cur_temp - @property def current_humidity(self): """Return the current humidity.""" @@ -116,24 +146,43 @@ def current_temperature(self): @property def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return self._current_operation + """Return current readable operation mode.""" + if self._cooling: + return "Cooling" + else: + return OPERATION_LIST.get(self._current_operation) @property def operation_list(self): - """List of available operation modes.""" - return self._operation_list + """List of available operation modes (readable).""" + return list(OPERATION_LIST.values()) + + @property + def current_fan_mode(self): + """Return the fan setting.""" + if self.ac_mode: + return FAN_MODES_LIST.get(self._current_fan) + else: + return None + + @property + def fan_list(self): + """List of available fan modes.""" + if self.ac_mode: + return list(FAN_MODES_LIST.values()) + else: + return None + + @property + def temperature_unit(self): + """The unit of measurement used by the platform.""" + return self._unit @property def is_away_mode_on(self): """Return true if away mode is on.""" return self._is_away - @property - def _is_device_active(self): - """If the toggleable device is currently active.""" - return self._device_is_active - @property def target_temperature(self): """Return the temperature we try to reach.""" @@ -145,172 +194,147 @@ def set_temperature(self, **kwargs): if temperature is None: return - self._current_operation = CONST_DEFAULT_OPERATION_MODE + self._current_operation = CONST_OVERLAY_TADO_MODE self._overlay_mode = None self._target_temp = temperature self._control_heating() - self.update_ha_state() - def set_operation_mode(self, operation_mode): - """Set new target temperature.""" + def set_operation_mode(self, readable_operation_mode): + """Set new operation mode.""" + operation_mode = CONST_MODE_SMART_SCHEDULE + + for mode, readable in OPERATION_LIST.items(): + if readable == readable_operation_mode: + operation_mode = mode + break + self._current_operation = operation_mode self._overlay_mode = None 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) + return super().min_temp @property def max_temp(self): """Return the maximum temperature.""" - # pylint: disable=no-member if self._max_temp: return self._max_temp else: - # Get default temp from super class - return ClimateDevice.max_temp.fget(self) + # Get default temp from super class + return super().max_temp - def sensor_changed(self, entity_id, old_state, new_state): - # pylint: disable=W0613 - """Called when a depending sensor changes.""" - if new_state is None or new_state.state is None: - return + def update(self): + """Update the state of this climate device.""" + self._store.update() - self.update_state(entity_id, new_state, True) + data = self._store.get_data(self._data_id) - def update_state(self, entity_type, state, update_ha): - """update the internal state.""" - if state.state == "unknown": + if data is None: + _LOGGER.debug("Recieved no data for zone %s", self.zone_name) return - _LOGGER.info("%s changed to %s", entity_type, state.state) + if 'sensorDataPoints' in data: + sensor_data = data['sensorDataPoints'] - try: - if entity_type.endswith("temperature"): - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + unit = TEMP_CELSIUS + if 'insideTemperature' in sensor_data: + temperature = float( + sensor_data['insideTemperature']['celsius']) self._cur_temp = self.hass.config.units.temperature( - float(state.state), unit) - + temperature, unit) + + if 'humidity' in sensor_data: + humidity = float( + sensor_data['humidity']['percentage']) + self._cur_humidity = humidity + + # temperature setting will not exist when device is off + if 'temperature' in data['setting'] and \ + data['setting']['temperature'] is not None: + setting = float( + data['setting']['temperature']['celsius']) self._target_temp = self.hass.config.units.temperature( - float(state.attributes.get("setting")), unit) - - elif entity_type.endswith("humidity"): - self._cur_humidity = float(state.state) - - elif entity_type.endswith("tado mode"): - self._is_away = state.state == "AWAY" - - elif entity_type.endswith("power"): - if state.state == "OFF": - self._current_operation = CONST_MODE_OFF - self._device_is_active = False - else: - self._device_is_active = True - - elif entity_type.endswith("overlay"): - # if you set mode manualy to off, there will be an overlay - # and a termination, but we want to see the mode "OFF" - overlay = state.state - termination = state.attributes.get("termination") - - if overlay == "True" and self._device_is_active: - # there is an overlay the device is on - self._overlay_mode = termination - self._current_operation = termination - elif overlay == "False": - # there is no overlay, the mode will always be - # "SMART_SCHEDULE" - self._overlay_mode = CONST_MODE_SMART_SCHEDULE - self._current_operation = CONST_MODE_SMART_SCHEDULE - - if update_ha: - self.schedule_update_ha_state() - - except ValueError: - _LOGGER.error("Unable to update from sensor: %s", entity_type) + setting, unit) + + if 'tadoMode' in data: + mode = data['tadoMode'] + self._is_away = mode == 'AWAY' + + if 'setting' in data: + power = data['setting']['power'] + if power == 'OFF': + self._current_operation = CONST_MODE_OFF + self._current_fan = CONST_MODE_OFF + # There is no overlay, the mode will always be + # "SMART_SCHEDULE" + self._overlay_mode = CONST_MODE_SMART_SCHEDULE + self._device_is_active = False + else: + self._device_is_active = True + + if self._device_is_active: + overlay = False + overlay_data = None + termination = self._current_operation + cooling = False + fan_speed = CONST_MODE_OFF + + if 'overlay' in data: + overlay_data = data['overlay'] + overlay = overlay_data is not None + + if overlay: + termination = overlay_data['termination']['type'] + + if 'setting' in overlay_data: + cooling = overlay_data['setting']['mode'] == 'COOL' + fan_speed = overlay_data['setting']['fanSpeed'] + + # If you set mode manualy to off, there will be an overlay + # and a termination, but we want to see the mode "OFF" + + self._overlay_mode = termination + self._current_operation = termination + self._cooling = cooling + self._current_fan = fan_speed def _control_heating(self): """Send new target temperature to mytado.""" - if not self._active and None not in (self._cur_temp, - self._target_temp): + if not self._active and None not in ( + self._cur_temp, self._target_temp): self._active = True - _LOGGER.info('Obtained current and target temperature. ' - 'tado thermostat active.') + _LOGGER.info("Obtained current and target temperature. " + "Tado thermostat active") if not self._active or self._current_operation == self._overlay_mode: return if self._current_operation == CONST_MODE_SMART_SCHEDULE: - _LOGGER.info('Switching mytado.com to SCHEDULE (default) for zone %s', self.zone_name) - self._tado.resetZoneOverlay(self.zone_id) + _LOGGER.info("Switching mytado.com to SCHEDULE (default) " + "for zone %s", self.zone_name) + self._store.reset_zone_overlay(self.zone_id) self._overlay_mode = self._current_operation return if self._current_operation == CONST_MODE_OFF: - _LOGGER.info('Switching mytado.com to OFF for zone %s', self.zone_name) - self._tado.setZoneOverlay(self.zone_id, CONST_DEFAULT_OFF_MODE) + _LOGGER.info("Switching mytado.com to OFF for zone %s", + self.zone_name) + self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL) self._overlay_mode = self._current_operation return _LOGGER.info("Switching mytado.com to %s mode for zone %s", self._current_operation, self.zone_name) - self._tado.setZoneOverlay(self.zone_id, self._current_operation, self._target_temp) - self._overlay_mode = self._current_operation + self._store.set_zone_overlay( + self.zone_id, self._current_operation, self._target_temp) -class TadoData(object): - """Tado data object to control the tado functionality""" - def __init__(self, tado): - self._tado = tado - self._tracking_active = False - - self.sensors = [] - - def create_climate_device(self, hass, name, tado_id): - """create a climate device""" - capabilities = self._tado.getCapabilities(tado_id) - - min_temp = float(capabilities["temperatures"]["celsius"]["min"]) - max_temp = float(capabilities["temperatures"]["celsius"]["max"]) - target_temp = 21 - ac_mode = capabilities["type"] != "HEATING" - - device_id = 'climate {} {}'.format(name, tado_id) - device = TadoClimate(self._tado, name, tado_id, - min_temp, max_temp, target_temp, ac_mode) - sensor = { - "id" : device_id, - "device" : device, - "sensors" : [] - } - - self.sensors.append(sensor) - - for sensor_type in SENSOR_TYPES: - entity_id = 'sensor.{} {}'.format(name, sensor_type).lower().replace(" ", "_") - sensor["sensors"].append(entity_id) - - sensor_state = hass.states.get(entity_id) - if sensor_state: - device.update_state(sensor_type, sensor_state, False) - - return device - - def activate_tracking(self, hass): - """activate tracking of dependend sensors""" - if self._tracking_active is False: - for data in self.sensors: - for entity_id in data["sensors"]: - track_state_change(hass, entity_id, data["device"].sensor_changed) - _LOGGER.info("activated state tracking for %s.", entity_id) - - self._tracking_active = True + self._overlay_mode = self._current_operation diff --git a/custom_components/sensor/tado_v1.py b/custom_components/sensor/tado_v1.py old mode 100644 new mode 100755 index 45c00ee..093ba3d --- a/custom_components/sensor/tado_v1.py +++ b/custom_components/sensor/tado_v1.py @@ -1,79 +1,95 @@ -""" -tado component to create some sensors for each zone -""" +"""tado component to create some sensors for each zone.""" import logging -from datetime import timedelta from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -# DOMAIN = 'tado_v1' +DATA_TADO = 'tado_v1_data' _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = ['temperature', 'humidity', 'power', 'link', 'heating', 'tado mode', 'overlay'] -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the sensor platform.""" - # pylint: disable=W0613 - - # get the PyTado object from the hub component - tado = hass.data['Mytado'] + # get the PyTado object from the hub component + tado = hass.data[DATA_TADO] try: - zones = tado.getZones() + zones = tado.get_zones() except RuntimeError: _LOGGER.error("Unable to get zone info from mytado") return False - tado_data = TadoData(tado, MIN_TIME_BETWEEN_SCANS) - sensor_items = [] for zone in zones: if zone['type'] == 'HEATING': for variable in SENSOR_TYPES: - sensor_items.append(tado_data.create_zone_sensor( - zone, zone['name'], zone['id'], variable)) + sensor_items.append(create_zone_sensor( + tado, zone, zone['name'], zone['id'], + variable)) - me_data = tado.getMe() - sensor_items.append(tado_data.create_device_sensor( - me_data, me_data['homes'][0]['name'], me_data['homes'][0]['id'], "tado bridge status")) - - tado_data.update() + me_data = tado.get_me() + sensor_items.append(create_device_sensor( + tado, me_data, me_data['homes'][0]['name'], + me_data['homes'][0]['id'], "tado bridge status")) if len(sensor_items) > 0: - add_devices(sensor_items) + add_devices(sensor_items, True) return True else: return False + +def create_zone_sensor(tado, zone, name, zone_id, variable): + """Create a zone sensor.""" + data_id = 'zone {} {}'.format(name, zone_id) + + tado.add_sensor(data_id, { + "zone": zone, + "name": name, + "id": zone_id, + "data_id": data_id + }) + + return TadoSensor(tado, name, zone_id, variable, data_id) + + +def create_device_sensor(tado, device, name, device_id, variable): + """Create a device sensor.""" + data_id = 'device {} {}'.format(name, device_id) + + tado.add_sensor(data_id, { + "device": device, + "name": name, + "id": device_id, + "data_id": data_id + }) + + return TadoSensor(tado, name, device_id, variable, data_id) + + class TadoSensor(Entity): """Representation of a tado Sensor.""" - # pylint: disable=R0902, R0913 - def __init__(self, tado_data, zone_name, zone_id, zone_variable, data_id): - self._tado_data = tado_data + def __init__(self, store, zone_name, zone_id, zone_variable, data_id): + """Initialization of TadoSensor class.""" + self._store = store + self.zone_name = zone_name self.zone_id = zone_id self.zone_variable = zone_variable + self._unique_id = '{} {}'.format(zone_variable, zone_id) self._data_id = data_id self._state = None self._state_attributes = None - @property - def should_poll(self): - """Polling needed for tado sensors.""" - return True - @property def unique_id(self): - """Return the unique id""" + """Return the unique id.""" return self._unique_id @property @@ -87,7 +103,7 @@ def state(self): return self._state @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes.""" return self._state_attributes @@ -95,7 +111,7 @@ def state_attributes(self): def unit_of_measurement(self): """Return the unit of measurement.""" if self.zone_variable == "temperature": - return TEMP_CELSIUS + return self.hass.config.units.temperature_unit elif self.zone_variable == "humidity": return '%' elif self.zone_variable == "heating": @@ -103,39 +119,57 @@ def unit_of_measurement(self): @property def icon(self): - """Icon for the sensor""" + """Icon for the sensor.""" if self.zone_variable == "temperature": return 'mdi:thermometer' elif self.zone_variable == "humidity": return 'mdi:water-percent' def update(self): - """Update method called when should_poll is true""" - self._tado_data.update() + """Update method called when should_poll is true.""" + self._store.update() + + data = self._store.get_data(self._data_id) + + if data is None: + _LOGGER.debug('Recieved no data for zone %s', + self.zone_name) + return - self.push_update(self._tado_data.get_data(self._data_id), True) + unit = TEMP_CELSIUS - def push_update(self, data, update_ha): - """Push the update to the current object""" # pylint: disable=R0912 if self.zone_variable == 'temperature': if 'sensorDataPoints' in data: - self._state = float(data['sensorDataPoints']['insideTemperature']['celsius']) + sensor_data = data['sensorDataPoints'] + temperature = float( + sensor_data['insideTemperature']['celsius']) + + self._state = self.hass.config.units.temperature( + temperature, unit) self._state_attributes = { - "time": data['sensorDataPoints']['insideTemperature']['timestamp'], - "setting": 0 # setting is used in climate device + "time": + sensor_data['insideTemperature']['timestamp'], + "setting": 0 # setting is used in climate device } # temperature setting will not exist when device is off - if 'temperature' in data['setting'] and data['setting']['temperature'] is not None: - self._state_attributes["setting"] = float( + if 'temperature' in data['setting'] and \ + data['setting']['temperature'] is not None: + temperature = float( data['setting']['temperature']['celsius']) + self._state_attributes["setting"] = \ + self.hass.config.units.temperature( + temperature, unit) + elif self.zone_variable == 'humidity': if 'sensorDataPoints' in data: - self._state = float(data['sensorDataPoints']['humidity']['percentage']) + sensor_data = data['sensorDataPoints'] + self._state = float( + sensor_data['humidity']['percentage']) self._state_attributes = { - "time": data['sensorDataPoints']['humidity']['timestamp'], + "time": sensor_data['humidity']['timestamp'], } elif self.zone_variable == 'power': @@ -148,9 +182,11 @@ def push_update(self, data, update_ha): elif self.zone_variable == 'heating': if 'activityDataPoints' in data: - self._state = float(data['activityDataPoints']['heatingPower']['percentage']) + activity_data = data['activityDataPoints'] + self._state = float( + activity_data['heatingPower']['percentage']) self._state_attributes = { - "time": data['activityDataPoints']['heatingPower']['timestamp'], + "time": activity_data['heatingPower']['timestamp'], } elif self.zone_variable == 'tado bridge status': @@ -163,7 +199,6 @@ def push_update(self, data, update_ha): elif self.zone_variable == 'overlay': if 'overlay' in data and data['overlay'] is not None: - # pylint: disable=R0204 self._state = True self._state_attributes = { "termination": data['overlay']['termination']['type'], @@ -171,74 +206,3 @@ def push_update(self, data, update_ha): else: self._state = False self._state_attributes = {} - - if update_ha: - super().update_ha_state() - -class TadoData(object): - """Tado data object to control the tado functionality""" - def __init__(self, tado, interval): - self._tado = tado - - self.sensors = {} - self.data = {} - - # Apply throttling to methods using configured interval - self.update = Throttle(interval)(self._update) - - def create_zone_sensor(self, zone, name, zone_id, variable): - """create a zone sensor""" - data_id = 'zone {} {}'.format(name, zone_id) - - self.sensors[data_id] = { - "zone" : zone, - "name" : name, - "id" : zone_id, - "data_id" : data_id - } - self.data[data_id] = None - - return TadoSensor(self, name, zone_id, variable, data_id) - - def create_device_sensor(self, device, name, device_id, variable): - """create a device sensor""" - data_id = 'device {} {}'.format(name, device_id) - - self.sensors[data_id] = { - "device" : device, - "name" : name, - "id" : device_id, - "data_id" : data_id - } - self.data[data_id] = None - - return TadoSensor(self, name, device_id, variable, data_id) - - def get_data(self, data_id): - """get the cached data""" - data = {"error" : "no data"} - - if data_id in self.data: - data = self.data[data_id] - - return data - - def _update(self): - """update the internal data-array from mytado.com""" - for data_id, sensor in self.sensors.items(): - data = None - - try: - if "zone" in sensor: - _LOGGER.info("querying mytado.com for zone %s %s", - sensor["id"], sensor["name"]) - data = self._tado.getState(sensor["id"]) - if "device" in sensor: - _LOGGER.info("querying mytado.com for device %s %s", - sensor["id"], sensor["name"]) - data = self._tado.getDevices()[0] - - except RuntimeError: - _LOGGER.error("Unable to connect to myTado. %s %s", sensor["id"], sensor["id"]) - - self.data[data_id] = data diff --git a/custom_components/tado_v1.py b/custom_components/tado_v1.py old mode 100644 new mode 100755 index 8a734e1..c450fe9 --- a/custom_components/tado_v1.py +++ b/custom_components/tado_v1.py @@ -1,54 +1,128 @@ """ -main file for the (unofficial) tado component -""" +Support for the (unofficial) Tado api. +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tado/ +""" import logging import urllib +from datetime import timedelta + +import voluptuous as vol from homeassistant.helpers.discovery import load_platform from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.util import Throttle -import voluptuous as vol - +REQUIREMENTS = ['https://github.com/wmalgadey/PyTado/archive/' + '0.2.1.zip#' + 'PyTado==0.2.1'] _LOGGER = logging.getLogger(__name__) +DATA_TADO = 'tado_v1_data' DOMAIN = 'tado_v1' -REQUIREMENTS = ['https://github.com/wmalgadey/PyTado/archive/0.2.1.zip#PyTado==0.2.1'] +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) -TADO_V1_COMPONENTS = [ +TADO_COMPONENTS = [ 'sensor', 'climate' ] -CONF_MYTADO_USERNAME = 'mytado_username' -CONF_MYTADO_PASSWORD = 'mytado_password' - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_MYTADO_USERNAME, default=''): cv.string, - vol.Required(CONF_MYTADO_PASSWORD, default=''): cv.string + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string }) }, extra=vol.ALLOW_EXTRA) def setup(hass, config): - """Your controller/hub specific code.""" - - username = config[DOMAIN][CONF_MYTADO_USERNAME] - password = config[DOMAIN][CONF_MYTADO_PASSWORD] + """Set up of the Tado component.""" + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] from PyTado.interface import Tado try: tado = Tado(username, password) + tado.setDebugging(True) except (RuntimeError, urllib.error.HTTPError): _LOGGER.error("Unable to connect to mytado with username and password") return False - hass.data['Mytado'] = tado + hass.data[DATA_TADO] = TadoDataStore(tado) - for component in TADO_V1_COMPONENTS: + for component in TADO_COMPONENTS: load_platform(hass, component, DOMAIN, {}, config) return True + + +class TadoDataStore: + """An object to store the Tado data.""" + + def __init__(self, tado): + """Initialize Tado data store.""" + self.tado = tado + + self.sensors = {} + self.data = {} + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update the internal data from mytado.com.""" + for data_id, sensor in list(self.sensors.items()): + data = None + + try: + if 'zone' in sensor: + _LOGGER.info("Querying mytado.com for zone %s %s", + sensor['id'], sensor['name']) + data = self.tado.getState(sensor['id']) + + if 'device' in sensor: + _LOGGER.info("Querying mytado.com for device %s %s", + sensor['id'], sensor['name']) + data = self.tado.getDevices()[0] + + except RuntimeError: + _LOGGER.error("Unable to connect to myTado. %s %s", + sensor['id'], sensor['id']) + + self.data[data_id] = data + + def add_sensor(self, data_id, sensor): + """Add a sensor to update in _update().""" + self.sensors[data_id] = sensor + self.data[data_id] = None + + def get_data(self, data_id): + """Get the cached data.""" + data = {'error': 'no data'} + + if data_id in self.data: + data = self.data[data_id] + + return data + + def get_zones(self): + """Wrapper for getZones().""" + return self.tado.getZones() + + def get_capabilities(self, tado_id): + """Wrapper for getCapabilities(..).""" + return self.tado.getCapabilities(tado_id) + + def get_me(self): + """Wrapper for getMet().""" + return self.tado.getMe() + + def reset_zone_overlay(self, zone_id): + """Wrapper for resetZoneOverlay(..).""" + return self.tado.resetZoneOverlay(zone_id) + + def set_zone_overlay(self, zone_id, mode, temperature=None, duration=None): + """Wrapper for setZoneOverlay(..).""" + return self.tado.setZoneOverlay(zone_id, mode, temperature, duration)