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:
parent
e9f96bfd7f
commit
19ebdf2cf1
4 changed files with 459 additions and 0 deletions
|
@ -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
|
||||
|
||||
|
|
171
homeassistant/components/greeneye_monitor.py
Normal file
171
homeassistant/components/greeneye_monitor.py
Normal 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
|
282
homeassistant/components/sensor/greeneye_monitor.py
Normal file
282
homeassistant/components/sensor/greeneye_monitor.py
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue