* Emoncms API provides a Unit The EmonCMS API has been amended to include a 'unit' as part of it's payload. By using this information, all the sensors can be created without the need for individual sensors to be setup by type. The change is backward compatible so if a unit type has been specified in the configuration, this will be used by default. If no unit is pecified either by the Home Assistant configuration, or the Emoncms API, then the default of W will be used as before. * Update sensor.py Check the 'unit' key is in the API call. Older systems may not have that key in the payload. * Modified approach with new configuration item * Removed new config item Removed the configuration item. The integration attempts to get the unit from the API. If this fails *or* the unit key of the API is blank, either the specified unit, or the default will be used. If approved, documentation will be updated. * Update homeassistant/components/emoncms/sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Update homeassistant/components/emoncms/sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review v2 * Update homeassistant/components/emoncms/sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Update sensor.py Update `config_unit` Co-authored-by: springstan <46536646+springstan@users.noreply.github.com>
256 lines
7.6 KiB
Python
256 lines
7.6 KiB
Python
"""Support for monitoring emoncms feeds."""
|
|
from datetime import timedelta
|
|
import logging
|
|
|
|
import requests
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
|
from homeassistant.const import (
|
|
CONF_API_KEY,
|
|
CONF_ID,
|
|
CONF_SCAN_INTERVAL,
|
|
CONF_UNIT_OF_MEASUREMENT,
|
|
CONF_URL,
|
|
CONF_VALUE_TEMPLATE,
|
|
POWER_WATT,
|
|
STATE_UNKNOWN,
|
|
)
|
|
from homeassistant.helpers import template
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.entity import Entity
|
|
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 = POWER_WATT
|
|
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 f"emoncms{sensorid}_{feedtag}_{feedname}_{feedid}_{feeduserid}"
|
|
|
|
|
|
def setup_platform(hass, config, add_entities, 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)
|
|
config_unit = 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)
|
|
|
|
unit = elem.get("unit")
|
|
if unit:
|
|
unit_of_measurement = unit
|
|
else:
|
|
unit_of_measurement = config_unit
|
|
|
|
sensors.append(
|
|
EmonCmsSensor(
|
|
hass,
|
|
data,
|
|
name,
|
|
value_template,
|
|
unit_of_measurement,
|
|
str(sensorid),
|
|
elem,
|
|
)
|
|
)
|
|
add_entities(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 f"Feed {elem['id']}"
|
|
self._name = f"EmonCMS{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 attributes 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:
|
|
"""The class for handling the data retrieval."""
|
|
|
|
def __init__(self, hass, url, apikey, interval):
|
|
"""Initialize the data object."""
|
|
self._apikey = apikey
|
|
self._url = f"{url}/feed/list.json"
|
|
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 configuration value "
|
|
"'%s' is correct! (HTTP Status_code = %d)",
|
|
CONF_URL,
|
|
req.status_code,
|
|
)
|