/* ==================================================================
* MockMeterDataSource.java - 10/06/2015 1:28:07 pm
*
* 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.ocpp.mock;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicLong;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import net.solarnetwork.node.DatumDataSource;
import net.solarnetwork.node.MultiDatumDataSource;
import net.solarnetwork.node.domain.ACEnergyDatum;
import net.solarnetwork.node.domain.ACPhase;
import net.solarnetwork.node.domain.Datum;
import net.solarnetwork.node.domain.GeneralNodeACEnergyDatum;
import net.solarnetwork.node.ocpp.ChargeSessionManager;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier;
import net.solarnetwork.node.util.ClassUtils;
import net.solarnetwork.util.OptionalService;
/**
* Mock implementation of {@link DatumDataSource} to simulate a power meter used
* in an OCPP charge point.
*
* @author matt
* @version 1.0
*/
public class MockMeterDataSource implements DatumDataSource<GeneralNodeACEnergyDatum>,
MultiDatumDataSource<GeneralNodeACEnergyDatum>, SettingSpecifierProvider, EventHandler {
private OptionalService<EventAdmin> eventAdmin;
private MessageSource messageSource;
private long sampleCacheMs = 5000;
private String uid = "MockMeter";
private String socketId = "/socket/mock";
private String groupUID;
private double watts = 5;
private double wattsRandomness = 0.2;
private double chargingWatts = 2400;
private double chargingWattsRandomness = 0.1;
private boolean charging;
private GeneralNodeACEnergyDatum sample;
private final Logger log = LoggerFactory.getLogger(getClass());
private final AtomicLong mockMeter = new AtomicLong(meterStartValue());
/**
* Get a mock starting value for our meter. As we expect meters to only
* increase, the value returned here is based on the current time. Thus
* starting/stopping this service won't roll the meter back.
*
* @return a starting meter value
*/
private long meterStartValue() {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
Date now = cal.getTime();
cal.set(2010, cal.getMinimum(Calendar.MONTH), 1, 0, 0, 0);
cal.set(Calendar.MILLISECOND, 0);
return (now.getTime() - cal.getTimeInMillis()) / (1000L * 60);
}
private GeneralNodeACEnergyDatum getCurrentSample() {
GeneralNodeACEnergyDatum currSample = sample;
if ( isSampleExpired(currSample) ) {
GeneralNodeACEnergyDatum newSample = new GeneralNodeACEnergyDatum();
newSample.setCreated(new Date());
newSample.setSourceId(uid);
newSample.setPhase(ACPhase.Total);
if ( currSample == null ) {
newSample.setWattHourReading(mockMeter.get());
} else {
double diffHours = ((newSample.getCreated().getTime()
- currSample.getCreated().getTime()) / (double) (1000 * 60 * 60));
double power;
if ( charging ) {
power = chargingWatts;
} else {
// we expect very little draw (ideally 0, but let's allow for a little)
power = watts;
}
power += (power
* (Math.random() * (charging ? chargingWattsRandomness : wattsRandomness))
* (Math.random() < 0.5 ? -1 : 1));
long wh = (long) (power * diffHours);
long newWh = currSample.getWattHourReading() + wh;
if ( mockMeter.compareAndSet(currSample.getWattHourReading(), newWh) ) {
newSample.setWattHourReading(newWh);
newSample.setWatts((int) power);
} else {
newSample.setWattHourReading(currSample.getWattHourReading());
}
}
log.debug("Read mock data: {}", newSample);
currSample = newSample;
sample = newSample;
}
return currSample;
}
private boolean isSampleExpired(GeneralNodeACEnergyDatum datum) {
if ( datum == null ) {
return true;
}
final long lastReadDiff = System.currentTimeMillis() - datum.getCreated().getTime();
if ( lastReadDiff > sampleCacheMs ) {
return true;
}
return false;
}
@Override
public String getUID() {
return uid;
}
@Override
public String getGroupUID() {
return groupUID;
}
@Override
public String getSettingUID() {
return "net.solarnetwork.node.ocpp.mock.meter";
}
@Override
public String getDisplayName() {
return getClass().getSimpleName();
}
@Override
public MessageSource getMessageSource() {
return messageSource;
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
MockMeterDataSource defaults = new MockMeterDataSource();
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(8);
results.add(new BasicTextFieldSettingSpecifier("uid", defaults.uid));
results.add(new BasicTextFieldSettingSpecifier("groupUID", defaults.groupUID));
results.add(new BasicTextFieldSettingSpecifier("socketId", defaults.socketId));
results.add(new BasicTextFieldSettingSpecifier("watts", String.valueOf(defaults.watts)));
results.add(new BasicTextFieldSettingSpecifier("wattsRandomness",
String.valueOf(defaults.wattsRandomness)));
results.add(new BasicTextFieldSettingSpecifier("chargingWatts",
String.valueOf(defaults.chargingWatts)));
results.add(new BasicTextFieldSettingSpecifier("chargingWattsRandomness",
String.valueOf(defaults.chargingWattsRandomness)));
return results;
}
@Override
public Class<? extends GeneralNodeACEnergyDatum> getMultiDatumType() {
return getDatumType();
}
@Override
public Collection<GeneralNodeACEnergyDatum> readMultipleDatum() {
return Collections.singletonList(readCurrentDatum());
}
@Override
public Class<? extends GeneralNodeACEnergyDatum> getDatumType() {
return GeneralNodeACEnergyDatum.class;
}
@Override
public GeneralNodeACEnergyDatum readCurrentDatum() {
final long start = System.currentTimeMillis();
final GeneralNodeACEnergyDatum d = getCurrentSample();
if ( d.getCreated().getTime() >= start ) {
// we read from the meter
postDatumCapturedEvent(d, ACEnergyDatum.class);
}
return d;
}
/**
* Post a {@link DatumDataSource#EVENT_TOPIC_DATUM_CAPTURED} {@link Event}.
*
* <p>
* This method calls {@link #createDatumCapturedEvent(Datum, Class)} to
* create the actual Event, which may be overridden by extending classes.
* </p>
*
* @param datum
* the {@link Datum} to post the event for
* @param eventDatumType
* the Datum class to use for the
* {@link DatumDataSource#EVENT_DATUM_CAPTURED_DATUM_TYPE} property
* @since 1.3
*/
protected final void postDatumCapturedEvent(final Datum datum,
final Class<? extends Datum> eventDatumType) {
EventAdmin ea = (eventAdmin == null ? null : eventAdmin.service());
if ( ea == null || datum == null ) {
return;
}
Event event = createDatumCapturedEvent(datum, eventDatumType);
ea.postEvent(event);
}
/**
* Create a new {@link DatumDataSource#EVENT_TOPIC_DATUM_CAPTURED}
* {@link Event} object out of a {@link Datum}.
*
* <p>
* This method will populate all simple properties of the given
* {@link Datum} into the event properties, along with the
* {@link DatumDataSource#EVENT_DATUM_CAPTURED_DATUM_TYPE}.
*
* @param datum
* the datum to create the event for
* @param eventDatumType
* the Datum class to use for the
* {@link DatumDataSource#EVENT_DATUM_CAPTURED_DATUM_TYPE} property
* @return the new Event instance
* @since 1.3
*/
protected Event createDatumCapturedEvent(final Datum datum,
final Class<? extends Datum> eventDatumType) {
Map<String, Object> props = ClassUtils.getSimpleBeanProperties(datum, null);
props.put(DatumDataSource.EVENT_DATUM_CAPTURED_DATUM_TYPE, eventDatumType.getName());
log.debug("Created {} event with props {}", DatumDataSource.EVENT_TOPIC_DATUM_CAPTURED, props);
return new Event(DatumDataSource.EVENT_TOPIC_DATUM_CAPTURED, props);
}
@Override
public void handleEvent(Event event) {
final String topic = event.getTopic();
final String socketId = (String) event
.getProperty(ChargeSessionManager.EVENT_PROPERTY_SOCKET_ID);
if ( socketId == null || !socketId.equals(this.socketId) ) {
return;
}
if ( ChargeSessionManager.EVENT_TOPIC_SOCKET_ACTIVATED.equals(topic) ) {
setCharging(true);
log.info("Mock OCPP meter {} simulating charging load ACTIVATED on socket {}", uid,
socketId);
} else if ( ChargeSessionManager.EVENT_TOPIC_SOCKET_DEACTIVATED.equals(topic) ) {
setCharging(false);
log.info("Mock OCPP meter {} simulating charging load DEACTIVATED on socket {}", uid,
socketId);
}
}
public void setSampleCacheMs(long sampleCacheMs) {
this.sampleCacheMs = sampleCacheMs;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public void setGroupUID(String groupUID) {
this.groupUID = groupUID;
}
public boolean isCharging() {
return charging;
}
public void setCharging(boolean charging) {
this.charging = charging;
}
public void setEventAdmin(OptionalService<EventAdmin> eventAdmin) {
this.eventAdmin = eventAdmin;
}
public String getSocketId() {
return socketId;
}
public void setSocketId(String socketId) {
this.socketId = socketId;
}
public void setWatts(double watts) {
this.watts = watts;
}
public void setWattsRandomness(double wattRandomness) {
this.wattsRandomness = wattRandomness;
}
public void setChargingWatts(double chargingWatts) {
this.chargingWatts = chargingWatts;
}
public void setChargingWattsRandomness(double chargingWattRandomness) {
this.chargingWattsRandomness = chargingWattRandomness;
}
}