From db8bb53984b964c62b66d14b2f301cd0de77508f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Fri, 7 Jul 2017 06:25:54 +0200 Subject: [PATCH] Add One-Time Password sensor (OTP) (#8332) --- .coveragerc | 1 + homeassistant/components/sensor/otp.py | 90 ++++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 94 insertions(+) create mode 100644 homeassistant/components/sensor/otp.py diff --git a/.coveragerc b/.coveragerc index 1f2454fc2928..bdab049329a5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -453,6 +453,7 @@ omit = homeassistant/components/sensor/openexchangerates.py homeassistant/components/sensor/opensky.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/otp.py homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py homeassistant/components/sensor/pocketcasts.py diff --git a/homeassistant/components/sensor/otp.py b/homeassistant/components/sensor/otp.py new file mode 100644 index 000000000000..5d7808ea4c72 --- /dev/null +++ b/homeassistant/components/sensor/otp.py @@ -0,0 +1,90 @@ +""" +Support for One-Time Password (OTP). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.otp/ +""" +import time +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_TOKEN) +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['pyotp==2.2.6'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'OTP Sensor' + +TIME_STEP = 30 # Default time step assumed by Google Authenticator + +ICON = 'mdi:update' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the OTP sensor.""" + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + + async_add_devices([TOTPSensor(name, token)], True) + return True + + +# Only TOTP supported at the moment, HOTP might be added later +class TOTPSensor(Entity): + """Representation of a TOTP sensor.""" + + def __init__(self, name, token): + """Initialize the sensor.""" + import pyotp + self._name = name + self._otp = pyotp.TOTP(token) + self._state = None + self._next_expiration = None + + @asyncio.coroutine + def async_added_to_hass(self): + """Handle when an entity is about to be added to Home Assistant.""" + self._call_loop() + + @callback + def _call_loop(self): + self._state = self._otp.now() + self.hass.async_add_job(self.async_update_ha_state()) + + # Update must occur at even TIME_STEP, e.g. 12:00:00, 12:00:30, + # 12:01:00, etc. in order to have synced time (see RFC6238) + self._next_expiration = TIME_STEP - (time.time() % TIME_STEP) + self.hass.loop.call_later(self._next_expiration, self._call_loop) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return ICON diff --git a/requirements_all.txt b/requirements_all.txt index cca647238fcd..814b61a3e58b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -643,6 +643,9 @@ pynut2==2.1.2 # homeassistant.components.binary_sensor.nx584 pynx584==0.4 +# homeassistant.components.sensor.otp +pyotp==2.2.6 + # homeassistant.components.sensor.openweathermap # homeassistant.components.weather.openweathermap pyowm==2.7.1