diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d21631a6..86330ce2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,6 +71,11 @@ General rule of thumb: * Why the custom NotNull/Nullable? We want good Kotlin support, and we want to be able to annotate more than just methods. * If something is exposed through GraphQL and it is not null, always put a `@NotNull` on it. +### Other Conventions +* Use `java.nio.file` rather than `java.io`. +* Use `this.myVariable = requireNonNull(myVariable)` over `requireNonNull(this.myVariable = myVariable)`. + There is legacy code with the ladder. + ### Branching If you are committing directly to the wildmountainfarms/solarthing project, you should be using branch names using the `my-branch-name` format. Unless you have access to commit to the solarthing project directly, diff --git a/action-lang/build.gradle b/action-lang/build.gradle index e69de29b..f8ecceec 100644 --- a/action-lang/build.gradle +++ b/action-lang/build.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' +} + +version = '0.0.1-SNAPSHOT' + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +dependencies { +} diff --git a/action-lang/src/main/java/me/retrodaredevil/action/lang/ActionLangUtil.java b/action-lang/src/main/java/me/retrodaredevil/action/lang/ActionLangUtil.java new file mode 100644 index 00000000..79a5d2b5 --- /dev/null +++ b/action-lang/src/main/java/me/retrodaredevil/action/lang/ActionLangUtil.java @@ -0,0 +1,55 @@ +package me.retrodaredevil.action.lang; + +import me.retrodaredevil.action.lang.translators.json.CustomNodeConfiguration; +import me.retrodaredevil.action.lang.translators.json.NodeConfiguration; +import me.retrodaredevil.action.lang.translators.json.SimpleNodeConfiguration; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class ActionLangUtil { + private ActionLangUtil() { throw new UnsupportedOperationException(); } + + public static final Map NODE_CONFIG_MAP; + static { + Map configMap = new HashMap<>(); + configMap.put("racer", CustomNodeConfiguration.RACER); + + SimpleNodeConfiguration.Builder builder = createDefaultNodeConfigurationBuilder(); + + // actions + configMap.put("race", builder.copy().subNodes("racers").build()); + configMap.put("scope", builder.copy().linkedNode("action").build()); + configMap.put("act", builder.copy().args("name").linkedNode("action").build()); + configMap.put("queue", builder.copy().subNodes("actions").build()); + configMap.put("parallel", builder.copy().subNodes("actions").build()); + configMap.put("print", builder.copy().args("message").linkedNode("expression").build()); + configMap.put("call", builder.copy().args("name").build()); + + configMap.put("init", builder.copy().args("name").linkedNode("expression").build()); + configMap.put("init-exp", builder.copy().args("name").linkedNode("expression").build()); + configMap.put("set", builder.copy().args("name").linkedNode("expression").build()); + configMap.put("set-exp", builder.copy().args("name").linkedNode("expression").build()); + + configMap.put("all", builder.copy().linkedNode("expression").build()); + configMap.put("any", builder.copy().linkedNode("expression").build()); + configMap.put("wait", builder.copy().args("duration").build()); + + // expressions + configMap.put("const", builder.copy().args("value").build()); + configMap.put("str", builder.copy().linkedNode("expression").build()); + configMap.put("ref", builder.copy().args("name").build()); + configMap.put("eval", builder.copy().args("name").build()); + configMap.put("join", builder.copy().linkedNode("expression").build()); + configMap.put("concat", builder.copy().subNodes("expressions").build()); + // TODO consider adding union operation and other set operations: https://www.math.net/union + + NODE_CONFIG_MAP = Collections.unmodifiableMap(configMap); + } + public static SimpleNodeConfiguration.Builder createDefaultNodeConfigurationBuilder() { + // we have the ability to modify the default builder in the future if we would like to + return SimpleNodeConfiguration.builder(); + } + +} diff --git a/action-lang/src/test/java/me/retrodaredevil/action/lang/ActionLangTest.java b/action-lang/src/test/java/me/retrodaredevil/action/lang/ActionLangTest.java index 06599877..b2b519a7 100644 --- a/action-lang/src/test/java/me/retrodaredevil/action/lang/ActionLangTest.java +++ b/action-lang/src/test/java/me/retrodaredevil/action/lang/ActionLangTest.java @@ -5,11 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import me.retrodaredevil.action.Action; import me.retrodaredevil.action.lang.antlr.NodeParser; -import me.retrodaredevil.action.lang.translators.json.CustomNodeConfiguration; import me.retrodaredevil.action.lang.translators.json.JsonNodeTranslator; -import me.retrodaredevil.action.lang.translators.json.NodeConfiguration; import me.retrodaredevil.action.lang.translators.json.SimpleConfigurationProvider; -import me.retrodaredevil.action.lang.translators.json.SimpleNodeConfiguration; import me.retrodaredevil.action.node.ActionNode; import me.retrodaredevil.action.node.environment.ActionEnvironment; import me.retrodaredevil.action.node.environment.InjectEnvironment; @@ -20,51 +17,11 @@ import java.io.IOException; import java.time.Duration; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; import static java.util.Objects.requireNonNull; public class ActionLangTest { private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final Map CONFIG_MAP; - static { - Map configMap = new HashMap<>(); - configMap.put("racer", CustomNodeConfiguration.RACER); - - // actions - configMap.put("race", new SimpleNodeConfiguration("type", emptyList(), emptyMap(), "racers", null)); - configMap.put("scope", new SimpleNodeConfiguration("type", emptyList(), emptyMap(), null, "action")); - configMap.put("act", new SimpleNodeConfiguration("type", Arrays.asList("name"), emptyMap(), null, "action")); - configMap.put("queue", new SimpleNodeConfiguration("type", emptyList(), emptyMap(), "actions", null)); - configMap.put("parallel", new SimpleNodeConfiguration("type", emptyList(), emptyMap(), "actions", null)); - configMap.put("print", new SimpleNodeConfiguration("type", Arrays.asList("message"), emptyMap(), null, "expression")); - configMap.put("log", new SimpleNodeConfiguration("type", Arrays.asList("message"), emptyMap(), null, null)); - configMap.put("call", new SimpleNodeConfiguration("type", Arrays.asList("name"), emptyMap(), null, null)); - - configMap.put("init", new SimpleNodeConfiguration("type", Arrays.asList("name"), emptyMap(), null, "expression")); - configMap.put("init-exp", new SimpleNodeConfiguration("type", Arrays.asList("name"), emptyMap(), null, "expression")); - configMap.put("set", new SimpleNodeConfiguration("type", Arrays.asList("name"), emptyMap(), null, "expression")); - configMap.put("set-exp", new SimpleNodeConfiguration("type", Arrays.asList("name"), emptyMap(), null, "expression")); - - configMap.put("all", new SimpleNodeConfiguration("type", emptyList(), emptyMap(), null, "expression")); - configMap.put("any", new SimpleNodeConfiguration("type", emptyList(), emptyMap(), null, "expression")); - configMap.put("wait", new SimpleNodeConfiguration("type", Arrays.asList("duration"), emptyMap(), null, null)); - - // expressions - configMap.put("const", new SimpleNodeConfiguration("type", Arrays.asList("value"), emptyMap(), null, null)); - configMap.put("str", new SimpleNodeConfiguration("type", emptyList(), emptyMap(), null, "expression")); - configMap.put("ref", new SimpleNodeConfiguration("type", Arrays.asList("name"), emptyMap(), null, null)); - configMap.put("eval", new SimpleNodeConfiguration("type", Arrays.asList("name"), emptyMap(), null, null)); - configMap.put("join", new SimpleNodeConfiguration("type", emptyList(), emptyMap(), null, "expression")); - configMap.put("union", new SimpleNodeConfiguration("type", emptyList(), emptyMap(), "expressions", null)); - CONFIG_MAP = Collections.unmodifiableMap(configMap); - } @Test void testCode() throws IOException { @@ -73,8 +30,8 @@ void testCode() throws IOException { )); NodeTranslator translator = new JsonNodeTranslator(new SimpleConfigurationProvider( - new SimpleNodeConfiguration("type", emptyList(), Collections.emptyMap(), null, null), - CONFIG_MAP + ActionLangUtil.createDefaultNodeConfigurationBuilder().build(), + ActionLangUtil.NODE_CONFIG_MAP )); System.out.println(translator.translate(node)); } @@ -90,8 +47,8 @@ private void runResource(String name) throws IOException { )); NodeTranslator translator = new JsonNodeTranslator(new SimpleConfigurationProvider( - new SimpleNodeConfiguration("type", emptyList(), Collections.emptyMap(), null, null), - CONFIG_MAP + ActionLangUtil.createDefaultNodeConfigurationBuilder().build(), + ActionLangUtil.NODE_CONFIG_MAP )); JsonNode json = translator.translate(node); System.out.println(json); diff --git a/action-lang/src/test/resources/test_code2.ns b/action-lang/src/test/resources/test_code2.ns index 7677a60c..216c2418 100644 --- a/action-lang/src/test/resources/test_code2.ns +++ b/action-lang/src/test/resources/test_code2.ns @@ -1,4 +1,6 @@ scope : queue { print "Hello there how are you?" - log("Cool message", summary = true); call someAction + // This code does not have to be run, just has to be parsed + // This code cannot contain SolarThing specific nodes + call someAction } diff --git a/action-lang/src/test/resources/test_code3.ns b/action-lang/src/test/resources/test_code3.ns index fff9af37..1e504e4d 100644 --- a/action-lang/src/test/resources/test_code3.ns +++ b/action-lang/src/test/resources/test_code3.ns @@ -22,10 +22,10 @@ scope : queue { init x : const 3 init y : ref x init-exp z : ref x - print : join : union { const "x: "; str : ref x } + print : join : concat { const "x: "; str : ref x } set x : const 4 - print : join : union { const "y: "; str : ref y } - print : join : union { const "z: "; str : ref z } + print : join : concat { const "y: "; str : ref y } + print : join : concat { const "z: "; str : ref z } call doSomething } diff --git a/action-node/src/main/java/me/retrodaredevil/action/node/convenient/WithLockActionNode.java b/action-node/src/main/java/me/retrodaredevil/action/node/convenient/WithLockActionNode.java index c33254f4..029b76be 100644 --- a/action-node/src/main/java/me/retrodaredevil/action/node/convenient/WithLockActionNode.java +++ b/action-node/src/main/java/me/retrodaredevil/action/node/convenient/WithLockActionNode.java @@ -33,15 +33,15 @@ public class WithLockActionNode implements ActionNode { private final ActionNode finallyAction; public WithLockActionNode( - @JsonProperty("name") String lockName, - @JsonProperty(value = "set") String lockSetName, - @JsonProperty("action") ActionNode action, + @JsonProperty(value = "name", required = true) String lockName, + @JsonProperty("set") String lockSetName, + @JsonProperty(value = "action", required = true) ActionNode action, @JsonProperty("timeout") ActionNode timeoutAction, @JsonProperty("ontimeout") ActionNode onTimeoutAction, @JsonProperty("finally") ActionNode finallyAction) { - requireNonNull(this.lockName = lockName); + this.lockName = requireNonNull(lockName); this.lockSetName = lockSetName; - requireNonNull(this.action = action); + this.action = requireNonNull(action); this.timeoutAction = timeoutAction == null ? PassActionNode.getInstance() : timeoutAction; this.onTimeoutAction = onTimeoutAction == null ? PassActionNode.getInstance() : onTimeoutAction; this.finallyAction = finallyAction == null ? PassActionNode.getInstance() : finallyAction; diff --git a/action-node/src/main/java/me/retrodaredevil/action/node/expression/Expression.java b/action-node/src/main/java/me/retrodaredevil/action/node/expression/Expression.java index ec6442ce..28ff4b2a 100644 --- a/action-node/src/main/java/me/retrodaredevil/action/node/expression/Expression.java +++ b/action-node/src/main/java/me/retrodaredevil/action/node/expression/Expression.java @@ -14,6 +14,8 @@ public interface Expression { * @return The known type of the expression or null. Null represents that the type is unknown until {@link #evaluate()} is called */ default ExpressionType getType() { + // TODO determine if this method is even needed. (When is it actually needed by something and when does it get in the way) + // As of 2023.02.02 we don't even implement it everywhere since null is a valid value in any scenario with the recent introduction of the ref implementation return null; } diff --git a/action-node/src/main/java/me/retrodaredevil/action/node/expression/node/UnionExpressionNode.java b/action-node/src/main/java/me/retrodaredevil/action/node/expression/node/ConcatExpressionNode.java similarity index 86% rename from action-node/src/main/java/me/retrodaredevil/action/node/expression/node/UnionExpressionNode.java rename to action-node/src/main/java/me/retrodaredevil/action/node/expression/node/ConcatExpressionNode.java index 74cc746d..632eb532 100644 --- a/action-node/src/main/java/me/retrodaredevil/action/node/expression/node/UnionExpressionNode.java +++ b/action-node/src/main/java/me/retrodaredevil/action/node/expression/node/ConcatExpressionNode.java @@ -13,12 +13,12 @@ import static java.util.Objects.requireNonNull; -@JsonTypeName("union") -public class UnionExpressionNode implements ExpressionNode { +@JsonTypeName("concat") +public class ConcatExpressionNode implements ExpressionNode { private final List expressionNodes; @JsonCreator - public UnionExpressionNode(@JsonProperty(value = "expressions", required = true) List expressionNodes) { + public ConcatExpressionNode(@JsonProperty(value = "expressions", required = true) List expressionNodes) { this.expressionNodes = requireNonNull(expressionNodes); } diff --git a/action-node/src/main/java/me/retrodaredevil/action/node/expression/node/ExpressionNode.java b/action-node/src/main/java/me/retrodaredevil/action/node/expression/node/ExpressionNode.java index f1c3fe08..30ab6b22 100644 --- a/action-node/src/main/java/me/retrodaredevil/action/node/expression/node/ExpressionNode.java +++ b/action-node/src/main/java/me/retrodaredevil/action/node/expression/node/ExpressionNode.java @@ -11,7 +11,7 @@ @JsonSubTypes.Type(VariableReferenceExpressionNode.class), @JsonSubTypes.Type(ToStringExpressionNode.class), @JsonSubTypes.Type(JoinStringExpressionNode.class), - @JsonSubTypes.Type(UnionExpressionNode.class), + @JsonSubTypes.Type(ConcatExpressionNode.class), }) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface ExpressionNode { diff --git a/build.gradle b/build.gradle index a77bbb26..b5d37f60 100644 --- a/build.gradle +++ b/build.gradle @@ -229,7 +229,7 @@ project(":common"){ apply plugin: 'java' dependencies { api project(":core") - api project(":action-node") + api project(":action-lang") annotationProcessor project(":process-annotations") } } @@ -240,7 +240,6 @@ project(":client"){ api project(":core") api project(":common") api project(":serviceapi") - api project(":action-node") annotationProcessor project(":process-annotations") } test { @@ -276,7 +275,6 @@ project(":graphql"){ api project(":core") api project(":common") api project(":serviceapi") - api project(":action-node") annotationProcessor project(":process-annotations") } } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/config/options/ActionConfig.java b/client/src/main/java/me/retrodaredevil/solarthing/config/options/ActionConfig.java new file mode 100644 index 00000000..db492faa --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/config/options/ActionConfig.java @@ -0,0 +1,52 @@ +package me.retrodaredevil.solarthing.config.options; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import me.retrodaredevil.solarthing.actions.config.ActionFormat; +import me.retrodaredevil.solarthing.actions.config.ActionReference; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +public final class ActionConfig { + public static final ActionConfig EMPTY = new ActionConfig(Collections.emptyList()); + private final List entries; + + @JsonCreator + public ActionConfig(@JsonProperty(value = "entries", required = true) List entries) { + this.entries = requireNonNull(entries); + } + + public List getEntries() { + return entries; + } + + public static final class Entry { + private final ActionReference actionReference; + private final boolean runOnce; + + @JsonCreator + public Entry( + @JsonProperty(value = "path", required = true) Path path, + @JsonProperty("format") ActionFormat format, + @JsonProperty("once") Boolean runOnce + ) { + this.actionReference = new ActionReference( + path, + format == null ? ActionFormat.NOTATION_SCRIPT : format + ); + this.runOnce = Boolean.TRUE.equals(runOnce); // by default false + } + + public ActionReference getActionReference() { + return actionReference; + } + + public boolean isRunOnce() { + return runOnce; + } + } +} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/config/options/ActionsOption.java b/client/src/main/java/me/retrodaredevil/solarthing/config/options/ActionsOption.java index 1b3560aa..5f2c8e5a 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/config/options/ActionsOption.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/config/options/ActionsOption.java @@ -8,5 +8,15 @@ * be executed each "iteration". */ public interface ActionsOption extends ProgramOptions { + /** + * A legacy configuration option that currently is still supported, but will be removed in a future version + */ List getActionNodeFiles(); + + /** + * This will not be null, but may be {@link ActionConfig#EMPTY} by default. + * + * @return The {@link ActionConfig}. + */ + ActionConfig getActionConfig(); } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/config/options/AutomationProgramOptions.java b/client/src/main/java/me/retrodaredevil/solarthing/config/options/AutomationProgramOptions.java index 5ed84250..da44d063 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/config/options/AutomationProgramOptions.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/config/options/AutomationProgramOptions.java @@ -6,6 +6,7 @@ import java.io.File; import java.time.Duration; +import java.util.Collections; import java.util.List; import static java.util.Objects.requireNonNull; @@ -14,7 +15,9 @@ @JsonExplicit public class AutomationProgramOptions extends DatabaseTimeZoneOptionBase implements ActionsOption { @JsonProperty("actions") - private List actionNodeFiles; + private List actionNodeFiles = Collections.emptyList(); + @JsonProperty("action_config") + private ActionConfig actionConfig = ActionConfig.EMPTY; @JsonProperty("period") private String periodDurationString = "PT5S"; @@ -26,8 +29,14 @@ public ProgramType getProgramType() { @Override public List getActionNodeFiles() { - return requireNonNull(actionNodeFiles); + return requireNonNull(actionNodeFiles, "You cannot supply a null value here!"); } + + @Override + public ActionConfig getActionConfig() { + return requireNonNull(actionConfig); + } + public long getPeriodMillis() { return Duration.parse(periodDurationString).toMillis(); } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/config/options/CommandConfig.java b/client/src/main/java/me/retrodaredevil/solarthing/config/options/CommandConfig.java index 0177a5e7..11424616 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/config/options/CommandConfig.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/config/options/CommandConfig.java @@ -2,47 +2,48 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import me.retrodaredevil.solarthing.SolarThingConstants; +import me.retrodaredevil.solarthing.actions.config.ActionFormat; +import me.retrodaredevil.solarthing.actions.config.ActionReference; import me.retrodaredevil.solarthing.commands.CommandInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.File; - -import static java.util.Objects.requireNonNull; +import java.nio.file.Path; public class CommandConfig { - private final String name; - private final String displayName; // default null - private final String description; // default empty - private final File actionFile; + private static final Logger LOGGER = LoggerFactory.getLogger(CommandConfig.class); + private final CommandInfo commandInfo; + private final ActionReference actionReference; @JsonCreator public CommandConfig( @JsonProperty(value = "name", required = true) String name, @JsonProperty(value = "display_name", required = true) String displayName, @JsonProperty(value = "description", required = true) String description, - @JsonProperty(value = "action", required = true) File actionFile) { - requireNonNull(this.name = name); - requireNonNull(this.displayName = displayName); - requireNonNull(this.description = description); - requireNonNull(this.actionFile = actionFile); - } - - public CommandInfo createCommandInfo() { - return new CommandInfo(name, displayName, description); + @JsonProperty(value = "action", required = true) Path actionPath, + @JsonProperty("format") ActionFormat format) { + this.commandInfo = new CommandInfo(name, displayName, description); + this.actionReference = new ActionReference(actionPath, inferFormat(actionPath, format)); } - - public String getName() { - return name; - } - - public String getDisplayName() { - return displayName; + private static ActionFormat inferFormat(Path actionPath, ActionFormat format) { + if (format == null) { + // This logic will allow us to default to NOTATION_SCRIPT, but to still support legacy configuration files + // We should be able to remove this logic in the future. + if (actionPath.getFileName().toString().endsWith(".json")) { + LOGGER.warn(SolarThingConstants.SUMMARY_MARKER, "(Deprecated) Implicitly inferring format=RAW_JSON for action path: '" + actionPath + "'. Please explicitly define format! Implicit definition for RAW_JSON is deprecated and will eventually be removed."); + return ActionFormat.RAW_JSON; + } + return ActionFormat.NOTATION_SCRIPT; + } + return format; } - public String getDescription() { - return description; + public CommandInfo getCommandInfo() { + return commandInfo; } - public File getActionFile() { - return actionFile; + public ActionReference getActionReference() { + return actionReference; } } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/config/options/CommandOption.java b/client/src/main/java/me/retrodaredevil/solarthing/config/options/CommandOption.java index c1cad114..44c72d5a 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/config/options/CommandOption.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/config/options/CommandOption.java @@ -1,10 +1,10 @@ package me.retrodaredevil.solarthing.config.options; +import me.retrodaredevil.solarthing.actions.config.ActionReference; import me.retrodaredevil.solarthing.annotations.NotNull; import me.retrodaredevil.solarthing.annotations.Nullable; import me.retrodaredevil.solarthing.commands.CommandInfo; -import java.io.File; import java.util.*; public interface CommandOption { @@ -17,10 +17,10 @@ public interface CommandOption { return commandConfigs; } - default Map getCommandFileMap() { - Map commandFileMap = new HashMap<>(); + default Map getCommandNameToActionReferenceMap() { + Map commandFileMap = new HashMap<>(); for (CommandConfig commandConfig : getDeclaredCommands()) { - commandFileMap.put(commandConfig.getName(), commandConfig.getActionFile()); + commandFileMap.put(commandConfig.getCommandInfo().getName(), commandConfig.getActionReference()); } return commandFileMap; } @@ -28,7 +28,7 @@ default List getCommandInfoList() { List commandConfigs = getDeclaredCommands(); List r = new ArrayList<>(commandConfigs.size()); for (CommandConfig commandConfig : commandConfigs) { - r.add(commandConfig.createCommandInfo()); + r.add(commandConfig.getCommandInfo()); } return r; } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/config/options/MateProgramOptions.java b/client/src/main/java/me/retrodaredevil/solarthing/config/options/MateProgramOptions.java index 043a9db5..77c123ad 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/config/options/MateProgramOptions.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/config/options/MateProgramOptions.java @@ -3,20 +3,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; -import me.retrodaredevil.solarthing.annotations.Nullable; import me.retrodaredevil.solarthing.util.IgnoreCheckSum; import java.io.File; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.Map; import static java.util.Objects.requireNonNull; @JsonTypeName("mate") @JsonIgnoreProperties("allow_commands") -public class MateProgramOptions extends PacketHandlingOptionBase implements IOBundleOption, AnalyticsOption, ProgramOptions, CommandOption, ActionsOption { +public class MateProgramOptions extends PacketHandlingOptionBase implements IOBundleOption, ProgramOptions { @JsonProperty("ignore_check_sum") private boolean ignoreCheckSum = false; @@ -29,21 +26,7 @@ public class MateProgramOptions extends PacketHandlingOptionBase implements IOBu @JsonProperty("fx_warning_ignore") private Map fxWarningIgnoreMap; - @JsonProperty("commands") - private List commandConfigs; - @JsonProperty(AnalyticsOption.PROPERTY_NAME) - private boolean isAnalyticsEnabled = AnalyticsOption.DEFAULT_IS_ANALYTICS_ENABLED; - - - @JsonProperty("actions") - private List actionNodeFiles = new ArrayList<>(); - - - @Override - public @Nullable List getDeclaredCommandsNullable() { - return commandConfigs; - } public boolean isIgnoreCheckSum() { return ignoreCheckSum; @@ -52,15 +35,6 @@ public boolean isCorrectCheckSum() { return correctCheckSum; } - @Override - public boolean isAnalyticsOptionEnabled() { - return isAnalyticsEnabled; - } - - @Override - public List getActionNodeFiles() { - return requireNonNull(actionNodeFiles, "You cannot use a null value for the actions property! Use an empty array or leave it undefined."); - } @Override public File getIOBundleFile() { diff --git a/client/src/main/java/me/retrodaredevil/solarthing/config/options/PacketHandlingOptionBase.java b/client/src/main/java/me/retrodaredevil/solarthing/config/options/PacketHandlingOptionBase.java index 7e31619e..5fff1e31 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/config/options/PacketHandlingOptionBase.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/config/options/PacketHandlingOptionBase.java @@ -7,11 +7,14 @@ import me.retrodaredevil.solarthing.config.request.DataRequester; import java.io.File; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static java.util.Objects.requireNonNull; + @SuppressWarnings("FieldCanBeLocal") -abstract class PacketHandlingOptionBase extends TimeZoneOptionBase implements PacketHandlingOption { +abstract class PacketHandlingOptionBase extends TimeZoneOptionBase implements PacketHandlingOption, ActionsOption, CommandOption, AnalyticsOption { // private static final Logger LOGGER = LoggerFactory.getLogger(PacketHandlingOptionBase.class); @JsonProperty @@ -29,8 +32,19 @@ abstract class PacketHandlingOptionBase extends TimeZoneOptionBase implements Pa @JsonProperty("request") private @Nullable List dataRequesterList; + @JsonProperty("commands") + private List commandConfigs; + + @JsonProperty(AnalyticsOption.PROPERTY_NAME) + private boolean isAnalyticsEnabled = AnalyticsOption.DEFAULT_IS_ANALYTICS_ENABLED; + + @JsonProperty("actions") + private List actionNodeFiles = new ArrayList<>(); + @JsonProperty("action_config") + private ActionConfig actionConfig = ActionConfig.EMPTY; + @Override - public @NotNull List getDatabaseConfigurationFiles() { + public final @NotNull List getDatabaseConfigurationFiles() { List r = databases; if(r == null){ return Collections.emptyList(); @@ -39,31 +53,50 @@ abstract class PacketHandlingOptionBase extends TimeZoneOptionBase implements Pa } @Override - public @NotNull String getSourceId() { + public final @NotNull String getSourceId() { return SourceIdValidator.validateSourceId(source); } @Override - public int getFragmentId() { + public final int getFragmentId() { return fragment; } @Override - public Integer getUniqueIdsInOneHour() { + public final Integer getUniqueIdsInOneHour() { return unique; } @Override - public boolean isDocumentIdShort() { + public final boolean isDocumentIdShort() { return isShortId; } @Override - public @NotNull List getDataRequesterList() { + public final @NotNull List getDataRequesterList() { List r = dataRequesterList; if (r == null) { return Collections.emptyList(); } return r; } + + @Override + public final @Nullable List getDeclaredCommandsNullable() { + return commandConfigs; + } + @Override + public final boolean isAnalyticsOptionEnabled() { + return isAnalyticsEnabled; + } + + @Override + public final List getActionNodeFiles() { + return requireNonNull(actionNodeFiles, "You cannot use a null value for the actions property! Use an empty array or leave it undefined."); + } + + @Override + public final ActionConfig getActionConfig() { + return requireNonNull(actionConfig); + } } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/config/options/RequestProgramOptionsBase.java b/client/src/main/java/me/retrodaredevil/solarthing/config/options/RequestProgramOptionsBase.java index 85733b59..0525e492 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/config/options/RequestProgramOptionsBase.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/config/options/RequestProgramOptionsBase.java @@ -1,18 +1,12 @@ package me.retrodaredevil.solarthing.config.options; import com.fasterxml.jackson.annotation.JsonProperty; -import me.retrodaredevil.solarthing.annotations.Nullable; -import java.io.File; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import static java.util.Objects.requireNonNull; -public abstract class RequestProgramOptionsBase extends PacketHandlingOptionBase implements AnalyticsOption, CommandOption, ActionsOption { - @JsonProperty(AnalyticsOption.PROPERTY_NAME) - private boolean isAnalyticsEnabled = AnalyticsOption.DEFAULT_IS_ANALYTICS_ENABLED; +public abstract class RequestProgramOptionsBase extends PacketHandlingOptionBase implements CommandOption, ActionsOption { // When defined as a Duration, Jackson will parse numbers as second values for the duration @JsonProperty("period") @@ -20,17 +14,6 @@ public abstract class RequestProgramOptionsBase extends PacketHandlingOptionBase @JsonProperty("minimum_wait") private Duration minimumWait = Duration.ofSeconds(1); - @JsonProperty("commands") - private List commandConfigs; - - @JsonProperty("actions") - private List actionNodeFiles = new ArrayList<>(); - - @Override - public boolean isAnalyticsOptionEnabled() { - return isAnalyticsEnabled; - } - public Duration getPeriod() { return requireNonNull(period); } @@ -38,13 +21,4 @@ public Duration getMinimumWait() { return requireNonNull(minimumWait); } - @Override - public @Nullable List getDeclaredCommandsNullable() { - return commandConfigs; - } - - @Override - public List getActionNodeFiles() { - return requireNonNull(actionNodeFiles, "You cannot use a null value for the actions property! Use an empty array or leave it undefined."); - } } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/ActionNodeEntry.java b/client/src/main/java/me/retrodaredevil/solarthing/program/ActionNodeEntry.java new file mode 100644 index 00000000..7e88f60e --- /dev/null +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/ActionNodeEntry.java @@ -0,0 +1,27 @@ +package me.retrodaredevil.solarthing.program; + +import me.retrodaredevil.action.node.ActionNode; + +import static java.util.Objects.requireNonNull; + +/** + * Represents an action node that should be run and created by the program periodically (or created only once). + * This may also contain "how" to run the action node or details such as if the action node should only be created once. + */ +public final class ActionNodeEntry { + private final ActionNode actionNode; + private final boolean runOnce; + + public ActionNodeEntry(ActionNode actionNode, boolean runOnce) { + this.actionNode = requireNonNull(actionNode); + this.runOnce = runOnce; + } + + public ActionNode getActionNode() { + return actionNode; + } + + public boolean isRunOnce() { + return runOnce; + } +} diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/ActionUtil.java b/client/src/main/java/me/retrodaredevil/solarthing/program/ActionUtil.java index a554d5cd..bcd952b4 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/ActionUtil.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/ActionUtil.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import me.retrodaredevil.action.node.ActionNode; +import me.retrodaredevil.solarthing.SolarThingConstants; import me.retrodaredevil.solarthing.actions.CommonActionUtil; import me.retrodaredevil.solarthing.actions.chatbot.WrappedSlackChatBotActionNode; import me.retrodaredevil.solarthing.actions.command.ExecutingCommandFeedbackActionNode; @@ -20,9 +21,15 @@ import me.retrodaredevil.solarthing.actions.tracer.TracerLoadActionNode; import me.retrodaredevil.solarthing.actions.tracer.modbus.TracerModbusActionNode; import me.retrodaredevil.solarthing.annotations.UtilityClass; +import me.retrodaredevil.solarthing.actions.config.ActionFormat; +import me.retrodaredevil.solarthing.actions.config.ActionReference; +import me.retrodaredevil.solarthing.config.options.ActionConfig; import me.retrodaredevil.solarthing.config.options.ActionsOption; import me.retrodaredevil.solarthing.config.options.CommandOption; import me.retrodaredevil.solarthing.util.JacksonUtil; +import org.jetbrains.annotations.Contract; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; @@ -34,9 +41,11 @@ @UtilityClass public final class ActionUtil { private ActionUtil() { throw new UnsupportedOperationException(); } + private static final Logger LOGGER = LoggerFactory.getLogger(ActionUtil.class); private static final ObjectMapper CONFIG_MAPPER = ActionUtil.registerActionNodes(JacksonUtil.defaultMapper()); + @Contract("null -> fail; _ -> param1") public static ObjectMapper registerActionNodes(ObjectMapper objectMapper) { objectMapper.registerSubtypes( MateCommandActionNode.class, @@ -66,22 +75,33 @@ public static ObjectMapper registerActionNodes(ObjectMapper objectMapper) { return CommonActionUtil.registerActionNodes(objectMapper); } - public static Map getActionNodeMap(ObjectMapper objectMapper, CommandOption options) throws IOException { + public static Map createCommandNameToActionNodeMap(CommandOption options) throws IOException { Map actionNodeMap = new HashMap<>(); - for (Map.Entry entry : options.getCommandFileMap().entrySet()) { + for (Map.Entry entry : options.getCommandNameToActionReferenceMap().entrySet()) { String name = entry.getKey(); - File file = entry.getValue(); - final ActionNode actionNode = objectMapper.readValue(file, ActionNode.class); - actionNodeMap.put(name, actionNode); + ActionReference actionReference = entry.getValue(); + actionNodeMap.put(name, CommonActionUtil.readActionReference(CONFIG_MAPPER, actionReference)); } return actionNodeMap; } - public static List getActionNodes(ActionsOption options) throws IOException { - List actionNodes = new ArrayList<>(); + public static List createActionNodeEntries(ActionsOption options) throws IOException { + List actionNodeEntries = new ArrayList<>(); for (File file : options.getActionNodeFiles()) { - actionNodes.add(CONFIG_MAPPER.readValue(file, ActionNode.class)); + // We hardcode RAW_JSON here because getActionNodeFiles() is a legacy configuration, + // so this for loop can be removed eventually in the future + ActionNode actionNode = CommonActionUtil.readActionReference(CONFIG_MAPPER, new ActionReference(file.toPath(), ActionFormat.RAW_JSON)); + actionNodeEntries.add(new ActionNodeEntry(actionNode, false)); } - return actionNodes; + if (!actionNodeEntries.isEmpty()) { + LOGGER.warn(SolarThingConstants.SUMMARY_MARKER, "(Deprecated) Please use action_config configuration instead of actions!"); + } + ActionConfig actionConfig = options.getActionConfig(); + for (ActionConfig.Entry entry : actionConfig.getEntries()) { + ActionNode actionNode = CommonActionUtil.readActionReference(CONFIG_MAPPER, entry.getActionReference()); + boolean runOnce = entry.isRunOnce(); + actionNodeEntries.add(new ActionNodeEntry(actionNode, runOnce)); + } + return actionNodeEntries; } } diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/AutomationMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/AutomationMain.java index c64ec328..320e9bd1 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/AutomationMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/AutomationMain.java @@ -2,7 +2,6 @@ import me.retrodaredevil.action.ActionMultiplexer; import me.retrodaredevil.action.Actions; -import me.retrodaredevil.action.node.ActionNode; import me.retrodaredevil.action.node.environment.ActionEnvironment; import me.retrodaredevil.action.node.environment.InjectEnvironment; import me.retrodaredevil.action.node.environment.NanoTimeProviderEnvironment; @@ -38,6 +37,8 @@ import java.io.IOException; import java.time.Clock; import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -49,7 +50,7 @@ public final class AutomationMain { private static final Logger LOGGER = LoggerFactory.getLogger(AutomationMain.class); public static int startAutomation(AutomationProgramOptions options) throws IOException { - return startAutomation(ActionUtil.getActionNodes(options), options, options.getPeriodMillis()); + return startAutomation(ActionUtil.createActionNodeEntries(options), options, options.getPeriodMillis()); } private static void queryAndFeed(MillisDatabase millisDatabase, ResourceManager databaseCacheManager, boolean useEndDate) { @@ -80,7 +81,7 @@ private static void queryAndFeed(MillisDatabase millisDatabase, ResourceManager< } @SuppressWarnings("InconsistentOverloads") - public static int startAutomation(List actionNodes, DatabaseTimeZoneOptionBase options, long periodMillis) { + public static int startAutomation(List originalActionNodeEntries, DatabaseTimeZoneOptionBase options, long periodMillis) { LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "Starting automation program."); final CouchDbDatabaseSettings couchSettings; try { @@ -123,6 +124,7 @@ public static int startAutomation(List actionNodes, DatabaseTimeZone .build(); ActionMultiplexer multiplexer = new Actions.ActionMultiplexerBuilder().build(); + List actionNodeEntries = new ArrayList<>(originalActionNodeEntries); // entries may be removed from this list while (!Thread.currentThread().isInterrupted()) { queryAndFeed(database.getStatusDatabase(), statusDatabaseCacheManager, true); queryAndFeed(database.getEventDatabase(), eventDatabaseCacheManager, true); @@ -145,8 +147,12 @@ public static int startAutomation(List actionNodes, DatabaseTimeZone FragmentedPacketGroup statusPacketGroup = statusPacketGroups.get(statusPacketGroups.size() - 1); latestPacketGroupReference.set(statusPacketGroup); } - for (ActionNode actionNode : actionNodes) { - multiplexer.add(actionNode.createAction(new ActionEnvironment(globalVariableEnvironment, injectEnvironment))); + for (Iterator iterator = actionNodeEntries.iterator(); iterator.hasNext(); ) { + ActionNodeEntry actionNodeEntry = iterator.next(); + multiplexer.add(actionNodeEntry.getActionNode().createAction(new ActionEnvironment(globalVariableEnvironment, injectEnvironment))); + if (actionNodeEntry.isRunOnce()) { + iterator.remove(); + } } multiplexer.update(); LOGGER.debug("There are " + multiplexer.getActiveActions().size() + " active actions"); diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/CouchDbSetupMain.java b/client/src/main/java/me/retrodaredevil/solarthing/program/CouchDbSetupMain.java index f4976841..5578a632 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/CouchDbSetupMain.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/CouchDbSetupMain.java @@ -188,6 +188,7 @@ public int doCouchDbSetupMain() throws CouchDbException { private static class UserEntry { @JsonProperty private final String name; + @SuppressWarnings("FieldCanBeStatic") @JsonProperty private final String type = "user"; @JsonProperty diff --git a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerInit.java b/client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerInit.java index 83d2dc2d..c8f92f89 100644 --- a/client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerInit.java +++ b/client/src/main/java/me/retrodaredevil/solarthing/program/PacketHandlerInit.java @@ -61,6 +61,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -73,7 +74,6 @@ public class PacketHandlerInit { private static final Logger LOGGER = LoggerFactory.getLogger(PacketHandlerInit.class); private static final ObjectMapper MAPPER = JacksonUtil.defaultMapper(); - private static final ObjectMapper CONFIG_MAPPER = ActionUtil.registerActionNodes(JacksonUtil.defaultMapper()); public static PacketHandlerBundle getPacketHandlerBundle(List configs, String uniqueStatusName, String uniqueEventName, String sourceId, int fragmentId){ List statusPacketHandlers = new ArrayList<>(); @@ -181,7 +181,7 @@ public static R LatestPacketHandler latestPacketHandler = new LatestPacketHandler(); // this is used to determine the state of the system when a command is requested statusPacketHandlers.add(latestPacketHandler); - Map actionNodeMap = ActionUtil.getActionNodeMap(CONFIG_MAPPER, options); + Map actionNodeMap = ActionUtil.createCommandNameToActionNodeMap(options); ActionNodeDataReceiver commandReceiver = new ActionNodeDataReceiver( actionNodeMap, (executionReason, injectEnvironmentBuilder) -> { @@ -225,12 +225,14 @@ public static R return new Result(bundle, updateCommandActions); } private static PacketHandler createActionExecutorPacketHandler(T options, Supplier environmentUpdaterSupplier) throws IOException { - List actionNodes = ActionUtil.getActionNodes(options); + requireNonNull(options); requireNonNull(environmentUpdaterSupplier); + List originalActionNodeEntries = ActionUtil.createActionNodeEntries(options); VariableEnvironment globalVariableEnvironment = new VariableEnvironment(); ActionMultiplexer multiplexer = new Actions.ActionMultiplexerBuilder().build(); + List actionNodeEntries = new ArrayList<>(originalActionNodeEntries); // entries may be removed from this list PacketCollection[] packetCollectionReference = new PacketCollection[] { null }; LatestPacketGroupEnvironment latestPacketGroupEnvironment = new LatestPacketGroupEnvironment(() -> requireNonNull(packetCollectionReference[0], "Using latestPacketGroupEnvironment before initializing packet collection!")); @@ -250,8 +252,12 @@ private static PacketHandler cr environmentUpdater.updateInjectEnvironment(executionReason, injectEnvironmentBuilder); InjectEnvironment injectEnvironment = injectEnvironmentBuilder.build(); - for (ActionNode actionNode : actionNodes) { - multiplexer.add(actionNode.createAction(new ActionEnvironment(globalVariableEnvironment, injectEnvironment))); + for (Iterator iterator = actionNodeEntries.iterator(); iterator.hasNext(); ) { + ActionNodeEntry actionNodeEntry = iterator.next(); + multiplexer.add(actionNodeEntry.getActionNode().createAction(new ActionEnvironment(globalVariableEnvironment, injectEnvironment))); + if (actionNodeEntry.isRunOnce()) { + iterator.remove(); + } } multiplexer.update(); }; diff --git a/client/src/test/java/me/retrodaredevil/solarthing/config/DeserializeTest.java b/client/src/test/java/me/retrodaredevil/solarthing/config/DeserializeTest.java index e615d9b2..7721ad02 100644 --- a/client/src/test/java/me/retrodaredevil/solarthing/config/DeserializeTest.java +++ b/client/src/test/java/me/retrodaredevil/solarthing/config/DeserializeTest.java @@ -11,6 +11,9 @@ import me.retrodaredevil.action.node.environment.VariableEnvironment; import me.retrodaredevil.io.serial.SerialConfigBuilder; import me.retrodaredevil.solarthing.FragmentedPacketGroupProvider; +import me.retrodaredevil.solarthing.actions.CommonActionUtil; +import me.retrodaredevil.solarthing.actions.config.ActionFormat; +import me.retrodaredevil.solarthing.actions.config.ActionReference; import me.retrodaredevil.solarthing.actions.environment.LatestFragmentedPacketGroupEnvironment; import me.retrodaredevil.solarthing.actions.environment.LatestPacketGroupEnvironment; import me.retrodaredevil.solarthing.config.databases.DatabaseSettings; @@ -34,8 +37,13 @@ import java.io.File; import java.io.FileFilter; import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.*; @@ -43,11 +51,13 @@ public class DeserializeTest { private static final File SOLARTHING_ROOT = new File(".."); // Working directory for tests are the /client folder private static final File BASE_CONFIG_DIRECTORY = new File(SOLARTHING_ROOT, "config_templates/base"); - private static final File ACTION_CONFIG_DIRECTORY = new File(SOLARTHING_ROOT, "config_templates/actions"); + private static final File ACTION_JSON_CONFIG_DIRECTORY = new File(SOLARTHING_ROOT, "config_templates/actions"); + private static final Path ACTION_NS_CONFIG_DIRECTORY = SOLARTHING_ROOT.toPath().resolve("config_templates/actions-ns"); private static final File DATABASE_CONFIG_DIRECTORY = new File(SOLARTHING_ROOT, "config_templates/databases"); private static final File IO_CONFIG_DIRECTORY = new File(SOLARTHING_ROOT, "config_templates/io"); private static final FileFilter JSON_FILTER = file -> file.getName().endsWith(".json"); + private static final DirectoryStream.Filter NOTATION_SCRIPT_FILTER = path -> path.getFileName().toString().endsWith(".ns"); private static final ObjectMapper MAPPER = ActionUtil.registerActionNodes(DatabaseSettingsUtil.registerDatabaseSettings(JacksonUtil.defaultMapper())); @@ -86,6 +96,14 @@ private File[] getJsonFiles(File directory) { assertTrue(files.length >= 3); return files; } + private List getNotationScriptFiles(Path directory) throws IOException { + List files = new ArrayList<>(); + try (DirectoryStream directoryStream = Files.newDirectoryStream(directory, NOTATION_SCRIPT_FILTER)) { + directoryStream.forEach(files::add); + } + assertTrue(files.size() >= 1); + return files; + } @Test void testAllBaseConfigs() { @@ -108,7 +126,7 @@ void testAllDatabases() { } } @Test - void testAllActions() { + void testJsonActions() { /* Also note that mattermost stuff was here, so we may not need it after it was removed on 2021.05.13 @@ -120,7 +138,7 @@ void testAllActions() { TODO in future, uncomment if https://github.com/FasterXML/jackson-databind/issues/3072 is implemented */ - for (File configFile : getJsonFiles(ACTION_CONFIG_DIRECTORY)) { + for (File configFile : getJsonFiles(ACTION_JSON_CONFIG_DIRECTORY)) { if (configFile.getName().equals("message_sender.json")) { // We cannot test this one because it tries to read the file "config/mattermost.json", and we currently don't have a mechanism to change that continue; @@ -133,6 +151,17 @@ void testAllActions() { } } @Test + void testNotationScriptActions() throws IOException { + for (Path notationScriptFile : getNotationScriptFiles(ACTION_NS_CONFIG_DIRECTORY)) { + ActionReference actionReference = new ActionReference(notationScriptFile, ActionFormat.NOTATION_SCRIPT); + try { + CommonActionUtil.readActionReference(MAPPER, actionReference); + } catch (IOException ex) { + fail("Failed parsing script: " + notationScriptFile, ex); + } + } + } + @Test void testAllIO() { ObjectMapper mapper = MAPPER.copy(); InjectableValues.Std iv = new InjectableValues.Std(); @@ -160,7 +189,7 @@ void testAllIO() { @Test void testRunAlertGeneratorOffWhileAuxOn() throws IOException, ParsePacketAsciiDecimalDigitException, CheckSumException { - File file = new File(ACTION_CONFIG_DIRECTORY, "alert_generator_off_while_aux_on.json"); + File file = new File(ACTION_JSON_CONFIG_DIRECTORY, "alert_generator_off_while_aux_on.json"); ActionNode actionNode = MAPPER.readValue(file, ActionNode.class); // We need to simulate an automation program environment to run this action Duration[] timeReference = new Duration [] { Duration.ZERO }; diff --git a/common/src/main/java/me/retrodaredevil/solarthing/actions/CommonActionUtil.java b/common/src/main/java/me/retrodaredevil/solarthing/actions/CommonActionUtil.java index 18d86543..f58a56ad 100644 --- a/common/src/main/java/me/retrodaredevil/solarthing/actions/CommonActionUtil.java +++ b/common/src/main/java/me/retrodaredevil/solarthing/actions/CommonActionUtil.java @@ -1,10 +1,21 @@ package me.retrodaredevil.solarthing.actions; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import me.retrodaredevil.action.lang.ActionLangUtil; +import me.retrodaredevil.action.lang.Node; +import me.retrodaredevil.action.lang.NodeTranslator; +import me.retrodaredevil.action.lang.antlr.NodeParser; +import me.retrodaredevil.action.lang.translators.json.JsonNodeTranslator; +import me.retrodaredevil.action.lang.translators.json.NodeConfiguration; +import me.retrodaredevil.action.lang.translators.json.SimpleConfigurationProvider; +import me.retrodaredevil.action.lang.translators.json.SimpleNodeConfiguration; import me.retrodaredevil.action.node.ActionNode; import me.retrodaredevil.action.node.expression.node.ExpressionNode; import me.retrodaredevil.solarthing.actions.command.FlagActionNode; import me.retrodaredevil.solarthing.actions.command.SendEncryptedActionNode; +import me.retrodaredevil.solarthing.actions.config.ActionFormat; +import me.retrodaredevil.solarthing.actions.config.ActionReference; import me.retrodaredevil.solarthing.actions.mate.ACModeActionNode; import me.retrodaredevil.solarthing.actions.mate.AuxStateActionNode; import me.retrodaredevil.solarthing.actions.mate.FXOperationalModeActionNode; @@ -12,11 +23,40 @@ import me.retrodaredevil.solarthing.annotations.UtilityClass; import me.retrodaredevil.solarthing.expression.BatteryVoltageExpressionNode; import me.retrodaredevil.solarthing.expression.NetChargeExpressionNode; +import org.antlr.v4.runtime.CharStreams; +import org.jetbrains.annotations.Contract; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; @UtilityClass public final class CommonActionUtil { private CommonActionUtil() { throw new UnsupportedOperationException(); } + private static final Logger LOGGER = LoggerFactory.getLogger(CommonActionUtil.class); + private static final NodeTranslator TRANSLATOR; + static { + Map configMap = new HashMap<>(ActionLangUtil.NODE_CONFIG_MAP); + SimpleNodeConfiguration.Builder builder = SimpleNodeConfiguration.builder(); + + configMap.put("log", builder.copy().args("message").build()); + configMap.put("with-lock", builder.copy().rename("withlock").args("name").linkedNode("action").build()); + configMap.put("aux-state", builder.copy().rename("auxstate").build()); + configMap.put("ac-mode", builder.copy().rename("acmode").build()); + + TRANSLATOR = new JsonNodeTranslator(new SimpleConfigurationProvider( + builder.build(), + configMap + )); + } + + + @Contract("null -> fail; _ -> param1") public static ObjectMapper registerActionNodes(ObjectMapper objectMapper) { objectMapper.registerSubtypes( ActionNode.class, @@ -42,5 +82,17 @@ public static ObjectMapper registerActionNodes(ObjectMapper objectMapper) { ); return objectMapper; } + public static ActionNode readActionReference(ObjectMapper objectMapper, ActionReference actionReference) throws IOException { + ActionFormat actionFormat = actionReference.getFormat(); + if (actionFormat == ActionFormat.RAW_JSON) { +// return objectMapper.readValue(actionReference.getFile(), ActionNode.class); + return objectMapper.readValue(Files.newInputStream(actionReference.getPath()), ActionNode.class); + } else if (actionFormat == ActionFormat.NOTATION_SCRIPT) { + Node node = NodeParser.parseFrom(CharStreams.fromPath(actionReference.getPath(), StandardCharsets.UTF_8)); + JsonNode jsonNode = TRANSLATOR.translate(node); + LOGGER.debug("Compiled notation script file to JSON (below). path: " + actionReference.getPath() + "\n" + jsonNode); + return objectMapper.treeToValue(jsonNode, ActionNode.class); + } else throw new AssertionError("Unknown ActionFormat: " + actionFormat); + } } diff --git a/common/src/main/java/me/retrodaredevil/solarthing/actions/LogActionNode.java b/common/src/main/java/me/retrodaredevil/solarthing/actions/LogActionNode.java index dc831acc..24a99426 100644 --- a/common/src/main/java/me/retrodaredevil/solarthing/actions/LogActionNode.java +++ b/common/src/main/java/me/retrodaredevil/solarthing/actions/LogActionNode.java @@ -17,6 +17,7 @@ public class LogActionNode implements ActionNode { private final boolean debug; private final boolean summary; + // TODO allow this to accept an expression similar to PrintActionNode public LogActionNode( @JsonProperty(value = "message", required = true) String message, @JsonProperty("debug") Boolean debug, diff --git a/common/src/main/java/me/retrodaredevil/solarthing/actions/config/ActionFormat.java b/common/src/main/java/me/retrodaredevil/solarthing/actions/config/ActionFormat.java new file mode 100644 index 00000000..dcf7cc40 --- /dev/null +++ b/common/src/main/java/me/retrodaredevil/solarthing/actions/config/ActionFormat.java @@ -0,0 +1,6 @@ +package me.retrodaredevil.solarthing.actions.config; + +public enum ActionFormat { + NOTATION_SCRIPT, + RAW_JSON, +} diff --git a/common/src/main/java/me/retrodaredevil/solarthing/actions/config/ActionReference.java b/common/src/main/java/me/retrodaredevil/solarthing/actions/config/ActionReference.java new file mode 100644 index 00000000..f9697c5e --- /dev/null +++ b/common/src/main/java/me/retrodaredevil/solarthing/actions/config/ActionReference.java @@ -0,0 +1,31 @@ +package me.retrodaredevil.solarthing.actions.config; + +import java.io.File; +import java.nio.file.Path; + +import static java.util.Objects.requireNonNull; + +/** + * Holds data necessary to construct an {@link me.retrodaredevil.action.node.ActionNode} such as the file and the format/language of this file. + */ +public final class ActionReference { + private final Path path; + private final ActionFormat format; + + public ActionReference(Path path, ActionFormat format) { + this.path = requireNonNull(path); + this.format = requireNonNull(format); + } + + @Deprecated + public File getFile() { + return path.toFile(); + } + public Path getPath() { + return path; + } + + public ActionFormat getFormat() { + return format; + } +} diff --git a/config_templates/actions-ns/alert_generator_off_while_aux_on.ns b/config_templates/actions-ns/alert_generator_off_while_aux_on.ns new file mode 100644 index 00000000..458a0447 --- /dev/null +++ b/config_templates/actions-ns/alert_generator_off_while_aux_on.ns @@ -0,0 +1,28 @@ +scope : queue { + act is_aux_on : aux-state(on=true) + act is_aux_off : aux-state(on=false) + act is_generator_on : ac-mode(mode="No AC", not=true) + act is_generator_off : ac-mode(mode="No AC") + //act is_generator_off : any(equals(const "No AC", select ac-mode : master-fx)) + act is_aux_on_and_generator_off : queue { + call is_aux_on + call is_generator_off + } + act is_aux_off_or_generator_on : race { + racer(call is_aux_off) : pass + racer(call is_generator_on) : pass + } + act send_alert : queue { + log "Aux is ON, but generator is not. Manually check generator for more info." + } + act continuous_check : with-lock "generator off while aux is on alert" : race { + racer(call is_aux_off_or_generator_on) : pass + racer(wait PT45S) : call send_alert + } + + act main : race { + racer(call is_aux_on_and_generator_off) : call continuous_check + racer(pass) : pass + } + call main +} diff --git a/config_templates/base/automation_eq_template.json b/config_templates/base/automation_eq_template.json index aac18b9d..940ad83f 100644 --- a/config_templates/base/automation_eq_template.json +++ b/config_templates/base/automation_eq_template.json @@ -2,7 +2,12 @@ "type": "automation", "database": "../config/couchdb.json", "source": "default", - "actions": [ - "config/actions/check_boost.json" - ] + "action_config": { + "entries": [ + { + "path": "config/actions/check_boost.json", + "format": "RAW_JSON" + } + ] + } } diff --git a/graphql/src/main/java/me/retrodaredevil/solarthing/rest/cache/CacheHandler.java b/graphql/src/main/java/me/retrodaredevil/solarthing/rest/cache/CacheHandler.java index 595f339d..4a68efd8 100644 --- a/graphql/src/main/java/me/retrodaredevil/solarthing/rest/cache/CacheHandler.java +++ b/graphql/src/main/java/me/retrodaredevil/solarthing/rest/cache/CacheHandler.java @@ -58,6 +58,7 @@ public class CacheHandler { new DefaultIdentificationCacheCreator<>(new FXAccumulationCacheNodeCreator()), new DefaultIdentificationCacheCreator<>(new BatteryRecordCacheNodeCreator()) ); + @SuppressWarnings("FieldCanBeStatic") // This is not static because there may be a time when we want to customize this value per object private final Duration duration = Duration.ofMinutes(15); private final ObjectMapper mapper; diff --git a/graphql/src/main/java/me/retrodaredevil/solarthing/rest/command/CommandHandler.java b/graphql/src/main/java/me/retrodaredevil/solarthing/rest/command/CommandHandler.java index 51034fd2..774f88b2 100644 --- a/graphql/src/main/java/me/retrodaredevil/solarthing/rest/command/CommandHandler.java +++ b/graphql/src/main/java/me/retrodaredevil/solarthing/rest/command/CommandHandler.java @@ -41,6 +41,7 @@ public boolean isAuthorized(String apiKey) { if (command == null) { return null; } + // TODO put some of this logic in the common module try { return CONFIG_MAPPER.readValue(command.getActionFile(), ActionNode.class); } catch (IOException e) { diff --git a/graphql/src/main/java/me/retrodaredevil/solarthing/rest/graphql/service/SolarThingGraphQLSolcastService.java b/graphql/src/main/java/me/retrodaredevil/solarthing/rest/graphql/service/SolarThingGraphQLSolcastService.java index 29fd0c6f..6cf0ef66 100644 --- a/graphql/src/main/java/me/retrodaredevil/solarthing/rest/graphql/service/SolarThingGraphQLSolcastService.java +++ b/graphql/src/main/java/me/retrodaredevil/solarthing/rest/graphql/service/SolarThingGraphQLSolcastService.java @@ -231,6 +231,7 @@ public SolarThingSolcastDayQuery(SolcastHandler handler, long to, ZoneId zoneId, } private static class SolcastHandler { + @SuppressWarnings("FieldCanBeLocal") private final SolcastService service; private final EstimatedActualCache cache; private final String sourceId; diff --git a/notation-script/build.gradle b/notation-script/build.gradle index 34fd682b..cbb12c5d 100644 --- a/notation-script/build.gradle +++ b/notation-script/build.gradle @@ -9,6 +9,7 @@ sourceCompatibility = 1.8 targetCompatibility = 1.8 dependencies { + // https://github.com/antlr/antlr4/releases antlr 'org.antlr:antlr4:4.11.1' // use ANTLR version 4 api "com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion" api "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" diff --git a/notation-script/src/main/antlr/me/retrodaredevil/actions/lang/antlr/Arithmetic.g4 b/notation-script/src/main/antlr/me/retrodaredevil/actions/lang/antlr/Arithmetic.g4 deleted file mode 100644 index 9f22d8d3..00000000 --- a/notation-script/src/main/antlr/me/retrodaredevil/actions/lang/antlr/Arithmetic.g4 +++ /dev/null @@ -1,49 +0,0 @@ -grammar Arithmetic; - -// Install this for good support: https://plugins.jetbrains.com/plugin/7358-antlr-v4 -// You may have to manually build the main module of this gradle module if you make changes in here - -@header { -package me.retrodaredevil.actions.lang.antlr; -} - -expression - : term (EXPRESSION_OP term)* - ; - -term - : factor (TERM_OP factor)* - ; - -factor - : '(' expression ')' - | NUMBER - ; - -EXPRESSION_OP - : (OP_PLUS | OP_MINUS) - ; -TERM_OP - : (OP_MULT | OP_DIV) - ; - -OP_PLUS - : '+' - ; -OP_MINUS - : '-' - ; -OP_MULT - : '*' - ; -OP_DIV - : '/' - ; - -NUMBER - : [0-9]+ ('.' [0-9]+)? - ; - -WS - : [ \t\r\n]+ -> skip - ; diff --git a/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/CustomNodeConfiguration.java b/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/CustomNodeConfiguration.java index c6bf6b50..e34b6b28 100644 --- a/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/CustomNodeConfiguration.java +++ b/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/CustomNodeConfiguration.java @@ -1,5 +1,6 @@ package me.retrodaredevil.action.lang.translators.json; public enum CustomNodeConfiguration implements NodeConfiguration { + // TODO move this action-lang logic to the action-lang module RACER, } diff --git a/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/JsonNodeTranslator.java b/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/JsonNodeTranslator.java index e1db6941..27113096 100644 --- a/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/JsonNodeTranslator.java +++ b/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/JsonNodeTranslator.java @@ -77,7 +77,7 @@ private JsonNode translateArgument(Argument argument) { } else if (argument instanceof BooleanArgument) { return BooleanNode.valueOf(((BooleanArgument) argument).getValue()); } else if (argument instanceof StringArgument) { - String rawValue = ((StringArgument) argument).getValue(); // TODO parse this correct if it begins with " + String rawValue = ((StringArgument) argument).getValue(); return TextNode.valueOf(rawValue); } else throw new AssertionError("Unknown argument type: " + argument.getClass().getName()); } @@ -92,12 +92,15 @@ public JsonNode translate(Node node) { } else if (config instanceof SimpleNodeConfiguration) { SimpleNodeConfiguration simpleNodeConfiguration = (SimpleNodeConfiguration) config; ObjectNode objectNode = new ObjectNode(JsonNodeFactory.instance); - objectNode.set(simpleNodeConfiguration.getIdentifierFieldKey(), new TextNode(node.getIdentifier())); + String typeName = simpleNodeConfiguration.getIdentifierFieldValueOverride() == null + ? node.getIdentifier() + : simpleNodeConfiguration.getIdentifierFieldValueOverride(); + objectNode.set(simpleNodeConfiguration.getIdentifierFieldKey(), new TextNode(typeName)); List positionalArgumentFieldNames = simpleNodeConfiguration.getPositionalArgumentFieldNames(); List positionalArguments = node.getPositionalArguments(); if (positionalArguments.size() > positionalArgumentFieldNames.size()) { // We check for too many positional arguments and let whatever parses the JSON decide if there is too few arguments - throw new IllegalArgumentException("Too many positional arguments!"); + throw new IllegalArgumentException("Too many positional arguments for type: " + typeName); } for (int i = 0; i < positionalArguments.size(); i++) { String fieldName = positionalArgumentFieldNames.get(i); diff --git a/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/SimpleNodeConfiguration.java b/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/SimpleNodeConfiguration.java index bd2c48e4..250a815e 100644 --- a/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/SimpleNodeConfiguration.java +++ b/notation-script/src/main/java/me/retrodaredevil/action/lang/translators/json/SimpleNodeConfiguration.java @@ -1,6 +1,7 @@ package me.retrodaredevil.action.lang.translators.json; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -10,12 +11,13 @@ public class SimpleNodeConfiguration implements NodeConfiguration { private final String identifierFieldKey; + private final String identifierFieldValueOverride; private final List positionalArgumentFieldNames; private final Map namedArgumentRenameMap; private final String subNodesFieldKey; private final String linkedNodeFieldKey; - public SimpleNodeConfiguration(String identifierFieldKey, List positionalArgumentFieldNames, Map namedArgumentRenameMap, String subNodesFieldKey, String linkedNodeFieldKey) { + public SimpleNodeConfiguration(String identifierFieldKey, String identifierFieldValueOverride, List positionalArgumentFieldNames, Map namedArgumentRenameMap, String subNodesFieldKey, String linkedNodeFieldKey) { if (positionalArgumentFieldNames.contains(identifierFieldKey)) { throw new IllegalArgumentException("The identifierFieldKey cannot be in positionalArgumentFieldNames"); } @@ -23,16 +25,23 @@ public SimpleNodeConfiguration(String identifierFieldKey, List positiona throw new IllegalArgumentException("The identifierFieldKey cannot be a value in namedArgumentRenameMap"); } this.identifierFieldKey = requireNonNull(identifierFieldKey); + this.identifierFieldValueOverride = identifierFieldValueOverride; this.positionalArgumentFieldNames = Collections.unmodifiableList(new ArrayList<>(positionalArgumentFieldNames)); this.namedArgumentRenameMap = Collections.unmodifiableMap(new HashMap<>(namedArgumentRenameMap)); this.subNodesFieldKey = subNodesFieldKey; this.linkedNodeFieldKey = linkedNodeFieldKey; } + public static Builder builder() { + return new Builder(); + } public String getIdentifierFieldKey() { return identifierFieldKey; } + public String getIdentifierFieldValueOverride() { + return identifierFieldValueOverride; + } public List getPositionalArgumentFieldNames() { return positionalArgumentFieldNames; @@ -49,4 +58,52 @@ public String getSubNodesFieldKey() { public String getLinkedNodeFieldKey() { return linkedNodeFieldKey; } + + public static final class Builder { + private String identifierFieldKey = "type"; + private String identifierFieldValueOverride = null; + private final List positionalArgumentFieldNames = new ArrayList<>(); + private final Map namedArgumentRenameMap = new HashMap<>(); + private String subNodesFieldKey = null; + private String linkedNodeFieldKey = null; + + public Builder copy() { + Builder newBuilder = new Builder(); + newBuilder.identifierFieldKey = identifierFieldKey; + newBuilder.identifierFieldValueOverride = identifierFieldValueOverride; + newBuilder.positionalArgumentFieldNames.addAll(positionalArgumentFieldNames); + newBuilder.namedArgumentRenameMap.putAll(namedArgumentRenameMap); + newBuilder.subNodesFieldKey = subNodesFieldKey; + newBuilder.linkedNodeFieldKey = linkedNodeFieldKey; + return newBuilder; + } + + public Builder identifierFieldKey(String identifierFieldKey) { + this.identifierFieldKey = requireNonNull(identifierFieldKey); + return this; + } + public Builder rename(String identifierFieldValueOverride) { + this.identifierFieldValueOverride = identifierFieldValueOverride; + return this; + } + public Builder args(String... positionalArguments) { + positionalArgumentFieldNames.addAll(Arrays.asList(positionalArguments)); + return this; + } + public Builder argRename(String sourceNamedArgument, String resultNamedArgument) { + namedArgumentRenameMap.put(sourceNamedArgument, resultNamedArgument); + return this; + } + public Builder subNodes(String subNodesFieldKey) { + this.subNodesFieldKey = subNodesFieldKey; + return this; + } + public Builder linkedNode(String linkedNodeFieldKey) { + this.linkedNodeFieldKey = linkedNodeFieldKey; + return this; + } + public SimpleNodeConfiguration build() { + return new SimpleNodeConfiguration(identifierFieldKey, identifierFieldValueOverride, positionalArgumentFieldNames, namedArgumentRenameMap, subNodesFieldKey, linkedNodeFieldKey); + } + } } diff --git a/other/docs/todo.md b/other/docs/todo.md index 7990b452..5e4b5876 100644 --- a/other/docs/todo.md +++ b/other/docs/todo.md @@ -120,6 +120,8 @@ and also react to messages that have been fully processed (request successfully * Allow jsonnet in configuration files * Alert to tell us if the revision ID is getting large on a particular document. This can help us know if the throttle factor is set too low on any SolarThing instance +* Use http://doc.forecast.solar/doku.php?id=api:estimate as alternative to solcast + * Also look at https://solargis.com/products/solar-power-forecast/overview ### Completed