diff --git a/net.solarnetwork.node.openadr.mockven/.classpath b/net.solarnetwork.node.openadr.mockven/.classpath new file mode 100644 index 000000000..aeeaff84d --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/net.solarnetwork.node.openadr.mockven/.gitignore b/net.solarnetwork.node.openadr.mockven/.gitignore new file mode 100644 index 000000000..c3dca1b96 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/.gitignore @@ -0,0 +1,2 @@ +/build +/target diff --git a/net.solarnetwork.node.openadr.mockven/.project b/net.solarnetwork.node.openadr.mockven/.project new file mode 100644 index 000000000..382984931 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/.project @@ -0,0 +1,28 @@ + + + net.solarnetwork.node.openadr.mockven + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/net.solarnetwork.node.openadr.mockven/.settings/org.eclipse.jdt.core.prefs b/net.solarnetwork.node.openadr.mockven/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..c537b6306 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/net.solarnetwork.node.openadr.mockven/.settings/org.eclipse.pde.core.prefs b/net.solarnetwork.node.openadr.mockven/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 000000000..e8ff8be0b --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +pluginProject.equinox=false +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/net.solarnetwork.node.openadr.mockven/META-INF/MANIFEST.MF b/net.solarnetwork.node.openadr.mockven/META-INF/MANIFEST.MF new file mode 100644 index 000000000..b16c26b90 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/META-INF/MANIFEST.MF @@ -0,0 +1,28 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: OpenADR VEN Mock +Bundle-SymbolicName: net.solarnetwork.node.openadr.mockven +Bundle-Version: 1.0.0 +Bundle-Vendor: SolarNetwork +Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Import-Package: net.solarnetwork.node;version="1.23.0", + net.solarnetwork.node.dao;version="1.8.0", + net.solarnetwork.node.domain;version="1.11.0", + net.solarnetwork.node.job;version="1.13.4", + net.solarnetwork.node.settings;version="1.10.0", + net.solarnetwork.node.settings.support;version="1.8.0", + net.solarnetwork.node.support;version="1.14.0", + net.solarnetwork.node.util;version="1.7.2", + net.solarnetwork.util;version="1.28.0", + openadr.model.v20b;version="2.1.0", + org.quartz;version="[2.2.3,3.0.0)", + org.quartz.impl.triggers;version="2.2.3", + org.quartz.simpl;version="[2.2.3,3.0.0)", + org.slf4j;version="[1.7.24,2.0.0)", + org.springframework.beans;version="[4.2.9.RELEASE,5.0.0)", + org.springframework.context.support;version="[4.2.9.RELEASE,5.0.0)", + org.springframework.core;version="[4.2.9.RELEASE,5.0.0)", + org.springframework.http;version="4.2.9.RELEASE", + org.springframework.scheduling.quartz;version="[4.2.9.RELEASE,5.0.0)", + org.springframework.web.client;version="4.2.9.RELEASE" +Require-Bundle: net.solarnetwork.external.openadr.model diff --git a/net.solarnetwork.node.openadr.mockven/OSGI-INF/blueprint/module.xml b/net.solarnetwork.node.openadr.mockven/OSGI-INF/blueprint/module.xml new file mode 100644 index 000000000..9598662f4 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/OSGI-INF/blueprint/module.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + net.solarnetwork.node.job.ManagedTriggerAndJobDetail + net.solarnetwork.node.job.ServiceProvider + net.solarnetwork.node.settings.SettingSpecifierProvider + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/net.solarnetwork.node.openadr.mockven/build.properties b/net.solarnetwork.node.openadr.mockven/build.properties new file mode 100644 index 000000000..668f1032f --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = build/eclipse/ +bin.includes = META-INF/,\ + . diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/MockVen.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/MockVen.java new file mode 100644 index 000000000..df9213a9b --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/MockVen.java @@ -0,0 +1,171 @@ + +package net.solarnetwork.node.openadr.mockven; + +import openadr.model.v20b.OadrCreatedPartyRegistration; +import openadr.model.v20b.OadrDistributeEvent; +import openadr.model.v20b.OadrPayload; +import openadr.model.v20b.OadrSignedObject; + +/** + * + * Class to simulate a Virtual End Node (VEN). This class is designed to talk to + * a Virtual Top Node (VTN) via OpenADR 2.0b + * + * + * @author robert + * @version 1.0 + */ +public class MockVen extends OadrParams { + + private boolean registered = false; + private String url; + private TopNodeConnection connection; + + public MockVen() { + connection = new TopNodeConnection(); + } + + @Override + public void setVenName(String venName) { + //if this parameter changes we assume we are no longer registered + if ( !venName.equals(getVenName()) ) { + registered = false; + } + super.setVenName(venName); + } + + public void setVtnURL(String url) { + //ensure the URL ends with a / as we will be needing to call subdomains from OadrSubDomains + if ( !url.endsWith("/") ) { + url = url + "/"; + } + //if this parameter changes we assume we are no longer registered + if ( !url.equals(this.url) ) { + registered = false; + } + + this.url = url; + } + + /** + * sends a OadrPoll message to the VTN and acts according to the message + * sent back + */ + public void pollAndRespond() { + + if ( registered == false ) { + queryAndRegister(); + } + + OadrSignedObject response = pollVTN().getOadrSignedObject(); + + /** + * currently the only supported response is for a OadrDistrubuteEvent. + * There are other possible message a VTN can send to a VEN such as + * asking for a report. + * + */ + if ( response.getOadrDistributeEvent() != null ) { + respondToDistributeEvent(response.getOadrDistributeEvent()); + } + } + + /** + * This method takes a OadrDistrubuteEvent and sends the VTN a response on + * whether the VEN is opting in or not. As well as applying a demand + * response strategy if the VTN has chosen to opt into said event. + * + * Modify this method for custom behaviours + * + * @param event + */ + public void respondToDistributeEvent(OadrDistributeEvent event) { + + //I don't think it makes sense for the decision to opt in to be in the generator + OadrCreatedEventGenerator generator = new OadrCreatedEventGenerator(); + + /** + * You can potentially use this location for interacting with a demand + * response engine or some other mechanism for doing demand response on + * the solarnetwork all of the parameters for the event are inside the + * OadrDistributeEvent + * + * https://pastebin.com/zrNwcQAX + * + * e.g. Link above is an XML generated from the EPRI VTN asking for + * 250kW of demand response (specific LOAD_CONTROL with + * X_LOAD_CONTROL_CAPACITY) + * + * if this was to be implemented here you can write some code to extract + * the "payloadFloat" value and pass it to a demand response engine + * + * + */ + OadrPayload payload = generator.createPayload(this, event); + + OadrPayload response = connection.postPayload(url + OadrSubDomains.EiEvent, payload); + + } + + /** + * Polls the VTN with an OadrPoll payload and returns the OadrPayload + * response. If the VEN is not registered to the VTN it tries to register + * first before polling. + * + * @return + */ + private OadrPayload pollVTN() { + if ( registered == false ) { + queryAndRegister(); + } + + OadrPollGenerator generator = new OadrPollGenerator(); + + OadrPayload payload = generator.createPayload(this); + + OadrPayload response = connection.postPayload(url + OadrSubDomains.OadrPoll, payload); + return response; + + } + + /** + * sends a OadrQueryRegistration payload to the VTN and then proceeds to + * register with the VTN. Currently there is no error correction this method + * can fail if the VTN refuses registration, or if the VEN fails to make a + * connection to the VTN. + */ + public void queryAndRegister() { + OadrQueryRegistrationGenerator payloadGen = new OadrQueryRegistrationGenerator(); + OadrPayload payload = payloadGen.createPayload(this); + + //Currently there is no procedure in place to handle if the VTN is unreachable eg invalid url + OadrPayload response = connection.postPayload(url + OadrSubDomains.EiRegisterParty, payload); + OadrCreatedPartyRegistration partyReg = response.getOadrSignedObject() + .getOadrCreatedPartyRegistration(); + + //Check to see if that the VTN is okay before proceeding + if ( partyReg.getEiResponse().getResponseDescription().equalsIgnoreCase("OK") ) { + setVtnID(partyReg.getVtnID()); + OadrCreatePartyRegistrationGenerator payloadGen2 = new OadrCreatePartyRegistrationGenerator(); + payload = payloadGen2.createPayload(this); + response = connection.postPayload(url + OadrSubDomains.EiRegisterParty, payload); + partyReg = response.getOadrSignedObject().getOadrCreatedPartyRegistration(); + setRegistrationID(partyReg.getRegistrationID()); + setVenID(partyReg.getVenID()); + registered = true; + //skip register report for now + /** + * TODO normally at this stage the VTN returns a register report and + * the VEN does a response to that, I have noticed that with the + * EPRI mocks that this step is not needed and one can just go and + * start polling. I don't know if this is allowed in the standard so + * it is best for in the future to follow proper steps. + */ + + } else { + //Perhaps in future this could be a checked exception + throw new RuntimeException("Failed at registering with VTN"); + } + } + +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/MockVenJobService.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/MockVenJobService.java new file mode 100644 index 000000000..d38e573fa --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/MockVenJobService.java @@ -0,0 +1,93 @@ + +package net.solarnetwork.node.openadr.mockven; + +import java.util.List; +import net.solarnetwork.node.job.JobService; +import net.solarnetwork.node.settings.SettingSpecifier; +import net.solarnetwork.node.settings.SettingSpecifierProvider; +import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier; +import net.solarnetwork.node.support.DatumDataSourceSupport; + +/** + * + * Setting specifier for the MockVEN it also implements JobService to allow + * periodic polling of the VTN + * + * @author robert + * @version 1.0 + */ +public class MockVenJobService extends DatumDataSourceSupport + implements JobService, SettingSpecifierProvider { + + private String venName; + private String vtnAddress; + private String vtnName; + private MockVen mockVen = null; + + @Override + public String getSettingUID() { + return "net.solarnetwork.node.openadr.mockven"; + } + + @Override + public String getDisplayName() { + return "OpenADR Ven Mock"; + } + + @Override + public List getSettingSpecifiers() { + List results = getIdentifiableSettingSpecifiers(); + //default names used by EPRI's VEN and VTN + //see results.add(new BasicTextFieldSettingSpecifier("vtnName", "EPRI")); + //results.add(new BasicTextFieldSettingSpecifier("vtnName", "EPRI")); + results.add(new BasicTextFieldSettingSpecifier("venName", "Test_VEN_Name")); + results.add(new BasicTextFieldSettingSpecifier("vtnName", "EPRI")); + + results.add(new BasicTextFieldSettingSpecifier("vtnAddress", null)); + return results; + } + + public String getVenName() { + return venName; + } + + public void setVenName(String venName) { + this.venName = venName; + } + + public String getVtnAddress() { + return vtnAddress; + } + + public void setVtnAddress(String vtnAddress) { + this.vtnAddress = vtnAddress; + } + + public String getVtnName() { + return vtnName; + } + + public void setVtnName(String vtnName) { + this.vtnName = vtnName; + } + + private void runMockVen() { + //create a MockVen instance when we don't have one + if ( mockVen == null ) { + mockVen = new MockVen(); + } + //put the settings into the mock ven + mockVen.setVtnURL(vtnAddress); + mockVen.setVenName(venName); + + //the poll and respond method will register to the VTN if it is not already registered + //otherwise it will send poll request to the VTN and respond to any events + mockVen.pollAndRespond(); + } + + @Override + public void executeJobService() throws Exception { + runMockVen(); + } + +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/MockVenJobService.properties b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/MockVenJobService.properties new file mode 100644 index 000000000..dbf613baa --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/MockVenJobService.properties @@ -0,0 +1,17 @@ +title = OpenADR Mock Virtual End Node +triggerCronExpression.key = Schedule +triggerCronExpression.desc = A \ + \ + cron expression representing the schedule to sample data at. +uid.key = Service Name +uid.desc = A unique name to identify this service with. +groupUID.key = Service Group +groupUID.desc = An optional group to include this service in. +venName.key = VEN Name +venName.desc = The human readable name of the VEN to be given to the VTN +venID.key = VEN ID +venID.desc = The ID number this VEN has associated with VTN +vtnAddress.key = VTN URL +vtnAddress.desc = The URL of the VTN to where OpenADR 2.0b messages get sent to. These typically end with /OpenADR2/Simple/2.0b/ +vtnName.key = VTN Name +vtnName.desc = The human readable name of the VTN diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrCreatePartyRegistrationGenerator.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrCreatePartyRegistrationGenerator.java new file mode 100644 index 000000000..d80e222d0 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrCreatePartyRegistrationGenerator.java @@ -0,0 +1,35 @@ + +package net.solarnetwork.node.openadr.mockven; + +import openadr.model.v20b.OadrCreatePartyRegistration; +import openadr.model.v20b.OadrPayload; +import openadr.model.v20b.OadrSignedObject; + +/** + * Class to generate OadrPayloads with OadrCreatePartyRegistration used for + * registering with the VTN + * + * @author robert + * @version 1.0 + */ +public class OadrCreatePartyRegistrationGenerator extends OadrPayloadGenerator { + + public OadrPayload createPayload(OadrParams params) { + OadrPayload payload = new OadrPayload(); + OadrSignedObject signedObject = new OadrSignedObject(); + OadrCreatePartyRegistration partyReg = new OadrCreatePartyRegistration(); + payload.withOadrSignedObject(signedObject.withOadrCreatePartyRegistration(partyReg)); + + //most of these values are defaults, I don't know what some of them mean I just say the EPRI VEN use them so im using them + partyReg.setSchemaVersion(params.getProfileName()); + partyReg.setRequestID(genRandomRequestID()); + partyReg.setOadrHttpPullModel(params.isHttpPullModel()); + partyReg.setOadrVenName(params.getVenName()); + partyReg.setOadrXmlSignature(params.isXmlSignature()); + partyReg.setOadrReportOnly(params.isReportOnly()); + partyReg.setOadrTransportName(params.getTransportName()); + + return payload; + + } +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrCreatedEventGenerator.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrCreatedEventGenerator.java new file mode 100644 index 000000000..df912f069 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrCreatedEventGenerator.java @@ -0,0 +1,230 @@ + +package net.solarnetwork.node.openadr.mockven; + +import java.time.Duration; +import java.util.Calendar; +import java.util.GregorianCalendar; +import openadr.model.v20b.OadrCreatedEvent; +import openadr.model.v20b.OadrDistributeEvent; +import openadr.model.v20b.OadrDistributeEvent.OadrEvent; +import openadr.model.v20b.OadrPayload; +import openadr.model.v20b.OadrSignedObject; +import openadr.model.v20b.ei.EiEventSignal; +import openadr.model.v20b.ei.EiResponse; +import openadr.model.v20b.ei.EventDescriptor; +import openadr.model.v20b.ei.EventResponses; +import openadr.model.v20b.ei.EventResponses.EventResponse; +import openadr.model.v20b.ei.OptTypeType; +import openadr.model.v20b.ei.QualifiedEventID; +import openadr.model.v20b.ei.ResponseCode; +import openadr.model.v20b.ei.SignalTypeEnumeratedType; +import openadr.model.v20b.pyld.EiCreatedEvent; + +/** + * + * Class to generate OadrPayloads containing OadrCreatedEvent. This class is + * different to my other generators as it requires a OadrDistrubuteEvent object. + * This class also has some logic in deciding whether to opt into events or not. + * + * @author robert + * @version 1.0 + */ +public class OadrCreatedEventGenerator { + + /** + * + * @param params + * @param event + * @return Payload with a OadrCreatedEvent + */ + public OadrPayload createPayload(OadrParams params, OadrDistributeEvent event) { + //NOTE HARDCODED FOR ONE EVENT + EventDescriptor eventParams = event.getOadrEvents().get(0).getEiEvent().getEventDescriptor(); + String eventID = eventParams.getEventID(); + Long modNum = eventParams.getModificationNumber(); + String requestId = event.getRequestID(); + + //we have received and event + OadrPayload payload = new OadrPayload(); + OadrSignedObject signedObject = new OadrSignedObject(); + OadrCreatedEvent createdEvent = new OadrCreatedEvent(); + payload.setOadrSignedObject(signedObject.withOadrCreatedEvent(createdEvent)); + + createdEvent.setSchemaVersion(params.getProfileName()); + EiCreatedEvent eiCreatedEvent = new EiCreatedEvent(); + eiCreatedEvent.setVenID(params.getVenID()); + + EiResponse eiResponse = new EiResponse(); + eiResponse.setRequestID(requestId);//check that is not from event + eiResponse.setResponseCode(new ResponseCode().withValue("200")); + + eiCreatedEvent.setEiResponse(eiResponse); + + EventResponse eventResponse = new EventResponse(); + + eventResponse.setOptType(OptInLogic(params, event.getOadrEvents().get(0))); + eventResponse.setQualifiedEventID( + new QualifiedEventID().withEventID(eventID).withModificationNumber(modNum)); + + EventResponses eventResponses = new EventResponses(); + eventResponses.getEventResponses().add(eventResponse); + eiCreatedEvent.setEventResponses(eventResponses); + + createdEvent.setEiCreatedEvent(eiCreatedEvent); + + return payload; + } + + //Feel free to overwrite this method to change opt in behavior. + /** + * This method is used to decide whether to opt into the event or not it + * should not be used for applying a demand response instead put that logic + * in MocVen.respondToDistributeEvent() + * + * @param params + * @param event + * @return + */ + public OptTypeType OptInLogic(OadrParams params, OadrEvent event) { + + //ensure that the signals are valid if not opt out. This is the behavior of the EPRI VEN + for ( EiEventSignal eventSig : event.getEiEvent().getEiEventSignals().getEiEventSignals() ) { + if ( !validSignalNameAndType(eventSig) ) { + return OptTypeType.OPT_OUT; + } + } + if ( eventOver(event) ) { + return OptTypeType.OPT_OUT; + } + + //proof of concept showing how to select a specific signalName + if ( event.getEiEvent().getEiEventSignals().getEiEventSignals().get(0).getSignalName() + .equals(SignalNameEnumeratedType.LOAD_CONTROL.toString()) ) { + return OptTypeType.OPT_IN; + } + return OptTypeType.OPT_IN; + } + + /** + * Returns true if the EiEventSignal has a singal name compatible with the + * signal type. This is a translation of the C# implementation in the EPRI + * mock VEN. + * + * @param event + * @return + */ + public boolean validSignalNameAndType(EiEventSignal event) { + String singalName = event.getSignalName(); + SignalTypeEnumeratedType signalType = event.getSignalType(); + + SignalNameEnumeratedType sigName = SignalNameEnumeratedType.valueOf(singalName); + + switch (sigName) { + case simple: + if ( signalType.equals(SignalTypeEnumeratedType.LEVEL) ) { + break; + } + return false; + case SIMPLE: + if ( signalType.equals(SignalTypeEnumeratedType.LEVEL) ) { + break; + } + return false; + case ELECTRICITY_PRICE: + if ( signalType.equals(SignalTypeEnumeratedType.PRICE) + || signalType.equals(SignalTypeEnumeratedType.PRICE_RELATIVE) + || signalType.equals(SignalTypeEnumeratedType.PRICE_MULTIPLIER) ) { + break; + } + return false; + case ENERGY_PRICE: + if ( signalType.equals(SignalTypeEnumeratedType.PRICE) + || signalType.equals(SignalTypeEnumeratedType.PRICE_RELATIVE) + || signalType.equals(SignalTypeEnumeratedType.PRICE_MULTIPLIER) ) { + break; + } + return false; + case DEMAND_CHARGE: + if ( signalType.equals(SignalTypeEnumeratedType.PRICE) + || signalType.equals(SignalTypeEnumeratedType.PRICE_RELATIVE) + || signalType.equals(SignalTypeEnumeratedType.PRICE_MULTIPLIER) ) { + break; + } + return false; + case BID_PRICE: + if ( signalType.equals(SignalTypeEnumeratedType.PRICE) ) { + break; + } + return false; + case BID_LOAD: + if ( signalType.equals(SignalTypeEnumeratedType.SETPOINT) ) { + break; + } + return false; + case BID_ENERGY: + if ( signalType.equals(SignalTypeEnumeratedType.SETPOINT) ) { + break; + } + return false; + case CHARGE_STATE: + if ( signalType.equals(SignalTypeEnumeratedType.SETPOINT) + || signalType.equals(SignalTypeEnumeratedType.DELTA) + || signalType.equals(SignalTypeEnumeratedType.MULTIPLIER) ) { + break; + } + return false; + case LOAD_DISPATCH: + if ( signalType.equals(SignalTypeEnumeratedType.SETPOINT) + || signalType.equals(SignalTypeEnumeratedType.DELTA) + || signalType.equals(SignalTypeEnumeratedType.MULTIPLIER) + || signalType.equals(SignalTypeEnumeratedType.LEVEL) ) { + break; + } + return false; + case LOAD_CONTROL: + if ( signalType.equals(SignalTypeEnumeratedType.X_LOAD_CONTROL_CAPACITY) + || signalType.equals(SignalTypeEnumeratedType.X_LOAD_CONTROL_LEVEL_OFFSET) + || signalType.equals(SignalTypeEnumeratedType.X_LOAD_CONTROL_PERCENT_OFFSET) + || signalType.equals(SignalTypeEnumeratedType.X_LOAD_CONTROL_SETPOINT) ) { + break; + } + return false; + } + + return true; + } + + /** + * returns true if the OadrEvent has already ended. A event is ended when + * the starting time plus the duration is less than the current time. This + * method is included to match the behaviour + * + * @param event + * @return + */ + public boolean eventOver(OadrEvent event) { + + GregorianCalendar now = (GregorianCalendar) GregorianCalendar.getInstance(); + + String durationString = event.getEiEvent().getEiActivePeriod().getProperties().getDuration() + .getDuration().getValue(); + + GregorianCalendar eventdate = event.getEiEvent().getEiActivePeriod().getProperties().getDtstart() + .getDateTime().getValue().toGregorianCalendar(); + + Duration d = Duration.parse(durationString); + + eventdate.add(Calendar.SECOND, (int) d.getSeconds()); + + int dateCompare = eventdate.compareTo(now); + if ( dateCompare > 0 ) { + //event still has some time to go + return true; + } else { + //event has already been completed + return false; + } + + } + +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrParams.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrParams.java new file mode 100644 index 000000000..d15ad0e8e --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrParams.java @@ -0,0 +1,128 @@ + +package net.solarnetwork.node.openadr.mockven; + +import openadr.model.v20b.OadrTransportType; + +/** + * This abstract class just has a bunch of getters and setters of parameters + * that often go into OadrPayloads. These are used in the generators of the + * various payloads. + * + * @author robert + * @version 1.0 + */ +public class OadrParams { + + private String venName; + + private String venID; + private String registrationID; + private String requestID; + + private String vtnID; + private int modificationNumber; + private String eventID; + + //default values I copied from the EPRI VEN + private OadrTransportType transportType = OadrTransportType.SIMPLE_HTTP; + private String profileName = "2.0b"; + private Boolean reportOnly = false; + private Boolean xmlSignature = false; + private Boolean httpPullModel = true; + + public String getVenName() { + return venName; + } + + public void setVenName(String venName) { + this.venName = venName; + } + + public String getVenID() { + return venID; + } + + public void setVenID(String venID) { + this.venID = venID; + } + + public String getRegistrationID() { + return registrationID; + } + + public void setRegistrationID(String registrationID) { + this.registrationID = registrationID; + } + + public String getRequestID() { + return requestID; + } + + public void setRequestID(String requestID) { + this.requestID = requestID; + } + + public OadrTransportType getTransportName() { + return transportType; + } + + public void setTransportName(OadrTransportType transportName) { + this.transportType = transportName; + } + + public String getProfileName() { + return profileName; + } + + public void setProfileName(String profileName) { + this.profileName = profileName; + } + + public Boolean isReportOnly() { + return reportOnly; + } + + public void setReportOnly(boolean reportOnly) { + this.reportOnly = reportOnly; + } + + public Boolean isXmlSignature() { + return xmlSignature; + } + + public void setXmlSignature(boolean xmlSignature) { + this.xmlSignature = xmlSignature; + } + + public Boolean isHttpPullModel() { + return httpPullModel; + } + + public void setHttpPullModel(boolean httpPullModel) { + this.httpPullModel = httpPullModel; + } + + public String getVtnID() { + return vtnID; + } + + public void setVtnID(String vtnID) { + this.vtnID = vtnID; + } + + public int getModificationNumber() { + return modificationNumber; + } + + public void setModificationNumber(int modificationNumber) { + this.modificationNumber = modificationNumber; + } + + public String getEventID() { + return eventID; + } + + public void setEventID(String eventID) { + this.eventID = eventID; + } +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrPayloadGenerator.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrPayloadGenerator.java new file mode 100644 index 000000000..eda5cc877 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrPayloadGenerator.java @@ -0,0 +1,35 @@ + +package net.solarnetwork.node.openadr.mockven; + +import java.util.Random; + +/** + * Abstract class containing useful functions often used in OadrPayload + * generators + * + * @author robert + * @version 1.0 + */ +public abstract class OadrPayloadGenerator { + + Random rand = new Random(); + + //returns a hex string 10 characters in length used for generating request IDs + public String genRandomRequestID() { + StringBuffer sb = new StringBuffer(); + for ( int i = 0; i < 10; i++ ) { + //this produces lowercase letters + sb.append(Integer.toHexString(rand.nextInt(16))); + } + return sb.toString(); + } + + /** + * Dependency Injection for increased controllability to allow testing. + * + * @param newrandom + */ + public void setRandom(Random newrandom) { + rand = newrandom; + } +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrPollGenerator.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrPollGenerator.java new file mode 100644 index 000000000..c3381f1a9 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrPollGenerator.java @@ -0,0 +1,32 @@ + +package net.solarnetwork.node.openadr.mockven; + +import openadr.model.v20b.OadrPayload; +import openadr.model.v20b.OadrPoll; +import openadr.model.v20b.OadrSignedObject; + +/** + * + * Creates a OadrPayload containing a OadrPoll. An OadrPoll is used to ask the + * VTN has anything happened and to ensure there is still communication with the + * VTN. + * + * @author robert + * @version 1.0 + */ +public class OadrPollGenerator extends OadrPayloadGenerator { + + public OadrPayload createPayload(OadrParams params) { + OadrPayload payload = new OadrPayload(); + OadrSignedObject signedObject = new OadrSignedObject(); + OadrPoll poll = new OadrPoll(); + + payload.withOadrSignedObject(signedObject.withOadrPoll(poll)); + + //These values should be set from the registration + poll.setVenID(params.getVenID()); + poll.setSchemaVersion(params.getProfileName()); + + return payload; + } +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrQueryRegistrationGenerator.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrQueryRegistrationGenerator.java new file mode 100644 index 000000000..9c4ae7bb1 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrQueryRegistrationGenerator.java @@ -0,0 +1,32 @@ + +package net.solarnetwork.node.openadr.mockven; + +import openadr.model.v20b.OadrPayload; +import openadr.model.v20b.OadrQueryRegistration; +import openadr.model.v20b.OadrSignedObject; + +/** + * + * Creates a OadrPayload with a OadrQueryRegistration used in registering with + * the VTN + * + * @author robert + * @version 1.0 + */ +public class OadrQueryRegistrationGenerator extends OadrPayloadGenerator { + + public OadrPayload createPayload(OadrParams params) { + OadrPayload payload = new OadrPayload(); + OadrSignedObject signedObject = new OadrSignedObject(); + OadrQueryRegistration queryReg = new OadrQueryRegistration(); + payload.withOadrSignedObject(signedObject.withOadrQueryRegistration(queryReg)); + + queryReg.setRequestID(genRandomRequestID()); + + //TODO I not sure if profilename and schema version are the same thing from what I saw it is always "2.0b" + queryReg.setSchemaVersion(params.getProfileName()); + + return payload; + + } +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrSubDomains.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrSubDomains.java new file mode 100644 index 000000000..7b218ebc1 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/OadrSubDomains.java @@ -0,0 +1,17 @@ + +package net.solarnetwork.node.openadr.mockven; + +/** + * + * Class containing static constants of the URL subdomains in OpenADR requests + * + * @author robert + * @version 1.0 + */ +public class OadrSubDomains { + + public static final String EiRegisterParty = "EiRegisterParty"; + public static final String EiReport = "EiReport"; + public static final String EiEvent = "EiEvent"; + public static final String OadrPoll = "OadrPoll"; +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/PollTrigger.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/PollTrigger.java new file mode 100644 index 000000000..0a240d983 --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/PollTrigger.java @@ -0,0 +1,32 @@ + +package net.solarnetwork.node.openadr.mockven; + +import java.util.Date; +import org.quartz.impl.triggers.SimpleTriggerImpl; + +/** + * + * FIXME + * + * This class currently does not do what I want it to do. I want to be able to + * poll the VTN every 10 seconds. Currently instead I am using a CronTrigger + * which limits my polling to every minute or second. + * + * @author robert + * @version 1.0 + */ +public class PollTrigger extends SimpleTriggerImpl { + + /** + * + */ + private static final long serialVersionUID = 5752379851134828607L; + + public PollTrigger() { + setStartTime(new Date()); + setEndTime(null); + setRepeatInterval(10000L); + setRepeatCount(REPEAT_INDEFINITELY); + + } +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/SignalNameEnumeratedType.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/SignalNameEnumeratedType.java new file mode 100644 index 000000000..279ba620d --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/SignalNameEnumeratedType.java @@ -0,0 +1,44 @@ + +package net.solarnetwork.node.openadr.mockven; + +/** + * + * enum of the OpenADR signal names. perhaps this should be moved into the + * OpenADR package. Followed the design pattern of SignalTypeEnumeratedType + * + * @author robert + * @version 1.0 + */ +public enum SignalNameEnumeratedType { + SIMPLE("SIMPLE"), + simple("simple"), + ELECTRICITY_PRICE("ELECTRICITY_PRICE"), + ENERGY_PRICE("ENERGY_PRICE"), + DEMAND_CHARGE("DEMAND_CHARGE"), + BID_PRICE("BID_PRICE"), + BID_LOAD("BID_LOAD"), + BID_ENERGY("BID_ENERGY"), + CHARGE_STATE("CHARGE_STATE"), + LOAD_DISPATCH("LOAD_DISPATCH"), + LOAD_CONTROL("LOAD_CONTROL"); + + private final String value; + + SignalNameEnumeratedType(String v) { + value = v; + } + + @Override + public String toString() { + return value; + } + + public static SignalNameEnumeratedType fromValue(String v) { + for ( SignalNameEnumeratedType c : SignalNameEnumeratedType.values() ) { + if ( c.value.equals(v) ) { + return c; + } + } + throw new IllegalArgumentException(v); + } +} diff --git a/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/TopNodeConnection.java b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/TopNodeConnection.java new file mode 100644 index 000000000..7ff8c2d3e --- /dev/null +++ b/net.solarnetwork.node.openadr.mockven/src/net/solarnetwork/node/openadr/mockven/TopNodeConnection.java @@ -0,0 +1,84 @@ + +package net.solarnetwork.node.openadr.mockven; + +import java.io.StringReader; +import java.io.StringWriter; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; +import org.xml.sax.InputSource; +import openadr.model.v20b.OadrPayload; +import openadr.model.v20b.ObjectFactory; + +/** + * + * Class that handles the connection to the VTN. Supports posting OadrPayloads + * getting the response. + * + * @author robert + * @version 1.0 + */ +public class TopNodeConnection { + + private HttpHeaders headers; + private RestTemplate rest; + private JAXBContext jaxbContext; + private Marshaller marshaller; + + public TopNodeConnection() { + headers = new HttpHeaders(); + rest = new RestTemplate(); + headers.setContentType(MediaType.TEXT_XML); + + //Require a class loader when using OSGI otherwise you get JAXBException + //https://stackoverflow.com/a/1043807 + ClassLoader cl = ObjectFactory.class.getClassLoader(); + try { + jaxbContext = JAXBContext.newInstance("openadr.model.v20b:" + "openadr.model.v20b.atom:" + + "openadr.model.v20b.currency:" + "openadr.model.v20b.ei:" + + "openadr.model.v20b.emix:" + "openadr.model.v20b.gml:" + + "openadr.model.v20b.greenbutton:" + "openadr.model.v20b.power:" + + "openadr.model.v20b.pyld:" + "openadr.model.v20b.siscale:" + + "openadr.model.v20b.strm:" + "openadr.model.v20b.xcal:" + + "openadr.model.v20b.xmldsig:" + "openadr.model.v20b.xmldsig11", cl); + + marshaller = jaxbContext.createMarshaller(); + } catch ( JAXBException e ) { + //have no way of recovering from exception. So I turn it into a RuntimeException so I don't have to declare that this class throws and exception + throw new RuntimeException(e); + } + } + + //I assume I have to synchronize this method to prevent trying to connect at the same time. Never tested multipal connections. + public synchronized OadrPayload postPayload(String url, OadrPayload payload) { + StringWriter out = new StringWriter(); + try { + //turns the payload into a XML string and writes it to the StringWriter + marshaller.marshal(payload, out); + + //send the xml to the VTN and get the response + ResponseEntity response = rest.postForEntity(url, out.toString(), String.class); + + Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller(); + + //convert the response into a OadrPayload object + OadrPayload returnPayload = (OadrPayload) jaxbUnmarshaller + .unmarshal(new InputSource(new StringReader(response.getBody()))); + + //TODO remove print statment + System.out.println(response.getBody()); + return returnPayload; + + } catch ( JAXBException e ) { + //have no way of recovering from exception. So I turn it into a RuntimeException so I don't have to declare that this class throws and exception + throw new RuntimeException(e); + } + + } + +}