diff --git a/.coveragerc b/.coveragerc index f8222dde899..3bfd983dc30 100644 --- a/.coveragerc +++ b/.coveragerc @@ -517,6 +517,7 @@ omit = homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py + homeassistant/components/sensor/lacrosse.py homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/loopenergy.py diff --git a/homeassistant/components/sensor/lacrosse.py b/homeassistant/components/sensor/lacrosse.py new file mode 100755 index 00000000000..28cba7da0b4 --- /dev/null +++ b/homeassistant/components/sensor/lacrosse.py @@ -0,0 +1,217 @@ +""" +Support for LaCrosse sensor components. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.lacrosse/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.components.sensor import (ENTITY_ID_FORMAT, PLATFORM_SCHEMA) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, CONF_DEVICE, CONF_NAME, CONF_ID, + CONF_SENSORS, CONF_TYPE, TEMP_CELSIUS) +from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util import dt as dt_util + +REQUIREMENTS = ['pylacrosse==0.2.7'] + +_LOGGER = logging.getLogger(__name__) + +CONF_BAUD = 'baud' +CONF_EXPIRE_AFTER = 'expire_after' + +DEFAULT_DEVICE = '/dev/ttyUSB0' +DEFAULT_BAUD = '57600' +DEFAULT_EXPIRE_AFTER = 300 + +TYPES = ['battery', 'humidity', 'temperature'] + +SENSOR_SCHEMA = vol.Schema({ + vol.Required(CONF_ID): cv.positive_int, + vol.Required(CONF_TYPE): vol.In(TYPES), + vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, + vol.Optional(CONF_NAME): cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), + vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string, + vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the LaCrosse sensors.""" + import pylacrosse + from serial import SerialException + + usb_device = config.get(CONF_DEVICE) + baud = int(config.get(CONF_BAUD)) + expire_after = config.get(CONF_EXPIRE_AFTER) + + _LOGGER.debug("%s %s", usb_device, baud) + + try: + lacrosse = pylacrosse.LaCrosse(usb_device, baud) + lacrosse.open() + except SerialException as exc: + _LOGGER.warning("Unable to open serial port: %s", exc) + return False + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lacrosse.close) + + sensors = [] + for device, device_config in config[CONF_SENSORS].items(): + _LOGGER.debug("%s %s", device, device_config) + + typ = device_config.get(CONF_TYPE) + sensor_class = TYPE_CLASSES[typ] + name = device_config.get(CONF_NAME, device) + + sensors.append( + sensor_class( + hass, lacrosse, device, name, expire_after, device_config + ) + ) + + add_devices(sensors) + + +class LaCrosseSensor(Entity): + """Implementation of a Lacrosse sensor.""" + + _temperature = None + _humidity = None + _low_battery = None + _new_battery = None + + def __init__(self, hass, lacrosse, device_id, name, expire_after, config): + """Initialize the sensor.""" + self.hass = hass + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, device_id, hass=hass) + self._config = config + self._name = name + self._value = None + self._expire_after = expire_after + self._expiration_trigger = None + + lacrosse.register_callback( + int(self._config['id']), self._callback_lacrosse, None) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + def update(self, *args): + """Get the latest data.""" + pass + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attributes = { + 'low_battery': self._low_battery, + 'new_battery': self._new_battery, + } + return attributes + + def _callback_lacrosse(self, lacrosse_sensor, user_data): + """Callback function that is called from pylacrosse with new values.""" + if self._expire_after is not None and self._expire_after > 0: + # Reset old trigger + if self._expiration_trigger: + self._expiration_trigger() + self._expiration_trigger = None + + # Set new trigger + expiration_at = ( + dt_util.utcnow() + timedelta(seconds=self._expire_after)) + + self._expiration_trigger = async_track_point_in_utc_time( + self.hass, self.value_is_expired, expiration_at) + + self._temperature = round(lacrosse_sensor.temperature * 2) / 2 + self._humidity = lacrosse_sensor.humidity + self._low_battery = lacrosse_sensor.low_battery + self._new_battery = lacrosse_sensor.new_battery + + @callback + def value_is_expired(self, *_): + """Triggered when value is expired.""" + self._expiration_trigger = None + self._value = None + self.async_schedule_update_ha_state() + + +class LaCrosseTemperature(LaCrosseSensor): + """Implementation of a Lacrosse temperature sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def state(self): + """Return the state of the sensor.""" + return self._temperature + + +class LaCrosseHumidity(LaCrosseSensor): + """Implementation of a Lacrosse humidity sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return '%' + + @property + def state(self): + """Return the state of the sensor.""" + return self._humidity + + @property + def icon(self): + """Icon to use in the frontend.""" + return 'mdi:water-percent' + + +class LaCrosseBattery(LaCrosseSensor): + """Implementation of a Lacrosse battery sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self._low_battery is None: + state = None + elif self._low_battery is True: + state = 'low' + else: + state = 'ok' + return state + + @property + def icon(self): + """Icon to use in the frontend.""" + if self._low_battery is None: + icon = 'mdi:battery-unknown' + elif self._low_battery is True: + icon = 'mdi:battery-alert' + else: + icon = 'mdi:battery' + return icon + + +TYPE_CLASSES = { + 'temperature': LaCrosseTemperature, + 'humidity': LaCrosseHumidity, + 'battery': LaCrosseBattery +} diff --git a/requirements_all.txt b/requirements_all.txt index e617b37fe1e..c65927ce3eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -695,6 +695,9 @@ pykira==0.1.1 # homeassistant.components.sensor.kwb pykwb==0.0.8 +# homeassistant.components.sensor.lacrosse +pylacrosse==0.2.7 + # homeassistant.components.sensor.lastfm pylast==2.0.0