/*
* Copyright 2011-16 Fraunhofer ISE
*
* This file is part of OpenMUC.
* For more information visit http://www.openmuc.org
*
* OpenMUC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenMUC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenMUC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openmuc.framework.driver.aggregator;
import java.util.List;
import org.openmuc.framework.config.ArgumentSyntaxException;
import org.openmuc.framework.config.ChannelScanInfo;
import org.openmuc.framework.config.DriverInfo;
import org.openmuc.framework.config.ScanException;
import org.openmuc.framework.config.ScanInterruptedException;
import org.openmuc.framework.data.DoubleValue;
import org.openmuc.framework.data.Flag;
import org.openmuc.framework.data.Record;
import org.openmuc.framework.dataaccess.DataAccessService;
import org.openmuc.framework.driver.spi.ChannelRecordContainer;
import org.openmuc.framework.driver.spi.ChannelValueContainer;
import org.openmuc.framework.driver.spi.Connection;
import org.openmuc.framework.driver.spi.ConnectionException;
import org.openmuc.framework.driver.spi.DriverDeviceScanListener;
import org.openmuc.framework.driver.spi.DriverService;
import org.openmuc.framework.driver.spi.RecordsReceivedListener;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Driver which performs aggregation of logged values from a channel. It uses the DriverService and the
* DataAccessService. It is therefore a kind of OpenMUC driver/application mix. The aggregator is fully configurable
* through the channel config file.
*
* <b>Synopsis</b><br>
* <ul>
* <li><b>driver id</b> = aggregator</li>
* <li><b>channelAddress</b> = <sourceChannelId>:<aggregationType>[:<quality>]
* <ul>
* <li><b>sourceChannelId</b> = id of channel to be aggregated</li>
* <li><b>aggregationType</b>
* <ul>
* <li>AVG: calculates the average of all values of interval (e.g. for average power)</li>
* <li>LAST: takes the last value of interval (e.g. for energy)</li>
* <li>DIFF: calculates difference of first and last value of interval</li>
* <li>PULS_ENERGY,<pulses per Wh>,<max counter>: calculates energy from pulses of interval (e.g. for pulse
* counter/meter)
* <ul>
* <li>Example: PULSE_ENERGY,10,65535</li>
* </ul>
* </li>
* </ul>
* </li>
* </ul>
* </li>
* <li><b>quality</b> = Range 0.0 - 1.0. Percentage of the expected valid/available logged records for aggregation.
* Default value is 1.0. Example: Aggregation of 5s values to 15min. The 15min interval consists of 180 5s values. If
* quality is 0.9 then at least 162 of 180 values must be valid/available for aggregation. NOTE: The missing/invalid
* values could appear as block at the beginning or end of the interval, which might be problematic for some aggregation
* types</li>
* </ul>
*
* Example: <br>
* Channel A (channelA) is sampled and logged every 10 seconds.
*
* <pre>
* <channelid="channelA">
* <samplingInterval>10s</samplingInterval>
* <loggingInterval>10s</loggingInterval>
* </channel>
* </pre>
*
*
* Now you want a channel B (channelB) which contains the same values as channel A but in a 1 minute resolution by using
* the 'average' as aggregation type. You can achieve this by simply adding the aggregator driver to your channel config
* file and define a the channel B as follows:
*
* <pre>
* <driver id="aggregator">
* <device id="aggregatordevice">
* <channelid="channelB">
* <channelAddress>channelA:avg</channelAddress>
* <samplingInterval>60s</samplingInterval>
* <loggingInterval>60s</loggingInterval>
* </channel>
* </device>
* </driver>
* </pre>
*
* The new (aggregated) channel has the id channelB. The channel address consists of the channel id of the original
* channel and the aggregation type which is channelA:avg in this example. OpenMUC calls the read method of the
* aggregator every minute. The aggregator then gets all logged records from channelA of the last minute, calculates the
* average and sets this value for the record of channelB.
*
* <p>
*
* NOTE: It's recommended to specify the samplingTimeOffset for channelB. It should be between samplingIntervalB -
* samplingIntervalA and samplingIntervalB. In this example: 50 < offset < 60. This constraint ensures that values
* are AGGREGATED CORRECTLY. At hh:mm:55 the aggregator gets the logged values of channelA and at hh:mm:60 respectively
* hh:mm:00 the aggregated value is logged.
*
* <pre>
* <driver id="aggregator">
* <device id="aggregatordevice">
* <channelid="channelB">
* <channelAddress>channelA:avg</channelAddress>
* <samplingInterval>60s</samplingInterval>
* <samplingTimeOffset>55s</samplingTimeOffset>
* <loggingInterval>60s</loggingInterval>
* </channel>
* </device>
* </driver>
* </pre>
*
*
*/
// TODO: Performance: Some checks of aggregatorUtil.getDoubleRecordValue() could be removed since
// AggregatorChannel.removeErrorRecords() removes all invalid records.
@Component(service = DriverService.class)
public class Aggregator implements DriverService, Connection {
private final static Logger logger = LoggerFactory.getLogger(Aggregator.class);
private DataAccessService dataAccessService;
// <id><type,param1,param2><quality>
//
// PULSES_ENERGY
// - register size // needed vor overflow
// - impulse pro wh
//
// [:<optionalLongSetting1>][:<optionalLongSetting2>]
@Override
public DriverInfo getInfo() {
String driverId = "aggregator";
String description = "Is able to aggregate logged values of a channel and writes the aggregated value into a new channel. Different aggregation types supported.";
String deviceAddressSyntax = "not needed";
String parametersSyntax = "not needed";
String channelAddressSyntax = "<id of channel which should be aggregated>:<type>[:<quality>]";
String deviceScanParametersSyntax = "not supported";
return new DriverInfo(driverId, description, deviceAddressSyntax, parametersSyntax, channelAddressSyntax,
deviceScanParametersSyntax);
}
@Override
public Object read(List<ChannelRecordContainer> containers, Object containerListHandle, String samplingGroup)
throws UnsupportedOperationException, ConnectionException {
long currentTimestamp = getCurrentTimestamp();
long endTimestamp = getEndTimestamp(currentTimestamp);
for (ChannelRecordContainer container : containers) {
AggregatorChannel aggregatorChannel;
try {
aggregatorChannel = AggregatorChannelFactory.createAggregatorChannel(container, dataAccessService);
double aggregatedValue = aggregatorChannel.aggregate(currentTimestamp, endTimestamp);
container.setRecord(new Record(new DoubleValue(aggregatedValue), currentTimestamp, Flag.VALID));
} catch (AggregationException e) {
logger.debug("Unable to perform aggregation for channel " + container.getChannel().getId() + ". "
+ e.getMessage());
setRecordWithErrorFlag(container, currentTimestamp);
} catch (Exception e) {
setRecordWithErrorFlag(container, currentTimestamp);
logger.error("Unexpected Exception: Unable to perform aggregation for channel "
+ container.getChannel().getId(), e);
}
}
return null;
}
/**
* @return the current timestamp where milliseconds are set to 000: e.g. 10:45:00.015 --> 10:45:00.000
*/
private long getCurrentTimestamp() {
return (System.currentTimeMillis() / 1000) * 1000;
}
/**
* endTimestamp must be slightly before the currentTimestamp Example: Aggregate a channel from 10:30:00 to 10:45:00
* to 15 min values. 10:45:00 should be the timestamp of the aggregated value therefore the aggregator has to get
* logged values from 10:30:00,000 till 10:44:59,999. 10:45:00 is part of the next 15 min interval.
*
* @param currentTimestamp
* @return current timestamp
*/
private long getEndTimestamp(long currentTimestamp) {
return currentTimestamp - 1;
}
private void setRecordWithErrorFlag(ChannelRecordContainer container, long endTimestamp) {
container.setRecord(new Record(null, endTimestamp, Flag.DRIVER_ERROR_READ_FAILURE));
}
@Override
public void startListening(List<ChannelRecordContainer> containers, RecordsReceivedListener listener)
throws UnsupportedOperationException, ConnectionException {
throw new UnsupportedOperationException();
}
@Override
public Object write(List<ChannelValueContainer> containers, Object containerListHandle)
throws UnsupportedOperationException, ConnectionException {
throw new UnsupportedOperationException();
}
@Override
public void scanForDevices(String settings, DriverDeviceScanListener listener)
throws UnsupportedOperationException, ArgumentSyntaxException, ScanException, ScanInterruptedException {
throw new UnsupportedOperationException();
}
@Override
public void interruptDeviceScan() throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
@Override
public List<ChannelScanInfo> scanForChannels(String settings)
throws UnsupportedOperationException, ArgumentSyntaxException, ScanException, ConnectionException {
throw new UnsupportedOperationException();
}
@Override
public Connection connect(String deviceAddress, String settings)
throws ArgumentSyntaxException, ConnectionException {
// no connection needed so far
return this;
}
@Override
public void disconnect() {
// no disconnect needed so far
}
@Reference
protected void setDataAccessService(DataAccessService dataAccessService) {
this.dataAccessService = dataAccessService;
}
protected void unsetDataAccessService(DataAccessService dataAccessService) {
this.dataAccessService = null;
}
}