hass-core/homeassistant/components/emoncms/sensor.py
Paulus Schoutsen daff87fe5d
Emoncms API now provides a Unit of Measurement (#32042)
* 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>
2020-03-04 18:03:51 -08:00

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,
)