From 3d83eea5f76f011a5df39d22a83852921e636623 Mon Sep 17 00:00:00 2001 From: Flyte Date: Sun, 14 Feb 2016 00:03:56 +0000 Subject: [PATCH 1/5] 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 From cf93644d54fc1ad75f956a3994121bea4d855ea6 Mon Sep 17 00:00:00 2001 From: Flyte Date: Wed, 17 Feb 2016 17:26:53 +0000 Subject: [PATCH 2/5] Move generic tcp sensor entity to specific sensor component --- homeassistant/components/binary_sensor/tcp.py | 11 +- homeassistant/components/sensor/tcp.py | 108 +++++++++++++++++- homeassistant/components/tcp.py | 103 ----------------- 3 files changed, 110 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/binary_sensor/tcp.py b/homeassistant/components/binary_sensor/tcp.py index 4e6e75e3555..056a59fbdf1 100644 --- a/homeassistant/components/binary_sensor/tcp.py +++ b/homeassistant/components/binary_sensor/tcp.py @@ -6,10 +6,11 @@ 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 +from homeassistant.components.tcp import DOMAIN, CONF_VALUE_ON +from homeassistant.components.sensor.tcp import Sensor -DEPENDENCIES = [tcp.DOMAIN] +DEPENDENCIES = [DOMAIN] _LOGGER = logging.getLogger(__name__) @@ -21,10 +22,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities((BinarySensor(config),)) -class BinarySensor(tcp.TCPEntity, BinarySensorDevice): +class BinarySensor(Sensor, BinarySensorDevice): """ A binary sensor which is on when its state == CONF_VALUE_ON. """ - required = (tcp.CONF_VALUE_ON,) + required = (CONF_VALUE_ON,) @property def is_on(self): - return self._state == self._config[tcp.CONF_VALUE_ON] + return self._state == self._config[CONF_VALUE_ON] diff --git a/homeassistant/components/sensor/tcp.py b/homeassistant/components/sensor/tcp.py index 53e91c6c728..b45f35434af 100644 --- a/homeassistant/components/sensor/tcp.py +++ b/homeassistant/components/sensor/tcp.py @@ -4,17 +4,117 @@ homeassistant.components.sensor.tcp Provides a sensor which gets its values from a TCP socket. """ import logging +import socket +import re +from select import select -from homeassistant.components import tcp +from homeassistant.const import CONF_NAME, CONF_HOST +from homeassistant.helpers.entity import Entity +from homeassistant.components.tcp import ( + DOMAIN, CONF_PORT, CONF_TIMEOUT, CONF_PAYLOAD, CONF_UNIT, CONF_VALUE_REGEX, + CONF_VALUE_ON, CONF_BUFFER_SIZE, DEFAULT_TIMEOUT, DEFAULT_BUFFER_SIZE +) -DEPENDENCIES = [tcp.DOMAIN] +DEPENDENCIES = [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): + if not Sensor.validate_config(config): return False - add_entities((tcp.TCPEntity(config),)) + add_entities((Sensor(config),)) + + +class Sensor(Entity): + """ Sensor 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(Sensor, 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 diff --git a/homeassistant/components/tcp.py b/homeassistant/components/tcp.py index 89c39e9f0db..9b09ceaf68e 100644 --- a/homeassistant/components/tcp.py +++ b/homeassistant/components/tcp.py @@ -3,15 +3,6 @@ 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" @@ -25,101 +16,7 @@ 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 From 348b7abe7d7d2c6f792a3ab7139cfd85876180d0 Mon Sep 17 00:00:00 2001 From: Flyte Date: Wed, 17 Feb 2016 18:12:36 +0000 Subject: [PATCH 3/5] Change TCP component to use Jinja2 instead of regex --- homeassistant/components/binary_sensor/tcp.py | 2 +- homeassistant/components/sensor/tcp.py | 41 +++++++++++-------- homeassistant/components/tcp.py | 1 + 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/binary_sensor/tcp.py b/homeassistant/components/binary_sensor/tcp.py index 056a59fbdf1..e6d01b893df 100644 --- a/homeassistant/components/binary_sensor/tcp.py +++ b/homeassistant/components/binary_sensor/tcp.py @@ -19,7 +19,7 @@ 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),)) + add_entities((BinarySensor(hass, config),)) class BinarySensor(Sensor, BinarySensorDevice): diff --git a/homeassistant/components/sensor/tcp.py b/homeassistant/components/sensor/tcp.py index b45f35434af..29d6c260fdd 100644 --- a/homeassistant/components/sensor/tcp.py +++ b/homeassistant/components/sensor/tcp.py @@ -5,14 +5,16 @@ Provides a sensor which gets its values from a TCP socket. """ import logging import socket -import re from select import select from homeassistant.const import CONF_NAME, CONF_HOST +from homeassistant.util import template +from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import Entity from homeassistant.components.tcp import ( DOMAIN, CONF_PORT, CONF_TIMEOUT, CONF_PAYLOAD, CONF_UNIT, CONF_VALUE_REGEX, - CONF_VALUE_ON, CONF_BUFFER_SIZE, DEFAULT_TIMEOUT, DEFAULT_BUFFER_SIZE + CONF_VALUE_TEMPLATE, CONF_VALUE_ON, CONF_BUFFER_SIZE, DEFAULT_TIMEOUT, + DEFAULT_BUFFER_SIZE ) @@ -25,15 +27,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ Create the Sensor. """ if not Sensor.validate_config(config): return False - add_entities((Sensor(config),)) + add_entities((Sensor(hass, config),)) class Sensor(Entity): """ Sensor Entity which gets its value from a TCP socket. """ required = tuple() - def __init__(self, config): + def __init__(self, hass, config): """ Set all the config values if they exist and get initial state. """ + self._hass = hass self._config = { CONF_NAME: config.get(CONF_NAME), CONF_HOST: config[CONF_HOST], @@ -42,6 +45,7 @@ class Sensor(Entity): CONF_PAYLOAD: config[CONF_PAYLOAD], CONF_UNIT: config.get(CONF_UNIT), CONF_VALUE_REGEX: config.get(CONF_VALUE_REGEX), + CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), CONF_VALUE_ON: config.get(CONF_VALUE_ON), CONF_BUFFER_SIZE: config.get( CONF_BUFFER_SIZE, DEFAULT_BUFFER_SIZE), @@ -78,6 +82,7 @@ class Sensor(Entity): 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: @@ -85,6 +90,7 @@ class Sensor(Entity): "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: @@ -93,6 +99,7 @@ class Sensor(Entity): 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( @@ -101,20 +108,20 @@ class Sensor(Entity): 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 + + if self._config[CONF_VALUE_TEMPLATE] is not None: 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]) + self._state = template.render( + self._hass, + self._config[CONF_VALUE_TEMPLATE], + value=value) return - return + except TemplateError as err: + _LOGGER.error( + "Unable to render template of %r with value: %r", + self._config[CONF_VALUE_TEMPLATE], value) + return + self._state = value diff --git a/homeassistant/components/tcp.py b/homeassistant/components/tcp.py index 9b09ceaf68e..c82bf935910 100644 --- a/homeassistant/components/tcp.py +++ b/homeassistant/components/tcp.py @@ -10,6 +10,7 @@ CONF_TIMEOUT = "timeout" CONF_PAYLOAD = "payload" CONF_UNIT = "unit" CONF_VALUE_REGEX = "value_regex" +CONF_VALUE_TEMPLATE = "value_template" CONF_VALUE_ON = "value_on" CONF_BUFFER_SIZE = "buffer_size" From c1d39a2fce4baf344e69cd7d32670274d3eaa286 Mon Sep 17 00:00:00 2001 From: Flyte Date: Thu, 18 Feb 2016 16:57:32 +0000 Subject: [PATCH 4/5] Remove unnecessary top-level TCP component. Fix order of inheritance on TCP BinarySensor. --- homeassistant/components/binary_sensor/tcp.py | 5 ++-- homeassistant/components/sensor/tcp.py | 21 +++++++++++------ homeassistant/components/tcp.py | 23 ------------------- 3 files changed, 16 insertions(+), 33 deletions(-) delete mode 100644 homeassistant/components/tcp.py diff --git a/homeassistant/components/binary_sensor/tcp.py b/homeassistant/components/binary_sensor/tcp.py index e6d01b893df..1f8bf2387c9 100644 --- a/homeassistant/components/binary_sensor/tcp.py +++ b/homeassistant/components/binary_sensor/tcp.py @@ -6,8 +6,7 @@ Provides a binary_sensor which gets its values from a TCP socket. import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.tcp import DOMAIN, CONF_VALUE_ON -from homeassistant.components.sensor.tcp import Sensor +from homeassistant.components.sensor.tcp import Sensor, DOMAIN, CONF_VALUE_ON DEPENDENCIES = [DOMAIN] @@ -22,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities((BinarySensor(hass, config),)) -class BinarySensor(Sensor, BinarySensorDevice): +class BinarySensor(BinarySensorDevice, Sensor): """ A binary sensor which is on when its state == CONF_VALUE_ON. """ required = (CONF_VALUE_ON,) diff --git a/homeassistant/components/sensor/tcp.py b/homeassistant/components/sensor/tcp.py index 29d6c260fdd..ad27ddef0bd 100644 --- a/homeassistant/components/sensor/tcp.py +++ b/homeassistant/components/sensor/tcp.py @@ -11,14 +11,22 @@ from homeassistant.const import CONF_NAME, CONF_HOST from homeassistant.util import template from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import Entity -from homeassistant.components.tcp import ( - DOMAIN, CONF_PORT, CONF_TIMEOUT, CONF_PAYLOAD, CONF_UNIT, CONF_VALUE_REGEX, - CONF_VALUE_TEMPLATE, CONF_VALUE_ON, CONF_BUFFER_SIZE, DEFAULT_TIMEOUT, - DEFAULT_BUFFER_SIZE -) -DEPENDENCIES = [DOMAIN] +# DEPENDENCIES = [DOMAIN] + +DOMAIN = "tcp" + +CONF_PORT = "port" +CONF_TIMEOUT = "timeout" +CONF_PAYLOAD = "payload" +CONF_UNIT = "unit" +CONF_VALUE_TEMPLATE = "value_template" +CONF_VALUE_ON = "value_on" +CONF_BUFFER_SIZE = "buffer_size" + +DEFAULT_TIMEOUT = 10 +DEFAULT_BUFFER_SIZE = 1024 _LOGGER = logging.getLogger(__name__) @@ -44,7 +52,6 @@ class Sensor(Entity): 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_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), CONF_VALUE_ON: config.get(CONF_VALUE_ON), CONF_BUFFER_SIZE: config.get( diff --git a/homeassistant/components/tcp.py b/homeassistant/components/tcp.py deleted file mode 100644 index c82bf935910..00000000000 --- a/homeassistant/components/tcp.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -homeassistant.components.tcp -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A generic TCP socket component. -""" -DOMAIN = "tcp" - -CONF_PORT = "port" -CONF_TIMEOUT = "timeout" -CONF_PAYLOAD = "payload" -CONF_UNIT = "unit" -CONF_VALUE_REGEX = "value_regex" -CONF_VALUE_TEMPLATE = "value_template" -CONF_VALUE_ON = "value_on" -CONF_BUFFER_SIZE = "buffer_size" - -DEFAULT_TIMEOUT = 10 -DEFAULT_BUFFER_SIZE = 1024 - - -def setup(hass, config): - """ Nothing to do! """ - return True From 4e6b755b26c41ddcde3233f6861d986052daa7e6 Mon Sep 17 00:00:00 2001 From: Flyte Date: Fri, 19 Feb 2016 17:41:51 +0000 Subject: [PATCH 5/5] Add tests for TCP component. --- homeassistant/components/sensor/tcp.py | 55 +++--- tests/components/binary_sensor/test_tcp.py | 0 tests/components/sensor/test_tcp.py | 203 +++++++++++++++++++++ 3 files changed, 231 insertions(+), 27 deletions(-) create mode 100644 tests/components/binary_sensor/test_tcp.py create mode 100644 tests/components/sensor/test_tcp.py diff --git a/homeassistant/components/sensor/tcp.py b/homeassistant/components/sensor/tcp.py index ad27ddef0bd..a2e14f12c39 100644 --- a/homeassistant/components/sensor/tcp.py +++ b/homeassistant/components/sensor/tcp.py @@ -5,7 +5,7 @@ Provides a sensor which gets its values from a TCP socket. """ import logging import socket -from select import select +import select from homeassistant.const import CONF_NAME, CONF_HOST from homeassistant.util import template @@ -88,35 +88,36 @@ class Sensor(Entity): def update(self): """ Get the latest value for this sensor. """ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + 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.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 - 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.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 - 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() + value = sock.recv(self._config[CONF_BUFFER_SIZE]).decode() if self._config[CONF_VALUE_TEMPLATE] is not None: try: diff --git a/tests/components/binary_sensor/test_tcp.py b/tests/components/binary_sensor/test_tcp.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/sensor/test_tcp.py b/tests/components/sensor/test_tcp.py new file mode 100644 index 00000000000..1acb8aa79c0 --- /dev/null +++ b/tests/components/sensor/test_tcp.py @@ -0,0 +1,203 @@ +""" +tests.components.sensor.tcp +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests TCP sensor. +""" +import socket +from copy import copy + +from unittest.mock import patch + +from homeassistant.components.sensor import tcp +from tests.common import get_test_home_assistant + + +TEST_CONFIG = { + tcp.CONF_NAME: "test_name", + tcp.CONF_HOST: "test_host", + tcp.CONF_PORT: 12345, + tcp.CONF_TIMEOUT: tcp.DEFAULT_TIMEOUT + 1, + tcp.CONF_PAYLOAD: "test_payload", + tcp.CONF_UNIT: "test_unit", + tcp.CONF_VALUE_TEMPLATE: "test_template", + tcp.CONF_VALUE_ON: "test_on", + tcp.CONF_BUFFER_SIZE: tcp.DEFAULT_BUFFER_SIZE + 1 +} +KEYS_AND_DEFAULTS = { + tcp.CONF_NAME: None, + tcp.CONF_TIMEOUT: tcp.DEFAULT_TIMEOUT, + tcp.CONF_UNIT: None, + tcp.CONF_VALUE_TEMPLATE: None, + tcp.CONF_VALUE_ON: None, + tcp.CONF_BUFFER_SIZE: tcp.DEFAULT_BUFFER_SIZE +} + + +# class TestTCPSensor(unittest.TestCase): +class TestTCPSensor(): + """ Test the TCP Sensor. """ + + def setup_class(cls): + cls.hass = get_test_home_assistant() + + def teardown_class(cls): + cls.hass.stop() + + @patch("homeassistant.components.sensor.tcp.Sensor.update") + def test_config_valid_keys(self, *args): + """ + Should store valid keys in _config. + """ + sensor = tcp.Sensor(self.hass, TEST_CONFIG) + for key in TEST_CONFIG: + assert key in sensor._config + + def test_validate_config_valid_keys(self): + """ + Should return True when provided with the correct keys. + """ + assert tcp.Sensor.validate_config(TEST_CONFIG) + + @patch("homeassistant.components.sensor.tcp.Sensor.update") + def test_config_invalid_keys(self, *args): + """ + Shouldn't store invalid keys in _config. + """ + config = copy(TEST_CONFIG) + config.update({ + "a": "test_a", + "b": "test_b", + "c": "test_c" + }) + sensor = tcp.Sensor(self.hass, config) + for invalid_key in tuple("abc"): + assert invalid_key not in sensor._config + + @patch("homeassistant.components.sensor.tcp.Sensor.update") + def test_validate_config_invalid_keys(self, *args): + """ + Should return True when provided with the correct keys plus some extra. + """ + config = copy(TEST_CONFIG) + config.update({ + "a": "test_a", + "b": "test_b", + "c": "test_c" + }) + assert tcp.Sensor.validate_config(config) + + @patch("homeassistant.components.sensor.tcp.Sensor.update") + def test_config_uses_defaults(self, *args): + """ + Should use defaults where appropriate. + """ + config = copy(TEST_CONFIG) + for key in KEYS_AND_DEFAULTS.keys(): + del config[key] + sensor = tcp.Sensor(self.hass, config) + for key, default in KEYS_AND_DEFAULTS.items(): + assert sensor._config[key] == default + + def test_validate_config_missing_defaults(self): + """ + Should return True when defaulted keys are not provided. + """ + config = copy(TEST_CONFIG) + for key in KEYS_AND_DEFAULTS.keys(): + del config[key] + assert tcp.Sensor.validate_config(config) + + def test_validate_config_missing_required(self): + """ + Should return False when required config items are missing. + """ + for key in TEST_CONFIG: + if key in KEYS_AND_DEFAULTS: + continue + config = copy(TEST_CONFIG) + del config[key] + assert not tcp.Sensor.validate_config(config), ( + "validate_config() should have returned False since %r was not" + "provided." % key) + + @patch("homeassistant.components.sensor.tcp.Sensor.update") + def test_init_calls_update(self, mock_update): + """ + Should call update() method during __init__(). + """ + tcp.Sensor(self.hass, TEST_CONFIG) + assert mock_update.called + + @patch("socket.socket") + @patch("select.select", return_value=(True, False, False)) + def test_update_connects_to_host_and_port(self, mock_select, mock_socket): + """ + Should connect to the configured host and port. + """ + tcp.Sensor(self.hass, TEST_CONFIG) + mock_socket = mock_socket().__enter__() + mock_socket.connect.assert_called_with(( + TEST_CONFIG[tcp.CONF_HOST], + TEST_CONFIG[tcp.CONF_PORT])) + + @patch("socket.socket.connect", side_effect=socket.error()) + def test_update_returns_if_connecting_fails(self, mock_socket): + """ + Should return if connecting to host fails. + """ + with patch("homeassistant.components.sensor.tcp.Sensor.update"): + sensor = tcp.Sensor(self.hass, TEST_CONFIG) + assert sensor.update() is None + + @patch("socket.socket") + @patch("select.select", return_value=(True, False, False)) + def test_update_sends_payload(self, mock_select, mock_socket): + """ + Should send the configured payload as bytes. + """ + tcp.Sensor(self.hass, TEST_CONFIG) + mock_socket = mock_socket().__enter__() + mock_socket.send.assert_called_with( + TEST_CONFIG[tcp.CONF_PAYLOAD].encode() + ) + + @patch("socket.socket") + @patch("select.select", return_value=(True, False, False)) + def test_update_calls_select_with_timeout(self, mock_select, mock_socket): + """ + Should provide the timeout argument to select. + """ + tcp.Sensor(self.hass, TEST_CONFIG) + mock_socket = mock_socket().__enter__() + mock_select.assert_called_with( + [mock_socket], [], [], TEST_CONFIG[tcp.CONF_TIMEOUT]) + + @patch("socket.socket") + @patch("select.select", return_value=(True, False, False)) + def test_update_receives_packet_and_sets_as_state( + self, mock_select, mock_socket): + """ + Should receive the response from the socket and set it as the state. + """ + test_value = "test_value" + mock_socket = mock_socket().__enter__() + mock_socket.recv.return_value = test_value.encode() + config = copy(TEST_CONFIG) + del config[tcp.CONF_VALUE_TEMPLATE] + sensor = tcp.Sensor(self.hass, config) + assert sensor._state == test_value + + @patch("socket.socket") + @patch("select.select", return_value=(True, False, False)) + def test_update_renders_value_in_template(self, mock_select, mock_socket): + """ + Should render the value in the provided template. + """ + test_value = "test_value" + mock_socket = mock_socket().__enter__() + mock_socket.recv.return_value = test_value.encode() + config = copy(TEST_CONFIG) + config[tcp.CONF_VALUE_TEMPLATE] = "{{ value }} {{ 1+1 }}" + sensor = tcp.Sensor(self.hass, config) + assert sensor._state == "%s 2" % test_value