The default names for the feeds created by the EmonCMS component are like 'emoncms1_feedid_10', but - This is the display name. The ID should be lowercase and underscored, but the display name should be readable. The ID gets derived from it and comes out formatted correctly. - EmonCMS lets you assign names to feeds, so it makes sense to use those if they exist, rather than feed IDs. The ID is pretty meaningless and basically means you have to override every name to make it readable. - Including the ID identifying the EmonCMS instance (i.e. the '1') makes the name clunkier and would only be useful for people with multiple EmonCMS instances, which is likely to be an extremely small group since one hub can run as many feeds as you need it to. This changes the default behavior but still uses configured 'name' if it's set, so it won't break the configuration of people who have customized their feed names in HA config.
220 lines
7.3 KiB
Python
220 lines
7.3 KiB
Python
"""
|
|
Support for monitoring emoncms feeds.
|
|
|
|
For more details about this component, please refer to the documentation
|
|
at https://home-assistant.io/components/sensor.emoncms/
|
|
"""
|
|
from datetime import timedelta
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
import requests
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
|
from homeassistant.const import (
|
|
CONF_API_KEY, CONF_URL, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT,
|
|
CONF_ID, CONF_SCAN_INTERVAL, STATE_UNKNOWN)
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers import template
|
|
from homeassistant.util import Throttle
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
ATTR_FEEDID = 'FeedId'
|
|
ATTR_FEEDNAME = 'FeedName'
|
|
ATTR_LASTUPDATETIME = 'LastUpdated'
|
|
ATTR_LASTUPDATETIMESTR = 'LastUpdatedStr'
|
|
ATTR_SIZE = 'Size'
|
|
ATTR_TAG = 'Tag'
|
|
ATTR_USERID = 'UserId'
|
|
|
|
CONF_EXCLUDE_FEEDID = 'exclude_feed_id'
|
|
CONF_ONLY_INCLUDE_FEEDID = 'include_only_feed_id'
|
|
CONF_SENSOR_NAMES = 'sensor_names'
|
|
|
|
DECIMALS = 2
|
|
DEFAULT_UNIT = 'W'
|
|
|
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
|
|
|
ONLY_INCL_EXCL_NONE = 'only_include_exclude_or_none'
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
vol.Required(CONF_API_KEY): cv.string,
|
|
vol.Required(CONF_URL): cv.string,
|
|
vol.Required(CONF_ID): cv.positive_int,
|
|
vol.Exclusive(CONF_ONLY_INCLUDE_FEEDID, ONLY_INCL_EXCL_NONE):
|
|
vol.All(cv.ensure_list, [cv.positive_int]),
|
|
vol.Exclusive(CONF_EXCLUDE_FEEDID, ONLY_INCL_EXCL_NONE):
|
|
vol.All(cv.ensure_list, [cv.positive_int]),
|
|
vol.Optional(CONF_SENSOR_NAMES):
|
|
vol.All({cv.positive_int: vol.All(cv.string, vol.Length(min=1))}),
|
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
|
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=DEFAULT_UNIT): cv.string,
|
|
})
|
|
|
|
|
|
def get_id(sensorid, feedtag, feedname, feedid, feeduserid):
|
|
"""Return unique identifier for feed / sensor."""
|
|
return "emoncms{}_{}_{}_{}_{}".format(
|
|
sensorid, feedtag, feedname, feedid, feeduserid)
|
|
|
|
|
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
|
"""Set up the Emoncms sensor."""
|
|
apikey = config.get(CONF_API_KEY)
|
|
url = config.get(CONF_URL)
|
|
sensorid = config.get(CONF_ID)
|
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
|
unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
|
|
exclude_feeds = config.get(CONF_EXCLUDE_FEEDID)
|
|
include_only_feeds = config.get(CONF_ONLY_INCLUDE_FEEDID)
|
|
sensor_names = config.get(CONF_SENSOR_NAMES)
|
|
interval = config.get(CONF_SCAN_INTERVAL)
|
|
|
|
if value_template is not None:
|
|
value_template.hass = hass
|
|
|
|
data = EmonCmsData(hass, url, apikey, interval)
|
|
|
|
data.update()
|
|
|
|
if data.data is None:
|
|
return False
|
|
|
|
sensors = []
|
|
|
|
for elem in data.data:
|
|
|
|
if exclude_feeds is not None:
|
|
if int(elem["id"]) in exclude_feeds:
|
|
continue
|
|
|
|
if include_only_feeds is not None:
|
|
if int(elem["id"]) not in include_only_feeds:
|
|
continue
|
|
|
|
name = None
|
|
if sensor_names is not None:
|
|
name = sensor_names.get(int(elem["id"]), None)
|
|
|
|
sensors.append(EmonCmsSensor(hass, data, name, value_template,
|
|
unit_of_measurement, str(sensorid),
|
|
elem))
|
|
add_devices(sensors)
|
|
|
|
|
|
class EmonCmsSensor(Entity):
|
|
"""Implementation of an Emoncms sensor."""
|
|
|
|
def __init__(self, hass, data, name, value_template,
|
|
unit_of_measurement, sensorid, elem):
|
|
"""Initialize the sensor."""
|
|
if name is None:
|
|
# Suppress ID in sensor name if it's 1, since most people won't
|
|
# have more than one EmonCMS source and it's redundant to show the
|
|
# ID if there's only one.
|
|
id_for_name = '' if str(sensorid) == '1' else sensorid
|
|
# Use the feed name assigned in EmonCMS or fall back to the feed ID
|
|
feed_name = elem.get('name') or 'Feed {}'.format(elem['id'])
|
|
self._name = "EmonCMS{} {}".format(id_for_name, feed_name)
|
|
else:
|
|
self._name = name
|
|
self._identifier = get_id(
|
|
sensorid, elem["tag"], elem["name"], elem["id"], elem["userid"])
|
|
self._hass = hass
|
|
self._data = data
|
|
self._value_template = value_template
|
|
self._unit_of_measurement = unit_of_measurement
|
|
self._sensorid = sensorid
|
|
self._elem = elem
|
|
|
|
if self._value_template is not None:
|
|
self._state = self._value_template.render_with_possible_json_value(
|
|
elem["value"], STATE_UNKNOWN)
|
|
else:
|
|
self._state = round(float(elem["value"]), DECIMALS)
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return self._name
|
|
|
|
@property
|
|
def unit_of_measurement(self):
|
|
"""Return the unit of measurement of this entity, if any."""
|
|
return self._unit_of_measurement
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return the state of the device."""
|
|
return self._state
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the atrributes of the sensor."""
|
|
return {
|
|
ATTR_FEEDID: self._elem["id"],
|
|
ATTR_TAG: self._elem["tag"],
|
|
ATTR_FEEDNAME: self._elem["name"],
|
|
ATTR_SIZE: self._elem["size"],
|
|
ATTR_USERID: self._elem["userid"],
|
|
ATTR_LASTUPDATETIME: self._elem["time"],
|
|
ATTR_LASTUPDATETIMESTR: template.timestamp_local(
|
|
float(self._elem["time"])),
|
|
}
|
|
|
|
def update(self):
|
|
"""Get the latest data and updates the state."""
|
|
self._data.update()
|
|
|
|
if self._data.data is None:
|
|
return
|
|
|
|
elem = next((elem for elem in self._data.data
|
|
if get_id(self._sensorid, elem["tag"],
|
|
elem["name"], elem["id"],
|
|
elem["userid"]) == self._identifier),
|
|
None)
|
|
|
|
if elem is None:
|
|
return
|
|
|
|
self._elem = elem
|
|
|
|
if self._value_template is not None:
|
|
self._state = self._value_template.render_with_possible_json_value(
|
|
elem["value"], STATE_UNKNOWN)
|
|
else:
|
|
self._state = round(float(elem["value"]), DECIMALS)
|
|
|
|
|
|
class EmonCmsData(object):
|
|
"""The class for handling the data retrieval."""
|
|
|
|
def __init__(self, hass, url, apikey, interval):
|
|
"""Initialize the data object."""
|
|
self._apikey = apikey
|
|
self._url = '{}/feed/list.json'.format(url)
|
|
self._interval = interval
|
|
self._hass = hass
|
|
self.data = None
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
def update(self):
|
|
"""Get the latest data from Emoncms."""
|
|
try:
|
|
parameters = {"apikey": self._apikey}
|
|
req = requests.get(
|
|
self._url, params=parameters, allow_redirects=True, timeout=5)
|
|
except requests.exceptions.RequestException as exception:
|
|
_LOGGER.error(exception)
|
|
return
|
|
else:
|
|
if req.status_code == 200:
|
|
self.data = req.json()
|
|
else:
|
|
_LOGGER.error("Please verify if the specified config value "
|
|
"'%s' is correct! (HTTP Status_code = %d)",
|
|
CONF_URL, req.status_code)
|