diff --git a/.coveragerc b/.coveragerc index 3e24b33718c..2d2e4530b88 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..8abb06b54ea --- /dev/null +++ b/homeassistant/components/apcupsd.py @@ -0,0 +1,82 @@ +""" +homeassistant.components.apcupsd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +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" +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" + +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 DATA + + host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST) + port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT) + + 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: + 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 new file mode 100644 index 00000000000..8524b06e4ba --- /dev/null +++ b/homeassistant/components/binary_sensor/apcupsd.py @@ -0,0 +1,43 @@ +""" +homeassistant.components.binary_sensor.apcupsd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides a binary sensor to track online status of a UPS. +""" +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(config, apcupsd.DATA),)) + + +class OnlineStatus(BinarySensorDevice): + """ Binary sensor to represent UPS online status. """ + def __init__(self, config, data): + self._config = config + self._data = data + self._state = None + self.update() + + @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 status report from APCUPSd (or cache) and set this entity's + state. + """ + self._state = self._data.status[apcupsd.KEY_STATUS] diff --git a/homeassistant/components/sensor/apcupsd.py b/homeassistant/components/sensor/apcupsd.py new file mode 100644 index 00000000000..d72dc4ae4b9 --- /dev/null +++ b/homeassistant/components/sensor/apcupsd.py @@ -0,0 +1,87 @@ +""" +homeassistant.components.sensor.apcupsd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides a sensor to track various status aspects of a UPS. +""" +import logging + +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 False + typ = typ.upper() + + 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(config, apcupsd.DATA, unit=SPECIFIC_UNITS.get(typ)), + )) + + +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, config, data, unit=None): + self._config = config + self._unit = unit + self._data = data + self._inferred_unit = None + self.update() + + @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. """ + key = self._config[apcupsd.CONF_TYPE].upper() + self._state, self._inferred_unit = infer_unit(self._data.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