From bb8981b611a9335e9798264ae28bca501cefdc77 Mon Sep 17 00:00:00 2001 From: Flyte Date: Wed, 10 Feb 2016 16:32:18 +0000 Subject: [PATCH 1/2] Add apcupsd component. --- .coveragerc | 3 + homeassistant/components/apcupsd.py | 51 +++++++++ .../components/binary_sensor/apcupsd.py | 46 ++++++++ homeassistant/components/sensor/apcupsd.py | 107 ++++++++++++++++++ requirements_all.txt | 3 + 5 files changed, 210 insertions(+) create mode 100644 homeassistant/components/apcupsd.py create mode 100644 homeassistant/components/binary_sensor/apcupsd.py create mode 100644 homeassistant/components/sensor/apcupsd.py diff --git a/.coveragerc b/.coveragerc index 8fefa8a44b8..6f94f344061 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,9 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py + homeassistant/components/apcupsd.py + homeassistant/components/*/apcupsd.py + homeassistant/components/bloomsky.py homeassistant/components/*/bloomsky.py diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py new file mode 100644 index 00000000000..8c4a964d330 --- /dev/null +++ b/homeassistant/components/apcupsd.py @@ -0,0 +1,51 @@ +""" +homeassistant.components.apcupsd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Sets up and provides access to the status output of APCUPSd via its Network +Information Server (NIS). +""" +import logging + + +DOMAIN = "apcupsd" +REQUIREMENTS = ("apcaccess==0.0.4",) + +CONF_HOST = "host" +CONF_PORT = "port" +CONF_TYPE = "type" + +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 3551 + +KEY_STATUS = "STATUS" + +VALUE_ONLINE = "ONLINE" + +GET_STATUS = None + +_LOGGER = logging.getLogger(__name__) + + +def setup(hass, config): + """ Use config values to set up a function enabling status retrieval. """ + global GET_STATUS + from apcaccess import status + + host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST) + port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT) + + def get_status(): + """ Get the status from APCUPSd and parse it into a dict. """ + return status.parse(status.get(host=host, port=port)) + + GET_STATUS = get_status + + # It doesn't really matter why we're not able to get the status, just that + # we can't. + # pylint: disable=broad-except + try: + GET_STATUS() + except Exception: + _LOGGER.exception("Failure while testing APCUPSd status retrieval.") + return False + return True diff --git a/homeassistant/components/binary_sensor/apcupsd.py b/homeassistant/components/binary_sensor/apcupsd.py new file mode 100644 index 00000000000..a6eaa7f7ea9 --- /dev/null +++ b/homeassistant/components/binary_sensor/apcupsd.py @@ -0,0 +1,46 @@ +""" +homeassistant.components.binary_sensor.apcupsd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides a binary sensor to track online status of a UPS. +""" +from homeassistant.core import JobPriority +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components import apcupsd + + +DEPENDENCIES = [apcupsd.DOMAIN] + +DEFAULT_NAME = "UPS Online Status" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ Instantiate an OnlineStatus binary sensor entity and add it to HA. """ + add_entities((OnlineStatus(hass, config),)) + + +class OnlineStatus(BinarySensorDevice): + """ Binary sensor to represent UPS online status. """ + def __init__(self, hass, config): + self._config = config + self._state = None + # Get initial state + hass.pool.add_job( + JobPriority.EVENT_STATE, (self.update_ha_state, True)) + + @property + def name(self): + """ The name of the UPS online status sensor. """ + return self._config.get("name", DEFAULT_NAME) + + @property + def is_on(self): + """ True if the UPS is online, else False. """ + return self._state == apcupsd.VALUE_ONLINE + + def update(self): + """ + Get the latest status report from APCUPSd and establish whether the + UPS is online. + """ + status = apcupsd.GET_STATUS() + self._state = status[apcupsd.KEY_STATUS] diff --git a/homeassistant/components/sensor/apcupsd.py b/homeassistant/components/sensor/apcupsd.py new file mode 100644 index 00000000000..99d61d74384 --- /dev/null +++ b/homeassistant/components/sensor/apcupsd.py @@ -0,0 +1,107 @@ +""" +homeassistant.components.sensor.apcupsd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides a sensor to track various status aspects of a UPS. +""" +import logging + +from homeassistant.core import JobPriority +from homeassistant.const import TEMP_CELCIUS +from homeassistant.helpers.entity import Entity +from homeassistant.components import apcupsd + + +DEPENDENCIES = [apcupsd.DOMAIN] + +DEFAULT_NAME = "UPS Status" + +SPECIFIC_UNITS = { + "ITEMP": TEMP_CELCIUS +} + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ + Ensure that the 'type' config value has been set and use a specific unit + of measurement if required. + """ + typ = config.get(apcupsd.CONF_TYPE) + if typ is None: + _LOGGER.error( + "You must include a '%s' when configuring an APCUPSd sensor.", + apcupsd.CONF_TYPE) + return + typ = typ.upper() + + # Get a status reading from APCUPSd and check whether the user provided + # 'type' is present in the output. If we're not able to check, then assume + # the user knows what they're doing. + # pylint: disable=broad-except + status = None + try: + status = apcupsd.GET_STATUS() + if typ not in status: + _LOGGER.error( + "Specified '%s' of '%s' does not appear in the APCUPSd status " + "output.", apcupsd.CONF_TYPE, typ) + return + except Exception as exc: + _LOGGER.warning( + "Unable to fetch initial value from ACPUPSd to check that '%s' is " + "a supported '%s': %s", typ, apcupsd.CONF_TYPE, exc) + unit = SPECIFIC_UNITS.get(typ) + add_entities(( + Sensor(hass, config, unit=unit, initial_status=status), + )) + + +def infer_unit(value): + """ + If the value ends with any of the units from ALL_UNITS, split the unit + off the end of the value and return the value, unit tuple pair. Else return + the original value and None as the unit. + """ + from apcaccess.status import ALL_UNITS + for unit in ALL_UNITS: + if value.endswith(unit): + return value[:-len(unit)], unit + return value, None + + +class Sensor(Entity): + """ Generic sensor entity for APCUPSd status values. """ + def __init__(self, hass, config, unit=None, initial_status=None): + self._config = config + self._unit = unit + self._state = None + self._inferred_unit = None + if initial_status is None: + hass.pool.add_job( + JobPriority.EVENT_STATE, (self.update_ha_state, True)) + else: + self._update_from_status(initial_status) + + @property + def name(self): + return self._config.get("name", DEFAULT_NAME) + + @property + def state(self): + return self._state + + @property + def unit_of_measurement(self): + if self._unit is None: + return self._inferred_unit + return self._unit + + def update(self): + """ Get the latest status and use it to update our sensor state. """ + self._update_from_status(apcupsd.GET_STATUS()) + + def _update_from_status(self, status): + """ Set state and infer unit from status. """ + key = self._config[apcupsd.CONF_TYPE].upper() + self._state, self._inferred_unit = infer_unit(status[key]) diff --git a/requirements_all.txt b/requirements_all.txt index f72daaecccf..b5313e5b7c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -21,6 +21,9 @@ SoCo==0.11.1 # homeassistant.components.notify.twitter TwitterAPI==2.3.6 +# homeassistant.components.apcupsd +apcaccess==0.0.4 + # homeassistant.components.sun astral==0.9 From 91fb2764ccee2cfaf3373d91ab84ce03ba55a8ea Mon Sep 17 00:00:00 2001 From: Flyte Date: Thu, 11 Feb 2016 07:33:53 +0000 Subject: [PATCH 2/2] Use a cache object to reduce the frequency of calls to APCUPSd --- homeassistant/components/apcupsd.py | 49 +++++++++++++++---- .../components/binary_sensor/apcupsd.py | 17 +++---- homeassistant/components/sensor/apcupsd.py | 44 +++++------------ 3 files changed, 59 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py index 8c4a964d330..8abb06b54ea 100644 --- a/homeassistant/components/apcupsd.py +++ b/homeassistant/components/apcupsd.py @@ -5,6 +5,9 @@ Sets up and provides access to the status output of APCUPSd via its Network Information Server (NIS). """ import logging +from datetime import timedelta + +from homeassistant.util import Throttle DOMAIN = "apcupsd" @@ -21,31 +24,59 @@ KEY_STATUS = "STATUS" VALUE_ONLINE = "ONLINE" -GET_STATUS = None +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +DATA = None _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Use config values to set up a function enabling status retrieval. """ - global GET_STATUS - from apcaccess import status + global DATA host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST) port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT) - def get_status(): - """ Get the status from APCUPSd and parse it into a dict. """ - return status.parse(status.get(host=host, port=port)) - - GET_STATUS = get_status + DATA = APCUPSdData(host, port) # It doesn't really matter why we're not able to get the status, just that # we can't. # pylint: disable=broad-except try: - GET_STATUS() + DATA.update(no_throttle=True) except Exception: _LOGGER.exception("Failure while testing APCUPSd status retrieval.") return False return True + + +class APCUPSdData(object): + """ + Stores the data retrieved from APCUPSd for each entity to use, acts as the + single point responsible for fetching updates from the server. + """ + def __init__(self, host, port): + from apcaccess import status + self._host = host + self._port = port + self._status = None + self._get = status.get + self._parse = status.parse + + @property + def status(self): + """ Get latest update if throttle allows. Return status. """ + self.update() + return self._status + + def _get_status(self): + """ Get the status from APCUPSd and parse it into a dict. """ + return self._parse(self._get(host=self._host, port=self._port)) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self, **kwargs): + """ + Fetch the latest status from APCUPSd and store it in self._status. + """ + self._status = self._get_status() diff --git a/homeassistant/components/binary_sensor/apcupsd.py b/homeassistant/components/binary_sensor/apcupsd.py index a6eaa7f7ea9..8524b06e4ba 100644 --- a/homeassistant/components/binary_sensor/apcupsd.py +++ b/homeassistant/components/binary_sensor/apcupsd.py @@ -3,7 +3,6 @@ homeassistant.components.binary_sensor.apcupsd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides a binary sensor to track online status of a UPS. """ -from homeassistant.core import JobPriority from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components import apcupsd @@ -15,17 +14,16 @@ DEFAULT_NAME = "UPS Online Status" def setup_platform(hass, config, add_entities, discovery_info=None): """ Instantiate an OnlineStatus binary sensor entity and add it to HA. """ - add_entities((OnlineStatus(hass, config),)) + add_entities((OnlineStatus(config, apcupsd.DATA),)) class OnlineStatus(BinarySensorDevice): """ Binary sensor to represent UPS online status. """ - def __init__(self, hass, config): + def __init__(self, config, data): self._config = config + self._data = data self._state = None - # Get initial state - hass.pool.add_job( - JobPriority.EVENT_STATE, (self.update_ha_state, True)) + self.update() @property def name(self): @@ -39,8 +37,7 @@ class OnlineStatus(BinarySensorDevice): def update(self): """ - Get the latest status report from APCUPSd and establish whether the - UPS is online. + Get the status report from APCUPSd (or cache) and set this entity's + state. """ - status = apcupsd.GET_STATUS() - self._state = status[apcupsd.KEY_STATUS] + self._state = self._data.status[apcupsd.KEY_STATUS] diff --git a/homeassistant/components/sensor/apcupsd.py b/homeassistant/components/sensor/apcupsd.py index 99d61d74384..d72dc4ae4b9 100644 --- a/homeassistant/components/sensor/apcupsd.py +++ b/homeassistant/components/sensor/apcupsd.py @@ -5,7 +5,6 @@ Provides a sensor to track various status aspects of a UPS. """ import logging -from homeassistant.core import JobPriority from homeassistant.const import TEMP_CELCIUS from homeassistant.helpers.entity import Entity from homeassistant.components import apcupsd @@ -32,28 +31,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error( "You must include a '%s' when configuring an APCUPSd sensor.", apcupsd.CONF_TYPE) - return + return False typ = typ.upper() - # Get a status reading from APCUPSd and check whether the user provided - # 'type' is present in the output. If we're not able to check, then assume - # the user knows what they're doing. - # pylint: disable=broad-except - status = None - try: - status = apcupsd.GET_STATUS() - if typ not in status: - _LOGGER.error( - "Specified '%s' of '%s' does not appear in the APCUPSd status " - "output.", apcupsd.CONF_TYPE, typ) - return - except Exception as exc: - _LOGGER.warning( - "Unable to fetch initial value from ACPUPSd to check that '%s' is " - "a supported '%s': %s", typ, apcupsd.CONF_TYPE, exc) - unit = SPECIFIC_UNITS.get(typ) + if typ not in apcupsd.DATA.status: + _LOGGER.error( + "Specified '%s' of '%s' does not appear in the APCUPSd status " + "output.", apcupsd.CONF_TYPE, typ) + return False + add_entities(( - Sensor(hass, config, unit=unit, initial_status=status), + Sensor(config, apcupsd.DATA, unit=SPECIFIC_UNITS.get(typ)), )) @@ -72,16 +60,12 @@ def infer_unit(value): class Sensor(Entity): """ Generic sensor entity for APCUPSd status values. """ - def __init__(self, hass, config, unit=None, initial_status=None): + def __init__(self, config, data, unit=None): self._config = config self._unit = unit - self._state = None + self._data = data self._inferred_unit = None - if initial_status is None: - hass.pool.add_job( - JobPriority.EVENT_STATE, (self.update_ha_state, True)) - else: - self._update_from_status(initial_status) + self.update() @property def name(self): @@ -99,9 +83,5 @@ class Sensor(Entity): def update(self): """ Get the latest status and use it to update our sensor state. """ - self._update_from_status(apcupsd.GET_STATUS()) - - def _update_from_status(self, status): - """ Set state and infer unit from status. """ key = self._config[apcupsd.CONF_TYPE].upper() - self._state, self._inferred_unit = infer_unit(status[key]) + self._state, self._inferred_unit = infer_unit(self._data.status[key])