diff --git a/.coveragerc b/.coveragerc index cbc8f5c78a9..c74de0026b1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/homeassistant/components/comfoconnect.py b/homeassistant/components/comfoconnect.py new file mode 100644 index 00000000000..fa1af540a96 --- /dev/null +++ b/homeassistant/components/comfoconnect.py @@ -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) diff --git a/homeassistant/components/fan/comfoconnect.py b/homeassistant/components/fan/comfoconnect.py new file mode 100644 index 00000000000..a0818445aaf --- /dev/null +++ b/homeassistant/components/fan/comfoconnect.py @@ -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() diff --git a/homeassistant/components/sensor/comfoconnect.py b/homeassistant/components/sensor/comfoconnect.py new file mode 100644 index 00000000000..c953ee53260 --- /dev/null +++ b/homeassistant/components/sensor/comfoconnect.py @@ -0,0 +1,141 @@ +""" +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/sensor.comfoconnect/ +""" +import logging + +from homeassistant.components.comfoconnect import ( + DOMAIN, ComfoConnectBridge, ATTR_CURRENT_TEMPERATURE, + ATTR_CURRENT_HUMIDITY, ATTR_OUTSIDE_TEMPERATURE, + ATTR_OUTSIDE_HUMIDITY, ATTR_AIR_FLOW_SUPPLY, + ATTR_AIR_FLOW_EXHAUST, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED) +from homeassistant.const import ( + CONF_RESOURCES, TEMP_CELSIUS, STATE_UNKNOWN) +from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['comfoconnect'] + +SENSOR_TYPES = {} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the ComfoConnect fan platform.""" + from pycomfoconnect import ( + SENSOR_TEMPERATURE_EXTRACT, SENSOR_HUMIDITY_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, SENSOR_HUMIDITY_OUTDOOR, + SENSOR_FAN_SUPPLY_FLOW, SENSOR_FAN_EXHAUST_FLOW) + + global SENSOR_TYPES + SENSOR_TYPES = { + ATTR_CURRENT_TEMPERATURE: [ + 'Inside Temperature', + TEMP_CELSIUS, + 'mdi:thermometer', + SENSOR_TEMPERATURE_EXTRACT + ], + ATTR_CURRENT_HUMIDITY: [ + 'Inside Humidity', + '%', + 'mdi:water-percent', + SENSOR_HUMIDITY_EXTRACT + ], + ATTR_OUTSIDE_TEMPERATURE: [ + 'Outside Temperature', + TEMP_CELSIUS, + 'mdi:thermometer', + SENSOR_TEMPERATURE_OUTDOOR + ], + ATTR_OUTSIDE_HUMIDITY: [ + 'Outside Humidity', + '%', + 'mdi:water-percent', + SENSOR_HUMIDITY_OUTDOOR + ], + ATTR_AIR_FLOW_SUPPLY: [ + 'Supply airflow', + 'm³/h', + 'mdi:air-conditioner', + SENSOR_FAN_SUPPLY_FLOW + ], + ATTR_AIR_FLOW_EXHAUST: [ + 'Exhaust airflow', + 'm³/h', + 'mdi:air-conditioner', + SENSOR_FAN_EXHAUST_FLOW + ], + } + + ccb = hass.data[DOMAIN] + + sensors = [] + for resource in config[CONF_RESOURCES]: + sensor_type = resource.lower() + + if sensor_type not in SENSOR_TYPES: + _LOGGER.warning("Sensor type: %s is not a valid sensor.", + sensor_type) + continue + + sensors.append( + ComfoConnectSensor( + hass, + name="%s %s" % (ccb.name, SENSOR_TYPES[sensor_type][0]), + ccb=ccb, + sensor_type=sensor_type + ) + ) + + add_devices(sensors, True) + + return + + +class ComfoConnectSensor(Entity): + """Representation of a ComfoConnect sensor.""" + + def __init__(self, hass, name, ccb: ComfoConnectBridge, sensor_type): + """Initialize the ComfoConnect sensor.""" + self._ccb = ccb + self._sensor_type = sensor_type + self._sensor_id = SENSOR_TYPES[self._sensor_type][3] + self._name = name + + # Register the requested sensor + self._ccb.comfoconnect.register_sensor(self._sensor_id) + + def _handle_update(var): + if var == self._sensor_id: + _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 state(self): + """Return the state of the entity.""" + try: + return self._ccb.data[self._sensor_id] + except KeyError: + return STATE_UNKNOWN + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return SENSOR_TYPES[self._sensor_type][2] + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return SENSOR_TYPES[self._sensor_type][1] diff --git a/requirements_all.txt b/requirements_all.txt index 98f5078f9bc..a050e4ba0ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -533,6 +533,9 @@ pychromecast==0.8.1 # homeassistant.components.media_player.cmus pycmus==0.1.0 +# homeassistant.components.comfoconnect +pycomfoconnect==0.3 + # homeassistant.components.sensor.cups # pycups==1.9.73