diff --git a/README.md b/README.md index 68f4283..b28c69c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Currently supports: - Reading current temperature - Reading target temperature - Setting target temperature +- Changing hold_mode (i.e. preset) +- Getting scheduled state (and schedules) ## Installation - Download release or the master branch as zip @@ -20,3 +22,13 @@ climate: port: port_number scan_interval: 10 ``` + +## Changing hold_mode + +This is done using HASS service `climate.set_hold_mode` with service data like: + +```json +{ + "entity_id": "climate.anna_thermostaat","hold_mode":"away" +} +``` diff --git a/custom_components/anna/__init__.py b/custom_components/anna/__init__.py new file mode 100644 index 0000000..eba8bf0 --- /dev/null +++ b/custom_components/anna/__init__.py @@ -0,0 +1 @@ +REQUIREMENTS = ['haanna==0.6.2'] diff --git a/custom_components/anna/climate.py b/custom_components/anna/climate.py new file mode 100644 index 0000000..f1b9e8d --- /dev/null +++ b/custom_components/anna/climate.py @@ -0,0 +1,245 @@ +""" +Plugwise Anna component for HomeAssistant + +configurations.yaml + +climate: + - platform: anna + name: Anna Thermostat + username: smile + password: short_id + host: 192.168.1.60 + port: 80 + min_temp: 4 + max_temp: 30 + scan_interval: 10 # optional +""" + +import voluptuous as vol +import logging + +import xml.etree.cElementTree as Etree + +import haanna + +from homeassistant.components.climate import ( + ClimateDevice, + PLATFORM_SCHEMA) + +from homeassistant.components.climate.const import ( + DOMAIN, + SUPPORT_HOLD_MODE, + SUPPORT_AWAY_MODE, + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + STATE_AUTO, + STATE_IDLE, + SERVICE_SET_HOLD_MODE) + +from homeassistant.const import ( + CONF_NAME, + CONF_HOST, + CONF_PORT, + CONF_USERNAME, + CONF_PASSWORD, + TEMP_CELSIUS, + ATTR_TEMPERATURE, + STATE_ON, + STATE_OFF) + +import homeassistant.helpers.config_validation as cv + +from homeassistant.exceptions import PlatformNotReady + +SUPPORT_FLAGS = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE ) +#SUPPORT_FLAGS = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_HOLD_MODE | SUPPORT_AWAY_MODE ) + +_LOGGER = logging.getLogger(__name__) + +# Configuration directives +CONF_MIN_TEMP = 'min_temp' +CONF_MAX_TEMP = 'max_temp' + +DEFAULT_NAME = 'Anna Thermostat' +DEFAULT_USERNAME = 'smile' +DEFAULT_TIMEOUT = 10 +BASE_URL = 'http://{0}:{1}{2}' +DEFAULT_ICON = "mdi:thermometer" + +# Hold modes +HOLD_MODE_HOME = "home" +HOLD_MODE_VACATION = "vacation" +HOLD_MODE_NO_FROST = "no_frost" +HOLD_MODE_SLEEP = "asleep" +HOLD_MODE_AWAY = "away" + +HOLD_MODES = [ HOLD_MODE_HOME, HOLD_MODE_VACATION, HOLD_MODE_NO_FROST, HOLD_MODE_SLEEP, HOLD_MODE_AWAY ] +# Operation list +# todo; read these (schedules) from API +DEFAULT_OPERATION_LIST = [ STATE_AUTO, STATE_IDLE ] + +# Change defaults to match Anna +DEFAULT_MIN_TEMP = 4 +DEFAULT_MAX_TEMP = 30 + +# Read platform configuration +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): cv.positive_int, + vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): cv.positive_int, + vol.Required(CONF_PASSWORD): cv.string +}) + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Anna thermostat""" + add_devices([ + ThermostatDevice( + config.get(CONF_NAME), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(CONF_MIN_TEMP), + config.get(CONF_MAX_TEMP) + ) + ]) + +_LOGGER.debug("Anna: custom component loading (Anna PlugWise climate)") + +class ThermostatDevice(ClimateDevice): + """Representation of an Anna thermostat""" + def __init__(self, name, username, password, host, port, min_temp, max_temp): + _LOGGER.debug("Anna: Init called") + self._name = name + self._username = username + self._password = password + self._host = host + self._port = port + self._temperature = None + self._current_temperature = None + self._outdoor_temperature = None + self._state = None + self._hold_mode = None + self._away_mode = False + self._min_temp = min_temp + self._max_temp = max_temp + self._operation_list = DEFAULT_OPERATION_LIST + + _LOGGER.debug("Anna: Initializing API") + self._api = haanna.Haanna(self._username, self._password, self._host, self._port) + try: + self._api.ping_anna_thermostat() + except: + _LOGGER.error("Anna: Unable to ping, platform not ready") + raise PlatformNotReady + _LOGGER.debug("Anna: platform ready") + self.update() + + @property + def should_poll(self): + """Polling is needed""" + return True + + @property + def state(self): + """Return the current state""" + return self._state + + def update(self): + """Update the data from the thermostat""" + _LOGGER.debug("Anna: Update called") + domain_objects = self._api.get_domain_objects() + self._current_temperature = self._api.get_temperature(domain_objects) + self._outdoor_temperature = self._api.get_outdoor_temperature(domain_objects) + self._temperature = self._api.get_target_temperature(domain_objects) + self._hold_mode = self._api.get_current_preset(domain_objects) + if self._api.get_mode(domain_objects) == True: + self._operation_mode=STATE_AUTO + else: + self._operation_mode=STATE_IDLE + if self._api.get_heating_status(domain_objects) == True: + self._state=STATE_ON + else: + self._state=STATE_OFF + + @property + def name(self): + return self._name + + @property + def current_hold_mode(self): + """Return the current hold mode, e.g., home, away, temp.""" + return self._hold_mode + + @property + def operation_list(self): + """Return the operation modes list.""" + return self._operation_list + + @property + def current_operation(self): + """Return current operation ie. auto, idle.""" + return self._operation_mode + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return DEFAULT_ICON + + @property + def current_temperature(self): + return self._current_temperature + + @property + def min_temp(self): + return self._min_temp + + @property + def max_temp(self): + return self._max_temp + + @property + def target_temperature(self): + return self._temperature + + @property + def outdoor_temperature(self): + return self._outdoor_temperature + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def temperature_unit(self): + return TEMP_CELSIUS + + def set_temperature(self, **kwargs): + """Set new target temperature""" + _LOGGER.debug("Anna: Adjusting temperature") + import haanna + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is not None and temperature > CONF_MIN_TEMP and temperature < CONF_MAX_TEMP: + self._temperature = temperature + domain_objects = self._api.get_domain_objects() + self._api.set_temperature(domain_objects, temperature) + self.schedule_update_ha_state() + _LOGGER.debug('Anna: Changing temporary temperature') + else: + _LOGGER.error('Anna: Failed to change temperature (invalid temperature given)') + + def set_hold_mode(self, hold_mode): + """Set the hold mode.""" + _LOGGER.debug("Anna: Adjusting hold_mode (i.e. preset)") + if hold_mode is not None and hold_mode in HOLD_MODES: + domain_objects = self._api.get_domain_objects() + self._hold_mode = hold_mode + self._api.set_preset(domain_objects, hold_mode) + _LOGGER.debug('Anna: Changing hold mode/preset') + else: + _LOGGER.error('Anna: Failed to change hold mode (invalid or no preset given)') + diff --git a/custom_components/anna/manifest.json b/custom_components/anna/manifest.json new file mode 100644 index 0000000..63e752f --- /dev/null +++ b/custom_components/anna/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "anna", + "name": "Plugwise_Anna", + "documentation": "https://github.com/laetificat/anna-ha", + "dependencies": [], + "codeowners": ["@laetificat","CoMPaTech"], + "requirements": ["haanna==0.6.2"] +} diff --git a/custom_components/climate/anna.py b/custom_components/climate/anna.py deleted file mode 100644 index e962f72..0000000 --- a/custom_components/climate/anna.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Plugwise Anna component for HomeAssistant - -configurations.yaml - -climate: - - platform: anna - name: Anna Thermostat - username: smile - password: short_id - host: 192.168.1.60 - port: 80 - scan_interval: 10 -""" -REQUIREMENTS = ['haanna==0.6.1'] - -import voluptuous as vol -import logging - -from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, ATTR_TEMPERATURE) -import homeassistant.helpers.config_validation as cv - -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Anna Thermostat' -DEFAULT_USERNAME = 'smile' -DEFAULT_TIMEOUT = 10 -BASE_URL = 'http://{0}:{1}{2}' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=80): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Anna thermostat""" - add_devices([ - ThermostatDevice( - config.get(CONF_NAME), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - config.get(CONF_HOST), - config.get(CONF_PORT) - ) - ]) - - -class ThermostatDevice(ClimateDevice): - """Representation of an Anna thermostat""" - def __init__(self, name, username, password, host, port): - self._name = name - self._username = username - self._password = password - self._host = host - self._port = port - self._temperature = None - self._current_temperature = None - self._outdoor_temperature = None - self._target_min_temperature = 4 - self._target_max_temperature = 30 - self._away_mode = False - _LOGGER.debug("Init called") - self.update() - - @property - def should_poll(self): - """Polling is needed""" - return True - - def update(self): - """Update the data from the thermostat""" - import haanna - api = haanna.Haanna(self._username, self._password, self._host) - domain_objects = api.get_domain_objects() - self._current_temperature = api.get_temperature(domain_objects) - self._outdoor_temperature = api.get_outdoor_temperature(domain_objects) - self._temperature = api.get_target_temperature(domain_objects) - _LOGGER.debug("Update called") - - @property - def name(self): - return self._name - - @property - def current_temperature(self): - return self._current_temperature - - @property - def target_temperature(self): - return self._temperature - - def outdoor_temperature(self): - return self._outdoor_temperature - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def temperature_unit(self): - return TEMP_CELSIUS - - def set_temperature(self, **kwargs): - """Set new target temperature""" - import haanna - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is not None: - self._temperature = temperature - api = haanna.Haanna(self._username, self._password, self._host) - domain_objects = api.get_domain_objects() - api.set_temperature(domain_objects, temperature) - self.schedule_update_ha_state() -