From 87bc2134adfd5523e42cc12f69b80e0ba0dba1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Thu, 1 Aug 2019 22:18:52 +0200 Subject: [PATCH] Add each fronius sensor as own template (#25608) * Fix formatting in history test * Add each sensor as own template * Make adapters non-entities * Externalize and organize data fetching, improve system view * Small fixes Rename fetching adapters to adapters throw away non-working system overviews slightly change naming remove scan_interval from schema formatting * Scan interval is already timedelta and unnecessary return * Formatting * Ensure better codestyle by storing cell variables explicitely in different places --- homeassistant/components/fronius/sensor.py | 188 +++++++++++++++------ 1 file changed, 136 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index ec1922e0d56..ff0694afaab 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,5 +1,6 @@ """Support for Fronius devices.""" import copy +from datetime import timedelta import logging import voluptuous as vol @@ -11,10 +12,13 @@ from homeassistant.const import ( CONF_SENSOR_TYPE, CONF_DEVICE, CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval + _LOGGER = logging.getLogger(__name__) @@ -30,6 +34,7 @@ SCOPE_SYSTEM = "system" DEFAULT_SCOPE = SCOPE_DEVICE DEFAULT_DEVICE = 0 DEFAULT_INVERTER = 1 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) SENSOR_TYPES = [TYPE_INVERTER, TYPE_STORAGE, TYPE_METER, TYPE_POWER_FLOW] SCOPE_TYPES = [SCOPE_DEVICE, SCOPE_SYSTEM] @@ -78,47 +83,64 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= session = async_get_clientsession(hass) fronius = Fronius(session, config[CONF_RESOURCE]) - sensors = [] + scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + adapters = [] + # Creates all adapters for monitored conditions for condition in config[CONF_MONITORED_CONDITIONS]: device = condition[CONF_DEVICE] - name = "Fronius {} {} {}".format( - condition[CONF_SENSOR_TYPE].replace("_", " ").capitalize(), - device, - config[CONF_RESOURCE], - ) sensor_type = condition[CONF_SENSOR_TYPE] scope = condition[CONF_SCOPE] + name = "Fronius {} {} {}".format( + condition[CONF_SENSOR_TYPE].replace("_", " ").capitalize(), + device if scope == SCOPE_DEVICE else SCOPE_SYSTEM, + config[CONF_RESOURCE], + ) if sensor_type == TYPE_INVERTER: if scope == SCOPE_SYSTEM: - sensor_cls = FroniusInverterSystem + adapter_cls = FroniusInverterSystem else: - sensor_cls = FroniusInverterDevice + adapter_cls = FroniusInverterDevice elif sensor_type == TYPE_METER: if scope == SCOPE_SYSTEM: - sensor_cls = FroniusMeterSystem + adapter_cls = FroniusMeterSystem else: - sensor_cls = FroniusMeterDevice + adapter_cls = FroniusMeterDevice elif sensor_type == TYPE_POWER_FLOW: - sensor_cls = FroniusPowerFlow + adapter_cls = FroniusPowerFlow else: - sensor_cls = FroniusStorage + adapter_cls = FroniusStorage - sensors.append(sensor_cls(fronius, name, device)) + adapters.append(adapter_cls(fronius, name, device, async_add_entities)) - async_add_entities(sensors, True) + # Creates a lamdba that fetches an update when called + def adapter_data_fetcher(data_adapter): + async def fetch_data(*_): + await data_adapter.async_update() + + return fetch_data + + # Set up the fetching in a fixed interval for each adapter + for adapter in adapters: + fetch = adapter_data_fetcher(adapter) + # fetch data once at set-up + await fetch() + async_track_time_interval(hass, fetch, scan_interval) -class FroniusSensor(Entity): - """The Fronius sensor implementation.""" +class FroniusAdapter: + """The Fronius sensor fetching component.""" - def __init__(self, data, name, device): + def __init__(self, bridge, name, device, add_entities): """Initialize the sensor.""" - self.data = data + self.bridge = bridge self._name = name self._device = device - self._state = None - self._attributes = {} + self._fetched = {} + + self.sensors = set() + self._registered_sensors = set() + self._add_entities = add_entities @property def name(self): @@ -126,14 +148,9 @@ class FroniusSensor(Entity): return self._name @property - def state(self): - """Return the current state.""" - return self._state - - @property - def device_state_attributes(self): + def data(self): """Return the state attributes.""" - return self._attributes + return self._fetched async def async_update(self): """Retrieve and update latest state.""" @@ -148,62 +165,129 @@ class FroniusSensor(Entity): "Maybe the configured device is not supported" ) - if values: - self._state = values["status"]["Code"] - attributes = {} - for key in values: - if "value" in values[key]: - attributes[key] = values[key].get("value", 0) - self._attributes = attributes + if not values: + return + attributes = self._fetched + # Copy data of current fronius device + for key, entry in values.items(): + # If the data is directly a sensor + if "value" in entry: + attributes[key] = entry + self._fetched = attributes + + # Add discovered value fields as sensors + # because some fields are only sent temporarily + new_sensors = [] + for key in attributes: + if key not in self.sensors: + self.sensors.add(key) + _LOGGER.info("Discovered %s, adding as sensor", key) + new_sensors.append(FroniusTemplateSensor(self, key)) + self._add_entities(new_sensors, True) + + # Schedule an update for all included sensors + for sensor in self._registered_sensors: + sensor.async_schedule_update_ha_state(True) async def _update(self): """Return values of interest.""" pass + async def register(self, sensor): + """Register child sensor for update subscriptions.""" + self._registered_sensors.add(sensor) -class FroniusInverterSystem(FroniusSensor): - """Sensor for the fronius inverter with system scope.""" + +class FroniusInverterSystem(FroniusAdapter): + """Adapter for the fronius inverter with system scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_system_inverter_data() + return await self.bridge.current_system_inverter_data() -class FroniusInverterDevice(FroniusSensor): - """Sensor for the fronius inverter with device scope.""" +class FroniusInverterDevice(FroniusAdapter): + """Adapter for the fronius inverter with device scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_inverter_data(self._device) + return await self.bridge.current_inverter_data(self._device) -class FroniusStorage(FroniusSensor): - """Sensor for the fronius battery storage.""" +class FroniusStorage(FroniusAdapter): + """Adapter for the fronius battery storage.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_storage_data(self._device) + return await self.bridge.current_storage_data(self._device) -class FroniusMeterSystem(FroniusSensor): - """Sensor for the fronius meter with system scope.""" +class FroniusMeterSystem(FroniusAdapter): + """Adapter for the fronius meter with system scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_system_meter_data() + return await self.bridge.current_system_meter_data() -class FroniusMeterDevice(FroniusSensor): - """Sensor for the fronius meter with device scope.""" +class FroniusMeterDevice(FroniusAdapter): + """Adapter for the fronius meter with device scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_meter_data(self._device) + return await self.bridge.current_meter_data(self._device) -class FroniusPowerFlow(FroniusSensor): - """Sensor for the fronius power flow.""" +class FroniusPowerFlow(FroniusAdapter): + """Adapter for the fronius power flow.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_power_flow() + return await self.bridge.current_power_flow() + + +class FroniusTemplateSensor(Entity): + """Sensor for the single values (e.g. pv power, ac power).""" + + def __init__(self, parent: FroniusAdapter, name): + """Initialize a singular value sensor.""" + self._name = name + self.parent = parent + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format( + self._name.replace("_", " ").capitalize(), self.parent.name + ) + + @property + def state(self): + """Return the current state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def should_poll(self): + """Device should not be polled, returns False.""" + return False + + async def async_update(self): + """Update the internal state.""" + state = self.parent.data.get(self._name) + self._state = state.get("value") + self._unit = state.get("unit") + + async def async_added_to_hass(self): + """Register at parent component for updates.""" + await self.parent.register(self) + + def __hash__(self): + """Hash sensor by hashing its name.""" + return hash(self.name)