Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ omit =
homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py

homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py

homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py

Expand Down
137 changes: 137 additions & 0 deletions homeassistant/components/comfoconnect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/comfoconnect/
"""
import logging

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_TOKEN, CONF_PIN,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import (discovery)
from homeassistant.helpers.dispatcher import (dispatcher_send)

REQUIREMENTS = ['pycomfoconnect==0.3']

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'comfoconnect'

SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = 'comfoconnect_update_received'

ATTR_CURRENT_TEMPERATURE = 'current_temperature'
ATTR_CURRENT_HUMIDITY = 'current_humidity'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
ATTR_OUTSIDE_HUMIDITY = 'outside_humidity'
ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply'
ATTR_AIR_FLOW_EXHAUST = 'air_flow_exhaust'

CONF_USER_AGENT = 'user_agent'

DEFAULT_PIN = 0
DEFAULT_TOKEN = '00000000000000000000000000000001'
DEFAULT_NAME = 'ComfoAirQ'
DEFAULT_USER_AGENT = 'Home Assistant'

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
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,
}),
}, extra=vol.ALLOW_EXTRA)

DEVICE = None


def setup(hass, config):
"""Setup the ComfoConnect bridge."""
from pycomfoconnect import (Bridge)

conf = config[DOMAIN]
host = conf.get(CONF_HOST)
name = conf.get(CONF_NAME)
token = conf.get(CONF_TOKEN)
user_agent = conf.get(CONF_USER_AGENT)
pin = conf.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
bridge = bridges[0]
_LOGGER.info('Bridge found: %s (%s)', bridge.uuid.hex(), bridge.host)

# Setup ComfoConnect Bridge
ccb = ComfoConnectBridge(hass, bridge, name, token, user_agent, pin)
hass.data[DOMAIN] = ccb

# Start connection with bridge
ccb.connect()

# Schedule disconnect on shutdown
def _shutdown(_event):
ccb.disconnect()

hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)

# Load platforms
discovery.load_platform(hass, 'fan', DOMAIN, {}, config)

return True


class ComfoConnectBridge(object):
"""Representation of a ComfoConnect bridge."""

def __init__(self, hass, bridge, name, token, friendly_name, pin):
"""Initialize the ComfoConnect bridge."""
from pycomfoconnect import (ComfoConnect)

self.data = {}
self.name = name
self.hass = hass

self.comfoconnect = ComfoConnect(bridge=bridge,
local_uuid=bytes.fromhex(token),
local_devicename=friendly_name,
pin=pin)
self.comfoconnect.callback_sensor = self.sensor_callback

def connect(self):
"""Connect with the bridge."""
_LOGGER.debug('Connecting with bridge.')
self.comfoconnect.connect(True)

def disconnect(self):
"""Disconnect from the bridge."""
_LOGGER.debug('Disconnecting from bridge.')
self.comfoconnect.disconnect()

def sensor_callback(self, var, value):
"""Callback function for sensor updates."""
_LOGGER.debug('Got value from bridge: %d = %d', var, value)

from pycomfoconnect import (
SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR)

if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]:
self.data[var] = value / 10
else:
self.data[var] = value

# Notify listeners that we have received an update
dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var)

def subscribe_sensor(self, sensor_id):
"""Subscribe for the specified sensor."""
self.comfoconnect.register_sensor(sensor_id)
128 changes: 128 additions & 0 deletions homeassistant/components/fan/comfoconnect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""
Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/fan.comfoconnect/
"""
import logging

from homeassistant.components.comfoconnect import (
DOMAIN, ComfoConnectBridge, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED)
from homeassistant.components.fan import (
FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.dispatcher import (dispatcher_connect)

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['comfoconnect']

SPEED_MAPPING = {
0: SPEED_OFF,
1: SPEED_LOW,
2: SPEED_MEDIUM,
3: SPEED_HIGH
}


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ComfoConnect fan platform."""
ccb = hass.data[DOMAIN]

add_devices([
ComfoConnectFan(
hass,
name=ccb.name,
ccb=ccb
)
], True)

return


class ComfoConnectFan(FanEntity):
"""Representation of the fan platform."""

def __init__(self, hass, name, ccb: ComfoConnectBridge):
"""Initialize the ComfoConnect fan."""
from pycomfoconnect import (
SENSOR_FAN_SPEED_MODE
)

self._ccb = ccb
self._name = name

# Ask the bridge to keep us updated
self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE)

def _handle_update(var):
if var == SENSOR_FAN_SPEED_MODE:
_LOGGER.debug('Dispatcher update for %s.', var)
self.schedule_update_ha_state()

# Register for dispatcher updates
dispatcher_connect(
hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update)

@property
def name(self):
"""Return the name of the fan."""
return self._name

@property
def icon(self):
"""Return the icon to use in the frontend."""
return 'mdi:air-conditioner'

@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_SET_SPEED

@property
def speed(self):
"""Return the current fan mode."""
from pycomfoconnect import (SENSOR_FAN_SPEED_MODE)

try:
speed = self._ccb.data[SENSOR_FAN_SPEED_MODE]
return SPEED_MAPPING[speed]
except KeyError:
return STATE_UNKNOWN

@property
def speed_list(self):
"""List of available fan modes."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]

def turn_on(self, speed: str=None, **kwargs) -> None:
"""Turn on the fan."""
if speed is None:
speed = SPEED_LOW
self.set_speed(speed)

def turn_off(self) -> None:
"""Turn off the fan (to away)."""
self.set_speed(SPEED_OFF)

def set_speed(self, mode):
"""Set fan speed."""
_LOGGER.debug('Changing fan mode to %s.', mode)

from pycomfoconnect import (
CMD_FAN_MODE_AWAY, CMD_FAN_MODE_LOW, CMD_FAN_MODE_MEDIUM,
CMD_FAN_MODE_HIGH
)

if mode == SPEED_OFF:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY)
elif mode == SPEED_LOW:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW)
elif mode == SPEED_MEDIUM:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM)
elif mode == SPEED_HIGH:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH)

# Update current mode
self.schedule_update_ha_state()
Loading