From aad375b713c521cfe0d7b845c4c1802487bbbe80 Mon Sep 17 00:00:00 2001 From: Fabrizio Furnari Date: Mon, 24 Apr 2017 22:01:00 +0200 Subject: [PATCH] Add https certificate expiry sensor (#7272) * fixing rebase issues * cert_expiry: added .coveragerc entry * cert_expiry: renamed to SCAN_INTERVAL, removed Throttle * cert_expiry: better socket exception management * cert_expiry: splitted line too long * Update cert_expiry.py * Fix hass style --- .coveragerc | 1 + .../components/sensor/cert_expiry.py | 97 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 homeassistant/components/sensor/cert_expiry.py diff --git a/.coveragerc b/.coveragerc index f9936f26256..c2448f4143a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -335,6 +335,7 @@ omit = homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/coinmarketcap.py + homeassistant/components/sensor/cert_expiry.py homeassistant/components/sensor/comed_hourly_pricing.py homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/crimereports.py diff --git a/homeassistant/components/sensor/cert_expiry.py b/homeassistant/components/sensor/cert_expiry.py new file mode 100644 index 00000000000..5aca7c49d13 --- /dev/null +++ b/homeassistant/components/sensor/cert_expiry.py @@ -0,0 +1,97 @@ +"""Counts the days an HTTPS (TLS) certificate will expire (days). + +For more details about this sensor please refer to the +documentation at https://home-assistant.io/components/sensor.cert_expiry +""" +import logging +import ssl +import socket +import datetime +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'SSL Certificate Expiry' +DEFAULT_PORT = 443 + +SCAN_INTERVAL = datetime.timedelta(hours=12) +TIMEOUT = 10.0 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup certificate expiry sensor.""" + server_name = config.get(CONF_HOST) + server_port = config.get(CONF_PORT) + sensor_name = config.get(CONF_NAME) + add_devices([SSLCertificate(sensor_name, server_name, server_port)]) + + +class SSLCertificate(Entity): + """Implements certificate expiry sensor.""" + + def __init__(self, sensor_name, server_name, server_port): + """Initialize the sensor.""" + self.server_name = server_name + self.server_port = server_port + self._name = sensor_name + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'days' + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return 'mdi:certificate' + + def update(self): + """Fetch certificate information.""" + try: + ctx = ssl.create_default_context() + sock = ctx.wrap_socket(socket.socket(), + server_hostname=self.server_name) + sock.settimeout(TIMEOUT) + sock.connect((self.server_name, self.server_port)) + except socket.gaierror: + _LOGGER.error('Cannot resolve name %s', self.server_name) + except socket.timeout: + _LOGGER.error('Connection timeout with server %s', + self.server_name) + except OSError as excp: + _LOGGER.error('Cannot connect to %s', self.server_name) + raise excp + + try: + cert = sock.getpeercert() + except OSError as excp: + _LOGGER.error('Cannot fetch certificate from %s', + (self.server_name)) + raise excp + + ts_seconds = ssl.cert_time_to_seconds(cert['notAfter']) + timestamp = datetime.datetime.fromtimestamp(ts_seconds) + expiry = timestamp - datetime.datetime.today() + self._state = expiry.days