"""Support for monitoring emoncms feeds.""" from __future__ import annotations from datetime import timedelta import logging from typing import Any from pyemoncms import EmoncmsClient import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, ) from homeassistant.const import ( CONF_API_KEY, CONF_ID, CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_URL, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, UnitOfPower, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import template from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .coordinator import EmoncmsCoordinator _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 = UnitOfPower.WATT ONLY_INCL_EXCL_NONE = "only_include_exclude_or_none" PLATFORM_SCHEMA = SENSOR_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, } ) async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Emoncms sensor.""" apikey = config[CONF_API_KEY] url = config[CONF_URL] sensorid = config[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) scan_interval = config.get(CONF_SCAN_INTERVAL, timedelta(seconds=30)) if value_template is not None: value_template.hass = hass emoncms_client = EmoncmsClient(url, apikey, session=async_get_clientsession(hass)) coordinator = EmoncmsCoordinator(hass, emoncms_client, scan_interval) await coordinator.async_refresh() elems = coordinator.data if elems is None: return sensors: list[EmonCmsSensor] = [] for idx, elem in enumerate(elems): if exclude_feeds is not None and int(elem["id"]) in exclude_feeds: continue if include_only_feeds is not None and 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) if unit := elem.get("unit"): unit_of_measurement = unit else: unit_of_measurement = config_unit sensors.append( EmonCmsSensor( coordinator, name, value_template, unit_of_measurement, str(sensorid), idx, ) ) async_add_entities(sensors) class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity): """Implementation of an Emoncms sensor.""" def __init__( self, coordinator: EmoncmsCoordinator, name: str | None, value_template: template.Template | None, unit_of_measurement: str | None, sensorid: str, idx: int, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.idx = idx elem = {} if self.coordinator.data: elem = self.coordinator.data[self.idx] 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", f"Feed {elem.get('id')}") self._attr_name = f"EmonCMS{id_for_name} {feed_name}" else: self._attr_name = name self._value_template = value_template self._attr_native_unit_of_measurement = unit_of_measurement self._sensorid = sensorid if unit_of_measurement in ("kWh", "Wh"): self._attr_device_class = SensorDeviceClass.ENERGY self._attr_state_class = SensorStateClass.TOTAL_INCREASING elif unit_of_measurement == "W": self._attr_device_class = SensorDeviceClass.POWER self._attr_state_class = SensorStateClass.MEASUREMENT elif unit_of_measurement == "V": self._attr_device_class = SensorDeviceClass.VOLTAGE self._attr_state_class = SensorStateClass.MEASUREMENT elif unit_of_measurement == "A": self._attr_device_class = SensorDeviceClass.CURRENT self._attr_state_class = SensorStateClass.MEASUREMENT elif unit_of_measurement == "VA": self._attr_device_class = SensorDeviceClass.APPARENT_POWER self._attr_state_class = SensorStateClass.MEASUREMENT elif unit_of_measurement in ("°C", "°F", "K"): self._attr_device_class = SensorDeviceClass.TEMPERATURE self._attr_state_class = SensorStateClass.MEASUREMENT elif unit_of_measurement == "Hz": self._attr_device_class = SensorDeviceClass.FREQUENCY self._attr_state_class = SensorStateClass.MEASUREMENT elif unit_of_measurement == "hPa": self._attr_device_class = SensorDeviceClass.PRESSURE self._attr_state_class = SensorStateClass.MEASUREMENT self._update_attributes(elem) def _update_attributes(self, elem: dict[str, Any]) -> None: """Update entity attributes.""" self._attr_extra_state_attributes = { ATTR_FEEDID: elem["id"], ATTR_TAG: elem["tag"], ATTR_FEEDNAME: elem["name"], } if elem["value"] is not None: self._attr_extra_state_attributes[ATTR_SIZE] = elem["size"] self._attr_extra_state_attributes[ATTR_USERID] = elem["userid"] self._attr_extra_state_attributes[ATTR_LASTUPDATETIME] = elem["time"] self._attr_extra_state_attributes[ATTR_LASTUPDATETIMESTR] = ( template.timestamp_local(float(elem["time"])) ) self._attr_native_value = None if self._value_template is not None: self._attr_native_value = ( self._value_template.render_with_possible_json_value( elem["value"], STATE_UNKNOWN ) ) elif elem["value"] is not None: self._attr_native_value = round(float(elem["value"]), DECIMALS) @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" data = self.coordinator.data if data: self._update_attributes(data[self.idx]) super()._handle_coordinator_update()