From af0a0391471a6f512b5af60304eddb1317cccf4f Mon Sep 17 00:00:00 2001 From: CoMPaTech Date: Tue, 23 Apr 2019 23:33:03 +0200 Subject: [PATCH 1/6] Modified to work on 0.91.2 and added hold_mode (i.e. presets), not sure if heater on/off works yet --- .../{climate/anna.py => anna/climate.py} | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) rename custom_components/{climate/anna.py => anna/climate.py} (70%) diff --git a/custom_components/climate/anna.py b/custom_components/anna/climate.py similarity index 70% rename from custom_components/climate/anna.py rename to custom_components/anna/climate.py index e962f72..65a8e03 100644 --- a/custom_components/climate/anna.py +++ b/custom_components/anna/climate.py @@ -17,11 +17,20 @@ 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 xml.etree.cElementTree as Etree + +#from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +try: + from homeassistant.components.climate.const import ( + SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE) +except ImportError: + from homeassistant.components.climate import ( + SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_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 -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE _LOGGER = logging.getLogger(__name__) @@ -65,6 +74,7 @@ def __init__(self, name, username, password, host, port): self._outdoor_temperature = None self._target_min_temperature = 4 self._target_max_temperature = 30 + self._state = None self._away_mode = False _LOGGER.debug("Init called") self.update() @@ -74,6 +84,11 @@ 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""" import haanna @@ -82,12 +97,26 @@ def update(self): 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) + self._hold_mode = api.get_current_preset(domain_objects) + # Determine heater state from domain object (Etree XML) if any, assume off + try: + if domain_objects.find("appliance[type='heater_central']/logs/point_log/period/measurement").text == 'on': + self._state=STATE_ON + else: + self._state=STATE_OFF + except: + self._state=STATE_OFF _LOGGER.debug("Update called") @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 current_temperature(self): return self._current_temperature From 2a336954ece63d182cbfa1b7c4a79d0448817685 Mon Sep 17 00:00:00 2001 From: CoMPaTech Date: Wed, 24 Apr 2019 22:33:30 +0200 Subject: [PATCH 2/6] Modified haannna accordingly and reworked operation_mode and heater --- custom_components/anna/climate.py | 105 +++++++++++++++++++++------ custom_components/anna/manifest.json | 8 ++ 2 files changed, 89 insertions(+), 24 deletions(-) create mode 100644 custom_components/anna/manifest.json diff --git a/custom_components/anna/climate.py b/custom_components/anna/climate.py index 65a8e03..4293c0c 100644 --- a/custom_components/anna/climate.py +++ b/custom_components/anna/climate.py @@ -12,33 +12,63 @@ port: 80 scan_interval: 10 """ -REQUIREMENTS = ['haanna==0.6.1'] +REQUIREMENTS = ['haanna==0.6.2'] import voluptuous as vol import logging import xml.etree.cElementTree as Etree -#from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA -try: - from homeassistant.components.climate.const import ( - SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE) -except ImportError: - from homeassistant.components.climate import ( - SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE) -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, ATTR_TEMPERATURE, STATE_ON, STATE_OFF) +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 -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__) +ICON = "mdi:thermometer" + DEFAULT_NAME = 'Anna Thermostat' DEFAULT_USERNAME = 'smile' DEFAULT_TIMEOUT = 10 BASE_URL = 'http://{0}:{1}{2}' +# Hold modes +MODE_HOME = "home" +MODE_VACATION = "vacation" +MODE_NO_FROST = "no_frost" +MODE_SLEEP = "asleep" +MODE_AWAY = "away" + +# Change defaults to match Anna +DEFAULT_MIN_TEMP = 4 +DEFAULT_MAX_TEMP = 30 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_HOST): cv.string, @@ -64,6 +94,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ThermostatDevice(ClimateDevice): """Representation of an Anna thermostat""" def __init__(self, name, username, password, host, port): + _LOGGER.debug("Init called") self._name = name self._username = username self._password = password @@ -72,11 +103,11 @@ def __init__(self, name, username, password, host, port): self._temperature = None self._current_temperature = None self._outdoor_temperature = None - self._target_min_temperature = 4 - self._target_max_temperature = 30 self._state = None + self._hold_mode = None self._away_mode = False - _LOGGER.debug("Init called") + self._operation_list = [ STATE_AUTO, STATE_IDLE ] + _LOGGER.debug("Initializing API") self.update() @property @@ -91,21 +122,20 @@ def state(self): 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) self._hold_mode = api.get_current_preset(domain_objects) - # Determine heater state from domain object (Etree XML) if any, assume off - try: - if domain_objects.find("appliance[type='heater_central']/logs/point_log/period/measurement").text == 'on': - self._state=STATE_ON - else: - self._state=STATE_OFF - except: - self._state=STATE_OFF + if api.get_mode(domain_objects) == True: + self._operation_mode=STATE_AUTO + else: + self._operation_mode=STATE_IDLE + if api.get_heating_status(domain_objects) == True: + self._state=STATE_ON + else: + self._state=STATE_OFF _LOGGER.debug("Update called") @property @@ -117,6 +147,21 @@ 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 ICON + @property def current_temperature(self): return self._current_temperature @@ -125,6 +170,7 @@ def current_temperature(self): def target_temperature(self): return self._temperature + @property def outdoor_temperature(self): return self._outdoor_temperature @@ -142,9 +188,20 @@ def set_temperature(self, **kwargs): import haanna temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is not None: - self._temperature = temperature api = haanna.Haanna(self._username, self._password, self._host) + self._temperature = temperature domain_objects = api.get_domain_objects() api.set_temperature(domain_objects, temperature) self.schedule_update_ha_state() + def set_hold_mode(self, hold_mode): + """Set the hold mode.""" + if hold_mode is not None: + api = haanna.Haanna(self._username, self._password, self._host) + domain_objects = api.get_domain_objects() + self._hold_mode = hold_mode + api.set_preset(domain_objects, hold_mode) + _LOGGER.info('Changing hold mode/preset') + else: + _LOGGER.error('Failed to change hold mode (invalid preset)') + diff --git a/custom_components/anna/manifest.json b/custom_components/anna/manifest.json new file mode 100644 index 0000000..af2342e --- /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": ["climate"], + "codeowners": ["@laetificat","CoMPaTech"], + "requirements": ["haanna==0.6.2"] +} From 4d8b74ab3253487abf48c370cd5d9eeab40e5665 Mon Sep 17 00:00:00 2001 From: CoMPaTech Date: Sat, 27 Apr 2019 15:05:31 +0200 Subject: [PATCH 3/6] Added .92 compatibility as well --- custom_components/anna/__init__.py | 1 + custom_components/anna/climate.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 custom_components/anna/__init__.py 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 index 4293c0c..d590b8b 100644 --- a/custom_components/anna/climate.py +++ b/custom_components/anna/climate.py @@ -12,7 +12,6 @@ port: 80 scan_interval: 10 """ -REQUIREMENTS = ['haanna==0.6.2'] import voluptuous as vol import logging From 743e75b2a90c2fd95dad3e135e73216439c94788 Mon Sep 17 00:00:00 2001 From: Kevin Heruer Date: Sun, 28 Apr 2019 09:35:57 +0200 Subject: [PATCH 4/6] Update custom_components/anna/manifest.json Remove breaking dependency Co-Authored-By: CoMPaTech --- custom_components/anna/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/anna/manifest.json b/custom_components/anna/manifest.json index af2342e..63e752f 100644 --- a/custom_components/anna/manifest.json +++ b/custom_components/anna/manifest.json @@ -2,7 +2,7 @@ "domain": "anna", "name": "Plugwise_Anna", "documentation": "https://github.com/laetificat/anna-ha", - "dependencies": ["climate"], + "dependencies": [], "codeowners": ["@laetificat","CoMPaTech"], "requirements": ["haanna==0.6.2"] } From 2192bf5a5ce6eb69adc14a1fba67ac0fb8189b24 Mon Sep 17 00:00:00 2001 From: CoMPaTech Date: Sun, 28 Apr 2019 11:35:20 +0200 Subject: [PATCH 5/6] Added PlatformNotReady (see haaanna commit) and resolved comments --- README.md | 12 ++++ custom_components/anna/climate.py | 109 ++++++++++++++++++++---------- 2 files changed, 86 insertions(+), 35 deletions(-) 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/climate.py b/custom_components/anna/climate.py index d590b8b..221dd60 100644 --- a/custom_components/anna/climate.py +++ b/custom_components/anna/climate.py @@ -10,7 +10,9 @@ password: short_id host: 192.168.1.60 port: 80 - scan_interval: 10 + min_temp: 4 + max_temp: 30 + scan_interval: 10 # optional """ import voluptuous as vol @@ -44,39 +46,53 @@ ATTR_TEMPERATURE, STATE_ON, STATE_OFF) + import homeassistant.helpers.config_validation as cv -SUPPORT_FLAGS = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_HOLD_MODE | SUPPORT_AWAY_MODE ) +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__) -ICON = "mdi:thermometer" +# 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 -MODE_HOME = "home" -MODE_VACATION = "vacation" -MODE_NO_FROST = "no_frost" -MODE_SLEEP = "asleep" -MODE_AWAY = "away" +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.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([ @@ -85,15 +101,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config.get(CONF_USERNAME), config.get(CONF_PASSWORD), config.get(CONF_HOST), - config.get(CONF_PORT) + config.get(CONF_PORT), + config.get(CONF_MIN_TEMP), + config.get(CONF_MAX_TEMP) ) ]) +_LOGGER.info("Anna: custom component loading (Anna PlugWise climate)") class ThermostatDevice(ClimateDevice): """Representation of an Anna thermostat""" - def __init__(self, name, username, password, host, port): - _LOGGER.debug("Init called") + 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 @@ -105,8 +124,18 @@ def __init__(self, name, username, password, host, port): self._state = None self._hold_mode = None self._away_mode = False - self._operation_list = [ STATE_AUTO, STATE_IDLE ] - _LOGGER.debug("Initializing API") + 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.warning("Anna: Unable to ping, platform not ready") + raise PlatformNotReady + _LOGGER.info("Anna: platform ready") self.update() @property @@ -121,21 +150,20 @@ def state(self): def update(self): """Update the data from the thermostat""" - 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) - self._hold_mode = api.get_current_preset(domain_objects) - if api.get_mode(domain_objects) == True: + _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 api.get_heating_status(domain_objects) == True: + if self._api.get_heating_status(domain_objects) == True: self._state=STATE_ON else: self._state=STATE_OFF - _LOGGER.debug("Update called") @property def name(self): @@ -159,12 +187,20 @@ def current_operation(self): @property def icon(self): """Return the icon to use in the frontend.""" - return ICON + 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 @@ -184,23 +220,26 @@ def temperature_unit(self): def set_temperature(self, **kwargs): """Set new target temperature""" + _LOGGER.info("Anna: Adjusting temperature") import haanna temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is not None: - api = haanna.Haanna(self._username, self._password, self._host) + if temperature is not None and temperature > CONF_MIN_TEMP and temperature < CONF_MAX_TEMP: self._temperature = temperature - domain_objects = api.get_domain_objects() - api.set_temperature(domain_objects, 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.""" - if hold_mode is not None: - api = haanna.Haanna(self._username, self._password, self._host) - domain_objects = api.get_domain_objects() + _LOGGER.info("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 - api.set_preset(domain_objects, hold_mode) - _LOGGER.info('Changing hold mode/preset') + self._api.set_preset(domain_objects, hold_mode) + _LOGGER.debug('Anna: Changing hold mode/preset') else: - _LOGGER.error('Failed to change hold mode (invalid preset)') + _LOGGER.error('Anna: Failed to change hold mode (invalid or no preset given)') From dd6e0aa5a30ae3e869694edc1557ecdc1d55eabd Mon Sep 17 00:00:00 2001 From: CoMPaTech Date: Sun, 28 Apr 2019 15:55:11 +0200 Subject: [PATCH 6/6] Info logging not allowed as per styleguide --- custom_components/anna/climate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/anna/climate.py b/custom_components/anna/climate.py index 221dd60..f1b9e8d 100644 --- a/custom_components/anna/climate.py +++ b/custom_components/anna/climate.py @@ -107,7 +107,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ) ]) -_LOGGER.info("Anna: custom component loading (Anna PlugWise climate)") +_LOGGER.debug("Anna: custom component loading (Anna PlugWise climate)") class ThermostatDevice(ClimateDevice): """Representation of an Anna thermostat""" @@ -133,9 +133,9 @@ def __init__(self, name, username, password, host, port, min_temp, max_temp): try: self._api.ping_anna_thermostat() except: - _LOGGER.warning("Anna: Unable to ping, platform not ready") + _LOGGER.error("Anna: Unable to ping, platform not ready") raise PlatformNotReady - _LOGGER.info("Anna: platform ready") + _LOGGER.debug("Anna: platform ready") self.update() @property @@ -220,7 +220,7 @@ def temperature_unit(self): def set_temperature(self, **kwargs): """Set new target temperature""" - _LOGGER.info("Anna: Adjusting 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: @@ -234,7 +234,7 @@ def set_temperature(self, **kwargs): def set_hold_mode(self, hold_mode): """Set the hold mode.""" - _LOGGER.info("Anna: Adjusting hold_mode (i.e. preset)") + _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