From 3d83eea5f76f011a5df39d22a83852921e636623 Mon Sep 17 00:00:00 2001 From: Flyte Date: Sun, 14 Feb 2016 00:03:56 +0000 Subject: [PATCH] Add tcp component. --- homeassistant/components/binary_sensor/tcp.py | 30 +++++ homeassistant/components/sensor/tcp.py | 20 +++ homeassistant/components/tcp.py | 125 ++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 homeassistant/components/binary_sensor/tcp.py create mode 100644 homeassistant/components/sensor/tcp.py create mode 100644 homeassistant/components/tcp.py diff --git a/homeassistant/components/binary_sensor/tcp.py b/homeassistant/components/binary_sensor/tcp.py new file mode 100644 index 00000000000..4e6e75e3555 --- /dev/null +++ b/homeassistant/components/binary_sensor/tcp.py @@ -0,0 +1,30 @@ +""" +homeassistant.components.binary_sensor.tcp +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides a binary_sensor which gets its values from a TCP socket. +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components import tcp + + +DEPENDENCIES = [tcp.DOMAIN] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ Create the BinarySensor. """ + if not BinarySensor.validate_config(config): + return False + add_entities((BinarySensor(config),)) + + +class BinarySensor(tcp.TCPEntity, BinarySensorDevice): + """ A binary sensor which is on when its state == CONF_VALUE_ON. """ + required = (tcp.CONF_VALUE_ON,) + + @property + def is_on(self): + return self._state == self._config[tcp.CONF_VALUE_ON] diff --git a/homeassistant/components/sensor/tcp.py b/homeassistant/components/sensor/tcp.py new file mode 100644 index 00000000000..53e91c6c728 --- /dev/null +++ b/homeassistant/components/sensor/tcp.py @@ -0,0 +1,20 @@ +""" +homeassistant.components.sensor.tcp +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides a sensor which gets its values from a TCP socket. +""" +import logging + +from homeassistant.components import tcp + + +DEPENDENCIES = [tcp.DOMAIN] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ Create the Sensor. """ + if not tcp.TCPEntity.validate_config(config): + return False + add_entities((tcp.TCPEntity(config),)) diff --git a/homeassistant/components/tcp.py b/homeassistant/components/tcp.py new file mode 100644 index 00000000000..89c39e9f0db --- /dev/null +++ b/homeassistant/components/tcp.py @@ -0,0 +1,125 @@ +""" +homeassistant.components.tcp +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A generic TCP socket component. +""" +import logging +import socket +import re +from select import select + +from homeassistant.const import CONF_NAME, CONF_HOST +from homeassistant.helpers.entity import Entity + + +DOMAIN = "tcp" + +CONF_PORT = "port" +CONF_TIMEOUT = "timeout" +CONF_PAYLOAD = "payload" +CONF_UNIT = "unit" +CONF_VALUE_REGEX = "value_regex" +CONF_VALUE_ON = "value_on" +CONF_BUFFER_SIZE = "buffer_size" + +DEFAULT_TIMEOUT = 10 +DEFAULT_BUFFER_SIZE = 1024 + +_LOGGER = logging.getLogger(__name__) + + +def setup(hass, config): + """ Nothing to do! """ + return True + + +class TCPEntity(Entity): + """ Generic Entity which gets its value from a TCP socket. """ + required = tuple() + + def __init__(self, config): + """ Set all the config values if they exist and get initial state. """ + self._config = { + CONF_NAME: config.get(CONF_NAME), + CONF_HOST: config[CONF_HOST], + CONF_PORT: config[CONF_PORT], + CONF_TIMEOUT: config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + CONF_PAYLOAD: config[CONF_PAYLOAD], + CONF_UNIT: config.get(CONF_UNIT), + CONF_VALUE_REGEX: config.get(CONF_VALUE_REGEX), + CONF_VALUE_ON: config.get(CONF_VALUE_ON), + CONF_BUFFER_SIZE: config.get( + CONF_BUFFER_SIZE, DEFAULT_BUFFER_SIZE), + } + self._state = None + self.update() + + @classmethod + def validate_config(cls, config): + """ Ensure the config has all of the necessary values. """ + always_required = (CONF_HOST, CONF_PORT, CONF_PAYLOAD) + for key in always_required + tuple(cls.required): + if key not in config: + _LOGGER.error( + "You must provide %r to create any TCP entity.", key) + return False + return True + + @property + def name(self): + name = self._config[CONF_NAME] + if name is not None: + return name + return super(TCPEntity, self).name + + @property + def state(self): + return self._state + + @property + def unit_of_measurement(self): + return self._config[CONF_UNIT] + + def update(self): + """ Get the latest value for this sensor. """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((self._config[CONF_HOST], self._config[CONF_PORT])) + except socket.error as err: + _LOGGER.error( + "Unable to connect to %s on port %s: %s", + self._config[CONF_HOST], self._config[CONF_PORT], err) + return + try: + sock.send(self._config[CONF_PAYLOAD].encode()) + except socket.error as err: + _LOGGER.error( + "Unable to send payload %r to %s on port %s: %s", + self._config[CONF_PAYLOAD], self._config[CONF_HOST], + self._config[CONF_PORT], err) + return + readable, _, _ = select([sock], [], [], self._config[CONF_TIMEOUT]) + if not readable: + _LOGGER.warning( + "Timeout (%s second(s)) waiting for a response after sending " + "%r to %s on port %s.", + self._config[CONF_TIMEOUT], self._config[CONF_PAYLOAD], + self._config[CONF_HOST], self._config[CONF_PORT]) + return + value = sock.recv(self._config[CONF_BUFFER_SIZE]).decode() + if self._config[CONF_VALUE_REGEX] is not None: + match = re.match(self._config[CONF_VALUE_REGEX], value) + if match is None: + _LOGGER.warning( + "Unable to match value using value_regex of %r: %r", + self._config[CONF_VALUE_REGEX], value) + return + try: + self._state = match.groups()[0] + except IndexError: + _LOGGER.error( + "You must include a capture group in the regex for %r: %r", + self.name, self._config[CONF_VALUE_REGEX]) + return + return + self._state = value