core/homeassistant/components/tcp/sensor.py

184 lines
6.1 KiB
Python
Raw Normal View History

"""Support for TCP socket based sensors."""
from __future__ import annotations
2016-02-14 00:03:56 +00:00
import logging
2016-02-19 17:41:51 +00:00
import select
import socket
import ssl
from typing import Any, Final
2016-02-14 00:03:56 +00:00
2016-10-22 04:14:35 +00:00
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
SensorEntity,
)
2016-10-22 04:14:35 +00:00
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
CONF_HOST,
CONF_NAME,
2019-07-31 19:25:30 +00:00
CONF_PAYLOAD,
CONF_PORT,
CONF_SSL,
2019-07-31 19:25:30 +00:00
CONF_TIMEOUT,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL,
2019-07-31 19:25:30 +00:00
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
2016-10-22 04:14:35 +00:00
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.template import Template
2021-05-22 17:16:11 +00:00
from homeassistant.helpers.typing import ConfigType, StateType
from .const import (
CONF_BUFFER_SIZE,
CONF_VALUE_ON,
DEFAULT_BUFFER_SIZE,
DEFAULT_NAME,
DEFAULT_SSL,
DEFAULT_TIMEOUT,
DEFAULT_VERIFY_SSL,
)
from .model import TcpSensorConfig
2016-10-22 04:14:35 +00:00
_LOGGER: Final = logging.getLogger(__name__)
2016-02-14 00:03:56 +00:00
PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend(
2019-07-31 19:25:30 +00:00
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_BUFFER_SIZE, default=DEFAULT_BUFFER_SIZE): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_VALUE_ON): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
2019-07-31 19:25:30 +00:00
}
)
2016-02-14 00:03:56 +00:00
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: dict[str, Any] | None = None,
) -> None:
2016-10-22 04:14:35 +00:00
"""Set up the TCP Sensor."""
add_entities([TcpSensor(hass, config)])
class TcpSensor(SensorEntity):
2016-03-08 15:46:34 +00:00
"""Implementation of a TCP socket based sensor."""
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
2016-02-22 13:42:11 +00:00
"""Set all the config values if they exist and get initial state."""
value_template: Template | None = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = hass
self._hass = hass
self._config: TcpSensorConfig = {
CONF_NAME: config[CONF_NAME],
CONF_HOST: config[CONF_HOST],
CONF_PORT: config[CONF_PORT],
CONF_TIMEOUT: config[CONF_TIMEOUT],
CONF_PAYLOAD: config[CONF_PAYLOAD],
2016-10-22 04:14:35 +00:00
CONF_UNIT_OF_MEASUREMENT: config.get(CONF_UNIT_OF_MEASUREMENT),
CONF_VALUE_TEMPLATE: value_template,
CONF_VALUE_ON: config.get(CONF_VALUE_ON),
CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE],
CONF_SSL: config[CONF_SSL],
CONF_VERIFY_SSL: config[CONF_VERIFY_SSL],
}
self._ssl_context: ssl.SSLContext | None = None
if self._config[CONF_SSL]:
self._ssl_context = ssl.create_default_context()
if not self._config[CONF_VERIFY_SSL]:
self._ssl_context.check_hostname = False
self._ssl_context.verify_mode = ssl.CERT_NONE
self._state: str | None = None
self.update()
@property
def name(self) -> str:
2016-03-08 15:46:34 +00:00
"""Return the name of this sensor."""
return self._config[CONF_NAME]
@property
2021-05-22 17:16:11 +00:00
def state(self) -> StateType:
2016-02-22 13:42:11 +00:00
"""Return the state of the device."""
return self._state
@property
def unit_of_measurement(self) -> str | None:
2016-03-08 15:46:34 +00:00
"""Return the unit of measurement of this entity."""
2016-10-22 04:14:35 +00:00
return self._config[CONF_UNIT_OF_MEASUREMENT]
def update(self) -> None:
2016-02-22 13:42:11 +00:00
"""Get the latest value for this sensor."""
2016-02-19 17:41:51 +00:00
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
2016-03-05 17:27:22 +00:00
sock.settimeout(self._config[CONF_TIMEOUT])
2016-02-19 17:41:51 +00:00
try:
2019-07-31 19:25:30 +00:00
sock.connect((self._config[CONF_HOST], self._config[CONF_PORT]))
except OSError as err:
2016-02-19 17:41:51 +00:00
_LOGGER.error(
"Unable to connect to %s on port %s: %s",
2019-07-31 19:25:30 +00:00
self._config[CONF_HOST],
self._config[CONF_PORT],
err,
)
2016-02-19 17:41:51 +00:00
return
if self._ssl_context is not None:
sock = self._ssl_context.wrap_socket(
sock, server_hostname=self._config[CONF_HOST]
)
2016-02-19 17:41:51 +00:00
try:
sock.send(self._config[CONF_PAYLOAD].encode())
except OSError as err:
2016-02-19 17:41:51 +00:00
_LOGGER.error(
"Unable to send payload %r to %s on port %s: %s",
2019-07-31 19:25:30 +00:00
self._config[CONF_PAYLOAD],
self._config[CONF_HOST],
self._config[CONF_PORT],
err,
)
2016-02-19 17:41:51 +00:00
return
2019-07-31 19:25:30 +00:00
readable, _, _ = select.select([sock], [], [], self._config[CONF_TIMEOUT])
2016-02-19 17:41:51 +00:00
if not readable:
_LOGGER.warning(
"Timeout (%s second(s)) waiting for a response after "
"sending %r to %s on port %s",
2019-07-31 19:25:30 +00:00
self._config[CONF_TIMEOUT],
self._config[CONF_PAYLOAD],
self._config[CONF_HOST],
self._config[CONF_PORT],
)
2016-02-19 17:41:51 +00:00
return
value = sock.recv(self._config[CONF_BUFFER_SIZE]).decode()
value_template = self._config[CONF_VALUE_TEMPLATE]
if value_template is not None:
try:
self._state = value_template.render(parse_result=False, value=value)
return
except TemplateError:
_LOGGER.error(
"Unable to render template of %r with value: %r",
2019-07-31 19:25:30 +00:00
self._config[CONF_VALUE_TEMPLATE],
value,
)
return
self._state = value