/* ==================================================================
* LoadShedder.java - 24/06/2015 6:55:15 am
*
* Copyright 2007-2015 SolarNetwork.net Dev Team
*
* This program 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 2 of
* the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.node.control.loadshedder;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import net.solarnetwork.node.DatumDataSource;
import net.solarnetwork.node.domain.EnergyDatum;
import net.solarnetwork.node.job.JobService;
import net.solarnetwork.node.reactor.Instruction;
import net.solarnetwork.node.reactor.InstructionHandler;
import net.solarnetwork.node.reactor.InstructionStatus;
import net.solarnetwork.node.reactor.support.BasicInstruction;
import net.solarnetwork.node.reactor.support.InstructionUtils;
import net.solarnetwork.node.settings.MappableSpecifier;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.support.BasicGroupSettingSpecifier;
import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier;
import net.solarnetwork.node.settings.support.BasicTitleSettingSpecifier;
import net.solarnetwork.node.settings.support.SettingsUtil;
import net.solarnetwork.node.util.PrefixedMessageSource;
import net.solarnetwork.util.OptionalService;
import net.solarnetwork.util.StaticOptionalService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
/**
* Service to monitor demand (consumption) from a
* {@link InstructionHandler#TOPIC_SHED_LOAD} instructions to a specific control
* to limit power draw to below a maximum threshold.
*
* @author matt
* @version 1.0
*/
public class LoadShedder implements SettingSpecifierProvider, JobService {
private OptionalService<LoadShedderStrategy> shedStrategy = new StaticOptionalService<LoadShedderStrategy>(
new DefaultLoadShedderStrategy());
private OptionalService<DatumDataSource<EnergyDatum>> consumptionDataSource;
private Collection<InstructionHandler> instructionHandlers = Collections.emptyList();
private List<LoadShedControlConfig> configs = new ArrayList<LoadShedControlConfig>(4);
private int consumptionSampleLimit = 10;
private final Deque<EnergyDatum> consumptionSamples = new ArrayDeque<EnergyDatum>(10);
private final ConcurrentMap<String, LoadShedControlInfo> switchInfos = new ConcurrentHashMap<String, LoadShedControlInfo>(
4);
private Date lastEvaluationDate;
private MessageSource messageSource;
private final Logger log = LoggerFactory.getLogger(getClass());
/**
* Evaluate current demand (consumption) and attempt to shed load as
* necessary.
*/
public synchronized InstructionStatus.InstructionState evaluatePowerLoad() {
final long now = System.currentTimeMillis();
LoadShedderStrategy strategy = getStrategy();
if ( strategy == null ) {
log.warn("No LoadShedderStrategy service avaialble");
return null;
}
lastEvaluationDate = new Date(now);
addPowerSample(consumptionDataSource, consumptionSamples); // adds to sample buffer
Deque<EnergyDatum> samples = consumptionSamples;
Collection<LoadShedAction> actions = strategy.evaulateRules(getConfigs(), switchInfos, now,
samples);
if ( actions == null || actions.size() < 1 ) {
return null;
}
InstructionStatus.InstructionState result = null;
for ( LoadShedAction action : actions ) {
final BasicInstruction instr = new BasicInstruction(InstructionHandler.TOPIC_SHED_LOAD,
new Date(now), Instruction.LOCAL_INSTRUCTION_ID, Instruction.LOCAL_INSTRUCTION_ID,
null);
instr.addParameter(action.getControlId(), action.getShedWatts().toString());
result = InstructionUtils.handleInstruction(instructionHandlers, instr);
if ( result == InstructionStatus.InstructionState.Completed ) {
log.info("Switch {} limit released for {}W", action.getControlId(),
action.getShedWatts());
updateSwitchInfo(action.getControlId(), action, samples.peek());
}
}
return (result == null ? InstructionStatus.InstructionState.Declined : result);
}
private LoadShedControlInfo updateSwitchInfo(String controlId, LoadShedAction action,
EnergyDatum sample) {
LoadShedControlInfo info = switchInfos.get(controlId);
if ( info == null ) {
info = new LoadShedControlInfo();
info.setControlId(controlId);
switchInfos.put(controlId, info);
}
info.setActionDate(new Date());
if ( sample != null ) {
info.setWattsBeforeAction(sample.getWatts());
}
info.setAction(action);
return info;
}
@Override
public void executeJobService() {
evaluatePowerLoad();
}
public LoadShedderStrategy getStrategy() {
if ( shedStrategy == null ) {
return null;
}
return shedStrategy.service();
}
// Datum support
private EnergyDatum addPowerSample(OptionalService<DatumDataSource<EnergyDatum>> service,
Deque<EnergyDatum> samples) {
if ( service == null ) {
return null;
}
DatumDataSource<EnergyDatum> dataSource = service.service();
if ( dataSource == null ) {
return null;
}
EnergyDatum datum = dataSource.readCurrentDatum();
if ( datum == null ) {
return null;
}
// maintain a buffer of samples so we can monitor the effect of limit operations
// buffer ordered from most recent to oldest
EnergyDatum previous = samples.peek();
if ( previous != null && previous.getCreated().equals(datum.getCreated()) ) {
// sample unchanged
return previous;
}
if ( samples.size() >= consumptionSampleLimit ) {
samples.removeLast();
}
samples.addFirst(datum);
return datum;
}
// Settings support
@Override
public String getSettingUID() {
return "net.solarnetwork.node.control.loadshedder";
}
@Override
public String getDisplayName() {
return "Load Shedder";
}
@Override
public MessageSource getMessageSource() {
LoadShedderStrategy strat = getStrategy();
MessageSource stratMessageSource = null;
if ( strat instanceof SettingSpecifierProvider ) {
stratMessageSource = ((SettingSpecifierProvider) strat).getMessageSource();
}
if ( stratMessageSource == null ) {
return messageSource;
}
PrefixedMessageSource ms = new PrefixedMessageSource();
ms.setPrefix("strategy.");
ms.setDelegate(stratMessageSource);
ms.setParentMessageSource(messageSource);
return ms;
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(8);
final Locale locale = Locale.getDefault();
final LoadShedderStrategy strat = getStrategy();
results.add(new BasicTitleSettingSpecifier("info", getInfoMessage(locale), true));
for ( LoadShedControlInfo info : switchInfos.values() ) {
String infoMessage = getInfoMessage(info, locale);
String stratMessage = (strat != null ? strat.getStatusMessage(info, locale) : null);
if ( stratMessage != null && stratMessage.length() > 0 ) {
infoMessage += " " + stratMessage;
}
results.add(new BasicTitleSettingSpecifier("info.control", infoMessage, true));
}
results.add(new BasicTextFieldSettingSpecifier("shedStrategy.propertyFilters['UID']", "Default"));
results.add(new BasicTextFieldSettingSpecifier("consumptionDataSource.propertyFilters['UID']",
"Main"));
LoadShedderStrategy strategy = getStrategy();
if ( strategy instanceof SettingSpecifierProvider ) {
SettingSpecifierProvider stratSettingProvider = (SettingSpecifierProvider) strategy;
List<SettingSpecifier> strategySpecifiers = stratSettingProvider.getSettingSpecifiers();
if ( strategySpecifiers != null && strategySpecifiers.size() > 0 ) {
for ( SettingSpecifier spec : strategySpecifiers ) {
if ( spec instanceof MappableSpecifier ) {
results.add(((MappableSpecifier) spec).mappedTo("strategy."));
} else {
results.add(spec);
}
}
}
}
// dynamic list of configs
Collection<LoadShedControlConfig> configList = getConfigs();
BasicGroupSettingSpecifier listComplexGroup = SettingsUtil.dynamicListSettingSpecifier(
"configs", configList, new SettingsUtil.KeyedListCallback<LoadShedControlConfig>() {
@Override
public Collection<SettingSpecifier> mapListSettingKey(LoadShedControlConfig value,
int index, String key) {
BasicGroupSettingSpecifier configGroup = new BasicGroupSettingSpecifier(value
.settings(key + "."));
return Collections.<SettingSpecifier> singletonList(configGroup);
}
});
results.add(listComplexGroup);
return results;
}
private String getInfoMessage(final Locale locale) {
if ( lastEvaluationDate == null ) {
return messageSource.getMessage("info.noEvaluations", null, locale);
}
StringBuilder buf = new StringBuilder();
buf.append(messageSource.getMessage("info.basic", new Object[] { lastEvaluationDate }, locale));
EnergyDatum latest = consumptionSamples.peek();
if ( latest != null ) {
buf.append(" ").append(
messageSource.getMessage("info.reading",
new Object[] { latest.getWatts(), latest.getCreated() }, locale));
}
return buf.toString();
}
private String getInfoMessage(final LoadShedControlInfo info, final Locale locale) {
assert info != null;
final String mode = messageSource.getMessage(
(info.getAction() != null && info.getAction().getShedWatts() != null
&& info.getAction().getShedWatts() > 0 ? "info.control.shedding"
: "info.control.notshedding"), null, locale);
StringBuilder buf = new StringBuilder();
buf.append(messageSource.getMessage("info.control.basic", new Object[] { info.getControlId(),
mode }, locale));
if ( info.getActionDate() != null ) {
buf.append(" ").append(
messageSource.getMessage("info.control.action", new Object[] { info.getActionDate(),
info.getWattsBeforeAction() }, locale));
LoadShedControlConfig rule = configForControlId(info.getControlId());
if ( info.getAction() != null && info.getAction().getShedWatts() != null
&& info.getAction().getShedWatts() > 0 && rule != null
&& rule.getMinimumLimitMinutes() != null ) {
long nextActionAllowed = info.getActionDate().getTime()
+ rule.getMinimumLimitMinutes().longValue() * 60000L;
if ( nextActionAllowed > System.currentTimeMillis() ) {
buf.append(" ").append(
messageSource.getMessage("info.control.action.lock",
new Object[] { rule.getMinimumLimitMinutes(),
new Date(nextActionAllowed) }, locale));
}
}
}
return buf.toString();
}
private LoadShedControlConfig configForControlId(String controlId) {
List<LoadShedControlConfig> list = configs;
if ( list == null ) {
return null;
}
for ( LoadShedControlConfig c : list ) {
if ( controlId.equals(c.getControlId()) ) {
return c;
}
}
return null;
}
// Accessors
public OptionalService<DatumDataSource<EnergyDatum>> getConsumptionDataSource() {
return consumptionDataSource;
}
public void setConsumptionDataSource(
OptionalService<DatumDataSource<EnergyDatum>> consumptionDataSource) {
this.consumptionDataSource = consumptionDataSource;
}
public void setInstructionHandlers(Collection<InstructionHandler> instructionHandlers) {
this.instructionHandlers = instructionHandlers;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
public List<LoadShedControlConfig> getConfigs() {
return configs;
}
public void setConfigs(List<LoadShedControlConfig> configs) {
this.configs = configs;
}
/**
* Get the number of configured {@code configs} elements.
*
* @return The number of {@code configs} elements.
*/
public int getConfigsCount() {
List<LoadShedControlConfig> l = getConfigs();
return (l == null ? 0 : l.size());
}
/**
* Adjust the number of configured {@code configs} elements.
*
* @param count
* The desired number of {@code configs} elements.
*/
public void setConfigsCount(int count) {
if ( count < 0 ) {
count = 0;
}
List<LoadShedControlConfig> l = getConfigs();
if ( l == null ) {
l = new ArrayList<LoadShedControlConfig>(count);
setConfigs(l);
}
int lCount = l.size();
while ( lCount > count ) {
l.remove(l.size() - 1);
lCount--;
}
while ( lCount < count ) {
l.add(new LoadShedControlConfig());
lCount++;
}
}
public void setConsumptionSampleLimit(int consumptionSampleLimit) {
this.consumptionSampleLimit = consumptionSampleLimit;
}
public OptionalService<LoadShedderStrategy> getShedStrategy() {
return shedStrategy;
}
public void setShedStrategy(OptionalService<LoadShedderStrategy> shedStrategy) {
this.shedStrategy = shedStrategy;
}
}