diff --git a/client/build.gradle b/client/build.gradle index 3121089e..4d7f0097 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -26,9 +26,13 @@ dependencies { // https://mvnrepository.com/artifact/org.java-websocket/Java-WebSocket implementation 'org.java-websocket:Java-WebSocket:1.5.5' + // TODO Remove jewelcli dependency and transition to commons-cli // https://mvnrepository.com/artifact/com.lexicalscope.jewelcli/jewelcli implementation group: 'com.lexicalscope.jewelcli', name: 'jewelcli', version: '0.8.9' + // https://mvnrepository.com/artifact/commons-cli/commons-cli + implementation group: 'commons-cli', name: 'commons-cli', version: '1.9.0' + implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-jul implementation group: 'org.apache.logging.log4j', name: 'log4j-jul', version: log4jVersion diff --git a/client/src/main/java/me/retrodaredevil/solarthing/actions/chatbot/SlackChatBotAction.java b/client/src/main/java/me/retrodaredevil/solarthing/actions/chatbot/SlackChatBotAction.java index 6ec48a1d..a82a44c3 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/actions/chatbot/SlackChatBotAction.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/actions/chatbot/SlackChatBotAction.java @@ -145,7 +145,9 @@ private void handle(EventsApiEnvelope eventsApiEnvelope) { LOGGER.debug("Got a message! type: " + eventsApiEnvelope.getType() + " payload: " + eventsApiEnvelope.getPayload()); if ("events_api".equals(eventsApiEnvelope.getType())) { JsonObject payload = eventsApiEnvelope.getPayload().getAsJsonObject(); + // https://api.slack.com/events/message JsonObject message = payload.getAsJsonObject("event"); + // TODO create a hash of this message if (message.get("bot_id") != null) { LOGGER.debug("Got a message from a bot! Ignoring."); return; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/actions/command/AlterManagerAction.java b/client/src/main/java/me/retrodaredevil/solarthing/actions/command/AlterManagerAction.java index 129e609e..cf506fe3 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/actions/command/AlterManagerAction.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/actions/command/AlterManagerAction.java @@ -29,7 +29,7 @@ import me.retrodaredevil.solarthing.packets.security.crypto.InvalidKeyException; import me.retrodaredevil.solarthing.packets.security.crypto.KeyUtil; import me.retrodaredevil.solarthing.packets.security.crypto.PublicKeyLookUp; -import me.retrodaredevil.solarthing.program.SecurityPacketReceiver; +import me.retrodaredevil.solarthing.program.subprogram.run.SecurityPacketReceiver; import me.retrodaredevil.solarthing.reason.ExecutionReason; import me.retrodaredevil.solarthing.reason.OpenSourceExecutionReason; import me.retrodaredevil.solarthing.type.alter.AlterPacket; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/actions/command/ExecutingCommandFeedbackActionNode.java b/client/src/main/java/me/retrodaredevil/solarthing/actions/command/ExecutingCommandFeedbackActionNode.java index 7f147b7d..066af8ed 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/actions/command/ExecutingCommandFeedbackActionNode.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/actions/command/ExecutingCommandFeedbackActionNode.java @@ -10,7 +10,7 @@ import me.retrodaredevil.solarthing.actions.environment.EventReceiverEnvironment; import me.retrodaredevil.solarthing.actions.environment.ExecutionReasonEnvironment; import me.retrodaredevil.solarthing.packets.Packet; -import me.retrodaredevil.solarthing.program.PacketListReceiverHandler; +import me.retrodaredevil.solarthing.program.subprogram.run.PacketListReceiverHandler; import me.retrodaredevil.solarthing.reason.ExecutionReason; import me.retrodaredevil.solarthing.type.event.feedback.ImmutableExecutionFeedbackPacket; import org.slf4j.Logger; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/actions/environment/EventReceiverEnvironment.java b/client/src/main/java/me/retrodaredevil/solarthing/actions/environment/EventReceiverEnvironment.java index e5a53c6a..92dc84ed 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/actions/environment/EventReceiverEnvironment.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/actions/environment/EventReceiverEnvironment.java @@ -1,6 +1,6 @@ package me.retrodaredevil.solarthing.actions.environment; -import me.retrodaredevil.solarthing.program.PacketListReceiverHandler; +import me.retrodaredevil.solarthing.program.subprogram.run.PacketListReceiverHandler; /** * An environment that contains objects used to put data in the event database diff --git a/client/src/main/java/me/retrodaredevil/solarthing/config/options/PVOutputUploadProgramOptions.java b/client/src/main/java/me/retrodaredevil/solarthing/config/options/PVOutputUploadProgramOptions.java index a097f632..09cb8d97 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/config/options/PVOutputUploadProgramOptions.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/config/options/PVOutputUploadProgramOptions.java @@ -6,10 +6,10 @@ import me.retrodaredevil.solarthing.annotations.JsonExplicit; import me.retrodaredevil.solarthing.packets.identification.IdentifierFragmentMatcher; import me.retrodaredevil.solarthing.packets.identification.IdentifierRepFragment; -import me.retrodaredevil.solarthing.program.pvoutput.provider.PacketVoltageProvider; -import me.retrodaredevil.solarthing.program.pvoutput.provider.PacketTemperatureCelsiusProvider; -import me.retrodaredevil.solarthing.program.pvoutput.provider.TemperatureCelsiusProvider; -import me.retrodaredevil.solarthing.program.pvoutput.provider.VoltageProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.PacketVoltageProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.PacketTemperatureCelsiusProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.TemperatureCelsiusProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.VoltageProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/Constants.java b/client/src/main/java/me/retrodaredevil/solarthing/program/Constants.java deleted file mode 100644 index eb55908b..00000000 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/Constants.java +++ /dev/null @@ -1,14 +0,0 @@ -package me.retrodaredevil.solarthing.program; - -import me.retrodaredevil.solarthing.annotations.UtilityClass; - -@UtilityClass -public final class Constants { - private Constants(){ throw new UnsupportedOperationException(); } - - @Deprecated - public static final String DATABASE_UPLOAD_ID = "packet_upload"; - @Deprecated - public static final String DATABASE_COMMAND_DOWNLOAD_ID = "command_download"; - -} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/SolarMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/SolarMain.java index 39004e59..656bf17e 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/SolarMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/SolarMain.java @@ -1,244 +1,19 @@ package me.retrodaredevil.solarthing.program; -import com.lexicalscope.jewel.cli.ArgumentValidationException; -import com.lexicalscope.jewel.cli.Cli; -import com.lexicalscope.jewel.cli.CliFactory; -import com.lexicalscope.jewel.cli.HelpRequestedException; -import me.retrodaredevil.couchdbjava.exception.CouchDbCodeException; -import me.retrodaredevil.couchdbjava.exception.CouchDbException; -import me.retrodaredevil.couchdbjava.response.ErrorResponse; import me.retrodaredevil.solarthing.SolarThingConstants; import me.retrodaredevil.solarthing.annotations.UtilityClass; -import me.retrodaredevil.solarthing.config.ConfigException; -import me.retrodaredevil.solarthing.config.ConfigUtil; -import me.retrodaredevil.solarthing.config.databases.DatabaseConfig; -import me.retrodaredevil.solarthing.config.databases.DatabaseSettings; -import me.retrodaredevil.solarthing.config.databases.implementations.CouchDbDatabaseSettings; -import me.retrodaredevil.solarthing.config.options.*; -import me.retrodaredevil.solarthing.packets.Packet; -import me.retrodaredevil.solarthing.packets.collection.HourIntervalPacketCollectionIdGenerator; -import me.retrodaredevil.solarthing.packets.collection.PacketCollectionIdGenerator; -import me.retrodaredevil.solarthing.packets.creation.TextPacketCreator; -import me.retrodaredevil.solarthing.packets.handling.PacketListReceiver; -import me.retrodaredevil.solarthing.packets.handling.RawPacketReceiver; -import me.retrodaredevil.solarthing.packets.instance.InstanceFragmentIndicatorPackets; -import me.retrodaredevil.solarthing.packets.instance.InstanceSourcePackets; import me.retrodaredevil.solarthing.program.action.RunActionMain; +import me.retrodaredevil.solarthing.program.subprogram.analyze.AnalyzeMain; import me.retrodaredevil.solarthing.program.check.CheckMain; -import me.retrodaredevil.solarthing.program.couchdb.CouchDbSetupMain; -import me.retrodaredevil.solarthing.program.pvoutput.PVOutputUploadMain; -import org.apache.logging.log4j.LogManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import me.retrodaredevil.solarthing.program.subprogram.run.RunMain; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Instant; -import java.util.List; -import java.util.Random; - -import static java.util.Objects.requireNonNull; @UtilityClass public final class SolarMain { private SolarMain(){ throw new UnsupportedOperationException(); } - /* - So you think this class is complicated? Well I'll have to agree with you on that. This is probably the most - complicated class in this project. However, that's a good thing. By having most of the complexity in this class, - we have less complexity in other classes. Most configuration of how the actual program works goes on in here. - I'm sure there's a better way to write this class. However the main advantage of it right now is that - it's pretty easy to change behaviour, and that's what's important. - */ - - private static final Logger LOGGER = LoggerFactory.getLogger(SolarMain.class); - - public static int initReader(InputStream in, Runnable reloadIO, TextPacketCreator packetCreator, RawPacketReceiver rawPacketReceiver) { - SolarReader solarReader = new SolarReader(in, packetCreator, rawPacketReceiver); - try { - while (!Thread.currentThread().isInterrupted()) { - try { - solarReader.update(); - } catch (EOFException e) { - return 0; - } catch (IOException e) { - LOGGER.error("Got IOException!", e); - Thread.sleep(500); - reloadIO.run(); - LOGGER.debug("Reloaded IO bundle"); - Thread.sleep(1000); - } - Thread.sleep(5); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - return SolarThingConstants.EXIT_CODE_INTERRUPTED; - } - - public static PacketListReceiver getSourceAndFragmentUpdater(PacketHandlingOption options){ - String source = options.getSourceId(); - int fragment = options.getFragmentId(); - requireNonNull(source); - Packet sourcePacket = InstanceSourcePackets.create(source); - Packet fragmentPacket = InstanceFragmentIndicatorPackets.create(fragment); - return (list) -> { - list.add(sourcePacket); - list.add(fragmentPacket); - }; - } - public static PacketCollectionIdGenerator createIdGenerator(Integer uniqueIdsInOneHour, boolean shortDocumentId){ - if(uniqueIdsInOneHour == null){ - return PacketCollectionIdGenerator.Defaults.UNIQUE_GENERATOR; - } - return new HourIntervalPacketCollectionIdGenerator(uniqueIdsInOneHour, new Random().nextInt(), shortDocumentId); - } - - private static String getJarInfo() { - JarUtil.Data data = JarUtil.getData(); - Instant lastModified = data.getLastModifiedInstantOrNull(); - return "Jar: " + JarUtil.getJarFileName() - + " Last Modified: " + (lastModified == null ? "unknown" : lastModified.toString()) - + " Java version: " + System.getProperty("java.version"); - } - - public static int doMainCommand(CommandOptions commandOptions, Path baseConfigFile) { - String user = System.getProperty("user.name"); - if (!"solarthing".equals(user) && !SolarThingEnvironment.isRunningInDocker()) { - if (user.equals("root")) { - LOGGER.warn("Running as root user!"); - System.out.println("\n\nHey! We noticed you are running as root! Instead of\n sudo ./run.sh\nPlease do\n sudo -u solarthing ./run.sh\n instead.\n"); - } else { - LOGGER.info("Running as " + user); - } - } - - LOGGER.info("Using base configuration file: " + baseConfigFile); - if (Files.notExists(baseConfigFile)) { - LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Base configuration file does not exist!"); - return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; - } - final ProgramOptions options; - try { - options = ConfigUtil.readConfig(baseConfigFile, ProgramOptions.class); - } catch (ConfigException e) { - LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Error while parsing ProgramOptions.", e); - LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "Hey! The error you got above might be scary, but this message might be helpful:\n\n" + e.getMessage()); - return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; - } - - // Note we used to have the creation of the .data directory here. We may consider adding it back in the future should we need it - final ProgramType programType = options.getProgramType(); - try { - if(programType == ProgramType.MATE) { - return OutbackMateMain.connectMate((MateProgramOptions) options, commandOptions.isValidate()); - } else if(programType == ProgramType.ROVER_SETUP){ - return RoverMain.connectRoverSetup((RoverSetupProgramOptions) options, commandOptions.isValidate()); - } else if(programType == ProgramType.PVOUTPUT_UPLOAD){ - return PVOutputUploadMain.startPVOutputUpload((PVOutputUploadProgramOptions) options, commandOptions, commandOptions.isValidate()); - } else if(programType == ProgramType.REQUEST) { - return RequestMain.startRequestProgram((RequestProgramOptions) options, commandOptions.isValidate()); - } else if(programType == ProgramType.AUTOMATION) { - return AutomationMain.startAutomation((AutomationProgramOptions) options, commandOptions.isValidate()); - } - throw new AssertionError("Unknown program type... type=" + programType + " programOptions=" + options); - } catch (ConfigException e) { - String logMessage = "Ending SolarThing. " + getJarInfo(); - LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "[LOG] " + logMessage); - System.out.println("[stdout] " + logMessage); - System.err.println("[stderr] " + logMessage); - LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Got config exception", e); - LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "SolarThing is shutting down. This is caused by an error in your configuration or in how your environment is set up. Detailed message below:\n\n" + e.getUserMessage()); - LogManager.shutdown(); - return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; - } catch (Throwable t) { - boolean invalidJar = t instanceof ClassNotFoundException || t instanceof NoClassDefFoundError; - if (invalidJar) { - LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "We're about to give you an error with some technical stuff, but this error is likely caused by you switching out jar files while SolarThing is running. If it isn't, please report this error."); - } - boolean uncommonError = t instanceof UnsatisfiedLinkError; - if (uncommonError) { - LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "Got an UnsatisfiedLinkError which is uncommon. If setup correctly, after this crash program will relaunch (hopefully successfully)."); - } - String logMessage = "Ending SolarThing. " + getJarInfo(); - LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "[LOG] " + logMessage); - System.out.println("[stdout] " + logMessage); - System.err.println("[stderr] " + logMessage); - LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Got throwable", t); - LOGGER.debug("Going to shutdown LogManager."); - LogManager.shutdown(); // makes sure all buffered logs are flushed // this should be done automatically, but we'll do it anyway - System.err.println(); - t.printStackTrace(System.err); // print to stderr just in case logging isn't going well - if (invalidJar) { - return SolarThingConstants.EXIT_CODE_RESTART_NEEDED_JAR_UPDATED; - } - if (uncommonError) { - return SolarThingConstants.EXIT_CODE_RESTART_NEEDED_UNCOMMON_ERROR; - } - return SolarThingConstants.EXIT_CODE_CRASH; - } - } - - private static int doMain(String[] args){ - String logMessage = "Beginning main. " + getJarInfo(); - LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "[LOG] " + logMessage); - System.out.println("[stdout] " + logMessage); - System.err.println("[stderr] " + logMessage); - Cli cli = CliFactory.createCli(CommandOptions.class); - final CommandOptions commandOptions; - try { - commandOptions = cli.parseArguments(args); - } catch (ArgumentValidationException ex) { - System.out.println(cli.getHelpMessage()); - if (ex instanceof HelpRequestedException) { - return 0; - } - LOGGER.error(SolarThingConstants.SUMMARY_MARKER, ex.getMessage()); - LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Incorrect args"); - return SolarThingConstants.EXIT_CODE_INVALID_OPTIONS; - } - if (commandOptions.getBaseConfigFile() != null) { - return doMainCommand(commandOptions, Path.of(commandOptions.getBaseConfigFile())); - } - if (commandOptions.getCouchDbSetupFile() != null) { - final DatabaseConfig config; - try { - config = ConfigUtil.readConfig(Path.of(commandOptions.getCouchDbSetupFile()), DatabaseConfig.class); - } catch (ConfigException e) { - e.printStackTrace(); - System.err.println("Problem reading CouchDB database settings file."); - return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; - } - DatabaseSettings settings = config.requireDatabaseSettings(); - if (!(settings instanceof CouchDbDatabaseSettings)) { - System.err.println("Must be CouchDB database settings!"); - return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; - } - try { - return CouchDbSetupMain.createFrom((CouchDbDatabaseSettings) settings).doCouchDbSetupMain(); - } catch (CouchDbException e) { - if (e instanceof CouchDbCodeException) { - ErrorResponse error = ((CouchDbCodeException) e).getErrorResponse(); - if (error != null) { - System.err.println(error.getError()); - System.err.println(error.getReason()); - } - } - throw new RuntimeException(e); - } - } - List legacyArguments = commandOptions.getLegacyOptionsRaw(); - if (legacyArguments == null || legacyArguments.isEmpty()) { - System.err.println(cli.getHelpMessage()); - return SolarThingConstants.EXIT_CODE_INVALID_OPTIONS; - } - System.err.println("Invalid sub command: " + legacyArguments.get(0)); - return SolarThingConstants.EXIT_CODE_INVALID_OPTIONS; - } private static int outputVersion() { JarUtil.Data data = JarUtil.getData(); Instant lastModified = data.getLastModifiedInstantOrNull(); @@ -252,6 +27,7 @@ private static int outputVersion() { "Java version: " + System.getProperty("java.version")); return 0; } + private static int determineMainSubprogram(String[] args) { if (args.length == 0) { System.err.println("Usage: solarthing [command] [args]\n\n" + @@ -259,7 +35,8 @@ private static int determineMainSubprogram(String[] args) { " run [options]\n" + " version\n" + " check --port [--type ]\n" + - " action [file]"); + " action [file]\n" + + " analyze [options]"); return SolarThingConstants.EXIT_CODE_INVALID_OPTIONS; } String firstArg = args[0]; @@ -267,13 +44,15 @@ private static int determineMainSubprogram(String[] args) { System.arraycopy(args, 1, subArgs, 0, args.length - 1); switch (firstArg) { case "run": - return doMain(subArgs); + return RunMain.runMain(subArgs); case "version": return outputVersion(); case "action": return RunActionMain.runAction(subArgs); case "check": return CheckMain.doCheck(subArgs); + case "analyze": + return AnalyzeMain.analyze(subArgs); default: System.err.println("Unknown argument: " + firstArg + "\n" + "Previous SolarThing versions allowed the running of SolarThing without the 'run' prefix.\n" + diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/check/CheckMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/check/CheckMain.java index 407e6b26..9ded8ab8 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/check/CheckMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/check/CheckMain.java @@ -21,7 +21,7 @@ import me.retrodaredevil.solarthing.annotations.UtilityClass; import me.retrodaredevil.solarthing.packets.handling.PacketListReceiver; import me.retrodaredevil.solarthing.packets.handling.implementations.TimedPacketReceiver; -import me.retrodaredevil.solarthing.program.SolarReader; +import me.retrodaredevil.solarthing.program.subprogram.run.SolarReader; import me.retrodaredevil.solarthing.program.modbus.MutableAddressModbusSlave; import me.retrodaredevil.solarthing.solar.common.BatteryVoltage; import me.retrodaredevil.solarthing.solar.outback.MatePacketCreator49; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/receiver/RequestHeartbeatReceiver.java b/client/src/main/java/me/retrodaredevil/solarthing/program/receiver/RequestHeartbeatReceiver.java index 0af58920..e43ce9c6 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/receiver/RequestHeartbeatReceiver.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/receiver/RequestHeartbeatReceiver.java @@ -4,7 +4,7 @@ import me.retrodaredevil.solarthing.commands.packets.open.RequestHeartbeatPacket; import me.retrodaredevil.solarthing.packets.Packet; import me.retrodaredevil.solarthing.packets.collection.TargetPacketGroup; -import me.retrodaredevil.solarthing.program.PacketListReceiverHandler; +import me.retrodaredevil.solarthing.program.subprogram.run.PacketListReceiverHandler; import me.retrodaredevil.solarthing.reason.ExecutionReason; import me.retrodaredevil.solarthing.reason.OpenSourceExecutionReason; import me.retrodaredevil.solarthing.type.event.feedback.HeartbeatData; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/AnalysisException.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/AnalysisException.java new file mode 100644 index 00000000..2e052840 --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/AnalysisException.java @@ -0,0 +1,9 @@ +package me.retrodaredevil.solarthing.program.subprogram.analyze; + +import static java.util.Objects.requireNonNull; + +public class AnalysisException extends Exception { + public AnalysisException(String message, Throwable cause) { + super(requireNonNull(message), requireNonNull(cause)); + } +} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/AnalysisExecutor.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/AnalysisExecutor.java new file mode 100644 index 00000000..f6589c18 --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/AnalysisExecutor.java @@ -0,0 +1,96 @@ +package me.retrodaredevil.solarthing.program.subprogram.analyze; + +import me.retrodaredevil.solarthing.database.MillisQuery; +import me.retrodaredevil.solarthing.database.MillisQueryBuilder; +import me.retrodaredevil.solarthing.database.SolarThingDatabase; +import me.retrodaredevil.solarthing.database.exception.SolarThingDatabaseException; +import me.retrodaredevil.solarthing.packets.collection.StoredPacketGroup; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * The job of the AnalysisExecutor is to gather data from a period of time, and instruct an {@link Analyzer} to analyze that chunk of data. + *

+ * Each {@link Analyzer} is designed to be instructed to analyze only a portion of the data provided to it. + * For the generator run analysis example, you would give its analyzer 2 days worth of data, and then instruct it to only analyze generator runs that started on the first day. + * This is done to account for generator runs (or other sections of data) that may need to be analyzed over the period of multiple days. + */ +public class AnalysisExecutor { + private static final int MAX_RETRIES = 10; + + /** The SolarThing database instance */ + private final SolarThingDatabase database; + /** The analyzer */ + private final Analyzer analyzer; + /** The processing window */ + private final Duration processingWindowDuration; + /** The duration after the processing window to retrieve data for*/ + private final Duration beforeProcessingWindowFetchDuration; + /** The duration before the processing window to retrieve data for*/ + private final Duration afterProcessingWindowFetchDuration; + + public AnalysisExecutor(SolarThingDatabase database, Analyzer analyzer, Duration processingWindowDuration, Duration beforeProcessingWindowFetchDuration, Duration afterProcessingWindowFetchDuration) { + this.database = requireNonNull(database); + this.analyzer = requireNonNull(analyzer); + if (processingWindowDuration.compareTo(Duration.ZERO) <= 0) { + throw new IllegalArgumentException("processingWindowDuration is not a positive duration!"); + } + if (beforeProcessingWindowFetchDuration.isNegative()) { + throw new IllegalArgumentException("beforeProcessingWindowFetchDuration is negative!"); + } + if (afterProcessingWindowFetchDuration.isNegative()) { + throw new IllegalArgumentException("afterProcessingWindowFetchDuration is negative!"); + } + this.processingWindowDuration = processingWindowDuration; + this.beforeProcessingWindowFetchDuration = beforeProcessingWindowFetchDuration; + this.afterProcessingWindowFetchDuration = afterProcessingWindowFetchDuration; + } + + private DataChunk fetchDataChunk(Instant dataStartInstant, Instant dataEndInstant) throws SolarThingDatabaseException { + MillisQuery query = new MillisQueryBuilder() + .startKey(dataStartInstant.toEpochMilli()) + .endKey(dataEndInstant.toEpochMilli()) + .inclusiveEnd(false) + .build(); + + // TODO This is a perfect spot for structured concurrency + List statusPackets = database.getStatusDatabase().query(query); + List eventPackets = database.getEventDatabase().query(query); + return new DataChunk(statusPackets, eventPackets); + } + private DataChunk fetchDataChunkWithRetry(Instant dataStartInstant, Instant dataEndInstant) throws AnalysisException { + for (int i = 0; i < MAX_RETRIES; i++) { + try { + return fetchDataChunk(dataStartInstant, dataEndInstant); + } catch (SolarThingDatabaseException e) { + if (i == MAX_RETRIES - 1) { + throw new AnalysisException("Reached max retries while querying from " + dataStartInstant + " to " + dataEndInstant, e); + } + } + } + throw new AssertionError("This should never happen"); + } + + public List analyze(Instant startTime, Instant endTime) throws AnalysisException { + List combinedResults = new ArrayList<>(); + for (Instant processingStartInstant = startTime, processingEndInstant; processingStartInstant.compareTo(endTime) < 0; processingStartInstant = processingEndInstant) { + processingEndInstant = processingStartInstant.plus(processingWindowDuration); + if (processingEndInstant.isAfter(endTime)) { + processingEndInstant = endTime; + } + Instant dataStartInstant = processingStartInstant.minus(beforeProcessingWindowFetchDuration); + Instant dataEndInstant = processingEndInstant.plus(afterProcessingWindowFetchDuration); + + // TODO code after this comment in this for loop can be parallelized + DataChunk dataChunk = fetchDataChunkWithRetry(processingStartInstant, dataEndInstant); + List results = analyzer.analyze(dataStartInstant, processingStartInstant, processingEndInstant, dataEndInstant, dataChunk); + combinedResults.addAll(results); + } + return combinedResults; + } +} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/AnalyzeMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/AnalyzeMain.java new file mode 100644 index 00000000..53ace1df --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/AnalyzeMain.java @@ -0,0 +1,148 @@ +package me.retrodaredevil.solarthing.program.subprogram.analyze; + +import me.retrodaredevil.couchdb.CouchDbUtil; +import me.retrodaredevil.solarthing.SolarThingConstants; +import me.retrodaredevil.solarthing.annotations.NotNull; +import me.retrodaredevil.solarthing.config.ConfigException; +import me.retrodaredevil.solarthing.config.ConfigUtil; +import me.retrodaredevil.solarthing.config.databases.DatabaseConfig; +import me.retrodaredevil.solarthing.config.databases.implementations.CouchDbDatabaseSettings; +import me.retrodaredevil.solarthing.database.SolarThingDatabase; +import me.retrodaredevil.solarthing.database.couchdb.CouchDbSolarThingDatabase; +import me.retrodaredevil.solarthing.packets.collection.DefaultInstanceOptions; +import me.retrodaredevil.solarthing.program.subprogram.analyze.analyzers.generator.GeneratorRunAnalyzer; +import me.retrodaredevil.solarthing.program.subprogram.analyze.analyzers.generator.entry.GeneratorRunEntry; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; + +public final class AnalyzeMain { + private AnalyzeMain() { throw new UnsupportedOperationException(); } + + /** + * Parses a string to an Instant, trying multiple ways of parsing the string to support multiple format options of the string we are trying to parse. + * + * @param timeString The string to parse + * @param zoneId The zone ID to use if a local date is parsed. This timezone will be used to convert the local date to midnight in this timezone on that given day. + * @param daysToAddToLocalDate If a local date is parsed (YYYY-MM-dd), then this number of days will be added to that local date. + * @return The instant parsed from the given options + */ + public static Instant parseTime(String timeString, ZoneId zoneId, long daysToAddToLocalDate) { + try { + long dateMillis = Long.parseLong(timeString); + return Instant.ofEpochMilli(dateMillis); + } catch (NumberFormatException ignored) { + } + try { + LocalDate date = LocalDate.parse(timeString, DateTimeFormatter.ISO_LOCAL_DATE); + return date.plusDays(daysToAddToLocalDate).atStartOfDay(zoneId).toInstant(); + } catch (DateTimeParseException ignored) { + } + return Instant.parse(timeString); + } + + public static int analyze(String[] args) { + Options options = getOptions(); + + CommandLineParser parser = new DefaultParser(); + final CommandLine cmd; + try { + cmd = parser.parse(options, args); + } catch (ParseException e) { + System.err.println(e.getMessage()); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(System.err, false, Charset.defaultCharset()), formatter.getWidth(), "solarthing analyze", null, options, formatter.getLeftPadding(), formatter.getDescPadding(), null); + + return 1; + } + + String couchDbConfigurationFilePathString = cmd.getOptionValue("couchdb"); + String startString = cmd.getOptionValue("start"); + String endString = cmd.getOptionValue("end"); + String fragmentIdString = cmd.getOptionValue("fragment"); + + // TODO don't use system default ZoneId (maybe there's a better way of passing it in?) + ZoneId zoneId = ZoneId.systemDefault(); + Path couchDbConfig = Path.of(couchDbConfigurationFilePathString); + Instant startTime = parseTime(startString, zoneId, 0); + Instant endTime = parseTime(endString, zoneId, 1); + int fragmentId = Integer.parseInt(fragmentIdString); + + + DatabaseConfig databaseConfig; + try { + databaseConfig = ConfigUtil.readDatabaseConfig(couchDbConfig); + } catch (ConfigException e) { + e.printStackTrace(System.err); + System.err.println(e.getMessage()); + return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; + } + if(databaseConfig.getType() != CouchDbDatabaseSettings.TYPE){ + System.err.println("You must provide CouchDB options!"); + return SolarThingConstants.EXIT_CODE_INVALID_OPTIONS; + } + CouchDbDatabaseSettings couchDbDatabaseSettings = (CouchDbDatabaseSettings) databaseConfig.requireDatabaseSettings(); + SolarThingDatabase database = CouchDbSolarThingDatabase.create(CouchDbUtil.createInstance(couchDbDatabaseSettings.getCouchProperties(), couchDbDatabaseSettings.getOkHttpProperties())); + + AnalysisExecutor executor = new AnalysisExecutor<>( + database, + // TODO configure default instance options + new GeneratorRunAnalyzer(DefaultInstanceOptions.REQUIRE_NO_DEFAULTS, fragmentId), + Duration.ofHours(24), + Duration.ofHours(2), + Duration.ofHours(16) + ); + + System.out.println("Going to start analysis from " + startTime + " to " + endTime + " for fragment ID: " + fragmentId); + try { + List rows = executor.analyze(startTime, endTime); + for (GeneratorRunEntry entry : rows) { + System.out.println("Entry: Start: " + entry.startTime().atZone(zoneId) + " End: " + entry.endTime().atZone(zoneId) + " duration: " + Duration.between(entry.startTime(), entry.endTime())); + } + } catch (AnalysisException e) { + e.getCause().printStackTrace(System.err); + System.err.println(); + System.err.println(e.getMessage()); + return 1; + } + + return 0; + } + + private static @NotNull Options getOptions() { + Options options = new Options(); + + Option input = new Option(null, "couchdb", true, "CouchDB database configuration file path"); + input.setRequired(true); + options.addOption(input); + + Option startTime = new Option(null, "start", true, "The start time formatted as either YYYY-MM-dd or ISO 8601 time."); + startTime.setRequired(true); + options.addOption(startTime); + + Option endTime = new Option(null, "end", true, "The end time formatted as either YYYY-MM-dd or ISO 8601 time."); + endTime.setRequired(true); + options.addOption(endTime); + + Option fragmentId = new Option(null, "fragment", true, "The fragment ID that the FX(s) are associated with."); + fragmentId.setRequired(true); + options.addOption(fragmentId); + + return options; + } +} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/Analyzer.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/Analyzer.java new file mode 100644 index 00000000..bef90811 --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/Analyzer.java @@ -0,0 +1,40 @@ +package me.retrodaredevil.solarthing.program.subprogram.analyze; + +import java.time.Instant; +import java.util.List; + +public interface Analyzer { + /** + * An implementation should analyze the given {@link DataChunk} while taking the {@code processingStartInstant} and {@code processingEndInstant} constraints into account. + *

+ * {@code dataStartInstant <= processingStartInstant < processingEndInstant < dataEndInstant} must hold true when calling this method. + * If called incorrectly, undefined behavior may occur depending on the implementation. + *

+ *
+ *

+ * The simplest example of how an implementation may function and what the purpose of including data that is outside the processing window is that of a generator run analysis. + * Imagine that a generator run will never be longer than 12 hours at a time. + * When we ask an {@link Analyzer} to analyze many 24-hour periods, we may expect that a single generator run may span two different days (two different 24-hour periods). + * To account for this, we need to give the {@link Analyzer} about 36 hours worth of data. We then expect the implementation to only analyze and output + * generator runs that started within the processing window. + *

+ * This works great! We can now expect that analyzing generator runs that span two days will have all the data needed. + * Now, there's still another problem, what about when we try to analyze day 2 (the second day that the generator run in question spans)? + * A bad implementation will now count the tail end of that generator run as its own, shorter run! + * We account for this by giving the implementation data before the processing window and expect the implementation + * to disregard generator runs that started before the processing window. + *

+ * With data before and after the processing window, we expect implementations to be able to reliably count and analyze generator runs that span multiple days. + * Note that depending on the kind of analysis you want to do, data before and after the processing window may not be necessary. + * In some cases you may actually need to increase the data outside the processing window, or increase the data outside the processing window and the processing window itself. + * + * + * @param dataStartInstant The minimum (inclusive) instant which data may have been retrieved from + * @param processingStartInstant The minimum (inclusive) instant in time that data should start being processed at + * @param processingEndInstant The maximum (exclusive) instant in time that data should stop being processed at + * @param dataEndInstant The maximum (exclusive) instant which data may have been retrieved from + * @param data The chunk of data containing data between {@code dataStartInstant} and {@code dataEndInstant} + * @return A list of entries resulting from the analysis of data between {@code processingStartInstant} and {@code processingEndInstant} + */ + List analyze(Instant dataStartInstant, Instant processingStartInstant, Instant processingEndInstant, Instant dataEndInstant, DataChunk data); +} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/DataChunk.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/DataChunk.java new file mode 100644 index 00000000..68e1b0de --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/DataChunk.java @@ -0,0 +1,10 @@ +package me.retrodaredevil.solarthing.program.subprogram.analyze; + +import me.retrodaredevil.solarthing.packets.collection.StoredPacketGroup; + +import java.util.List; + +public record DataChunk( + List statusPackets, + List eventPackets +) {} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/GeneratorRunAnalyzer.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/GeneratorRunAnalyzer.java new file mode 100644 index 00000000..61fa3cc1 --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/GeneratorRunAnalyzer.java @@ -0,0 +1,103 @@ +package me.retrodaredevil.solarthing.program.subprogram.analyze.analyzers.generator; + +import me.retrodaredevil.solarthing.packets.collection.DefaultInstanceOptions; +import me.retrodaredevil.solarthing.packets.collection.InstancePacketGroup; +import me.retrodaredevil.solarthing.packets.collection.PacketGroups; +import me.retrodaredevil.solarthing.packets.collection.StoredPacketGroup; +import me.retrodaredevil.solarthing.program.subprogram.analyze.Analyzer; +import me.retrodaredevil.solarthing.program.subprogram.analyze.DataChunk; +import me.retrodaredevil.solarthing.program.subprogram.analyze.analyzers.generator.entry.GeneratorRunEntry; +import me.retrodaredevil.solarthing.solar.outback.fx.ACMode; +import me.retrodaredevil.solarthing.solar.outback.fx.FXStatusPacket; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * An analyzer that is designed to analyze individual generator runs and output statistics about each run. + */ +public class GeneratorRunAnalyzer implements Analyzer { + + private final DefaultInstanceOptions defaultInstanceOptions; + private final int fragmentId; + + public GeneratorRunAnalyzer(DefaultInstanceOptions defaultInstanceOptions, int fragmentId) { + this.defaultInstanceOptions = requireNonNull(defaultInstanceOptions); + this.fragmentId = fragmentId; + } + + /** + * Analyzes the row given data. + *

+ * Note that the implementation should copy any data necessary, as the caller may mutate the passed arguments after the call. + * @param packets A list of FX packets + * @return The {@link GeneratorRunEntry} created from the passed arguments + */ + private GeneratorRunEntry analyzeRow(List packets) { + var first = packets.get(0); + var last = packets.get(packets.size() - 1); + return new GeneratorRunEntry( + Instant.ofEpochMilli(first.getDateMillis()), + Instant.ofEpochMilli(last.getDateMillis()) + ); + } + + @Override + public List analyze(Instant dataStartInstant, Instant processingStartInstant, Instant processingEndInstant, Instant dataEndInstant, DataChunk data) { + long processingStartMillis = processingStartInstant.toEpochMilli(); + long processingEndMillis = processingEndInstant.toEpochMilli(); + List rows = new ArrayList<>(); + List packetGroups = new ArrayList<>(); + + boolean startedInsideRange = false; + boolean wasGeneratorOn = false; + + for (StoredPacketGroup storedPacketGroup : data.statusPackets()) { + InstancePacketGroup instancePacketGroup = PacketGroups.parseToInstancePacketGroup(storedPacketGroup, defaultInstanceOptions); + long dateMillis = instancePacketGroup.getDateMillis(); + + // TODO filtering out other fragment IDs is a great start for a POC, but we eventually need to do something like + // mergePackets because while we want to focus on this fragment ID, we also want data from other fragments. + if (instancePacketGroup.getFragmentId() != fragmentId) { + continue; + } + List fxStatusPackets = instancePacketGroup.getPackets().stream() + .filter(packet -> packet instanceof FXStatusPacket) + .map(packet -> (FXStatusPacket) packet) + .toList(); + if (fxStatusPackets.isEmpty()) { + System.err.println("[warning] No fx status packets found for dateMillis: " + dateMillis); + continue; + } + FXStatusPacket fx1 = fxStatusPackets.get(0); + boolean generatorOn = fx1.getACMode() != ACMode.NO_AC; + + boolean isInsideTimeRange = dateMillis >= processingStartMillis && dateMillis < processingEndMillis; + + if (generatorOn) { + if (!wasGeneratorOn) { + startedInsideRange = isInsideTimeRange; + } + packetGroups.add(instancePacketGroup); + } else { + if (wasGeneratorOn) { // generator just turned off + if (startedInsideRange) { + rows.add(analyzeRow(packetGroups)); + } + packetGroups.clear(); + } + + if (dateMillis >= processingEndMillis) { + break; + } + } + + wasGeneratorOn = generatorOn; + } + return rows; + } + +} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/entry/GeneratorFXStatistics.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/entry/GeneratorFXStatistics.java new file mode 100644 index 00000000..f2f09a5b --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/entry/GeneratorFXStatistics.java @@ -0,0 +1,29 @@ +package me.retrodaredevil.solarthing.program.subprogram.analyze.analyzers.generator.entry; + +import me.retrodaredevil.solarthing.solar.outback.OutbackIdentifier; + +/** + * Statistics over some period of time for a single FX. + * {@link me.retrodaredevil.solarthing.solar.outback.fx.FXStatusPacket} + * + * Depending on what was happening during the time period this represents (AC Use, AC Drop, Silent, Bulk, the entire generator run time), + * some properties may not be useful. + * Usually many of these properties become more useful for the shorter periods of time that are focused on. + * + * @param identifier + * @param averageChargerWattage + * @param averageBuyWattage + * @deprecated Not yet implemented + */ +@Deprecated +record GeneratorFXStatistics( + OutbackIdentifier identifier, + float averageInverterCurrent, + float averageInverterWattage, + + float averageChargerCurrent, + float averageChargerWattage, + + float averageBuyCurrent, + float averageBuyWattage +) {} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/entry/GeneratorRunEntry.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/entry/GeneratorRunEntry.java new file mode 100644 index 00000000..c315fc90 --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/entry/GeneratorRunEntry.java @@ -0,0 +1,22 @@ +package me.retrodaredevil.solarthing.program.subprogram.analyze.analyzers.generator.entry; + +import java.time.Instant; + + +/** + * Represents a single generator run and statistics associated with that run. + *

+ * TODO: + *

    + *
  • Standard deviation, percentiles (5th and 95th), duration of extremes, moving average
  • + *
  • Statistics on only the first hour of the run
  • + *
+ * + * @param startTime The time of the first packet showing the generator starting + * @param endTime The time of the last packet that the generator was running for + */ +public record GeneratorRunEntry( + Instant startTime, + Instant endTime +) { +} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/entry/GeneratorStatistics.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/entry/GeneratorStatistics.java new file mode 100644 index 00000000..7407b79d --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/generator/entry/GeneratorStatistics.java @@ -0,0 +1,16 @@ +package me.retrodaredevil.solarthing.program.subprogram.analyze.analyzers.generator.entry; + +import java.time.Instant; + +/** + * Represents statistics of some time period of a generator run, not necessarily the entire generator run + * + * @deprecated Not yet implemented + */ +@Deprecated +public record GeneratorStatistics( + Instant startTime, + Instant endTime, + float averageBatteryVoltage, + float average +) {} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/package-info.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/package-info.java new file mode 100644 index 00000000..ba3c6d25 --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/analyze/analyzers/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains implementations of {@link me.retrodaredevil.solarthing.program.subprogram.analyze.Analyzer} + */ +package me.retrodaredevil.solarthing.program.subprogram.analyze.analyzers; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/AutomationMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/automation/AutomationMain.java similarity index 96% rename from client/src/main/java/me/retrodaredevil/solarthing/program/AutomationMain.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/automation/AutomationMain.java index c06b0960..bf91d61f 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/AutomationMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/automation/AutomationMain.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.automation; import me.retrodaredevil.action.ActionMultiplexer; import me.retrodaredevil.action.Actions; @@ -12,6 +12,7 @@ import me.retrodaredevil.solarthing.SolarThingConstants; import me.retrodaredevil.solarthing.actions.environment.*; import me.retrodaredevil.solarthing.annotations.UtilityClass; +import me.retrodaredevil.solarthing.config.ConfigException; import me.retrodaredevil.solarthing.config.ConfigUtil; import me.retrodaredevil.solarthing.config.databases.implementations.CouchDbDatabaseSettings; import me.retrodaredevil.solarthing.config.options.AutomationProgramOptions; @@ -27,6 +28,9 @@ import me.retrodaredevil.solarthing.database.exception.SolarThingDatabaseException; import me.retrodaredevil.solarthing.packets.collection.FragmentedPacketGroup; import me.retrodaredevil.solarthing.packets.collection.StoredPacketGroup; +import me.retrodaredevil.solarthing.program.ActionNodeEntry; +import me.retrodaredevil.solarthing.program.ActionUtil; +import me.retrodaredevil.solarthing.program.PacketUtil; import me.retrodaredevil.solarthing.type.alter.StoredAlterPacket; import me.retrodaredevil.solarthing.type.closed.authorization.AuthorizationPacket; import me.retrodaredevil.solarthing.util.sync.BasicResourceManager; @@ -87,7 +91,7 @@ public static int startAutomation(List originalActionNodeEntrie final CouchDbDatabaseSettings couchSettings; try { couchSettings = ConfigUtil.expectCouchDbDatabaseSettings(options); - } catch (IllegalArgumentException ex) { + } catch (ConfigException ex) { LOGGER.error("(Fatal)", ex); return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/package-info.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/package-info.java new file mode 100644 index 00000000..3aee6f41 --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/package-info.java @@ -0,0 +1,4 @@ +/** + * Each package within this package represents a given subprogram and its specific implementation. + */ +package me.retrodaredevil.solarthing.program.subprogram; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/PVOutputConstants.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/PVOutputConstants.java similarity index 78% rename from client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/PVOutputConstants.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/PVOutputConstants.java index 573c5389..caee1623 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/PVOutputConstants.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/PVOutputConstants.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.pvoutput; +package me.retrodaredevil.solarthing.program.subprogram.pvoutput; import me.retrodaredevil.solarthing.annotations.UtilityClass; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/PVOutputHandler.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/PVOutputHandler.java similarity index 96% rename from client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/PVOutputHandler.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/PVOutputHandler.java index 3515dac0..2beead9a 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/PVOutputHandler.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/PVOutputHandler.java @@ -1,11 +1,11 @@ -package me.retrodaredevil.solarthing.program.pvoutput; +package me.retrodaredevil.solarthing.program.subprogram.pvoutput; import me.retrodaredevil.solarthing.packets.collection.FragmentedPacketGroup; import me.retrodaredevil.solarthing.packets.collection.PacketGroup; import me.retrodaredevil.solarthing.packets.identification.IdentifierFragment; -import me.retrodaredevil.solarthing.program.pvoutput.provider.DataProvider; -import me.retrodaredevil.solarthing.program.pvoutput.provider.TemperatureCelsiusProvider; -import me.retrodaredevil.solarthing.program.pvoutput.provider.VoltageProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.DataProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.TemperatureCelsiusProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.VoltageProvider; import me.retrodaredevil.solarthing.pvoutput.SimpleDate; import me.retrodaredevil.solarthing.pvoutput.SimpleTime; import me.retrodaredevil.solarthing.pvoutput.data.AddOutputParametersBuilder; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/PVOutputUploadMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/PVOutputUploadMain.java similarity index 98% rename from client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/PVOutputUploadMain.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/PVOutputUploadMain.java index e85d3412..130baf5d 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/PVOutputUploadMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/PVOutputUploadMain.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.pvoutput; +package me.retrodaredevil.solarthing.program.subprogram.pvoutput; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -15,12 +15,12 @@ import me.retrodaredevil.solarthing.database.exception.SolarThingDatabaseException; import me.retrodaredevil.solarthing.packets.collection.FragmentedPacketGroup; import me.retrodaredevil.solarthing.packets.collection.PacketGroup; -import me.retrodaredevil.solarthing.program.CommandOptions; +import me.retrodaredevil.solarthing.program.subprogram.run.CommandOptions; import me.retrodaredevil.solarthing.config.ConfigUtil; import me.retrodaredevil.solarthing.config.databases.DatabaseConfig; import me.retrodaredevil.solarthing.program.PacketUtil; -import me.retrodaredevil.solarthing.program.pvoutput.provider.TemperatureCelsiusProvider; -import me.retrodaredevil.solarthing.program.pvoutput.provider.VoltageProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.TemperatureCelsiusProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.VoltageProvider; import me.retrodaredevil.solarthing.pvoutput.CsvUtil; import me.retrodaredevil.solarthing.pvoutput.SimpleDate; import me.retrodaredevil.solarthing.pvoutput.data.AddBatchOutputParameters; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/AverageBatteryVoltageProvider.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/AverageBatteryVoltageProvider.java similarity index 93% rename from client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/AverageBatteryVoltageProvider.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/AverageBatteryVoltageProvider.java index c7ff84b8..2a541cc1 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/AverageBatteryVoltageProvider.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/AverageBatteryVoltageProvider.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.pvoutput.provider; +package me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonTypeName; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/DataProvider.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/DataProvider.java similarity index 94% rename from client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/DataProvider.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/DataProvider.java index 4107c7a3..0c7d885e 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/DataProvider.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/DataProvider.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.pvoutput.provider; +package me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider; import me.retrodaredevil.solarthing.annotations.Nullable; import me.retrodaredevil.solarthing.packets.collection.FragmentedPacketGroup; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/PacketTemperatureCelsiusProvider.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/PacketTemperatureCelsiusProvider.java similarity index 98% rename from client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/PacketTemperatureCelsiusProvider.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/PacketTemperatureCelsiusProvider.java index 0096f62b..0f2edd79 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/PacketTemperatureCelsiusProvider.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/PacketTemperatureCelsiusProvider.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.pvoutput.provider; +package me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/PacketVoltageProvider.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/PacketVoltageProvider.java similarity index 97% rename from client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/PacketVoltageProvider.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/PacketVoltageProvider.java index c1317480..42f599fa 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/PacketVoltageProvider.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/PacketVoltageProvider.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.pvoutput.provider; +package me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/TemperatureCelsiusProvider.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/TemperatureCelsiusProvider.java similarity index 83% rename from client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/TemperatureCelsiusProvider.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/TemperatureCelsiusProvider.java index 1c4b4d05..a5024075 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/TemperatureCelsiusProvider.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/TemperatureCelsiusProvider.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.pvoutput.provider; +package me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/VoltageProvider.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/VoltageProvider.java similarity index 84% rename from client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/VoltageProvider.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/VoltageProvider.java index fab97f12..202b199b 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/pvoutput/provider/VoltageProvider.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/pvoutput/provider/VoltageProvider.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.pvoutput.provider; +package me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/CommandOptions.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/CommandOptions.java similarity index 95% rename from client/src/main/java/me/retrodaredevil/solarthing/program/CommandOptions.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/CommandOptions.java index 66dc50d4..a97e007c 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/CommandOptions.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/CommandOptions.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import com.lexicalscope.jewel.cli.Option; import com.lexicalscope.jewel.cli.Unparsed; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/CommandUtil.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/CommandUtil.java similarity index 98% rename from client/src/main/java/me/retrodaredevil/solarthing/program/CommandUtil.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/CommandUtil.java index b1b9ddc8..c75a62b5 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/CommandUtil.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/CommandUtil.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.couchdb.CouchDbUtil; import me.retrodaredevil.couchdbjava.CouchDbInstance; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/MateCommandSender.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/MateCommandSender.java similarity index 97% rename from client/src/main/java/me/retrodaredevil/solarthing/program/MateCommandSender.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/MateCommandSender.java index 83f1daa4..4290fb30 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/MateCommandSender.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/MateCommandSender.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.solarthing.OnDataReceive; import me.retrodaredevil.solarthing.SolarThingConstants; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/OnMateCommandSent.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/OnMateCommandSent.java similarity index 96% rename from client/src/main/java/me/retrodaredevil/solarthing/program/OnMateCommandSent.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/OnMateCommandSent.java index 90804168..da8df6d0 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/OnMateCommandSent.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/OnMateCommandSent.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.solarthing.commands.command.OnCommandExecute; import me.retrodaredevil.solarthing.commands.command.SourcedCommand; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/OutbackMateMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/OutbackMateMain.java similarity index 86% rename from client/src/main/java/me/retrodaredevil/solarthing/program/OutbackMateMain.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/OutbackMateMain.java index 9d3784cd..a6d0c121 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/OutbackMateMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/OutbackMateMain.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.solarthing.SolarThingConstants; import me.retrodaredevil.solarthing.actions.command.EnvironmentUpdater; @@ -18,8 +18,10 @@ import me.retrodaredevil.solarthing.config.request.RequestObject; import me.retrodaredevil.solarthing.io.ReloadableIOBundle; import me.retrodaredevil.solarthing.misc.common.DataIdentifiablePacketListChecker; +import me.retrodaredevil.solarthing.packets.creation.TextPacketCreator; import me.retrodaredevil.solarthing.packets.handling.PacketListReceiver; import me.retrodaredevil.solarthing.packets.handling.PacketListReceiverMultiplexer; +import me.retrodaredevil.solarthing.packets.handling.RawPacketReceiver; import me.retrodaredevil.solarthing.packets.handling.implementations.TimedPacketReceiver; import me.retrodaredevil.solarthing.solar.DaySummaryLogListReceiver; import me.retrodaredevil.solarthing.solar.outback.FXStatusListUpdater; @@ -33,6 +35,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; import java.time.Duration; import java.util.*; import java.util.stream.Collectors; @@ -106,7 +111,7 @@ public static int connectMate(MateProgramOptions options, boolean isValidate) th return 0; } analyticsManager.sendStartUp(ProgramType.MATE); - return SolarMain.initReader( + return initReader( requireNonNull(ioBundle.getInputStream()), ioBundle::reload, new MatePacketCreator49(options.getIgnoreCheckSum()), @@ -127,4 +132,27 @@ public static int connectMate(MateProgramOptions options, boolean isValidate) th ); } } + + private static int initReader(InputStream in, Runnable reloadIO, TextPacketCreator packetCreator, RawPacketReceiver rawPacketReceiver) { + SolarReader solarReader = new SolarReader(in, packetCreator, rawPacketReceiver); + try { + while (!Thread.currentThread().isInterrupted()) { + try { + solarReader.update(); + } catch (EOFException e) { + return 0; + } catch (IOException e) { + LOGGER.error("Got IOException!", e); + Thread.sleep(500); + reloadIO.run(); + LOGGER.debug("Reloaded IO bundle"); + Thread.sleep(1000); + } + Thread.sleep(5); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return SolarThingConstants.EXIT_CODE_INTERRUPTED; + } } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerBundle.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketHandlerBundle.java similarity index 91% rename from client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerBundle.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketHandlerBundle.java index 4a2018ef..1118f4d4 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerBundle.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketHandlerBundle.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.solarthing.packets.handling.PacketHandler; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerInit.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketHandlerInit.java similarity index 97% rename from client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerInit.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketHandlerInit.java index adb6efba..c049a1ce 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerInit.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketHandlerInit.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import com.fasterxml.jackson.databind.ObjectMapper; import me.retrodaredevil.action.ActionMultiplexer; @@ -47,6 +47,8 @@ import me.retrodaredevil.solarthing.packets.handling.implementations.FileWritePacketHandler; import me.retrodaredevil.solarthing.packets.handling.implementations.JacksonStringPacketHandler; import me.retrodaredevil.solarthing.packets.handling.implementations.PostPacketHandler; +import me.retrodaredevil.solarthing.program.ActionNodeEntry; +import me.retrodaredevil.solarthing.program.ActionUtil; import me.retrodaredevil.solarthing.program.receiver.ActionNodeDataReceiver; import me.retrodaredevil.solarthing.program.receiver.RequestHeartbeatReceiver; import me.retrodaredevil.solarthing.reason.ExecutionReason; @@ -201,7 +203,7 @@ public static R .add(new TimeZoneEnvironment(options.getZoneId())) .add(new LatestPacketGroupEnvironment(latestPacketHandler::getLatestPacketCollection)) .add(new ExecutionReasonEnvironment(executionReason)) - .add(new EventReceiverEnvironment(PacketListReceiverHandlerBundle.createEventPacketListReceiverHandler(SolarMain.getSourceAndFragmentUpdater(options), options.getZoneId(), packetHandlerBundle))) + .add(new EventReceiverEnvironment(PacketListReceiverHandlerBundle.createEventPacketListReceiverHandler(RunMain.getSourceAndFragmentUpdater(options), options.getZoneId(), packetHandlerBundle))) ; EnvironmentUpdater environmentUpdater = environmentUpdaterSupplier.get(); if (environmentUpdater == null) { @@ -212,7 +214,7 @@ public static R ); PacketGroupReceiver mainPacketGroupReceiver = new PacketGroupReceiverMultiplexer(Arrays.asList( commandReceiver, - new RequestHeartbeatReceiver(PacketListReceiverHandlerBundle.createEventPacketListReceiverHandler(SolarMain.getSourceAndFragmentUpdater(options), options.getZoneId(), packetHandlerBundle)) + new RequestHeartbeatReceiver(PacketListReceiverHandlerBundle.createEventPacketListReceiverHandler(RunMain.getSourceAndFragmentUpdater(options), options.getZoneId(), packetHandlerBundle)) )); statusPacketHandlers.add((packetCollection) -> commandReceiver.getActionUpdater().update()); diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketListReceiverHandler.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketListReceiverHandler.java similarity index 98% rename from client/src/main/java/me/retrodaredevil/solarthing/program/PacketListReceiverHandler.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketListReceiverHandler.java index 8a892351..56b63a31 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketListReceiverHandler.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketListReceiverHandler.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.solarthing.packets.Packet; import me.retrodaredevil.solarthing.packets.collection.PacketCollection; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketListReceiverHandlerBundle.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketListReceiverHandlerBundle.java similarity index 94% rename from client/src/main/java/me/retrodaredevil/solarthing/program/PacketListReceiverHandlerBundle.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketListReceiverHandlerBundle.java index aebdcc91..bf0cad04 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketListReceiverHandlerBundle.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/PacketListReceiverHandlerBundle.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -68,7 +68,7 @@ public static PacketListReceiverHandler createEventPacketListReceiverHandler(Pac } public static PacketListReceiverHandlerBundle createFrom(T options, PacketHandlerBundle packetHandlerBundle, List statusPacketHandlers) { - PacketListReceiver sourceAndFragmentUpdater = SolarMain.getSourceAndFragmentUpdater(options); + PacketListReceiver sourceAndFragmentUpdater = RunMain.getSourceAndFragmentUpdater(options); PacketListReceiverHandler eventPacketListReceiverHandler = createEventPacketListReceiverHandler(sourceAndFragmentUpdater, options.getZoneId(), packetHandlerBundle); PacketListReceiverHandler statusPacketListReceiverHandler = new PacketListReceiverHandler( new PacketListReceiverMultiplexer( @@ -83,7 +83,7 @@ public static PacketListReceive } ), new PacketHandlerMultiplexer(statusPacketHandlers), - SolarMain.createIdGenerator(options.getUniqueIdsInOneHour(), options.isDocumentIdShort()), + RunMain.createIdGenerator(options.getUniqueIdsInOneHour(), options.isDocumentIdShort()), options.getZoneId() ); return new PacketListReceiverHandlerBundle(statusPacketListReceiverHandler, eventPacketListReceiverHandler); diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/RequestMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RequestMain.java similarity index 97% rename from client/src/main/java/me/retrodaredevil/solarthing/program/RequestMain.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RequestMain.java index 79cc51b2..03cc6786 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/RequestMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RequestMain.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.solarthing.SolarThingConstants; import me.retrodaredevil.solarthing.actions.command.EnvironmentUpdater; @@ -25,7 +25,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; public class RequestMain { private RequestMain() { throw new UnsupportedOperationException(); } @@ -49,7 +48,7 @@ private static int startRequestProgram(RequestProgramOptions options, AnalyticsM PacketListReceiverHandlerBundle bundle = handlersResult.getBundle(); List dataRequesterResults = options.getDataRequesterList().stream() .map(dataRequester -> dataRequester.create(new RequestObject(bundle.getEventHandler().getPacketListReceiverAccepter()))) - .collect(Collectors.toList()); + .toList(); List packetListReceiverList = new ArrayList<>(); List environmentUpdaters = new ArrayList<>(); diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/RoverMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RoverMain.java similarity index 97% rename from client/src/main/java/me/retrodaredevil/solarthing/program/RoverMain.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RoverMain.java index 96c4b874..68477119 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/RoverMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RoverMain.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.io.modbus.IOModbusSlaveBus; import me.retrodaredevil.io.modbus.ModbusSlaveBus; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/RoverSetupProgram.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RoverSetupProgram.java similarity index 99% rename from client/src/main/java/me/retrodaredevil/solarthing/program/RoverSetupProgram.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RoverSetupProgram.java index 0cb611e3..4ec96d88 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/RoverSetupProgram.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RoverSetupProgram.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.io.modbus.ModbusTimeoutException; import me.retrodaredevil.solarthing.annotations.Nullable; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RunMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RunMain.java new file mode 100644 index 00000000..c7e23a5c --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/RunMain.java @@ -0,0 +1,221 @@ +package me.retrodaredevil.solarthing.program.subprogram.run; + +import com.lexicalscope.jewel.cli.ArgumentValidationException; +import com.lexicalscope.jewel.cli.Cli; +import com.lexicalscope.jewel.cli.CliFactory; +import com.lexicalscope.jewel.cli.HelpRequestedException; +import me.retrodaredevil.couchdbjava.exception.CouchDbCodeException; +import me.retrodaredevil.couchdbjava.exception.CouchDbException; +import me.retrodaredevil.couchdbjava.response.ErrorResponse; +import me.retrodaredevil.solarthing.SolarThingConstants; +import me.retrodaredevil.solarthing.annotations.UtilityClass; +import me.retrodaredevil.solarthing.config.ConfigException; +import me.retrodaredevil.solarthing.config.ConfigUtil; +import me.retrodaredevil.solarthing.config.databases.DatabaseConfig; +import me.retrodaredevil.solarthing.config.databases.DatabaseSettings; +import me.retrodaredevil.solarthing.config.databases.implementations.CouchDbDatabaseSettings; +import me.retrodaredevil.solarthing.config.options.*; +import me.retrodaredevil.solarthing.packets.Packet; +import me.retrodaredevil.solarthing.packets.collection.HourIntervalPacketCollectionIdGenerator; +import me.retrodaredevil.solarthing.packets.collection.PacketCollectionIdGenerator; +import me.retrodaredevil.solarthing.packets.handling.PacketListReceiver; +import me.retrodaredevil.solarthing.packets.instance.InstanceFragmentIndicatorPackets; +import me.retrodaredevil.solarthing.packets.instance.InstanceSourcePackets; +import me.retrodaredevil.solarthing.program.JarUtil; +import me.retrodaredevil.solarthing.program.SolarThingEnvironment; +import me.retrodaredevil.solarthing.program.subprogram.automation.AutomationMain; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.PVOutputUploadMain; +import me.retrodaredevil.solarthing.program.subprogram.run.couchdb.CouchDbSetupMain; +import org.apache.logging.log4j.LogManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; +import java.util.Random; + +import static java.util.Objects.requireNonNull; + +@UtilityClass +public class RunMain { + private RunMain() { throw new UnsupportedOperationException(); } + + + private static final Logger LOGGER = LoggerFactory.getLogger(RunMain.class); + + /* + So you think this class is complicated? Well I'll have to agree with you on that. This is probably the most + complicated class in this project. However, that's a good thing. By having most of the complexity in this class, + we have less complexity in other classes. Most configuration of how the actual program works goes on in here. + I'm sure there's a better way to write this class. However the main advantage of it right now is that + it's pretty easy to change behaviour, and that's what's important. + */ + + // TODO Some of the methods in here could be moved to another file, as they aren't referenced from this file, but rather serve as utility methods + + + public static PacketListReceiver getSourceAndFragmentUpdater(PacketHandlingOption options){ + String source = options.getSourceId(); + int fragment = options.getFragmentId(); + requireNonNull(source); + Packet sourcePacket = InstanceSourcePackets.create(source); + Packet fragmentPacket = InstanceFragmentIndicatorPackets.create(fragment); + return (list) -> { + list.add(sourcePacket); + list.add(fragmentPacket); + }; + } + public static PacketCollectionIdGenerator createIdGenerator(Integer uniqueIdsInOneHour, boolean shortDocumentId){ + if(uniqueIdsInOneHour == null){ + return PacketCollectionIdGenerator.Defaults.UNIQUE_GENERATOR; + } + return new HourIntervalPacketCollectionIdGenerator(uniqueIdsInOneHour, new Random().nextInt(), shortDocumentId); + } + + + public static int doMainCommand(CommandOptions commandOptions, Path baseConfigFile) { + String user = System.getProperty("user.name"); + if (!"solarthing".equals(user) && !SolarThingEnvironment.isRunningInDocker()) { + if (user.equals("root")) { + LOGGER.warn("Running as root user!"); + System.out.println("\n\nHey! We noticed you are running as root! Instead of\n sudo ./run.sh\nPlease do\n sudo -u solarthing ./run.sh\n instead.\n"); + } else { + LOGGER.info("Running as " + user); + } + } + + LOGGER.info("Using base configuration file: " + baseConfigFile); + if (Files.notExists(baseConfigFile)) { + LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Base configuration file does not exist!"); + return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; + } + final ProgramOptions options; + try { + options = ConfigUtil.readConfig(baseConfigFile, ProgramOptions.class); + } catch (ConfigException e) { + LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Error while parsing ProgramOptions.", e); + LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "Hey! The error you got above might be scary, but this message might be helpful:\n\n" + e.getMessage()); + return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; + } + + // Note we used to have the creation of the .data directory here. We may consider adding it back in the future should we need it + + final ProgramType programType = options.getProgramType(); + try { + if(programType == ProgramType.MATE) { + return OutbackMateMain.connectMate((MateProgramOptions) options, commandOptions.isValidate()); + } else if(programType == ProgramType.ROVER_SETUP){ + return RoverMain.connectRoverSetup((RoverSetupProgramOptions) options, commandOptions.isValidate()); + } else if(programType == ProgramType.PVOUTPUT_UPLOAD){ + return PVOutputUploadMain.startPVOutputUpload((PVOutputUploadProgramOptions) options, commandOptions, commandOptions.isValidate()); + } else if(programType == ProgramType.REQUEST) { + return RequestMain.startRequestProgram((RequestProgramOptions) options, commandOptions.isValidate()); + } else if(programType == ProgramType.AUTOMATION) { + return AutomationMain.startAutomation((AutomationProgramOptions) options, commandOptions.isValidate()); + } + throw new AssertionError("Unknown program type... type=" + programType + " programOptions=" + options); + } catch (ConfigException e) { + String logMessage = "Ending SolarThing. " + getJarInfo(); + LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "[LOG] " + logMessage); + System.out.println("[stdout] " + logMessage); + System.err.println("[stderr] " + logMessage); + LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Got config exception", e); + LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "SolarThing is shutting down. This is caused by an error in your configuration or in how your environment is set up. Detailed message below:\n\n" + e.getUserMessage()); + LogManager.shutdown(); + return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; + } catch (Throwable t) { + boolean invalidJar = t instanceof ClassNotFoundException || t instanceof NoClassDefFoundError; + if (invalidJar) { + LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "We're about to give you an error with some technical stuff, but this error is likely caused by you switching out jar files while SolarThing is running. If it isn't, please report this error."); + } + boolean uncommonError = t instanceof UnsatisfiedLinkError; + if (uncommonError) { + LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "Got an UnsatisfiedLinkError which is uncommon. If setup correctly, after this crash program will relaunch (hopefully successfully)."); + } + String logMessage = "Ending SolarThing. " + getJarInfo(); + LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "[LOG] " + logMessage); + System.out.println("[stdout] " + logMessage); + System.err.println("[stderr] " + logMessage); + LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Got throwable", t); + LOGGER.debug("Going to shutdown LogManager."); + LogManager.shutdown(); // makes sure all buffered logs are flushed // this should be done automatically, but we'll do it anyway + System.err.println(); + t.printStackTrace(System.err); // print to stderr just in case logging isn't going well + if (invalidJar) { + return SolarThingConstants.EXIT_CODE_RESTART_NEEDED_JAR_UPDATED; + } + if (uncommonError) { + return SolarThingConstants.EXIT_CODE_RESTART_NEEDED_UNCOMMON_ERROR; + } + return SolarThingConstants.EXIT_CODE_CRASH; + } + } + + private static String getJarInfo() { + JarUtil.Data data = JarUtil.getData(); + Instant lastModified = data.getLastModifiedInstantOrNull(); + return "Jar: " + JarUtil.getJarFileName() + + " Last Modified: " + (lastModified == null ? "unknown" : lastModified.toString()) + + " Java version: " + System.getProperty("java.version"); + } + + + public static int runMain(String[] args){ + String logMessage = "Beginning main. " + getJarInfo(); + LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "[LOG] " + logMessage); + System.out.println("[stdout] " + logMessage); + System.err.println("[stderr] " + logMessage); + Cli cli = CliFactory.createCli(CommandOptions.class); + final CommandOptions commandOptions; + try { + commandOptions = cli.parseArguments(args); + } catch (ArgumentValidationException ex) { + System.out.println(cli.getHelpMessage()); + if (ex instanceof HelpRequestedException) { + return 0; + } + LOGGER.error(SolarThingConstants.SUMMARY_MARKER, ex.getMessage()); + LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "(Fatal)Incorrect args"); + return SolarThingConstants.EXIT_CODE_INVALID_OPTIONS; + } + if (commandOptions.getBaseConfigFile() != null) { + return doMainCommand(commandOptions, Path.of(commandOptions.getBaseConfigFile())); + } + if (commandOptions.getCouchDbSetupFile() != null) { + final DatabaseConfig config; + try { + config = ConfigUtil.readConfig(Path.of(commandOptions.getCouchDbSetupFile()), DatabaseConfig.class); + } catch (ConfigException e) { + e.printStackTrace(); + System.err.println("Problem reading CouchDB database settings file."); + return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; + } + DatabaseSettings settings = config.requireDatabaseSettings(); + if (!(settings instanceof CouchDbDatabaseSettings)) { + System.err.println("Must be CouchDB database settings!"); + return SolarThingConstants.EXIT_CODE_INVALID_CONFIG; + } + try { + return CouchDbSetupMain.createFrom((CouchDbDatabaseSettings) settings).doCouchDbSetupMain(); + } catch (CouchDbException e) { + if (e instanceof CouchDbCodeException) { + ErrorResponse error = ((CouchDbCodeException) e).getErrorResponse(); + if (error != null) { + System.err.println(error.getError()); + System.err.println(error.getReason()); + } + } + throw new RuntimeException(e); + } + } + List legacyArguments = commandOptions.getLegacyOptionsRaw(); + if (legacyArguments == null || legacyArguments.isEmpty()) { + System.err.println(cli.getHelpMessage()); + return SolarThingConstants.EXIT_CODE_INVALID_OPTIONS; + } + System.err.println("Invalid sub command: " + legacyArguments.get(0)); + return SolarThingConstants.EXIT_CODE_INVALID_OPTIONS; + } +} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/SecurityPacketReceiver.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/SecurityPacketReceiver.java similarity index 98% rename from client/src/main/java/me/retrodaredevil/solarthing/program/SecurityPacketReceiver.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/SecurityPacketReceiver.java index 47532f50..deb3b33e 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/SecurityPacketReceiver.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/SecurityPacketReceiver.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import com.fasterxml.jackson.core.Base64Variants; import com.fasterxml.jackson.core.JsonProcessingException; @@ -372,7 +372,7 @@ public boolean targets(TargetPacketGroup packetGroup, boolean isFromPayloadWithI LOGGER.debug(message); } if (packetSourceId.equals(InstanceSourcePacket.UNUSED_SOURCE_ID)) { - LOGGER.warn("Parsed to a target packet group with an unused source ID! dateMillis: " + packetGroup.getDateMillis()); + LOGGER.warn(SolarThingConstants.SUMMARY_MARKER, "Parsed to a target packet group with an unused source ID! dateMillis: " + packetGroup.getDateMillis()); } return false; } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/SolarReader.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/SolarReader.java similarity index 98% rename from client/src/main/java/me/retrodaredevil/solarthing/program/SolarReader.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/SolarReader.java index 9c3ccc03..db60ec0a 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/SolarReader.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/SolarReader.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program; +package me.retrodaredevil.solarthing.program.subprogram.run; import me.retrodaredevil.solarthing.SolarThingConstants; import me.retrodaredevil.solarthing.packets.Packet; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/couchdb/CouchDbSetupMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/couchdb/CouchDbSetupMain.java similarity index 99% rename from client/src/main/java/me/retrodaredevil/solarthing/program/couchdb/CouchDbSetupMain.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/couchdb/CouchDbSetupMain.java index 832cfc73..c773e39a 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/couchdb/CouchDbSetupMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/couchdb/CouchDbSetupMain.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.couchdb; +package me.retrodaredevil.solarthing.program.subprogram.run.couchdb; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/couchdb/CustomWmfCouchDbEdit20240318.java b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/couchdb/CustomWmfCouchDbEdit20240318.java similarity index 98% rename from client/src/main/java/me/retrodaredevil/solarthing/program/couchdb/CustomWmfCouchDbEdit20240318.java rename to client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/couchdb/CustomWmfCouchDbEdit20240318.java index 4e9dd451..a36e4a1c 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/couchdb/CustomWmfCouchDbEdit20240318.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/subprogram/run/couchdb/CustomWmfCouchDbEdit20240318.java @@ -1,4 +1,4 @@ -package me.retrodaredevil.solarthing.program.couchdb; +package me.retrodaredevil.solarthing.program.subprogram.run.couchdb; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; diff --git a/client/src/test/java/me/retrodaredevil/solarthing/integration/IntegrationSetup.java b/client/src/test/java/me/retrodaredevil/solarthing/integration/IntegrationSetup.java index 96fd7e7c..7a18c1f4 100644 --- a/client/src/test/java/me/retrodaredevil/solarthing/integration/IntegrationSetup.java +++ b/client/src/test/java/me/retrodaredevil/solarthing/integration/IntegrationSetup.java @@ -6,7 +6,7 @@ import me.retrodaredevil.solarthing.annotations.NotNull; import me.retrodaredevil.solarthing.annotations.Nullable; import me.retrodaredevil.solarthing.annotations.UtilityClass; -import me.retrodaredevil.solarthing.program.couchdb.CouchDbSetupMain; +import me.retrodaredevil.solarthing.program.subprogram.run.couchdb.CouchDbSetupMain; import java.io.OutputStream; import java.io.PrintStream; diff --git a/client/src/test/java/me/retrodaredevil/solarthing/program/pvoutput/provider/VoltageProviderTest.java b/client/src/test/java/me/retrodaredevil/solarthing/program/pvoutput/provider/VoltageProviderTest.java index 898c51db..8154fe3d 100644 --- a/client/src/test/java/me/retrodaredevil/solarthing/program/pvoutput/provider/VoltageProviderTest.java +++ b/client/src/test/java/me/retrodaredevil/solarthing/program/pvoutput/provider/VoltageProviderTest.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.AverageBatteryVoltageProvider; +import me.retrodaredevil.solarthing.program.subprogram.pvoutput.provider.VoltageProvider; import me.retrodaredevil.solarthing.util.JacksonUtil; import org.junit.jupiter.api.Test;