diff --git a/buildSrc/src/main/groovy/eraser.java-ragconnect-conventions.gradle b/buildSrc/src/main/groovy/eraser.java-ragconnect-conventions.gradle index 83a2995fa0e7bdb5697925dd0c3cf79f9782007a..4b0438ad16bf4eade12df57fc008c3432a30c167 100644 --- a/buildSrc/src/main/groovy/eraser.java-ragconnect-conventions.gradle +++ b/buildSrc/src/main/groovy/eraser.java-ragconnect-conventions.gradle @@ -2,6 +2,12 @@ plugins { id 'eraser.java-jastadd-conventions' } +configurations { + ragconnect +} + dependencies { - compileOnly group: 'de.tudresden.inf.st', name: 'ragconnect', version: '0.3.1' + // TODO: use upstream ragconnect version once available +// ragconnect group: 'de.tudresden.inf.st', name: 'ragconnect', version: '1.0.0' + ragconnect fileTree(include: ['ragconnect.base-fatjar-1.0.1.jar'], dir: './libs') } diff --git a/eraser-base/build.gradle b/eraser-base/build.gradle index 760fc30fde2ee3ba569742d7edc6109f844b6e1c..c749833004d9487549536e9f0e72ee8f1deec54c 100644 --- a/eraser-base/build.gradle +++ b/eraser-base/build.gradle @@ -1,24 +1,38 @@ +// --- Buildscripts (must be at the top) --- buildscript { - repositories.mavenLocal() - repositories.mavenCentral() + repositories { + mavenCentral() + } dependencies { classpath group: 'org.jastadd', name: 'jastaddgradle', version: '1.13.3' } } +// --- Plugin definitions --- plugins { id 'eraser.java-application-conventions' id 'eraser.java-ragconnect-conventions' } +// --- Dependencies --- +repositories { + mavenCentral() + maven { + name 'gitlab-maven' + url 'https://git-st.inf.tu-dresden.de/api/v4/groups/jastadd/-/packages/maven' + } +} + configurations { + relast coverageGenClasspath } dependencies { - jastadd2 "org.jastadd:jastadd:2.3.5" + jastadd2 group: 'org.jastadd', name: 'jastadd2', version: "${jastadd_version}" coverageGenClasspath group: 'de.tudresden.inf.st.jastadd', name: 'coverage-generator', version: '0.0.4' + relast group: 'org.jastadd', name: 'relast', version: "${relast_version}" api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "${jackson_version}" api group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.16' implementation group: 'org.influxdb', name: 'influxdb-java', version: '2.20' @@ -29,7 +43,10 @@ dependencies { testImplementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: "${log4j_version}" } -mainClassName = 'de.tudresden.inf.st.eraser.Main' +// --- Preprocessors --- +File genSrc = file("src/gen/java") +sourceSets.main.java.srcDir genSrc +idea.module.generatedSourceDirs += genSrc def ragConnectRelastFiles = fileTree('src/main/jastadd/') { include '**/*.relast' }.toList().toArray() @@ -37,35 +54,40 @@ String[] ragconnectArguments = [ '--o=src/gen/jastadd', '--logReads', '--logWrites', + '--logIncremental', // '--verbose', '--rootNode=Root', -// '--experimental-jastadd-329', -// '--incremental=param', -// '--tracing=cache,flush', + '--experimental-jastadd-329', + '--incremental=param', + '--tracing=cache,flush', 'src/main/jastadd/shem.connect', + '--List=JastAddList', + '--protocols=mqtt,java', + '--evaluationCounter' ] task ragConnect(type: JavaExec) { group = 'Build' main = 'org.jastadd.ragconnect.compiler.Compiler' - classpath = configurations.compileOnly + classpath = configurations.ragconnect args ragconnectArguments + ragConnectRelastFiles } String[] relastArguments = [ - "libs/relast.jar", "--grammarName=./src/gen/jastadd/mainGen", "--useJastAddNames", "--jastAddList=JastAddList", "--resolverHelper", + "--serializer=jackson", "--file" ] String[] relastFiles = ragConnectRelastFiles.collect { new File(it.toString().replace('/main/', '/gen/')) } task preprocess(type: JavaExec) { group = 'Build' - main = "-jar" + classpath = configurations.relast + main = 'org.jastadd.relast.compiler.Compiler' args relastArguments + relastFiles inputs.files relastFiles @@ -74,10 +96,10 @@ task preprocess(type: JavaExec) { } String[] coverageGenArguments = [ - '--List=JastAddList', - '--printYaml', - '--inputBaseDir=src/gen/jastadd', - '--outputBaseDir=src/gen/jastadd' + '--List=JastAddList', + '--printYaml', + '--inputBaseDir=src/gen/jastadd', + '--outputBaseDir=src/gen/jastadd' ] task generateCoverage(type: JavaExec) { main = 'org.jastadd.preprocessor.coverage_gen.Main' @@ -86,6 +108,7 @@ task generateCoverage(type: JavaExec) { args coverageGenArguments + relastFiles } +// --- JastAdd --- jastadd { configureModuleBuild() modules { @@ -120,7 +143,13 @@ jastadd { } module = "eraser" - extraJastAddOptions = ["--lineColumnNumbers", "--List=JastAddList"] + extraJastAddOptions = [ + "--lineColumnNumbers", + "--List=JastAddList", + "--incremental=param,debug", + "--tracing=cache,flush", + "--cache=all" + ] astPackage = 'de.tudresden.inf.st.eraser.jastadd.model' genDir = 'src/gen/java' @@ -132,25 +161,30 @@ jastadd { parser.genDir = "src/gen/java/de/tudresden/inf/st/eraser/jastadd/parser" } -idea.module.generatedSourceDirs += file('src/gen/java') +// --- Tests --- +task newTests(type: Test, dependsOn: testClasses) { + description = 'Run test tagged with tag "New"' + group = 'verification' -sourceSets.main { - java { - srcDir 'src/gen/java' + useJUnitPlatform { + includeTags 'New' } } -cleanGen.doFirst { - delete "src/gen/jastadd" - delete "src/gen/java" -} +// --- Versioning and Publishing --- +mainClassName = 'de.tudresden.inf.st.eraser.Main' +// --- Task order --- preprocess.dependsOn ragConnect generateCoverage.dependsOn ragConnect generateAst.dependsOn preprocess generateAst.dependsOn generateCoverage generateAst.inputs.files file("./src/main/jastadd/mainGen.ast"), file("./src/main/jastadd/mainGen.jadd") -//compileJava.dependsOn jastadd -// //// always run jastadd //jastadd.outputs.upToDateWhen {false} + +// --- Misc --- +cleanGen.doFirst { + delete "src/gen/jastadd" + delete "src/gen/java" +} diff --git a/eraser-base/libs/ragconnect.base-fatjar-1.0.1.jar b/eraser-base/libs/ragconnect.base-fatjar-1.0.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..2f36a2b7ae01b43acc5083e6243f4bf7b18c1796 Binary files /dev/null and b/eraser-base/libs/ragconnect.base-fatjar-1.0.1.jar differ diff --git a/eraser-base/src/main/jastadd/Item.jrag b/eraser-base/src/main/jastadd/Item.jrag index 967cc061d56453edbe95dee5cc4eba3583eb8c5a..5e481dc5c6072e44a1d4dd2587be05179494110e 100644 --- a/eraser-base/src/main/jastadd/Item.jrag +++ b/eraser-base/src/main/jastadd/Item.jrag @@ -191,7 +191,11 @@ aspect ItemHandling { //--- setStateFromColor --- public abstract void Item.setStateFromColor(TupleHSB value); public void ColorItem.setStateFromColor(TupleHSB value) { - this.setState(value.clone()); + try { + this.setState(value.clone()); + } catch (CloneNotSupportedException e) { + // should not happen; + } } public void DateTimeItem.setStateFromColor(TupleHSB value) { // there is no good way here @@ -311,6 +315,24 @@ aspect ItemHandling { } } + //uncache Item.triggerStateUpdated(); + syn String Item.triggerStateUpdated() { + logger.debug("triggerStateUpdated"); + getStateAsString(); + stateUpdated(sendState); + return getStateAsString(); + } + //uncache NumberItem.triggerStateUpdated(); + eq NumberItem.triggerStateUpdated() { + logger.debug("triggerStateUpdated (number-item)"); + get_state(); + stateUpdated(sendState); + return getStateAsString(); + } + + syn KeyValuePair Item.getFoo() { + return new KeyValuePair().setKey("State").setValue(getStateAsString()); + } //--- sendState --- protected void Item.sendState() throws Exception { @@ -523,3 +545,63 @@ aspect ItemHandling { } + +aspect Types { + /** + * Value class comprising three integral values hue, saturation, brightness ranging from 0 to 255. + * + * @author rschoene - Initial contribution + */ + public class TupleHSB { + public static TupleHSB of(int hue, int saturation, int brightness) { + return new TupleHSB() + .setHue(hue % 360) + .setSaturation(ensureBetweenZeroAndHundred(saturation)) + .setBrightness(ensureBetweenZeroAndHundred(brightness)); + } + + private static int ensureBetweenZeroAndHundred(int value) { + return Math.max(0, Math.min(value, 100)); + } + + public TupleHSB withDifferentHue(int hue) { + return TupleHSB.of(hue, this.getSaturation(), this.getBrightness()); + } + + public TupleHSB withDifferentSaturation(int saturation) { + return TupleHSB.of(this.getHue(), saturation, this.getBrightness()); + } + + public TupleHSB withDifferentBrightness(int brightness) { + return TupleHSB.of(this.getHue(), this.getSaturation(), brightness); + } + + public String toString() { + return String.format("%s,%s,%s", getHue(), getSaturation(), getBrightness()); + } + + public static TupleHSB parse(String s) { + String[] tokens = s.split(","); + return of(Integer.parseInt(tokens[0]), Integer.parseInt(tokens[1]), Integer.parseInt(tokens[2])); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TupleHSB tupleHSB = (TupleHSB) o; + return getHue() == tupleHSB.getHue() && + getSaturation() == tupleHSB.getSaturation() && + getBrightness() == tupleHSB.getBrightness(); + } + + @Override + public int hashCode() { + return Objects.hash(getHue(), getSaturation(), getBrightness()); + } + } + + refine ASTNode public TupleHSB TupleHSB.clone() throws CloneNotSupportedException { + return TupleHSB.of(getHue(), getSaturation(), getBrightness()); + } +} diff --git a/eraser-base/src/main/jastadd/MachineLearning.relast b/eraser-base/src/main/jastadd/MachineLearning.relast index 3632637bab0056693ee33b9905b92ea9dc6ab39e..be46592c52e3ccf8f8d05f602d9781b80eb1d05e 100644 --- a/eraser-base/src/main/jastadd/MachineLearning.relast +++ b/eraser-base/src/main/jastadd/MachineLearning.relast @@ -3,7 +3,7 @@ MachineLearningRoot ::= [ActivityRecognition:MachineLearningModel] [PreferenceLe Activity ::= <Identifier:int> <Label:String> ; -abstract ChangeEvent ::= <Identifier:int> <Created:Instant> ChangedItem* ; +abstract ChangeEvent ::= <Identifier:int> <Created:java.time.Instant> ChangedItem* ; ChangedItem ::= <NewStateAsString:String> ; rel ChangedItem.Item -> Item ; @@ -19,12 +19,13 @@ rel MachineLearningModel.TargetItem* <-> Item.TargetInMachineLearningModel* ; ExternalMachineLearningModel : MachineLearningModel ; -abstract InternalMachineLearningModel : MachineLearningModel ::= <OutputApplication:DoubleDoubleFunction> ; +abstract InternalMachineLearningModel : MachineLearningModel ; +// excluded: <OutputApplication:DoubleDoubleFunction> MachineLearningResult ::= ItemUpdate* ; abstract ItemUpdate ::= ; rel ItemUpdate.Item -> Item ; -ItemUpdateColor : ItemUpdate ::= <NewHSB:TupleHSB> ; +ItemUpdateColor : ItemUpdate ::= NewHSB:TupleHSB ; ItemUpdateDouble : ItemUpdate ::= <NewValue:double> ; diff --git a/eraser-base/src/main/jastadd/NeuralNetwork.relast b/eraser-base/src/main/jastadd/NeuralNetwork.relast index 1214eb199e7ae39e9bae4754eabe6ee0fcd14ae3..95e7ed845c66b5a5518123e6c0ba014da4396f2e 100644 --- a/eraser-base/src/main/jastadd/NeuralNetwork.relast +++ b/eraser-base/src/main/jastadd/NeuralNetwork.relast @@ -1,7 +1,8 @@ // ---------------- Neural Network ------------------------------ NeuralNetworkRoot : InternalMachineLearningModel ::= InputNeuron* HiddenNeuron* OutputLayer ; -OutputLayer ::= OutputNeuron* <Combinator:DoubleArrayDoubleFunction> ; +OutputLayer ::= OutputNeuron* ; +// excluded: <Combinator:DoubleArrayDoubleFunction> rel OutputLayer.AffectedItem -> Item ; abstract Neuron ::= Output:NeuronConnection* ; @@ -12,7 +13,8 @@ rel NeuronConnection.Neuron <-> Neuron.Input* ; InputNeuron : Neuron ; rel InputNeuron.Item -> Item ; -HiddenNeuron : Neuron ::= <ActivationFormula:DoubleArrayDoubleFunction> ; +HiddenNeuron : Neuron ; +// excluded: <ActivationFormula:DoubleArrayDoubleFunction> BiasNeuron : HiddenNeuron ; OutputNeuron : HiddenNeuron ::= <Label:String> ; diff --git a/eraser-base/src/main/jastadd/Rules.relast b/eraser-base/src/main/jastadd/Rules.relast index e6c3ab0138742149407d3a6279a958c2cc551ee4..72adc1f71e9a8d626e9eac16c34b75149244ac3e 100644 --- a/eraser-base/src/main/jastadd/Rules.relast +++ b/eraser-base/src/main/jastadd/Rules.relast @@ -7,7 +7,8 @@ rel ItemStateChangeCondition.Item -> Item; ExpressionCondition : Condition ::= LogicalExpression ; abstract Action ; NoopAction : Action ; -LambdaAction : Action ::= <Lambda:Action2EditConsumer> ; +LambdaAction : Action ::= ; +// excluded: <Lambda:Action2EditConsumer> TriggerRuleAction : Action ; rel TriggerRuleAction.Rule -> Rule ; @@ -18,10 +19,12 @@ rel SetStateAction.AffectedItem -> Item ; SetStateFromExpression : SetStateAction ::= NumberExpression ; SetStateFromConstantStringAction : SetStateAction ::= <NewState:String> ; -SetStateFromLambdaAction : SetStateAction ::= <NewStateProvider:NewStateProvider> ; +SetStateFromLambdaAction : SetStateAction ; +// excluded: <NewStateProvider:NewStateProvider> SetStateFromTriggeringItemAction : SetStateAction ::= ; -SetStateFromItemsAction : SetStateAction ::= <Combinator:ItemsToStringFunction> ; +SetStateFromItemsAction : SetStateAction ; +// excluded: <Combinator:ItemsToStringFunction> rel SetStateFromItemsAction.SourceItem* -> Item ; AddDoubleToStateAction : SetStateAction ::= <Increment:double> ; diff --git a/eraser-base/src/main/jastadd/SerializationExclusion.jadd b/eraser-base/src/main/jastadd/SerializationExclusion.jadd new file mode 100644 index 0000000000000000000000000000000000000000..289383ea6ed228f71f645cc8fe2a2c4560678282 --- /dev/null +++ b/eraser-base/src/main/jastadd/SerializationExclusion.jadd @@ -0,0 +1,34 @@ +aspect SerializationExclusion { + // add additional information to types of the AST not directly being part of it (excluding them from serialization) + class LambdaAction { + Action2EditConsumer _Lambda; + public Action2EditConsumer getLambda() { return _Lambda; } + public LambdaAction setLambda(Action2EditConsumer value) { _Lambda = value; return this; } + } + + class SetStateFromLambdaAction { + NewStateProvider _NewStateProvider; + public NewStateProvider getNewStateProvider() { return _NewStateProvider; } + public SetStateFromLambdaAction setNewStateProvider(NewStateProvider value) { _NewStateProvider = value; return this; } + } + class SetStateFromItemsAction { + ItemsToStringFunction _Combinator; + public ItemsToStringFunction getCombinator() { return _Combinator; } + public SetStateFromItemsAction setCombinator(ItemsToStringFunction value) { _Combinator = value; return this; } + } + class InternalMachineLearningModel { + DoubleDoubleFunction _OutputApplication; + public DoubleDoubleFunction getOutputApplication() { return _OutputApplication; } + public InternalMachineLearningModel setOutputApplication(DoubleDoubleFunction value) { _OutputApplication = value; return this; } + } + class OutputLayer { + DoubleArrayDoubleFunction _Combinator; + public DoubleArrayDoubleFunction getCombinator() { return _Combinator; } + public OutputLayer setCombinator(DoubleArrayDoubleFunction value) { _Combinator = value; return this; } + } + class HiddenNeuron { + DoubleArrayDoubleFunction _ActivationFormula; + public DoubleArrayDoubleFunction getActivationFormula() { return _ActivationFormula; } + public HiddenNeuron setActivationFormula(DoubleArrayDoubleFunction value) { _ActivationFormula = value; return this; } + } +} diff --git a/eraser-base/src/main/jastadd/mqtt.jrag b/eraser-base/src/main/jastadd/mqtt.jrag index f96d5a54cfdc5a8e77e11fd34f1f42cd2154fe03..1d99dabbd76ea0f236ff8c48036556a2b514d1be 100644 --- a/eraser-base/src/main/jastadd/mqtt.jrag +++ b/eraser-base/src/main/jastadd/mqtt.jrag @@ -17,7 +17,7 @@ aspect MQTT { syn ExternalHost MqttRoot.getHost() = new ExternalHost(); // --- connectAllItems --- - public boolean SmartHomeEntityModel.connectAllItems() throws IOException { + public boolean SmartHomeEntityModel.connectAllItems() throws java.io.IOException { MqttRoot mqttRoot = getRoot().getMqttRoot(); ExternalHost host = mqttRoot.getHost(); // TODO user/password not used yet (not supported by ragconnect yet) @@ -26,34 +26,38 @@ aspect MQTT { boolean success = true; for (Item item : this.items()) { String suffix = item.getTopicString().isBlank() ? item.getID() : item.getTopicString(); + getRoot().ragconnectJavaRegisterConsumer(suffix, bytes -> { + String state = new String(bytes, java.nio.charset.StandardCharsets.UTF_8); + item.stateUpdated(item.sendState); + System.out.println("java handler activated for " + item.getID() + " with '" + state + "'"); + }); ConnectReceive connectReceive; - ConnectSend connectSend; if (item.isItemWithDoubleState()) { connectReceive = item.asItemWithDoubleState()::connect_state; - connectSend = item.asItemWithDoubleState()::connect_state; } else if (item.isItemWithBooleanState()) { connectReceive = item.asItemWithBooleanState()::connect_state; - connectSend = item.asItemWithBooleanState()::connect_state; } else if (item.isItemWithStringState()) { connectReceive = item.asItemWithStringState()::connect_state; - connectSend = item.asItemWithStringState()::connect_state; } else { // unsupported item type continue; } success &= connectReceive.apply(prefix + mqttRoot.getIncomingPrefix() + suffix) & - connectSend.apply(prefix + mqttRoot.getOutgoingPrefix() + suffix, false); + item.connectTriggerStateUpdated("java://localhost/" + suffix, true) & + item.connectTriggerStateUpdated("mqtt://localhost/trigger/" + suffix, true) & + item.connectFoo("mqtt://localhost/foo/" + suffix, true) + ; } return success; } class SmartHomeEntityModel { interface ConnectReceive { - boolean apply(String uriString) throws IOException; + boolean apply(String uriString) throws java.io.IOException; } interface ConnectSend { - boolean apply(String uriString, boolean writeCurrentValue) throws IOException; + boolean apply(String uriString, boolean writeCurrentValue) throws java.io.IOException; } } } diff --git a/eraser-base/src/main/jastadd/shem.connect b/eraser-base/src/main/jastadd/shem.connect index 1f3f06ec92822ccea0104425b51550d2f6f73ba6..0a149718b3a676a75fd897d45df1801de95ae12d 100644 --- a/eraser-base/src/main/jastadd/shem.connect +++ b/eraser-base/src/main/jastadd/shem.connect @@ -1,6 +1,17 @@ receive ItemWithDoubleState._state using StringToDouble ; -send ItemWithDoubleState._state using DoubleToString ; +//send ItemWithDoubleState._state using DoubleToString ; +receive ItemWithBooleanState._state using StringToBoolean ; +send ItemWithBooleanState._state using BooleanToString ; + +receive ItemWithStringState._state ; +//send ItemWithStringState._state ; + +send Item.triggerStateUpdated(String); + +send Item.Foo; + +// Mappings StringToDouble maps String s to double {: return Double.parseDouble(s); :} @@ -9,9 +20,6 @@ DoubleToString maps double d to String {: return Double.toString(d); :} -receive ItemWithBooleanState._state using StringToBoolean ; -send ItemWithBooleanState._state using BooleanToString ; - StringToBoolean maps String s to boolean {: return Boolean.parseBoolean(s); :} @@ -19,6 +27,3 @@ StringToBoolean maps String s to boolean {: BooleanToString maps boolean b to String {: return Boolean.toString(b); :} - -receive ItemWithStringState._state ; -send ItemWithStringState._state ; diff --git a/eraser-base/src/main/jastadd/shem.jrag b/eraser-base/src/main/jastadd/shem.jrag index 030cf917bb221a93739effce1aef8b12f2a36ba3..abbfd07751887f5955b32eb0b45b779d1cbb49dc 100644 --- a/eraser-base/src/main/jastadd/shem.jrag +++ b/eraser-base/src/main/jastadd/shem.jrag @@ -70,5 +70,4 @@ aspect SmartHomeEntityModel { return result; }); } - } diff --git a/eraser-base/src/main/jastadd/shem.relast b/eraser-base/src/main/jastadd/shem.relast index 58092bfc7da38ab73ccc8ab73afbb6e3cf12ba2b..f64d20d1e1851ed42e894d543117f67633f26787 100644 --- a/eraser-base/src/main/jastadd/shem.relast +++ b/eraser-base/src/main/jastadd/shem.relast @@ -29,15 +29,27 @@ rel Channel.LinkedItem* <-> Item.Channel? ; Parameter : DescribableModelElement ::= <Type:ParameterValueType> [DefaultValue:ParameterDefaultValue] <Context:String> <Required:boolean> ; ParameterDefaultValue ::= <Value:String> ; -abstract Item : LabelledModelElement ::= <_fetched_data:boolean> <TopicString> [MetaData] /ItemObserver/ /LastChanged/; +abstract Item : LabelledModelElement ::= <_fetched_data:boolean> <TopicString> [MetaData] /ItemObserver/ /LastChanged/ /Foo:KeyValuePair/; rel Item.Category? <-> ItemCategory.Items* ; rel Item.FrequencySetting? -> FrequencySetting ; +//abstract AbstractItemWithBooleanState : Item ::= <State:boolean> ; +//abstract AbstractItemWithStringState : Item ::= <State:String> ; +//abstract AbstractItemWithDoubleState : Item ::= <State:double> ; +//abstract AbstractColorItem : Item ::= State:TupleHSB ; +//abstract AbstractDateTimeItem : Item ::= <State:java.time.Instant> ; +// +//abstract ItemWithBooleanState : AbstractItemWithBooleanState ; +//abstract ItemWithStringState : AbstractItemWithStringState ; +//abstract ItemWithDoubleState : AbstractItemWithDoubleState ; +//ColorItem : AbstractColorItem ; +//DateTimeItem : AbstractDateTimeItem ; + abstract ItemWithBooleanState : Item ::= <_state:boolean> ; abstract ItemWithStringState : Item ::= <_state:String> ; abstract ItemWithDoubleState : Item ::= <_state:double> ; -ColorItem : Item ::= <_state:TupleHSB> ; -DateTimeItem : Item ::= <_state:Instant> ; +ColorItem : Item ::= _state:TupleHSB ; +DateTimeItem : Item ::= <_state:java.time.Instant> ; ContactItem : ItemWithBooleanState ; DimmerItem : ItemWithDoubleState ; ImageItem : ItemWithStringState ; @@ -52,12 +64,14 @@ ActivityItem : ItemWithDoubleState ; ItemPrototype : ItemWithStringState ::= ItemWithCorrectType:Item ; // only used for parsing ItemPlaceHolder : ItemWithStringState ; // only used for parsing +TupleHSB ::= <Hue:int> <Saturation:int> <Brightness:int> ; + MetaData ::= KeyValuePair* ; KeyValuePair ::= <Key:String> <Value:String> ; ItemCategory ::= <Name:String> ; -LastChanged ::= <Value:Instant> ; +LastChanged ::= <Value:java.time.Instant> ; Group : LabelledModelElement ::= Group* Item* [AggregationFunction:GroupAggregationFunction] ; rel Group.FrequencySetting? -> FrequencySetting ; diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/Main.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/Main.java index 23c5bdcbb527abb5c831e8d37994aefc947ff96a..89ae5fe42aa5e3f2475e9416ecfde44183f3780b 100644 --- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/Main.java +++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/Main.java @@ -9,7 +9,6 @@ import de.tudresden.inf.st.eraser.jastadd.model.Root; import de.tudresden.inf.st.eraser.openhab2.OpenHab2Importer; import de.tudresden.inf.st.eraser.util.ParserUtils; import de.tudresden.inf.st.eraser.util.TestUtils; -import org.apache.logging.log4j.LogManager; import java.io.BufferedReader; import java.io.IOException; diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/TupleHSB.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/TupleHSB.java deleted file mode 100644 index 3caddf85cb769fd27066e6fa232dfbedf0b4c697..0000000000000000000000000000000000000000 --- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/TupleHSB.java +++ /dev/null @@ -1,78 +0,0 @@ -package de.tudresden.inf.st.eraser.jastadd.model; - -import java.util.Objects; - -/** - * Value class comprising three integral values hue, saturation, brightness ranging from 0 to 255. - * - * @author rschoene - Initial contribution - */ -public class TupleHSB implements Cloneable { - private int hue; - private int saturation; - private int brightness; - public static TupleHSB of(int hue, int saturation, int brightness) { - TupleHSB result = new TupleHSB(); - result.hue = hue % 360; - result.saturation = ensureBetweenZeroAndHundred(saturation); - result.brightness = ensureBetweenZeroAndHundred(brightness); - return result; - } - - private static int ensureBetweenZeroAndHundred(int value) { - return Math.max(0, Math.min(value, 100)); - } - - public int getHue() { - return hue; - } - - public int getSaturation() { - return saturation; - } - - public int getBrightness() { - return brightness; - } - - public TupleHSB withDifferentHue(int hue) { - return TupleHSB.of(hue, this.saturation, this.brightness); - } - - public TupleHSB withDifferentSaturation(int saturation) { - return TupleHSB.of(this.hue, saturation, this.brightness); - } - - public TupleHSB withDifferentBrightness(int brightness) { - return TupleHSB.of(this.hue, this.saturation, brightness); - } - - public String toString() { - return String.format("%s,%s,%s", hue, saturation, brightness); - } - - public static TupleHSB parse(String s) { - String[] tokens = s.split(","); - return of(Integer.parseInt(tokens[0]), Integer.parseInt(tokens[1]), Integer.parseInt(tokens[2])); - } - - @SuppressWarnings("MethodDoesntCallSuperMethod") - public TupleHSB clone() { - return TupleHSB.of(hue, saturation, brightness); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TupleHSB tupleHSB = (TupleHSB) o; - return hue == tupleHSB.hue && - saturation == tupleHSB.saturation && - brightness == tupleHSB.brightness; - } - - @Override - public int hashCode() { - return Objects.hash(hue, saturation, brightness); - } -} diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/TestUtils.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/TestUtils.java index 4873f2a35673881296f71b46d08f1d76f03d8d1f..62e517ab3fcea8a37b2c276d8de055c247026196 100644 --- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/TestUtils.java +++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/TestUtils.java @@ -12,6 +12,8 @@ import java.util.concurrent.TimeUnit; */ public class TestUtils { + public static final double DELTA = 0.01; + public static class ModelAndItem { public SmartHomeEntityModel model; public NumberItem item; diff --git a/eraser-base/src/main/resources/log4j2.xml b/eraser-base/src/main/resources/log4j2.xml index 18175a02521156259c8789745fb849fa893302e9..5d1091ea995c881e5985a0cfc925a952ff50d0bc 100644 --- a/eraser-base/src/main/resources/log4j2.xml +++ b/eraser-base/src/main/resources/log4j2.xml @@ -14,7 +14,7 @@ </RollingFile> </Appenders> <Loggers> - <Root level="info"> + <Root level="debug"> <AppenderRef ref="Console"/> <AppenderRef ref="RollingFile"/> </Root> diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttRulesTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttRulesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cd45e1d089f90bb46bb1e584c666e416ea824f84 --- /dev/null +++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttRulesTest.java @@ -0,0 +1,112 @@ +package de.tudresden.inf.st.eraser; + +import de.tudresden.inf.st.eraser.jastadd.model.*; +import de.tudresden.inf.st.eraser.util.ParserUtils; +import de.tudresden.inf.st.eraser.util.TestUtils; +import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.jupiter.api.*; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import static de.tudresden.inf.st.eraser.util.TestUtils.getMqttHost; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test for the combination of MQTT and rules. + * + * @author rschoene - Initial contribution + */ +@Tag("mqtt") +@Tag("New") +public class MqttRulesTest { + + private static final Logger logger = LogManager.getLogger(MqttRulesTest.class); + + private static final String topic = "a/b"; + private MqttHandler handler; + + @BeforeEach + public void activateHandler() throws IOException { + handler = new MqttHandler().dontSendWelcomeMessage().setHost(getMqttHost()); + assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS), "Could not connect to MQTT broker"); + handler.publish("test", ("before at " + Instant.now()).getBytes(StandardCharsets.UTF_8)); + } + + @AfterEach + public void deactivateHandler() { + if (handler != null) { + handler.publish("test", ("after at " + Instant.now()).getBytes(StandardCharsets.UTF_8)); + handler.close(); + } + } + + @Test + public void receiveItemTriggerRule(TestInfo testInfo) throws Exception { + String displayName = testInfo.getDisplayName(); + handler.publish("test", ("starting " + displayName).getBytes(StandardCharsets.UTF_8)); + // Given model with two items, one to receive updates from mqtt and another to be updated + ModelAndItem mai = MqttTests.createModelAndSetupMqttRoot(); + Root root = mai.model.getRoot(); + String incomingPrefix = root.getMqttRoot().getIncomingPrefix(); + String usedTopic = incomingPrefix + topic; + + NumberItem receivingItem = mai.item; + receivingItem.setTopicString(topic); + NumberItem itemToBeUpdated = TestUtils.addItemTo(mai.model, 2.0, true); + assertTrue(mai.model.connectAllItems(), "Could not connect items"); + TestUtils.waitForMqtt(); + + // ... and a rule is defined for item to be updated + Rule rule = new Rule(); + SetStateFromExpression action = new SetStateFromExpression(); + // TODO item1 should be referred to as triggering item + action.setNumberExpression(ParserUtils.parseNumberExpression("(" + receivingItem.getID() + " + 7)", root)); + action.setAffectedItem(itemToBeUpdated); + rule.addAction(action); + RulesTest.CountingAction counter = new RulesTest.CountingAction(); + rule.addAction(counter); +// root.ragconnectJavaRegisterConsumer(itemToBeUpdated.getID(), bytes -> { +// String state = new String(bytes, StandardCharsets.UTF_8); +// itemToBeUpdated.stateUpdated(itemToBeUpdated.sendState); +// }); + + root.addRule(rule); + rule.activateFor(receivingItem); + +// // try manually +// logger.warn(counter.counters); +// receivingItem.triggerStateUpdated(); +// logger.warn(counter.counters); +// receivingItem.set_state(3.0); +// receivingItem.triggerStateUpdated(); +// logger.warn(counter.counters); +// receivingItem.set_state(9.0); +// logger.warn(counter.counters); + + // When a message is published on the topic the first item is receiving +// itemToBeUpdated.dumpDependencies(); + logger.debug("before '{}' + {}", receivingItem.getState(), root.ragconnectEvaluationCounterSummary()); + handler.publish(usedTopic, "4.0".getBytes(StandardCharsets.UTF_8)); + TestUtils.waitForMqtt(); + logger.debug("after '{}' + {}", receivingItem.getState(), root.ragconnectEvaluationCounterSummary()); +// itemToBeUpdated.dumpDependencies(); + + // Then the state of both items should be updated + logger.warn(counter.counters); + // v-- this needs to be called automatically : TODO +// receivingItem.triggerStateUpdated(); +// logger.warn(counter.counters); + + assertEquals(4.0, receivingItem.getState(), TestUtils.DELTA); + assertEquals(11.0, itemToBeUpdated.getState(), TestUtils.DELTA); + } + +} diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttTests.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttTests.java index b41bf65cded47beabf4d5e22fef2bd4aaa568311..7eb324b3838c928c489518f5580c82f6ec321ddf 100644 --- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttTests.java +++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MqttTests.java @@ -6,6 +6,7 @@ import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -23,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @Tag("mqtt") public class MqttTests { - private static final String outgoingPrefix = "out"; private static final String firstPart = "a"; private static final String alternativeFirstPart = "x"; private static final String secondPart = "b"; @@ -32,16 +32,17 @@ public class MqttTests { private static final double thirdState = 3.0; @Test - public void itemUpdateSend1() throws Exception { + public void sendSingleItem() throws Exception { // Given model with single item - String expectedTopic = outgoingPrefix + "/" + firstPart + "/" + secondPart; + ModelAndItem modelAB = createModelAndSetupMqttRoot(); + String outgoingPrefix = modelAB.model.getRoot().getMqttRoot().getOutgoingPrefix(); + String expectedTopic = outgoingPrefix + firstPart + "/" + secondPart; MqttHandler handler = new MqttHandler().dontSendWelcomeMessage().setHost(getMqttHost()); assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS), "Could not connect to MQTT broker"); List<String> messages = new ArrayList<>(); handler.newConnection(expectedTopic, bytes -> messages.add(new String(bytes))); - ModelAndItem modelAB = createModelAndSetupMqttRoot(); NumberItem sut = modelAB.item; sut.setTopicString(firstPart + "/" + secondPart); assertTrue(modelAB.model.connectAllItems(), "Could not connect items"); @@ -65,10 +66,12 @@ public class MqttTests { } @Test - public void itemUpdateSend2() throws Exception { + public void sendItemsDifferentTopic() throws Exception { // Given model with two items, each with a different topic - String expectedTopic1 = outgoingPrefix + "/" + firstPart + "/" + secondPart; - String expectedTopic2 = outgoingPrefix + "/" + alternativeFirstPart + "/" + secondPart; + ModelAndItem modelAB = createModelAndSetupMqttRoot(); + String outgoingPrefix = modelAB.model.getRoot().getMqttRoot().getOutgoingPrefix(); + String expectedTopic1 = outgoingPrefix + firstPart + "/" + secondPart; + String expectedTopic2 = outgoingPrefix + alternativeFirstPart + "/" + secondPart; MqttHandler handler = new MqttHandler().dontSendWelcomeMessage().setHost(getMqttHost()); assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS), "Could not connect to MQTT broker"); @@ -77,7 +80,6 @@ public class MqttTests { handler.newConnection(expectedTopic1, bytes -> messagesTopic1.add(new String(bytes))); handler.newConnection(expectedTopic2, bytes -> messagesTopic2.add(new String(bytes))); - ModelAndItem modelAB = createModelAndSetupMqttRoot(); NumberItem item1 = modelAB.item; NumberItem item2 = TestUtils.addItemTo(modelAB.model, 0, true); item1.setTopicString(firstPart + "/" + secondPart); @@ -108,12 +110,35 @@ public class MqttTests { assertThat(messagesTopic2).contains(Double.toString(thirdState)); } - private ModelAndItem createModelAndSetupMqttRoot() { + @Test + public void receiveSingleItem() throws Exception { + // Given model with single item + ModelAndItem modelAB = createModelAndSetupMqttRoot(); + String incomingPrefix = modelAB.model.getRoot().getMqttRoot().getIncomingPrefix(); + String usedTopic = incomingPrefix + firstPart + "/" + secondPart; + + MqttHandler handler = new MqttHandler().dontSendWelcomeMessage().setHost(getMqttHost()); + assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS), "Could not connect to MQTT broker"); + + NumberItem sut = modelAB.item; + sut.setTopicString(firstPart + "/" + secondPart); + assertTrue(modelAB.model.connectAllItems(), "Could not connect items"); + TestUtils.waitForMqtt(); + + // When a message is published on the topic the item is receiving + handler.publish(usedTopic, "4.0".getBytes(StandardCharsets.UTF_8)); + TestUtils.waitForMqtt(); + + // Then the state of the item should be updated + assertEquals(4, sut.getState(), TestUtils.DELTA); + } + + static ModelAndItem createModelAndSetupMqttRoot() { ModelAndItem mai = TestUtils.createModelAndItem(0, true); SmartHomeEntityModel model = mai.model; MqttRoot mqttRoot = new MqttRoot(); mqttRoot.setIncomingPrefix("inc"); - mqttRoot.setOutgoingPrefix(outgoingPrefix); + mqttRoot.setOutgoingPrefix("out"); mqttRoot.getHost().setHostName(getMqttHost()).setPort(1883); mqttRoot.ensureCorrectPrefixes(); model.getRoot().setMqttRoot(mqttRoot); diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/RulesTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/RulesTest.java index a8c1f727a599a1f529173428dee1d11fa5974156..9b07fc3b99eebbe0913e3904747e88bc4ba62130 100644 --- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/RulesTest.java +++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/RulesTest.java @@ -5,6 +5,8 @@ import de.tudresden.inf.st.eraser.jastadd.model.*; import de.tudresden.inf.st.eraser.jastadd.model.Action; import de.tudresden.inf.st.eraser.util.ParserUtils; import de.tudresden.inf.st.eraser.util.TestUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Disabled; import org.testcontainers.shaded.com.google.common.collect.ImmutableList; @@ -30,13 +32,20 @@ import static org.junit.jupiter.api.Assertions.*; * @author rschoene - Initial contribution */ public class RulesTest { + private static Logger logger = LogManager.getLogger(RulesTest.class); private static final double DELTA = 0.01d; static class CountingAction extends NoopAction { final Map<Item, AtomicInteger> counters = new HashMap<>(); + final boolean verbose; CountingAction() { + this(false); + } + + CountingAction(boolean verbose) { + this.verbose = verbose; reset(); } @@ -47,6 +56,7 @@ public class RulesTest { @Override public void applyFor(Item item) { getAtomic(item).addAndGet(1); + logger.debug("Rule activated (counter = {})", get(item)); } int get(Item item) { @@ -606,7 +616,7 @@ public class RulesTest { NumberItem item2 = TestUtils.addItemTo(root.getSmartHomeEntityModel(), 3, true); Rule rule = new Rule(); - rule.addAction(new SetStateFromLambdaAction(item2, provider)); + rule.addAction(new SetStateFromLambdaAction().setNewStateProvider(provider).setAffectedItem(item2)); CountingAction counter = new CountingAction(); rule.addAction(counter); root.addRule(rule); @@ -674,7 +684,7 @@ public class RulesTest { StringItem affectedItem = addStringItem(root.getSmartHomeEntityModel(), "1"); Rule rule = new Rule(); - SetStateFromItemsAction action = new SetStateFromItemsAction(items -> + SetStateFromItemsAction action = new SetStateFromItemsAction().setCombinator(items -> Long.toString(StreamSupport.stream(items.spliterator(), false) .mapToLong(inner -> (long) inner.asItemWithDoubleState().getState()) .sum())); diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/jastadd_test/core/TupleHSBTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/jastadd_test/core/TupleHSBTest.java index 6976a52dd32ef2bbe7e5e5451afc64fcbd5deeaa..d349c7d5e4dbe83ee40a8a10a66feab57b38ba76 100644 --- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/jastadd_test/core/TupleHSBTest.java +++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/jastadd_test/core/TupleHSBTest.java @@ -87,7 +87,7 @@ public class TupleHSBTest { } @Test - public void testClone() { + public void testClone() throws CloneNotSupportedException { TupleHSB one = TupleHSB.of(361,2,3); TupleHSB two = TupleHSB.of(50,123,100); TupleHSB clone = one.clone(); diff --git a/gradle.properties b/gradle.properties index 7e890a083c617c251c1a24316ea846144a7f08d9..cca4fa3899fdeca87cb6403cee9fc2898d94a4ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,3 +4,5 @@ log4j_version = 2.14.0 gradle_lombok_version = 4.0.0 openscv_version = 5.3 jupiter_version = 5.7.0 +jastadd_version = 2.3.5-dresden-7 +relast_version = 0.4.0