diff --git a/.coveragerc b/.coveragerc index 5727ec1d43a..8cb2b06683b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -126,6 +126,9 @@ omit = homeassistant/components/google.py homeassistant/components/*/google.py + homeassistant/components/greeneye_monitor.py + homeassistant/components/sensor/greeneye_monitor.py + homeassistant/components/habitica/* homeassistant/components/*/habitica.py diff --git a/homeassistant/components/greeneye_monitor.py b/homeassistant/components/greeneye_monitor.py new file mode 100644 index 00000000000..f5c51da88be --- /dev/null +++ b/homeassistant/components/greeneye_monitor.py @@ -0,0 +1,171 @@ +""" +Support for monitoring a GreenEye Monitor energy monitor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/greeneye_monitor/ +""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, + CONF_PORT, + CONF_TEMPERATURE_UNIT, + EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +REQUIREMENTS = ['greeneye_monitor==0.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_CHANNELS = 'channels' +CONF_COUNTED_QUANTITY = 'counted_quantity' +CONF_COUNTED_QUANTITY_PER_PULSE = 'counted_quantity_per_pulse' +CONF_MONITOR_SERIAL_NUMBER = 'monitor' +CONF_MONITORS = 'monitors' +CONF_NET_METERING = 'net_metering' +CONF_NUMBER = 'number' +CONF_PULSE_COUNTERS = 'pulse_counters' +CONF_SERIAL_NUMBER = 'serial_number' +CONF_SENSORS = 'sensors' +CONF_SENSOR_TYPE = 'sensor_type' +CONF_TEMPERATURE_SENSORS = 'temperature_sensors' +CONF_TIME_UNIT = 'time_unit' + +DATA_GREENEYE_MONITOR = 'greeneye_monitor' +DOMAIN = 'greeneye_monitor' + +SENSOR_TYPE_CURRENT = 'current_sensor' +SENSOR_TYPE_PULSE_COUNTER = 'pulse_counter' +SENSOR_TYPE_TEMPERATURE = 'temperature_sensor' + +TEMPERATURE_UNIT_CELSIUS = 'C' + +TIME_UNIT_SECOND = 's' +TIME_UNIT_MINUTE = 'min' +TIME_UNIT_HOUR = 'h' + +TEMPERATURE_SENSOR_SCHEMA = vol.Schema({ + vol.Required(CONF_NUMBER): vol.Range(1, 8), + vol.Required(CONF_NAME): cv.string, +}) + +TEMPERATURE_SENSORS_SCHEMA = vol.Schema({ + vol.Required(CONF_TEMPERATURE_UNIT): cv.temperature_unit, + vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, + [TEMPERATURE_SENSOR_SCHEMA]), +}) + +PULSE_COUNTER_SCHEMA = vol.Schema({ + vol.Required(CONF_NUMBER): vol.Range(1, 4), + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_COUNTED_QUANTITY): cv.string, + vol.Optional( + CONF_COUNTED_QUANTITY_PER_PULSE, default=1.0): vol.Coerce(float), + vol.Optional(CONF_TIME_UNIT, default=TIME_UNIT_SECOND): vol.Any( + TIME_UNIT_SECOND, + TIME_UNIT_MINUTE, + TIME_UNIT_HOUR), +}) + +PULSE_COUNTERS_SCHEMA = vol.All(cv.ensure_list, [PULSE_COUNTER_SCHEMA]) + +CHANNEL_SCHEMA = vol.Schema({ + vol.Required(CONF_NUMBER): vol.Range(1, 48), + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_NET_METERING, default=False): cv.boolean, +}) + +CHANNELS_SCHEMA = vol.All(cv.ensure_list, [CHANNEL_SCHEMA]) + +MONITOR_SCHEMA = vol.Schema({ + vol.Required(CONF_SERIAL_NUMBER): cv.positive_int, + vol.Optional(CONF_CHANNELS, default=[]): CHANNELS_SCHEMA, + vol.Optional( + CONF_TEMPERATURE_SENSORS, + default={ + CONF_TEMPERATURE_UNIT: TEMPERATURE_UNIT_CELSIUS, + CONF_SENSORS: [], + }): TEMPERATURE_SENSORS_SCHEMA, + vol.Optional(CONF_PULSE_COUNTERS, default=[]): PULSE_COUNTERS_SCHEMA, +}) + +MONITORS_SCHEMA = vol.All(cv.ensure_list, [MONITOR_SCHEMA]) + +COMPONENT_SCHEMA = vol.Schema({ + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_MONITORS): MONITORS_SCHEMA, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: COMPONENT_SCHEMA, +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the GreenEye Monitor component.""" + from greeneye import Monitors + + monitors = Monitors() + hass.data[DATA_GREENEYE_MONITOR] = monitors + + server_config = config[DOMAIN] + server = await monitors.start_server(server_config[CONF_PORT]) + + async def close_server(*args): + """Close the monitoring server.""" + await server.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_server) + + all_sensors = [] + for monitor_config in server_config[CONF_MONITORS]: + monitor_serial_number = { + CONF_MONITOR_SERIAL_NUMBER: monitor_config[CONF_SERIAL_NUMBER], + } + + channel_configs = monitor_config[CONF_CHANNELS] + for channel_config in channel_configs: + all_sensors.append({ + CONF_SENSOR_TYPE: SENSOR_TYPE_CURRENT, + **monitor_serial_number, + **channel_config, + }) + + sensor_configs = \ + monitor_config[CONF_TEMPERATURE_SENSORS] + if sensor_configs: + temperature_unit = { + CONF_TEMPERATURE_UNIT: sensor_configs[CONF_TEMPERATURE_UNIT], + } + for sensor_config in sensor_configs[CONF_SENSORS]: + all_sensors.append({ + CONF_SENSOR_TYPE: SENSOR_TYPE_TEMPERATURE, + **monitor_serial_number, + **temperature_unit, + **sensor_config, + }) + + counter_configs = monitor_config[CONF_PULSE_COUNTERS] + for counter_config in counter_configs: + all_sensors.append({ + CONF_SENSOR_TYPE: SENSOR_TYPE_PULSE_COUNTER, + **monitor_serial_number, + **counter_config, + }) + + if not all_sensors: + _LOGGER.error("Configuration must specify at least one " + "channel, pulse counter or temperature sensor") + return False + + hass.async_create_task(async_load_platform( + hass, + 'sensor', + DOMAIN, + all_sensors, + config)) + + return True diff --git a/homeassistant/components/sensor/greeneye_monitor.py b/homeassistant/components/sensor/greeneye_monitor.py new file mode 100644 index 00000000000..3793ea7846c --- /dev/null +++ b/homeassistant/components/sensor/greeneye_monitor.py @@ -0,0 +1,282 @@ +""" +Support for the sensors in a GreenEye Monitor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensors.greeneye_monitor_temperature/ +""" +import logging + +from homeassistant.const import CONF_NAME, CONF_TEMPERATURE_UNIT +from homeassistant.helpers.entity import Entity + +from ..greeneye_monitor import ( + CONF_COUNTED_QUANTITY, + CONF_COUNTED_QUANTITY_PER_PULSE, + CONF_MONITOR_SERIAL_NUMBER, + CONF_NET_METERING, + CONF_NUMBER, + CONF_SENSOR_TYPE, + CONF_TIME_UNIT, + DATA_GREENEYE_MONITOR, + SENSOR_TYPE_CURRENT, + SENSOR_TYPE_PULSE_COUNTER, + SENSOR_TYPE_TEMPERATURE, + TIME_UNIT_HOUR, + TIME_UNIT_MINUTE, + TIME_UNIT_SECOND, +) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['greeneye_monitor'] + +DATA_PULSES = 'pulses' +DATA_WATT_SECONDS = 'watt_seconds' + +UNIT_WATTS = 'W' + +COUNTER_ICON = 'mdi:counter' +CURRENT_SENSOR_ICON = 'mdi:flash' +TEMPERATURE_ICON = 'mdi:thermometer' + + +async def async_setup_platform( + hass, + config, + async_add_entities, + discovery_info=None): + """Set up a single GEM temperature sensor.""" + if not discovery_info: + return + + entities = [] + for sensor in discovery_info: + sensor_type = sensor[CONF_SENSOR_TYPE] + if sensor_type == SENSOR_TYPE_CURRENT: + entities.append(CurrentSensor( + sensor[CONF_MONITOR_SERIAL_NUMBER], + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_NET_METERING])) + elif sensor_type == SENSOR_TYPE_PULSE_COUNTER: + entities.append(PulseCounter( + sensor[CONF_MONITOR_SERIAL_NUMBER], + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_COUNTED_QUANTITY], + sensor[CONF_TIME_UNIT], + sensor[CONF_COUNTED_QUANTITY_PER_PULSE])) + elif sensor_type == SENSOR_TYPE_TEMPERATURE: + entities.append(TemperatureSensor( + sensor[CONF_MONITOR_SERIAL_NUMBER], + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_TEMPERATURE_UNIT])) + + async_add_entities(entities) + + +class GEMSensor(Entity): + """Base class for GreenEye Monitor sensors.""" + + def __init__(self, monitor_serial_number, name, sensor_type, number): + """Construct the entity.""" + self._monitor_serial_number = monitor_serial_number + self._name = name + self._sensor = None + self._sensor_type = sensor_type + self._number = number + + @property + def should_poll(self): + """GEM pushes changes, so this returns False.""" + return False + + @property + def unique_id(self): + """Return a unique ID for this sensor.""" + return "{serial}-{sensor_type}-{number}".format( + serial=self._monitor_serial_number, + sensor_type=self._sensor_type, + number=self._number, + ) + + @property + def name(self): + """Return the name of the channel.""" + return self._name + + async def async_added_to_hass(self): + """Wait for and connect to the sensor.""" + monitors = self.hass.data[DATA_GREENEYE_MONITOR] + + if not self._try_connect_to_monitor(monitors): + monitors.add_listener(self._on_new_monitor) + + def _on_new_monitor(self, *args): + monitors = self.hass.data[DATA_GREENEYE_MONITOR] + if self._try_connect_to_monitor(monitors): + monitors.remove_listener(self._on_new_monitor) + + async def async_will_remove_from_hass(self): + """Remove listener from the sensor.""" + if self._sensor: + self._sensor.remove_listener(self._schedule_update) + else: + monitors = self.hass.data[DATA_GREENEYE_MONITOR] + monitors.remove_listener(self._on_new_monitor) + + def _try_connect_to_monitor(self, monitors): + monitor = monitors.monitors.get(self._monitor_serial_number) + if not monitor: + return False + + self._sensor = self._get_sensor(monitor) + self._sensor.add_listener(self._schedule_update) + + return True + + def _get_sensor(self, monitor): + raise NotImplementedError() + + def _schedule_update(self): + self.async_schedule_update_ha_state(False) + + +class CurrentSensor(GEMSensor): + """Entity showing power usage on one channel of the monitor.""" + + def __init__(self, monitor_serial_number, number, name, net_metering): + """Construct the entity.""" + super().__init__(monitor_serial_number, name, 'current', number) + self._net_metering = net_metering + + def _get_sensor(self, monitor): + return monitor.channels[self._number - 1] + + @property + def icon(self): + """Return the icon that should represent this sensor in the UI.""" + return CURRENT_SENSOR_ICON + + @property + def unit_of_measurement(self): + """Return the unit of measurement used by this sensor.""" + return UNIT_WATTS + + @property + def state(self): + """Return the current number of watts being used by the channel.""" + if not self._sensor: + return None + + return self._sensor.watts + + @property + def device_state_attributes(self): + """Return total wattseconds in the state dictionary.""" + if not self._sensor: + return None + + if self._net_metering: + watt_seconds = self._sensor.polarized_watt_seconds + else: + watt_seconds = self._sensor.absolute_watt_seconds + + return { + DATA_WATT_SECONDS: watt_seconds + } + + +class PulseCounter(GEMSensor): + """Entity showing rate of change in one pulse counter of the monitor.""" + + def __init__( + self, + monitor_serial_number, + number, + name, + counted_quantity, + time_unit, + counted_quantity_per_pulse): + """Construct the entity.""" + super().__init__(monitor_serial_number, name, 'pulse', number) + self._counted_quantity = counted_quantity + self._counted_quantity_per_pulse = counted_quantity_per_pulse + self._time_unit = time_unit + + def _get_sensor(self, monitor): + return monitor.pulse_counters[self._number - 1] + + @property + def icon(self): + """Return the icon that should represent this sensor in the UI.""" + return COUNTER_ICON + + @property + def state(self): + """Return the current rate of change for the given pulse counter.""" + if not self._sensor or self._sensor.pulses_per_second is None: + return None + + return (self._sensor.pulses_per_second * + self._counted_quantity_per_pulse * + self._seconds_per_time_unit) + + @property + def _seconds_per_time_unit(self): + """Return the number of seconds in the given display time unit.""" + if self._time_unit == TIME_UNIT_SECOND: + return 1 + if self._time_unit == TIME_UNIT_MINUTE: + return 60 + if self._time_unit == TIME_UNIT_HOUR: + return 3600 + + @property + def unit_of_measurement(self): + """Return the unit of measurement for this pulse counter.""" + return "{counted_quantity}/{time_unit}".format( + counted_quantity=self._counted_quantity, + time_unit=self._time_unit, + ) + + @property + def device_state_attributes(self): + """Return total pulses in the data dictionary.""" + if not self._sensor: + return None + + return { + DATA_PULSES: self._sensor.pulses + } + + +class TemperatureSensor(GEMSensor): + """Entity showing temperature from one temperature sensor.""" + + def __init__(self, monitor_serial_number, number, name, unit): + """Construct the entity.""" + super().__init__(monitor_serial_number, name, 'temp', number) + self._unit = unit + + def _get_sensor(self, monitor): + return monitor.temperature_sensors[self._number - 1] + + @property + def icon(self): + """Return the icon that should represent this sensor in the UI.""" + return TEMPERATURE_ICON + + @property + def state(self): + """Return the current temperature being reported by this sensor.""" + if not self._sensor: + return None + + return self._sensor.temperature + + @property + def unit_of_measurement(self): + """Return the unit of measurement for this sensor (user specified).""" + return self._unit diff --git a/requirements_all.txt b/requirements_all.txt index d6c8965d31a..2cce54c5548 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,6 +433,9 @@ googlemaps==2.5.1 # homeassistant.components.sensor.gpsd gps3==0.33.3 +# homeassistant.components.greeneye_monitor +greeneye_monitor==0.1 + # homeassistant.components.light.greenwave greenwavereality==0.5.1