Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ omit =
homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py
homeassistant/components/climate/comfoconnect.py
homeassistant/components/climate/eq3btsmart.py
homeassistant/components/climate/heatmiser.py
homeassistant/components/climate/homematic.py
Expand Down
10 changes: 7 additions & 3 deletions homeassistant/components/climate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,12 +413,16 @@ def state_attributes(self):
"""Return the optional state attributes."""
data = {
ATTR_CURRENT_TEMPERATURE:
self._convert_for_display(self.current_temperature),
self._convert_for_display(self.current_temperature),
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp),
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
ATTR_TEMPERATURE:
self._convert_for_display(self.target_temperature),
}

target_temperature = self.target_temperature
if target_temperature is not None:

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Contributor Author

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!

data[ATTR_TEMPERATURE] = self._convert_for_display(
target_temperature)

target_temp_high = self.target_temperature_high
if target_temp_high is not None:
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
Expand Down
279 changes: 279 additions & 0 deletions homeassistant/components/climate/comfoconnect.py
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 async_added_to_hass and hass.async_add_job. Make also sure that the callback thread is closing with a EVENT_HOMEASSISTANT_STOP event listener.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback thread of comfoconnect is only started when the update is executed (when self._comfoconnect.connect is executed). It is stopped in self._comfoconnect.disconnect in the finally clause. It only stays awake for maximum 5 seconds (during wait_for_sensor_values).

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 update.

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
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ https://github.com/joopert/nad_receiver/archive/0.0.3.zip#nad_receiver==0.0.3
# homeassistant.components.media_player.russound_rnet
https://github.com/laf/russound/archive/0.1.7.zip#russound==0.1.7

# homeassistant.components.climate.comfoconnect
https://github.com/michaelarnauts/comfoconnect/archive/0.1.zip#pycomfoconnect==0.1

# homeassistant.components.media_player.onkyo
https://github.com/miracle2k/onkyo-eiscp/archive/066023aec04770518d494c32fb72eea0ec5c1b7c.zip#onkyo-eiscp==1.0

Expand Down