diff --git a/.coveragerc b/.coveragerc index c7b5b07e02b..2cbc224164f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -88,6 +88,10 @@ omit = homeassistant/components/homematic.py homeassistant/components/*/homematic.py + homeassistant/components/knx.py + homeassistant/components/switch/knx.py + homeassistant/components/binary_sensor/knx.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py new file mode 100644 index 00000000000..f7730407b23 --- /dev/null +++ b/homeassistant/components/binary_sensor/knx.py @@ -0,0 +1,24 @@ +""" +Contains functionality to use a KNX group address as a binary. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.knx/ +""" +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.knx import ( + KNXConfig, KNXGroupAddress) + +DEPENDENCIES = ["knx"] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create and add an entity based on the configuration.""" + add_entities([ + KNXSwitch(hass, KNXConfig(config)) + ]) + + +class KNXSwitch(KNXGroupAddress, BinarySensorDevice): + """Representation of a KNX sensor device.""" + + pass diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py new file mode 100644 index 00000000000..f87d4de0d61 --- /dev/null +++ b/homeassistant/components/knx.py @@ -0,0 +1,297 @@ +""" +Support for KNX components. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/knx/ + +(c) 2016 Daniel Matuschek +""" +import logging + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity + +DOMAIN = "knx" +REQUIREMENTS = ['knxip==0.3.0'] + +EVENT_KNX_FRAME_RECEIVED = "knx_frame_received" + +CONF_HOST = "host" +CONF_PORT = "port" + +DEFAULT_PORT = "3671" + +KNXTUNNEL = None + +_LOGGER = logging.getLogger(__name__) + + +def setup(hass, config): + """Setup the connection to the KNX IP interface.""" + global KNXTUNNEL + + from knxip.ip import KNXIPTunnel + from knxip.core import KNXException + + host = config[DOMAIN].get(CONF_HOST, None) + + if host is None: + _LOGGER.debug("Will try to auto-detect KNX/IP gateway") + host = "0.0.0.0" + + try: + port = int(config[DOMAIN].get(CONF_PORT, DEFAULT_PORT)) + except ValueError: + _LOGGER.exception("Can't parse KNX IP interface port") + return False + + KNXTUNNEL = KNXIPTunnel(host, port) + try: + KNXTUNNEL.connect() + except KNXException as ex: + _LOGGER.exception("Can't connect to KNX/IP interface: %s", ex) + KNXTUNNEL = None + return False + + _LOGGER.info("KNX IP tunnel to %s:%i established", host, port) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_tunnel) + return True + + +def close_tunnel(_data): + """Close the NKX tunnel connection on shutdown.""" + global KNXTUNNEL + + KNXTUNNEL.disconnect() + KNXTUNNEL = None + + +class KNXConfig(object): + """Handle the fetching of configuration from the config file.""" + + def __init__(self, config): + """Initialize the configuration.""" + from knxip.core import parse_group_address + + self.config = config + self.should_poll = config.get("poll", True) + self._address = parse_group_address(config.get("address")) + if self.config.get("state_address"): + self._state_address = parse_group_address( + self.config.get("state_address")) + else: + self._state_address = None + + @property + def name(self): + """The name given to the entity.""" + return self.config["name"] + + @property + def address(self): + """The address of the device as an integer value. + + 3 types of addresses are supported: + integer - 0-65535 + 2 level - a/b + 3 level - a/b/c + """ + return self._address + + @property + def state_address(self): + """The group address the device sends its current state to. + + Some KNX devices can send the current state to a seperate + group address. This makes send e.g. when an actuator can + be switched but also have a timer functionality. + """ + return self._state_address + + +class KNXGroupAddress(Entity): + """Representation of devices connected to a KNX group address.""" + + def __init__(self, hass, config): + """Initialize the device.""" + self._config = config + self._state = False + self._data = None + _LOGGER.debug("Initalizing KNX group address %s", self.address) + + def handle_knx_message(addr, data): + """Handle an incoming KNX frame. + + Handle an incoming frame and update our status if it contains + information relating to this device. + """ + if (addr == self.state_address) or (addr == self.address): + self._state = data + self.update_ha_state() + + KNXTUNNEL.register_listener(self.address, handle_knx_message) + if self.state_address: + KNXTUNNEL.register_listener(self.state_address, handle_knx_message) + + @property + def name(self): + """The entity's display name.""" + return self._config.name + + @property + def config(self): + """The entity's configuration.""" + return self._config + + @property + def should_poll(self): + """Return the state of the polling, if needed.""" + return self._config.should_poll + + @property + def is_on(self): + """Return True if the value is not 0 is on, else False.""" + if self.should_poll: + self.update() + return self._state != 0 + + @property + def address(self): + """Return the KNX group address.""" + return self._config.address + + @property + def state_address(self): + """Return the KNX group address.""" + return self._config.state_address + + @property + def cache(self): + """The name given to the entity.""" + return self._config.config.get("cache", True) + + def group_write(self, value): + """Write to the group address.""" + KNXTUNNEL.group_write(self.address, [value]) + + def update(self): + """Get the state from KNX bus or cache.""" + from knxip.core import KNXException + + try: + if self.state_address: + res = KNXTUNNEL.group_read(self.state_address, + use_cache=self.cache) + else: + res = KNXTUNNEL.group_read(self.address, + use_cache=self.cache) + + if res: + self._state = res[0] + self._data = res + else: + _LOGGER.debug("Unable to read from KNX address: %s (None)", + self.address) + + except KNXException: + _LOGGER.exception("Unable to read from KNX address: %s", + self.address) + return False + + +class KNXMultiAddressDevice(KNXGroupAddress): + """Representation of devices connected to a multiple KNX group address. + + This is needed for devices like dimmers or shutter actuators as they have + to be controlled by multiple group addresses. + """ + + names = {} + values = {} + + def __init__(self, hass, config, required, optional=None): + """Initialize the device. + + The namelist argument lists the required addresses. E.g. for a dimming + actuators, the namelist might look like: + onoff_address: 0/0/1 + brightness_address: 0/0/2 + """ + from knxip.core import parse_group_address, KNXException + + super().__init__(self, hass, config) + + self.config = config + + # parse required addresses + for name in required: + paramname = name + "_address" + addr = self._config.config.get(paramname) + if addr is None: + _LOGGER.exception("Required KNX group address %s missing", + paramname) + raise KNXException("Group address missing in configuration") + addr = parse_group_address(addr) + self.names[addr] = name + + # parse optional addresses + for name in optional: + paramname = name + "_address" + addr = self._config.config.get(paramname) + if addr: + try: + addr = parse_group_address(addr) + except KNXException: + _LOGGER.exception("Cannot parse group address %s", addr) + self.names[addr] = name + + def handle_frame(frame): + """Handle an incoming KNX frame. + + Handle an incoming frame and update our status if it contains + information relating to this device. + """ + addr = frame.data[0] + + if addr in self.names: + self.values[addr] = frame.data[1] + self.update_ha_state() + + hass.bus.listen(EVENT_KNX_FRAME_RECEIVED, handle_frame) + + def group_write_address(self, name, value): + """Write to the group address with the given name.""" + KNXTUNNEL.group_write(self.address, [value]) + + def has_attribute(self, name): + """Check if the attribute with the given name is defined. + + This is mostly important for optional addresses. + """ + for attributename, dummy_attribute in self.names.items(): + if attributename == name: + return True + return False + + def value(self, name): + """Return the value to a given named attribute.""" + from knxip.core import KNXException + + addr = None + for attributename, attributeaddress in self.names.items(): + if attributename == name: + addr = attributeaddress + + if addr is None: + _LOGGER.exception("Attribute %s undefined", name) + return False + + try: + res = KNXTUNNEL.group_read(addr, use_cache=self.cache) + except KNXException: + _LOGGER.exception("Unable to read from KNX address: %s", + addr) + return False + + return res diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py new file mode 100644 index 00000000000..b2866223350 --- /dev/null +++ b/homeassistant/components/switch/knx.py @@ -0,0 +1,44 @@ +""" +Support KNX switching actuators. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.knx/ + +(c) 2016 Daniel Matuschek +""" +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.knx import ( + KNXConfig, KNXGroupAddress) + +DEPENDENCIES = ["knx"] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create and add an entity based on the configuration.""" + add_entities([ + KNXSwitch(hass, KNXConfig(config)) + ]) + + +class KNXSwitch(KNXGroupAddress, SwitchDevice): + """Representation of a KNX switch device.""" + + def turn_on(self, **kwargs): + """Turn the switch on. + + This sends a value 0 to the group address of the device + """ + self.group_write(1) + self._state = [1] + if not self.should_poll: + self.update_ha_state() + + def turn_off(self, **kwargs): + """Turn the switch off. + + This sends a value 1 to the group address of the device + """ + self.group_write(0) + self._state = [0] + if not self.should_poll: + self.update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 740bfa37cd5..5e08ee42f67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,6 +176,9 @@ insteon_hub==0.4.5 # homeassistant.components.media_player.kodi jsonrpc-requests==0.3 +# homeassistant.components.knx +knxip==0.3.0 + # homeassistant.components.light.lifx liffylights==0.9.4