-
-
Notifications
You must be signed in to change notification settings - Fork 38.2k
Add support for Zehnder Comfoconnect LAN C bridge. #7246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
3f9f93d
7605131
408ffd3
ccdde61
c57a02b
e63643c
3f869cf
cda7406
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,279 @@ | ||
| """ | ||
| Support for Zehnder ComfoConnect bridges. | ||
|
|
||
| For more details about this component, please refer to the documentation at | ||
| https://home-assistant.io/components/climate.comfoconnect/ | ||
| """ | ||
| import logging | ||
| import time | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.components.climate import ( | ||
| ClimateDevice, PLATFORM_SCHEMA, ATTR_FAN_MODE, ATTR_CURRENT_HUMIDITY, | ||
| ATTR_CURRENT_TEMPERATURE, ATTR_FAN_LIST) | ||
| from homeassistant.const import ( | ||
| CONF_HOST, CONF_TOKEN, CONF_PIN, TEMP_CELSIUS, CONF_NAME) | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| REQUIREMENTS = [ | ||
| 'https://github.com/michaelarnauts/comfoconnect' | ||
| '/archive/0.1.zip#pycomfoconnect==0.1'] | ||
|
|
||
| SPEED_AWAY = 'away' | ||
| SPEED_LOW = 'low' | ||
| SPEED_MEDIUM = 'medium' | ||
| SPEED_HIGH = 'high' | ||
|
|
||
| ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature' | ||
| ATTR_OUTSIDE_HUMIDITY = 'outside_humidity' | ||
| ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply' | ||
| ATTR_AIR_FLOW_EXTRACT = 'air_flow_extract' | ||
|
|
||
| CONF_USER_AGENT = 'user_agent' | ||
|
|
||
| DEFAULT_PIN = 0 | ||
| DEFAULT_TOKEN = '00000000000000000000000000000001' | ||
| DEFAULT_NAME = 'ComfoAirQ' | ||
| DEFAULT_USER_AGENT = 'Home Assistant' | ||
|
|
||
| PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ | ||
| vol.Required(CONF_HOST): cv.string, | ||
| vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | ||
| vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN): | ||
| vol.Length(min=32, max=32, msg='invalid token'), | ||
| vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): | ||
| cv.string, | ||
| vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int, | ||
| }) | ||
|
|
||
|
|
||
| def setup_platform(hass, config, add_devices, discovery_info=None): | ||
| """Setup the ComfoConnect bridge.""" | ||
| from pycomfoconnect import (Bridge) | ||
|
|
||
| host = config.get(CONF_HOST) | ||
| name = config.get(CONF_NAME) | ||
| token = config.get(CONF_TOKEN) | ||
| user_agent = config.get(CONF_USER_AGENT) | ||
| pin = config.get(CONF_PIN) | ||
|
|
||
| # Run discovery on the configured ip | ||
| bridges = Bridge.discover(host) | ||
| if not bridges: | ||
| _LOGGER.error('Could not connect to ComfoConnect bridge on %s', host) | ||
| return False | ||
|
|
||
| for bridge in bridges: | ||
| _LOGGER.info('Bridge found: %s (%s)', bridge.remote_uuid.hex(), | ||
| bridge.ip) | ||
| add_devices([ | ||
| ComfoConnectBridge(name, bridge, token, user_agent, pin) | ||
| ], True) | ||
|
|
||
| return | ||
|
|
||
|
|
||
| class ComfoConnectBridge(ClimateDevice): | ||
| """Representation of a ComfoConnect bridge.""" | ||
|
|
||
| def __init__(self, name, bridge, token, friendly_name, pin): | ||
| """Initialize the ComfoConnect bridge.""" | ||
| from pycomfoconnect import ( | ||
| ComfoConnect, SENSOR_FAN_SPEED_MODE, SENSOR_TEMPERATURE_EXTRACT, | ||
| SENSOR_TEMPERATURE_OUTDOOR, SENSOR_HUMIDITY_EXTRACT, | ||
| SENSOR_HUMIDITY_OUTDOOR, SENSOR_FAN_SUPPLY_FLOW, | ||
| SENSOR_FAN_EXHAUST_FLOW | ||
| ) | ||
|
|
||
| self._name = name | ||
| self._comfoconnect = ComfoConnect(bridge, self.sensor_callback) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sure that the callback only start after EVENT_HOMEASSISTANT_START event is receive with a event listener (You can also Register the callback inside
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The callback thread of comfoconnect is only started when the I initially used the EVENT_HOMEASSISTANT_[START|STOP] event, and kept the callback thread open, but the bridge only allows one open connection at the same time, so I decided to stay with polling and open the connection during |
||
| self._token = bytes.fromhex(token) | ||
| self._friendly_name = friendly_name | ||
| self._pin = pin | ||
| self._data = {} | ||
|
|
||
| self._subscribed_sensors = [ | ||
| SENSOR_FAN_SPEED_MODE, | ||
| SENSOR_FAN_SUPPLY_FLOW, | ||
| SENSOR_FAN_EXHAUST_FLOW, | ||
| SENSOR_TEMPERATURE_EXTRACT, | ||
| SENSOR_TEMPERATURE_OUTDOOR, | ||
| SENSOR_HUMIDITY_EXTRACT, | ||
| SENSOR_HUMIDITY_OUTDOOR, | ||
| ] | ||
|
|
||
| @property | ||
| def name(self): | ||
| """Return the name of the bridge.""" | ||
| return self._name | ||
|
|
||
| @property | ||
| def icon(self): | ||
| """Return the icon to use in the frontend, if any.""" | ||
| return 'mdi:air-conditioner' | ||
|
|
||
| @property | ||
| def temperature_unit(self): | ||
| """Return the unit of measurement.""" | ||
| return TEMP_CELSIUS | ||
|
|
||
| @property | ||
| def device_state_attributes(self): | ||
| """Return the state attributes.""" | ||
| from pycomfoconnect import ( | ||
| SENSOR_FAN_SPEED_MODE, SENSOR_FAN_SUPPLY_FLOW, | ||
| SENSOR_FAN_EXHAUST_FLOW, SENSOR_TEMPERATURE_EXTRACT, | ||
| SENSOR_TEMPERATURE_OUTDOOR, SENSOR_HUMIDITY_EXTRACT, | ||
| SENSOR_HUMIDITY_OUTDOOR | ||
| ) | ||
|
|
||
| data = { | ||
| ATTR_FAN_LIST: self.fan_list | ||
| } | ||
|
|
||
| for key, value in self._data.items(): | ||
| if key == SENSOR_FAN_SPEED_MODE: | ||
| if value == 0: | ||
| data[ATTR_FAN_MODE] = SPEED_AWAY | ||
| elif value == 1: | ||
| data[ATTR_FAN_MODE] = SPEED_LOW | ||
| elif value == 2: | ||
| data[ATTR_FAN_MODE] = SPEED_MEDIUM | ||
| elif value == 3: | ||
| data[ATTR_FAN_MODE] = SPEED_HIGH | ||
|
|
||
| elif key == SENSOR_HUMIDITY_EXTRACT: | ||
| data[ATTR_CURRENT_HUMIDITY] = value | ||
|
|
||
| elif key == SENSOR_HUMIDITY_OUTDOOR: | ||
| data[ATTR_OUTSIDE_HUMIDITY] = value | ||
|
|
||
| elif key == SENSOR_TEMPERATURE_EXTRACT: | ||
| data[ATTR_CURRENT_TEMPERATURE] = value / 10.0 | ||
|
|
||
| elif key == SENSOR_TEMPERATURE_OUTDOOR: | ||
| data[ATTR_OUTSIDE_TEMPERATURE] = value / 10.0 | ||
|
|
||
| elif key == SENSOR_FAN_SUPPLY_FLOW: | ||
| data[ATTR_AIR_FLOW_SUPPLY] = value | ||
|
|
||
| elif key == SENSOR_FAN_EXHAUST_FLOW: | ||
| data[ATTR_AIR_FLOW_EXTRACT] = value | ||
|
|
||
| return data | ||
|
|
||
| @property | ||
| def current_temperature(self): | ||
| """Return the current temperature.""" | ||
| if ATTR_CURRENT_TEMPERATURE in self._data: | ||
| return self._data[ATTR_CURRENT_TEMPERATURE] | ||
|
|
||
| @property | ||
| def current_humidity(self): | ||
| """Return the current humidity.""" | ||
| if ATTR_CURRENT_HUMIDITY in self._data: | ||
| return self._data[ATTR_CURRENT_HUMIDITY] | ||
|
|
||
| @property | ||
| def current_fan_mode(self): | ||
| """Return the current fan mode.""" | ||
| if ATTR_FAN_MODE in self._data: | ||
| return self._data[ATTR_FAN_MODE] | ||
|
|
||
| @property | ||
| def fan_list(self): | ||
| """List of available fan modes.""" | ||
| return [SPEED_AWAY, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] | ||
|
|
||
| def set_fan_mode(self, mode): | ||
| """Set fan speed.""" | ||
| from pycomfoconnect.error import ( | ||
| PyComfoConnectOtherSession, PyComfoConnectNotAllowed) | ||
| from pycomfoconnect.const import ( | ||
| FAN_MODE_AWAY, FAN_MODE_LOW, FAN_MODE_MEDIUM, | ||
| FAN_MODE_HIGH | ||
| ) | ||
|
|
||
| _LOGGER.debug('Changing fan mode...') | ||
| try: | ||
| self._comfoconnect.connect( | ||
| self._token, self._friendly_name, self._pin) | ||
| _LOGGER.debug('Connected to bridge.') | ||
|
|
||
| if mode == SPEED_AWAY: | ||
| self._comfoconnect.set_fan_mode(FAN_MODE_AWAY) | ||
| elif mode == SPEED_LOW: | ||
| self._comfoconnect.set_fan_mode(FAN_MODE_LOW) | ||
| elif mode == SPEED_MEDIUM: | ||
| self._comfoconnect.set_fan_mode(FAN_MODE_MEDIUM) | ||
| elif mode == SPEED_HIGH: | ||
| self._comfoconnect.set_fan_mode(FAN_MODE_HIGH) | ||
|
|
||
| # Update current mode | ||
| self._data[ATTR_FAN_MODE] = mode | ||
|
|
||
| except PyComfoConnectOtherSession as ex: | ||
| _LOGGER.error('Another session with "%s" is active.', | ||
| ex.devicename) | ||
|
|
||
| except PyComfoConnectNotAllowed: | ||
| _LOGGER.error('Could not register. Invalid PIN!') | ||
|
|
||
| finally: | ||
| self._comfoconnect.disconnect() | ||
| _LOGGER.debug('Disconnected from bridge.') | ||
|
|
||
| def update(self): | ||
| """Open connection to the Bridge.""" | ||
| from pycomfoconnect.error import ( | ||
| PyComfoConnectOtherSession, PyComfoConnectNotAllowed) | ||
|
|
||
| _LOGGER.debug('Updating sensor data...') | ||
| try: | ||
| self._comfoconnect.connect( | ||
| self._token, self._friendly_name, self._pin) | ||
| _LOGGER.debug('Connected to bridge.') | ||
|
|
||
| # Clean sensor data | ||
| self._data = {} | ||
|
|
||
| # Subscribe to sensor values. | ||
| for sensor in self._subscribed_sensors: | ||
| self._comfoconnect.request(sensor) | ||
| _LOGGER.debug('Subscribed to sensors.') | ||
|
|
||
| # Wait maximum of 5 seconds for all the sensor values. | ||
| self.wait_for_sensor_values(5) | ||
| _LOGGER.debug('Sensor data received.') | ||
|
|
||
| except PyComfoConnectOtherSession as ex: | ||
| _LOGGER.error('Another session with "%s" is active.', | ||
| ex.devicename) | ||
|
|
||
| except PyComfoConnectNotAllowed: | ||
| _LOGGER.error('Could not register. Invalid PIN!') | ||
|
|
||
| finally: | ||
| self._comfoconnect.disconnect() | ||
| _LOGGER.debug('Disconnected from bridge.') | ||
|
|
||
| def wait_for_sensor_values(self, max_seconds): | ||
| """Wait for all the sensor values to have arrived.""" | ||
| deadline = time.time() + max_seconds | ||
| for sensor in self._subscribed_sensors: | ||
| if sensor in self._data: | ||
| continue | ||
|
|
||
| if time.time() > deadline: | ||
| _LOGGER.error('Timeout while waiting for sensor data.') | ||
| return | ||
|
|
||
| time.sleep(1) | ||
|
|
||
| def sensor_callback(self, var, value): | ||
| """Callback function for sensor updates.""" | ||
| _LOGGER.info('Got value from bridge: %d = %d', var, value) | ||
| self._data[var] = value | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I already mentioned this in the chat, this is not allowed. Please revert this change.
The idea of the climate component is for devices that can control the temperature. If your device can't control the temperature, it is a fan. And so please implement a platform for the fan component.
You can have an extra sensor platform to expose the current temperature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I didn't know the rule that only temperature controllable things could be climate platforms. This contradicts the climate documentation:
The climate component is built for the controlling and monitoring of HVAC (heating, ventilating, and air conditioning) and thermostat devices.
I will try to find some time to move over to the fan component, although it's unfortunate that that component doesn't support the temperature view.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you implement the temperature as a separate sensor, you get graphs too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, I've setup my hass to use template sensors and fetch them from the attributes. Works fine!