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