Add a component for GreenEye Monitor (#16378)

* Add a component for GreenEye Monitor

[GreenEye Monitor](http://www.brultech.com/greeneye/) is an energy
monitor that can monitor emergy usage of individual circuits, count pulses
from things like water or gas meters, and monitor temperatures. This component
exposes these various sensors in Home Assistant, for both data tracking and
automation triggering purposes.

* Consolidate sensors

* lint

* .coveragerc

* - cv.ensure_list
- DOMAIN, where appropriate
- defaults to schema
- single invocation of async_load_platform
- async_create_task instead of async_add_job
- fail if no sensors
- monitors required
- async_add_entities
- call add_devices once
- remove unused schema
- use properties rather than set fields
- move _number and unique_id to GEMSensor
- remove unnecessary get(xxx, None)
- keep params on one line when possible
- new-style string format

* Fix `ensure_list` usage, log message

* Pass config through
This commit is contained in:
Jonathan Keljo 2018-11-01 02:46:11 -07:00 committed by Martin Hjelmare
parent e9f96bfd7f
commit 19ebdf2cf1
4 changed files with 459 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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