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