Skip to content
Snippets Groups Projects
Commit 1cfbb9a0 authored by René Schöne's avatar René Schöne
Browse files

WIP working towards incremental eraser version with ragconnect.

- apply style guide to build.gradle
- using locally built ragconnect with the latest fixes WIP
- refactor non-serializable stuff out of AST (SerializationExclusion)
- use incremental eval to trigger state sending (attribute: triggerStateUpdated), still WIP
- added test case to check that mqtt and rules work together (MqttRulesTest)
- added more logging while searching for errors
parent c6c8d574
No related branches found
No related tags found
1 merge request!21Draft: Replace mqtt handling with RagConnect
Pipeline #15484 failed
Showing
with 402 additions and 148 deletions
...@@ -2,6 +2,12 @@ plugins { ...@@ -2,6 +2,12 @@ plugins {
id 'eraser.java-jastadd-conventions' id 'eraser.java-jastadd-conventions'
} }
configurations {
ragconnect
}
dependencies { 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')
} }
// --- Buildscripts (must be at the top) ---
buildscript { buildscript {
repositories.mavenLocal() repositories {
repositories.mavenCentral() mavenCentral()
}
dependencies { dependencies {
classpath group: 'org.jastadd', name: 'jastaddgradle', version: '1.13.3' classpath group: 'org.jastadd', name: 'jastaddgradle', version: '1.13.3'
} }
} }
// --- Plugin definitions ---
plugins { plugins {
id 'eraser.java-application-conventions' id 'eraser.java-application-conventions'
id 'eraser.java-ragconnect-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 { configurations {
relast
coverageGenClasspath coverageGenClasspath
} }
dependencies { 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' 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: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "${jackson_version}"
api group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.16' api group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.16'
implementation group: 'org.influxdb', name: 'influxdb-java', version: '2.20' implementation group: 'org.influxdb', name: 'influxdb-java', version: '2.20'
...@@ -29,7 +43,10 @@ dependencies { ...@@ -29,7 +43,10 @@ dependencies {
testImplementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: "${log4j_version}" 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/') { def ragConnectRelastFiles = fileTree('src/main/jastadd/') {
include '**/*.relast' }.toList().toArray() include '**/*.relast' }.toList().toArray()
...@@ -37,35 +54,40 @@ String[] ragconnectArguments = [ ...@@ -37,35 +54,40 @@ String[] ragconnectArguments = [
'--o=src/gen/jastadd', '--o=src/gen/jastadd',
'--logReads', '--logReads',
'--logWrites', '--logWrites',
'--logIncremental',
// '--verbose', // '--verbose',
'--rootNode=Root', '--rootNode=Root',
// '--experimental-jastadd-329', '--experimental-jastadd-329',
// '--incremental=param', '--incremental=param',
// '--tracing=cache,flush', '--tracing=cache,flush',
'src/main/jastadd/shem.connect', 'src/main/jastadd/shem.connect',
'--List=JastAddList',
'--protocols=mqtt,java',
'--evaluationCounter'
] ]
task ragConnect(type: JavaExec) { task ragConnect(type: JavaExec) {
group = 'Build' group = 'Build'
main = 'org.jastadd.ragconnect.compiler.Compiler' main = 'org.jastadd.ragconnect.compiler.Compiler'
classpath = configurations.compileOnly classpath = configurations.ragconnect
args ragconnectArguments + ragConnectRelastFiles args ragconnectArguments + ragConnectRelastFiles
} }
String[] relastArguments = [ String[] relastArguments = [
"libs/relast.jar",
"--grammarName=./src/gen/jastadd/mainGen", "--grammarName=./src/gen/jastadd/mainGen",
"--useJastAddNames", "--useJastAddNames",
"--jastAddList=JastAddList", "--jastAddList=JastAddList",
"--resolverHelper", "--resolverHelper",
"--serializer=jackson",
"--file" "--file"
] ]
String[] relastFiles = ragConnectRelastFiles.collect { new File(it.toString().replace('/main/', '/gen/')) } String[] relastFiles = ragConnectRelastFiles.collect { new File(it.toString().replace('/main/', '/gen/')) }
task preprocess(type: JavaExec) { task preprocess(type: JavaExec) {
group = 'Build' group = 'Build'
main = "-jar" classpath = configurations.relast
main = 'org.jastadd.relast.compiler.Compiler'
args relastArguments + relastFiles args relastArguments + relastFiles
inputs.files relastFiles inputs.files relastFiles
...@@ -86,6 +108,7 @@ task generateCoverage(type: JavaExec) { ...@@ -86,6 +108,7 @@ task generateCoverage(type: JavaExec) {
args coverageGenArguments + relastFiles args coverageGenArguments + relastFiles
} }
// --- JastAdd ---
jastadd { jastadd {
configureModuleBuild() configureModuleBuild()
modules { modules {
...@@ -120,7 +143,13 @@ jastadd { ...@@ -120,7 +143,13 @@ jastadd {
} }
module = "eraser" 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' astPackage = 'de.tudresden.inf.st.eraser.jastadd.model'
genDir = 'src/gen/java' genDir = 'src/gen/java'
...@@ -132,25 +161,30 @@ jastadd { ...@@ -132,25 +161,30 @@ jastadd {
parser.genDir = "src/gen/java/de/tudresden/inf/st/eraser/jastadd/parser" 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 { useJUnitPlatform {
java { includeTags 'New'
srcDir 'src/gen/java'
} }
} }
cleanGen.doFirst { // --- Versioning and Publishing ---
delete "src/gen/jastadd" mainClassName = 'de.tudresden.inf.st.eraser.Main'
delete "src/gen/java"
}
// --- Task order ---
preprocess.dependsOn ragConnect preprocess.dependsOn ragConnect
generateCoverage.dependsOn ragConnect generateCoverage.dependsOn ragConnect
generateAst.dependsOn preprocess generateAst.dependsOn preprocess
generateAst.dependsOn generateCoverage generateAst.dependsOn generateCoverage
generateAst.inputs.files file("./src/main/jastadd/mainGen.ast"), file("./src/main/jastadd/mainGen.jadd") generateAst.inputs.files file("./src/main/jastadd/mainGen.ast"), file("./src/main/jastadd/mainGen.jadd")
//compileJava.dependsOn jastadd
//
//// always run jastadd //// always run jastadd
//jastadd.outputs.upToDateWhen {false} //jastadd.outputs.upToDateWhen {false}
// --- Misc ---
cleanGen.doFirst {
delete "src/gen/jastadd"
delete "src/gen/java"
}
File added
...@@ -191,7 +191,11 @@ aspect ItemHandling { ...@@ -191,7 +191,11 @@ aspect ItemHandling {
//--- setStateFromColor --- //--- setStateFromColor ---
public abstract void Item.setStateFromColor(TupleHSB value); public abstract void Item.setStateFromColor(TupleHSB value);
public void ColorItem.setStateFromColor(TupleHSB value) { public void ColorItem.setStateFromColor(TupleHSB value) {
try {
this.setState(value.clone()); this.setState(value.clone());
} catch (CloneNotSupportedException e) {
// should not happen;
}
} }
public void DateTimeItem.setStateFromColor(TupleHSB value) { public void DateTimeItem.setStateFromColor(TupleHSB value) {
// there is no good way here // there is no good way here
...@@ -311,6 +315,24 @@ aspect ItemHandling { ...@@ -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 --- //--- sendState ---
protected void Item.sendState() throws Exception { protected void Item.sendState() throws Exception {
...@@ -523,3 +545,63 @@ aspect ItemHandling { ...@@ -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());
}
}
...@@ -3,7 +3,7 @@ MachineLearningRoot ::= [ActivityRecognition:MachineLearningModel] [PreferenceLe ...@@ -3,7 +3,7 @@ MachineLearningRoot ::= [ActivityRecognition:MachineLearningModel] [PreferenceLe
Activity ::= <Identifier:int> <Label:String> ; Activity ::= <Identifier:int> <Label:String> ;
abstract ChangeEvent ::= <Identifier:int> <Created:Instant> ChangedItem* ; abstract ChangeEvent ::= <Identifier:int> <Created:java.time.Instant> ChangedItem* ;
ChangedItem ::= <NewStateAsString:String> ; ChangedItem ::= <NewStateAsString:String> ;
rel ChangedItem.Item -> Item ; rel ChangedItem.Item -> Item ;
...@@ -19,12 +19,13 @@ rel MachineLearningModel.TargetItem* <-> Item.TargetInMachineLearningModel* ; ...@@ -19,12 +19,13 @@ rel MachineLearningModel.TargetItem* <-> Item.TargetInMachineLearningModel* ;
ExternalMachineLearningModel : MachineLearningModel ; ExternalMachineLearningModel : MachineLearningModel ;
abstract InternalMachineLearningModel : MachineLearningModel ::= <OutputApplication:DoubleDoubleFunction> ; abstract InternalMachineLearningModel : MachineLearningModel ;
// excluded: <OutputApplication:DoubleDoubleFunction>
MachineLearningResult ::= ItemUpdate* ; MachineLearningResult ::= ItemUpdate* ;
abstract ItemUpdate ::= ; abstract ItemUpdate ::= ;
rel ItemUpdate.Item -> Item ; rel ItemUpdate.Item -> Item ;
ItemUpdateColor : ItemUpdate ::= <NewHSB:TupleHSB> ; ItemUpdateColor : ItemUpdate ::= NewHSB:TupleHSB ;
ItemUpdateDouble : ItemUpdate ::= <NewValue:double> ; ItemUpdateDouble : ItemUpdate ::= <NewValue:double> ;
// ---------------- Neural Network ------------------------------ // ---------------- Neural Network ------------------------------
NeuralNetworkRoot : InternalMachineLearningModel ::= InputNeuron* HiddenNeuron* OutputLayer ; NeuralNetworkRoot : InternalMachineLearningModel ::= InputNeuron* HiddenNeuron* OutputLayer ;
OutputLayer ::= OutputNeuron* <Combinator:DoubleArrayDoubleFunction> ; OutputLayer ::= OutputNeuron* ;
// excluded: <Combinator:DoubleArrayDoubleFunction>
rel OutputLayer.AffectedItem -> Item ; rel OutputLayer.AffectedItem -> Item ;
abstract Neuron ::= Output:NeuronConnection* ; abstract Neuron ::= Output:NeuronConnection* ;
...@@ -12,7 +13,8 @@ rel NeuronConnection.Neuron <-> Neuron.Input* ; ...@@ -12,7 +13,8 @@ rel NeuronConnection.Neuron <-> Neuron.Input* ;
InputNeuron : Neuron ; InputNeuron : Neuron ;
rel InputNeuron.Item -> Item ; rel InputNeuron.Item -> Item ;
HiddenNeuron : Neuron ::= <ActivationFormula:DoubleArrayDoubleFunction> ; HiddenNeuron : Neuron ;
// excluded: <ActivationFormula:DoubleArrayDoubleFunction>
BiasNeuron : HiddenNeuron ; BiasNeuron : HiddenNeuron ;
OutputNeuron : HiddenNeuron ::= <Label:String> ; OutputNeuron : HiddenNeuron ::= <Label:String> ;
......
...@@ -7,7 +7,8 @@ rel ItemStateChangeCondition.Item -> Item; ...@@ -7,7 +7,8 @@ rel ItemStateChangeCondition.Item -> Item;
ExpressionCondition : Condition ::= LogicalExpression ; ExpressionCondition : Condition ::= LogicalExpression ;
abstract Action ; abstract Action ;
NoopAction : Action ; NoopAction : Action ;
LambdaAction : Action ::= <Lambda:Action2EditConsumer> ; LambdaAction : Action ::= ;
// excluded: <Lambda:Action2EditConsumer>
TriggerRuleAction : Action ; TriggerRuleAction : Action ;
rel TriggerRuleAction.Rule -> Rule ; rel TriggerRuleAction.Rule -> Rule ;
...@@ -18,10 +19,12 @@ rel SetStateAction.AffectedItem -> Item ; ...@@ -18,10 +19,12 @@ rel SetStateAction.AffectedItem -> Item ;
SetStateFromExpression : SetStateAction ::= NumberExpression ; SetStateFromExpression : SetStateAction ::= NumberExpression ;
SetStateFromConstantStringAction : SetStateAction ::= <NewState:String> ; SetStateFromConstantStringAction : SetStateAction ::= <NewState:String> ;
SetStateFromLambdaAction : SetStateAction ::= <NewStateProvider:NewStateProvider> ; SetStateFromLambdaAction : SetStateAction ;
// excluded: <NewStateProvider:NewStateProvider>
SetStateFromTriggeringItemAction : SetStateAction ::= ; SetStateFromTriggeringItemAction : SetStateAction ::= ;
SetStateFromItemsAction : SetStateAction ::= <Combinator:ItemsToStringFunction> ; SetStateFromItemsAction : SetStateAction ;
// excluded: <Combinator:ItemsToStringFunction>
rel SetStateFromItemsAction.SourceItem* -> Item ; rel SetStateFromItemsAction.SourceItem* -> Item ;
AddDoubleToStateAction : SetStateAction ::= <Increment:double> ; AddDoubleToStateAction : SetStateAction ::= <Increment:double> ;
......
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; }
}
}
...@@ -17,7 +17,7 @@ aspect MQTT { ...@@ -17,7 +17,7 @@ aspect MQTT {
syn ExternalHost MqttRoot.getHost() = new ExternalHost(); syn ExternalHost MqttRoot.getHost() = new ExternalHost();
// --- connectAllItems --- // --- connectAllItems ---
public boolean SmartHomeEntityModel.connectAllItems() throws IOException { public boolean SmartHomeEntityModel.connectAllItems() throws java.io.IOException {
MqttRoot mqttRoot = getRoot().getMqttRoot(); MqttRoot mqttRoot = getRoot().getMqttRoot();
ExternalHost host = mqttRoot.getHost(); ExternalHost host = mqttRoot.getHost();
// TODO user/password not used yet (not supported by ragconnect yet) // TODO user/password not used yet (not supported by ragconnect yet)
...@@ -26,34 +26,38 @@ aspect MQTT { ...@@ -26,34 +26,38 @@ aspect MQTT {
boolean success = true; boolean success = true;
for (Item item : this.items()) { for (Item item : this.items()) {
String suffix = item.getTopicString().isBlank() ? item.getID() : item.getTopicString(); 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; ConnectReceive connectReceive;
ConnectSend connectSend;
if (item.isItemWithDoubleState()) { if (item.isItemWithDoubleState()) {
connectReceive = item.asItemWithDoubleState()::connect_state; connectReceive = item.asItemWithDoubleState()::connect_state;
connectSend = item.asItemWithDoubleState()::connect_state;
} else if (item.isItemWithBooleanState()) { } else if (item.isItemWithBooleanState()) {
connectReceive = item.asItemWithBooleanState()::connect_state; connectReceive = item.asItemWithBooleanState()::connect_state;
connectSend = item.asItemWithBooleanState()::connect_state;
} else if (item.isItemWithStringState()) { } else if (item.isItemWithStringState()) {
connectReceive = item.asItemWithStringState()::connect_state; connectReceive = item.asItemWithStringState()::connect_state;
connectSend = item.asItemWithStringState()::connect_state;
} else { } else {
// unsupported item type // unsupported item type
continue; continue;
} }
success &= connectReceive.apply(prefix + mqttRoot.getIncomingPrefix() + suffix) & 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; return success;
} }
class SmartHomeEntityModel { class SmartHomeEntityModel {
interface ConnectReceive { interface ConnectReceive {
boolean apply(String uriString) throws IOException; boolean apply(String uriString) throws java.io.IOException;
} }
interface ConnectSend { interface ConnectSend {
boolean apply(String uriString, boolean writeCurrentValue) throws IOException; boolean apply(String uriString, boolean writeCurrentValue) throws java.io.IOException;
} }
} }
} }
receive ItemWithDoubleState._state using StringToDouble ; 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 {: StringToDouble maps String s to double {:
return Double.parseDouble(s); return Double.parseDouble(s);
:} :}
...@@ -9,9 +20,6 @@ DoubleToString maps double d to String {: ...@@ -9,9 +20,6 @@ DoubleToString maps double d to String {:
return Double.toString(d); return Double.toString(d);
:} :}
receive ItemWithBooleanState._state using StringToBoolean ;
send ItemWithBooleanState._state using BooleanToString ;
StringToBoolean maps String s to boolean {: StringToBoolean maps String s to boolean {:
return Boolean.parseBoolean(s); return Boolean.parseBoolean(s);
:} :}
...@@ -19,6 +27,3 @@ StringToBoolean maps String s to boolean {: ...@@ -19,6 +27,3 @@ StringToBoolean maps String s to boolean {:
BooleanToString maps boolean b to String {: BooleanToString maps boolean b to String {:
return Boolean.toString(b); return Boolean.toString(b);
:} :}
receive ItemWithStringState._state ;
send ItemWithStringState._state ;
...@@ -70,5 +70,4 @@ aspect SmartHomeEntityModel { ...@@ -70,5 +70,4 @@ aspect SmartHomeEntityModel {
return result; return result;
}); });
} }
} }
...@@ -29,15 +29,27 @@ rel Channel.LinkedItem* <-> Item.Channel? ; ...@@ -29,15 +29,27 @@ rel Channel.LinkedItem* <-> Item.Channel? ;
Parameter : DescribableModelElement ::= <Type:ParameterValueType> [DefaultValue:ParameterDefaultValue] <Context:String> <Required:boolean> ; Parameter : DescribableModelElement ::= <Type:ParameterValueType> [DefaultValue:ParameterDefaultValue] <Context:String> <Required:boolean> ;
ParameterDefaultValue ::= <Value:String> ; 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.Category? <-> ItemCategory.Items* ;
rel Item.FrequencySetting? -> FrequencySetting ; 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 ItemWithBooleanState : Item ::= <_state:boolean> ;
abstract ItemWithStringState : Item ::= <_state:String> ; abstract ItemWithStringState : Item ::= <_state:String> ;
abstract ItemWithDoubleState : Item ::= <_state:double> ; abstract ItemWithDoubleState : Item ::= <_state:double> ;
ColorItem : Item ::= <_state:TupleHSB> ; ColorItem : Item ::= _state:TupleHSB ;
DateTimeItem : Item ::= <_state:Instant> ; DateTimeItem : Item ::= <_state:java.time.Instant> ;
ContactItem : ItemWithBooleanState ; ContactItem : ItemWithBooleanState ;
DimmerItem : ItemWithDoubleState ; DimmerItem : ItemWithDoubleState ;
ImageItem : ItemWithStringState ; ImageItem : ItemWithStringState ;
...@@ -52,12 +64,14 @@ ActivityItem : ItemWithDoubleState ; ...@@ -52,12 +64,14 @@ ActivityItem : ItemWithDoubleState ;
ItemPrototype : ItemWithStringState ::= ItemWithCorrectType:Item ; // only used for parsing ItemPrototype : ItemWithStringState ::= ItemWithCorrectType:Item ; // only used for parsing
ItemPlaceHolder : ItemWithStringState ; // only used for parsing ItemPlaceHolder : ItemWithStringState ; // only used for parsing
TupleHSB ::= <Hue:int> <Saturation:int> <Brightness:int> ;
MetaData ::= KeyValuePair* ; MetaData ::= KeyValuePair* ;
KeyValuePair ::= <Key:String> <Value:String> ; KeyValuePair ::= <Key:String> <Value:String> ;
ItemCategory ::= <Name:String> ; ItemCategory ::= <Name:String> ;
LastChanged ::= <Value:Instant> ; LastChanged ::= <Value:java.time.Instant> ;
Group : LabelledModelElement ::= Group* Item* [AggregationFunction:GroupAggregationFunction] ; Group : LabelledModelElement ::= Group* Item* [AggregationFunction:GroupAggregationFunction] ;
rel Group.FrequencySetting? -> FrequencySetting ; rel Group.FrequencySetting? -> FrequencySetting ;
......
...@@ -9,7 +9,6 @@ import de.tudresden.inf.st.eraser.jastadd.model.Root; ...@@ -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.openhab2.OpenHab2Importer;
import de.tudresden.inf.st.eraser.util.ParserUtils; import de.tudresden.inf.st.eraser.util.ParserUtils;
import de.tudresden.inf.st.eraser.util.TestUtils; import de.tudresden.inf.st.eraser.util.TestUtils;
import org.apache.logging.log4j.LogManager;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
......
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);
}
}
...@@ -12,6 +12,8 @@ import java.util.concurrent.TimeUnit; ...@@ -12,6 +12,8 @@ import java.util.concurrent.TimeUnit;
*/ */
public class TestUtils { public class TestUtils {
public static final double DELTA = 0.01;
public static class ModelAndItem { public static class ModelAndItem {
public SmartHomeEntityModel model; public SmartHomeEntityModel model;
public NumberItem item; public NumberItem item;
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
</RollingFile> </RollingFile>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="info"> <Root level="debug">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/> <AppenderRef ref="RollingFile"/>
</Root> </Root>
......
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);
}
}
...@@ -6,6 +6,7 @@ import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem; ...@@ -6,6 +6,7 @@ import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
...@@ -23,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; ...@@ -23,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("mqtt") @Tag("mqtt")
public class MqttTests { public class MqttTests {
private static final String outgoingPrefix = "out";
private static final String firstPart = "a"; private static final String firstPart = "a";
private static final String alternativeFirstPart = "x"; private static final String alternativeFirstPart = "x";
private static final String secondPart = "b"; private static final String secondPart = "b";
...@@ -32,16 +32,17 @@ public class MqttTests { ...@@ -32,16 +32,17 @@ public class MqttTests {
private static final double thirdState = 3.0; private static final double thirdState = 3.0;
@Test @Test
public void itemUpdateSend1() throws Exception { public void sendSingleItem() throws Exception {
// Given model with single item // 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()); MqttHandler handler = new MqttHandler().dontSendWelcomeMessage().setHost(getMqttHost());
assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS), "Could not connect to MQTT broker"); assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS), "Could not connect to MQTT broker");
List<String> messages = new ArrayList<>(); List<String> messages = new ArrayList<>();
handler.newConnection(expectedTopic, bytes -> messages.add(new String(bytes))); handler.newConnection(expectedTopic, bytes -> messages.add(new String(bytes)));
ModelAndItem modelAB = createModelAndSetupMqttRoot();
NumberItem sut = modelAB.item; NumberItem sut = modelAB.item;
sut.setTopicString(firstPart + "/" + secondPart); sut.setTopicString(firstPart + "/" + secondPart);
assertTrue(modelAB.model.connectAllItems(), "Could not connect items"); assertTrue(modelAB.model.connectAllItems(), "Could not connect items");
...@@ -65,10 +66,12 @@ public class MqttTests { ...@@ -65,10 +66,12 @@ public class MqttTests {
} }
@Test @Test
public void itemUpdateSend2() throws Exception { public void sendItemsDifferentTopic() throws Exception {
// Given model with two items, each with a different topic // Given model with two items, each with a different topic
String expectedTopic1 = outgoingPrefix + "/" + firstPart + "/" + secondPart; ModelAndItem modelAB = createModelAndSetupMqttRoot();
String expectedTopic2 = outgoingPrefix + "/" + alternativeFirstPart + "/" + secondPart; String outgoingPrefix = modelAB.model.getRoot().getMqttRoot().getOutgoingPrefix();
String expectedTopic1 = outgoingPrefix + firstPart + "/" + secondPart;
String expectedTopic2 = outgoingPrefix + alternativeFirstPart + "/" + secondPart;
MqttHandler handler = new MqttHandler().dontSendWelcomeMessage().setHost(getMqttHost()); MqttHandler handler = new MqttHandler().dontSendWelcomeMessage().setHost(getMqttHost());
assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS), "Could not connect to MQTT broker"); assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS), "Could not connect to MQTT broker");
...@@ -77,7 +80,6 @@ public class MqttTests { ...@@ -77,7 +80,6 @@ public class MqttTests {
handler.newConnection(expectedTopic1, bytes -> messagesTopic1.add(new String(bytes))); handler.newConnection(expectedTopic1, bytes -> messagesTopic1.add(new String(bytes)));
handler.newConnection(expectedTopic2, bytes -> messagesTopic2.add(new String(bytes))); handler.newConnection(expectedTopic2, bytes -> messagesTopic2.add(new String(bytes)));
ModelAndItem modelAB = createModelAndSetupMqttRoot();
NumberItem item1 = modelAB.item; NumberItem item1 = modelAB.item;
NumberItem item2 = TestUtils.addItemTo(modelAB.model, 0, true); NumberItem item2 = TestUtils.addItemTo(modelAB.model, 0, true);
item1.setTopicString(firstPart + "/" + secondPart); item1.setTopicString(firstPart + "/" + secondPart);
...@@ -108,12 +110,35 @@ public class MqttTests { ...@@ -108,12 +110,35 @@ public class MqttTests {
assertThat(messagesTopic2).contains(Double.toString(thirdState)); 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); ModelAndItem mai = TestUtils.createModelAndItem(0, true);
SmartHomeEntityModel model = mai.model; SmartHomeEntityModel model = mai.model;
MqttRoot mqttRoot = new MqttRoot(); MqttRoot mqttRoot = new MqttRoot();
mqttRoot.setIncomingPrefix("inc"); mqttRoot.setIncomingPrefix("inc");
mqttRoot.setOutgoingPrefix(outgoingPrefix); mqttRoot.setOutgoingPrefix("out");
mqttRoot.getHost().setHostName(getMqttHost()).setPort(1883); mqttRoot.getHost().setHostName(getMqttHost()).setPort(1883);
mqttRoot.ensureCorrectPrefixes(); mqttRoot.ensureCorrectPrefixes();
model.getRoot().setMqttRoot(mqttRoot); model.getRoot().setMqttRoot(mqttRoot);
......
...@@ -5,6 +5,8 @@ import de.tudresden.inf.st.eraser.jastadd.model.*; ...@@ -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.jastadd.model.Action;
import de.tudresden.inf.st.eraser.util.ParserUtils; import de.tudresden.inf.st.eraser.util.ParserUtils;
import de.tudresden.inf.st.eraser.util.TestUtils; 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.Test;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.testcontainers.shaded.com.google.common.collect.ImmutableList; import org.testcontainers.shaded.com.google.common.collect.ImmutableList;
...@@ -30,13 +32,20 @@ import static org.junit.jupiter.api.Assertions.*; ...@@ -30,13 +32,20 @@ import static org.junit.jupiter.api.Assertions.*;
* @author rschoene - Initial contribution * @author rschoene - Initial contribution
*/ */
public class RulesTest { public class RulesTest {
private static Logger logger = LogManager.getLogger(RulesTest.class);
private static final double DELTA = 0.01d; private static final double DELTA = 0.01d;
static class CountingAction extends NoopAction { static class CountingAction extends NoopAction {
final Map<Item, AtomicInteger> counters = new HashMap<>(); final Map<Item, AtomicInteger> counters = new HashMap<>();
final boolean verbose;
CountingAction() { CountingAction() {
this(false);
}
CountingAction(boolean verbose) {
this.verbose = verbose;
reset(); reset();
} }
...@@ -47,6 +56,7 @@ public class RulesTest { ...@@ -47,6 +56,7 @@ public class RulesTest {
@Override @Override
public void applyFor(Item item) { public void applyFor(Item item) {
getAtomic(item).addAndGet(1); getAtomic(item).addAndGet(1);
logger.debug("Rule activated (counter = {})", get(item));
} }
int get(Item item) { int get(Item item) {
...@@ -606,7 +616,7 @@ public class RulesTest { ...@@ -606,7 +616,7 @@ public class RulesTest {
NumberItem item2 = TestUtils.addItemTo(root.getSmartHomeEntityModel(), 3, true); NumberItem item2 = TestUtils.addItemTo(root.getSmartHomeEntityModel(), 3, true);
Rule rule = new Rule(); Rule rule = new Rule();
rule.addAction(new SetStateFromLambdaAction(item2, provider)); rule.addAction(new SetStateFromLambdaAction().setNewStateProvider(provider).setAffectedItem(item2));
CountingAction counter = new CountingAction(); CountingAction counter = new CountingAction();
rule.addAction(counter); rule.addAction(counter);
root.addRule(rule); root.addRule(rule);
...@@ -674,7 +684,7 @@ public class RulesTest { ...@@ -674,7 +684,7 @@ public class RulesTest {
StringItem affectedItem = addStringItem(root.getSmartHomeEntityModel(), "1"); StringItem affectedItem = addStringItem(root.getSmartHomeEntityModel(), "1");
Rule rule = new Rule(); Rule rule = new Rule();
SetStateFromItemsAction action = new SetStateFromItemsAction(items -> SetStateFromItemsAction action = new SetStateFromItemsAction().setCombinator(items ->
Long.toString(StreamSupport.stream(items.spliterator(), false) Long.toString(StreamSupport.stream(items.spliterator(), false)
.mapToLong(inner -> (long) inner.asItemWithDoubleState().getState()) .mapToLong(inner -> (long) inner.asItemWithDoubleState().getState())
.sum())); .sum()));
......
...@@ -87,7 +87,7 @@ public class TupleHSBTest { ...@@ -87,7 +87,7 @@ public class TupleHSBTest {
} }
@Test @Test
public void testClone() { public void testClone() throws CloneNotSupportedException {
TupleHSB one = TupleHSB.of(361,2,3); TupleHSB one = TupleHSB.of(361,2,3);
TupleHSB two = TupleHSB.of(50,123,100); TupleHSB two = TupleHSB.of(50,123,100);
TupleHSB clone = one.clone(); TupleHSB clone = one.clone();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment